aboutsummaryrefslogtreecommitdiff
path: root/launcher/ui
diff options
context:
space:
mode:
Diffstat (limited to 'launcher/ui')
-rw-r--r--launcher/ui/ColorCache.cpp35
-rw-r--r--launcher/ui/ColorCache.h119
-rw-r--r--launcher/ui/GuiUtil.cpp136
-rw-r--r--launcher/ui/GuiUtil.h11
-rw-r--r--launcher/ui/InstanceWindow.cpp237
-rw-r--r--launcher/ui/InstanceWindow.h76
-rw-r--r--launcher/ui/MainWindow.cpp1949
-rw-r--r--launcher/ui/MainWindow.h221
-rw-r--r--launcher/ui/dialogs/AboutDialog.cpp149
-rw-r--r--launcher/ui/dialogs/AboutDialog.h47
-rw-r--r--launcher/ui/dialogs/AboutDialog.ui316
-rw-r--r--launcher/ui/dialogs/CopyInstanceDialog.cpp144
-rw-r--r--launcher/ui/dialogs/CopyInstanceDialog.h58
-rw-r--r--launcher/ui/dialogs/CopyInstanceDialog.ui182
-rw-r--r--launcher/ui/dialogs/CustomMessageBox.cpp35
-rw-r--r--launcher/ui/dialogs/CustomMessageBox.h26
-rw-r--r--launcher/ui/dialogs/EditAccountDialog.cpp61
-rw-r--r--launcher/ui/dialogs/EditAccountDialog.h56
-rw-r--r--launcher/ui/dialogs/EditAccountDialog.ui94
-rw-r--r--launcher/ui/dialogs/ExportInstanceDialog.cpp482
-rw-r--r--launcher/ui/dialogs/ExportInstanceDialog.h54
-rw-r--r--launcher/ui/dialogs/ExportInstanceDialog.ui83
-rw-r--r--launcher/ui/dialogs/IconPickerDialog.cpp163
-rw-r--r--launcher/ui/dialogs/IconPickerDialog.h49
-rw-r--r--launcher/ui/dialogs/IconPickerDialog.ui67
-rw-r--r--launcher/ui/dialogs/LoginDialog.cpp119
-rw-r--r--launcher/ui/dialogs/LoginDialog.h59
-rw-r--r--launcher/ui/dialogs/LoginDialog.ui77
-rw-r--r--launcher/ui/dialogs/MSALoginDialog.cpp141
-rw-r--r--launcher/ui/dialogs/MSALoginDialog.h63
-rw-r--r--launcher/ui/dialogs/MSALoginDialog.ui65
-rw-r--r--launcher/ui/dialogs/NewComponentDialog.cpp106
-rw-r--r--launcher/ui/dialogs/NewComponentDialog.h48
-rw-r--r--launcher/ui/dialogs/NewComponentDialog.ui101
-rw-r--r--launcher/ui/dialogs/NewInstanceDialog.cpp255
-rw-r--r--launcher/ui/dialogs/NewInstanceDialog.h80
-rw-r--r--launcher/ui/dialogs/NewInstanceDialog.ui87
-rw-r--r--launcher/ui/dialogs/NotificationDialog.cpp86
-rw-r--r--launcher/ui/dialogs/NotificationDialog.h44
-rw-r--r--launcher/ui/dialogs/NotificationDialog.ui85
-rw-r--r--launcher/ui/dialogs/ProfileSelectDialog.cpp115
-rw-r--r--launcher/ui/dialogs/ProfileSelectDialog.h90
-rw-r--r--launcher/ui/dialogs/ProfileSelectDialog.ui62
-rw-r--r--launcher/ui/dialogs/ProfileSetupDialog.cpp250
-rw-r--r--launcher/ui/dialogs/ProfileSetupDialog.h88
-rw-r--r--launcher/ui/dialogs/ProfileSetupDialog.ui74
-rw-r--r--launcher/ui/dialogs/ProgressDialog.cpp196
-rw-r--r--launcher/ui/dialogs/ProgressDialog.h71
-rw-r--r--launcher/ui/dialogs/ProgressDialog.ui66
-rw-r--r--launcher/ui/dialogs/SkinUploadDialog.cpp155
-rw-r--r--launcher/ui/dialogs/SkinUploadDialog.h29
-rw-r--r--launcher/ui/dialogs/SkinUploadDialog.ui97
-rw-r--r--launcher/ui/dialogs/UpdateDialog.cpp182
-rw-r--r--launcher/ui/dialogs/UpdateDialog.h67
-rw-r--r--launcher/ui/dialogs/UpdateDialog.ui91
-rw-r--r--launcher/ui/dialogs/VersionSelectDialog.cpp141
-rw-r--r--launcher/ui/dialogs/VersionSelectDialog.h78
-rw-r--r--launcher/ui/instanceview/AccessibleInstanceView.cpp778
-rw-r--r--launcher/ui/instanceview/AccessibleInstanceView.h6
-rw-r--r--launcher/ui/instanceview/AccessibleInstanceView_p.h118
-rw-r--r--launcher/ui/instanceview/InstanceDelegate.cpp428
-rw-r--r--launcher/ui/instanceview/InstanceDelegate.h39
-rw-r--r--launcher/ui/instanceview/InstanceProxyModel.cpp71
-rw-r--r--launcher/ui/instanceview/InstanceProxyModel.h35
-rw-r--r--launcher/ui/instanceview/InstanceView.cpp1010
-rw-r--r--launcher/ui/instanceview/InstanceView.h153
-rw-r--r--launcher/ui/instanceview/VisualGroup.cpp317
-rw-r--r--launcher/ui/instanceview/VisualGroup.h106
-rw-r--r--launcher/ui/pagedialog/PageDialog.cpp62
-rw-r--r--launcher/ui/pagedialog/PageDialog.h35
-rw-r--r--launcher/ui/pages/BasePage.h58
-rw-r--r--launcher/ui/pages/BasePageContainer.h10
-rw-r--r--launcher/ui/pages/BasePageProvider.h68
-rw-r--r--launcher/ui/pages/global/AccountListPage.cpp260
-rw-r--r--launcher/ui/pages/global/AccountListPage.h85
-rw-r--r--launcher/ui/pages/global/AccountListPage.ui129
-rw-r--r--launcher/ui/pages/global/CustomCommandsPage.cpp51
-rw-r--r--launcher/ui/pages/global/CustomCommandsPage.h55
-rw-r--r--launcher/ui/pages/global/ExternalToolsPage.cpp233
-rw-r--r--launcher/ui/pages/global/ExternalToolsPage.h74
-rw-r--r--launcher/ui/pages/global/ExternalToolsPage.ui194
-rw-r--r--launcher/ui/pages/global/JavaPage.cpp153
-rw-r--r--launcher/ui/pages/global/JavaPage.h72
-rw-r--r--launcher/ui/pages/global/JavaPage.ui260
-rw-r--r--launcher/ui/pages/global/LanguagePage.cpp51
-rw-r--r--launcher/ui/pages/global/LanguagePage.h60
-rw-r--r--launcher/ui/pages/global/LauncherPage.cpp466
-rw-r--r--launcher/ui/pages/global/LauncherPage.h103
-rw-r--r--launcher/ui/pages/global/LauncherPage.ui584
-rw-r--r--launcher/ui/pages/global/MinecraftPage.cpp91
-rw-r--r--launcher/ui/pages/global/MinecraftPage.h70
-rw-r--r--launcher/ui/pages/global/MinecraftPage.ui196
-rw-r--r--launcher/ui/pages/global/PasteEEPage.cpp81
-rw-r--r--launcher/ui/pages/global/PasteEEPage.h62
-rw-r--r--launcher/ui/pages/global/PasteEEPage.ui128
-rw-r--r--launcher/ui/pages/global/ProxyPage.cpp106
-rw-r--r--launcher/ui/pages/global/ProxyPage.h66
-rw-r--r--launcher/ui/pages/global/ProxyPage.ui203
-rw-r--r--launcher/ui/pages/instance/GameOptionsPage.cpp37
-rw-r--r--launcher/ui/pages/instance/GameOptionsPage.h63
-rw-r--r--launcher/ui/pages/instance/GameOptionsPage.ui88
-rw-r--r--launcher/ui/pages/instance/InstanceSettingsPage.cpp341
-rw-r--r--launcher/ui/pages/instance/InstanceSettingsPage.h76
-rw-r--r--launcher/ui/pages/instance/InstanceSettingsPage.ui548
-rw-r--r--launcher/ui/pages/instance/LegacyUpgradePage.cpp51
-rw-r--r--launcher/ui/pages/instance/LegacyUpgradePage.h64
-rw-r--r--launcher/ui/pages/instance/LegacyUpgradePage.ui47
-rw-r--r--launcher/ui/pages/instance/LogPage.cpp330
-rw-r--r--launcher/ui/pages/instance/LogPage.h86
-rw-r--r--launcher/ui/pages/instance/LogPage.ui182
-rw-r--r--launcher/ui/pages/instance/ModFolderPage.cpp366
-rw-r--r--launcher/ui/pages/instance/ModFolderPage.h120
-rw-r--r--launcher/ui/pages/instance/ModFolderPage.ui164
-rw-r--r--launcher/ui/pages/instance/NotesPage.cpp21
-rw-r--r--launcher/ui/pages/instance/NotesPage.h60
-rw-r--r--launcher/ui/pages/instance/NotesPage.ui49
-rw-r--r--launcher/ui/pages/instance/OtherLogsPage.cpp314
-rw-r--r--launcher/ui/pages/instance/OtherLogsPage.h81
-rw-r--r--launcher/ui/pages/instance/OtherLogsPage.ui150
-rw-r--r--launcher/ui/pages/instance/ResourcePackPage.h23
-rw-r--r--launcher/ui/pages/instance/ScreenshotsPage.cpp423
-rw-r--r--launcher/ui/pages/instance/ScreenshotsPage.h89
-rw-r--r--launcher/ui/pages/instance/ScreenshotsPage.ui87
-rw-r--r--launcher/ui/pages/instance/ServersPage.cpp768
-rw-r--r--launcher/ui/pages/instance/ServersPage.h94
-rw-r--r--launcher/ui/pages/instance/ServersPage.ui194
-rw-r--r--launcher/ui/pages/instance/ShaderPackPage.h22
-rw-r--r--launcher/ui/pages/instance/TexturePackPage.h22
-rw-r--r--launcher/ui/pages/instance/VersionPage.cpp641
-rw-r--r--launcher/ui/pages/instance/VersionPage.h104
-rw-r--r--launcher/ui/pages/instance/VersionPage.ui285
-rw-r--r--launcher/ui/pages/instance/WorldListPage.cpp412
-rw-r--r--launcher/ui/pages/instance/WorldListPage.h99
-rw-r--r--launcher/ui/pages/instance/WorldListPage.ui161
-rw-r--r--launcher/ui/pages/modplatform/ImportPage.cpp132
-rw-r--r--launcher/ui/pages/modplatform/ImportPage.h70
-rw-r--r--launcher/ui/pages/modplatform/ImportPage.ui52
-rw-r--r--launcher/ui/pages/modplatform/VanillaPage.cpp103
-rw-r--r--launcher/ui/pages/modplatform/VanillaPage.h75
-rw-r--r--launcher/ui/pages/modplatform/VanillaPage.ui169
-rw-r--r--launcher/ui/pages/modplatform/atlauncher/AtlFilterModel.cpp81
-rw-r--r--launcher/ui/pages/modplatform/atlauncher/AtlFilterModel.h34
-rw-r--r--launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp193
-rw-r--r--launcher/ui/pages/modplatform/atlauncher/AtlListModel.h52
-rw-r--r--launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp209
-rw-r--r--launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h66
-rw-r--r--launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.ui65
-rw-r--r--launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp171
-rw-r--r--launcher/ui/pages/modplatform/atlauncher/AtlPage.h86
-rw-r--r--launcher/ui/pages/modplatform/atlauncher/AtlPage.ui92
-rw-r--r--launcher/ui/pages/modplatform/flame/FlameModel.cpp258
-rw-r--r--launcher/ui/pages/modplatform/flame/FlameModel.h76
-rw-r--r--launcher/ui/pages/modplatform/flame/FlamePage.cpp186
-rw-r--r--launcher/ui/pages/modplatform/flame/FlamePage.h80
-rw-r--r--launcher/ui/pages/modplatform/flame/FlamePage.ui90
-rw-r--r--launcher/ui/pages/modplatform/ftb/FtbFilterModel.cpp76
-rw-r--r--launcher/ui/pages/modplatform/ftb/FtbFilterModel.h35
-rw-r--r--launcher/ui/pages/modplatform/ftb/FtbListModel.cpp278
-rw-r--r--launcher/ui/pages/modplatform/ftb/FtbListModel.h61
-rw-r--r--launcher/ui/pages/modplatform/ftb/FtbPage.cpp150
-rw-r--r--launcher/ui/pages/modplatform/ftb/FtbPage.h83
-rw-r--r--launcher/ui/pages/modplatform/ftb/FtbPage.ui79
-rw-r--r--launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp259
-rw-r--r--launcher/ui/pages/modplatform/legacy_ftb/ListModel.h78
-rw-r--r--launcher/ui/pages/modplatform/legacy_ftb/Page.cpp371
-rw-r--r--launcher/ui/pages/modplatform/legacy_ftb/Page.h119
-rw-r--r--launcher/ui/pages/modplatform/legacy_ftb/Page.ui135
-rw-r--r--launcher/ui/pages/modplatform/technic/TechnicData.h42
-rw-r--r--launcher/ui/pages/modplatform/technic/TechnicModel.cpp237
-rw-r--r--launcher/ui/pages/modplatform/technic/TechnicModel.h70
-rw-r--r--launcher/ui/pages/modplatform/technic/TechnicPage.cpp201
-rw-r--r--launcher/ui/pages/modplatform/technic/TechnicPage.h78
-rw-r--r--launcher/ui/pages/modplatform/technic/TechnicPage.ui95
-rw-r--r--launcher/ui/setupwizard/AnalyticsWizardPage.cpp63
-rw-r--r--launcher/ui/setupwizard/AnalyticsWizardPage.h25
-rw-r--r--launcher/ui/setupwizard/BaseWizardPage.h33
-rw-r--r--launcher/ui/setupwizard/JavaWizardPage.cpp98
-rw-r--r--launcher/ui/setupwizard/JavaWizardPage.h29
-rw-r--r--launcher/ui/setupwizard/LanguageWizardPage.cpp49
-rw-r--r--launcher/ui/setupwizard/LanguageWizardPage.h26
-rw-r--r--launcher/ui/setupwizard/SetupWizard.cpp89
-rw-r--r--launcher/ui/setupwizard/SetupWizard.h45
-rw-r--r--launcher/ui/themes/BrightTheme.cpp56
-rw-r--r--launcher/ui/themes/BrightTheme.h19
-rw-r--r--launcher/ui/themes/CustomTheme.cpp244
-rw-r--r--launcher/ui/themes/CustomTheme.h31
-rw-r--r--launcher/ui/themes/DarkTheme.cpp55
-rw-r--r--launcher/ui/themes/DarkTheme.h18
-rw-r--r--launcher/ui/themes/FusionTheme.cpp6
-rw-r--r--launcher/ui/themes/FusionTheme.h11
-rw-r--r--launcher/ui/themes/ITheme.cpp47
-rw-r--r--launcher/ui/themes/ITheme.h27
-rw-r--r--launcher/ui/themes/SystemTheme.cpp83
-rw-r--r--launcher/ui/themes/SystemTheme.h24
-rw-r--r--launcher/ui/widgets/Common.cpp27
-rw-r--r--launcher/ui/widgets/Common.h6
-rw-r--r--launcher/ui/widgets/CustomCommands.cpp49
-rw-r--r--launcher/ui/widgets/CustomCommands.h43
-rw-r--r--launcher/ui/widgets/CustomCommands.ui107
-rw-r--r--launcher/ui/widgets/DropLabel.cpp41
-rw-r--r--launcher/ui/widgets/DropLabel.h20
-rw-r--r--launcher/ui/widgets/FocusLineEdit.cpp25
-rw-r--r--launcher/ui/widgets/FocusLineEdit.h17
-rw-r--r--launcher/ui/widgets/IconLabel.cpp43
-rw-r--r--launcher/ui/widgets/IconLabel.h26
-rw-r--r--launcher/ui/widgets/InstanceCardWidget.ui58
-rw-r--r--launcher/ui/widgets/JavaSettingsWidget.cpp432
-rw-r--r--launcher/ui/widgets/JavaSettingsWidget.h102
-rw-r--r--launcher/ui/widgets/LabeledToolButton.cpp115
-rw-r--r--launcher/ui/widgets/LabeledToolButton.h40
-rw-r--r--launcher/ui/widgets/LanguageSelectionWidget.cpp66
-rw-r--r--launcher/ui/widgets/LanguageSelectionWidget.h41
-rw-r--r--launcher/ui/widgets/LineSeparator.cpp37
-rw-r--r--launcher/ui/widgets/LineSeparator.h18
-rw-r--r--launcher/ui/widgets/LogView.cpp144
-rw-r--r--launcher/ui/widgets/LogView.h36
-rw-r--r--launcher/ui/widgets/MCModInfoFrame.cpp168
-rw-r--r--launcher/ui/widgets/MCModInfoFrame.h52
-rw-r--r--launcher/ui/widgets/MCModInfoFrame.ui92
-rw-r--r--launcher/ui/widgets/ModListView.cpp66
-rw-r--r--launcher/ui/widgets/ModListView.h25
-rw-r--r--launcher/ui/widgets/PageContainer.cpp240
-rw-r--r--launcher/ui/widgets/PageContainer.h89
-rw-r--r--launcher/ui/widgets/PageContainer_p.h123
-rw-r--r--launcher/ui/widgets/ProgressWidget.cpp73
-rw-r--r--launcher/ui/widgets/ProgressWidget.h32
-rw-r--r--launcher/ui/widgets/VersionListView.cpp163
-rw-r--r--launcher/ui/widgets/VersionListView.h56
-rw-r--r--launcher/ui/widgets/VersionSelectWidget.cpp205
-rw-r--r--launcher/ui/widgets/VersionSelectWidget.h81
-rw-r--r--launcher/ui/widgets/WideBar.cpp116
-rw-r--r--launcher/ui/widgets/WideBar.h26
232 files changed, 30872 insertions, 0 deletions
diff --git a/launcher/ui/ColorCache.cpp b/launcher/ui/ColorCache.cpp
new file mode 100644
index 00000000..ef268dd2
--- /dev/null
+++ b/launcher/ui/ColorCache.cpp
@@ -0,0 +1,35 @@
+#include "ColorCache.h"
+
+
+/**
+ * Blend the color with the front color, adapting to the back color
+ */
+QColor ColorCache::blend(QColor color)
+{
+ if (Rainbow::luma(m_front) > Rainbow::luma(m_back))
+ {
+ // for dark color schemes, produce a fitting color first
+ color = Rainbow::tint(m_front, color, 0.5);
+ }
+ // adapt contrast
+ return Rainbow::mix(m_front, color, m_bias);
+}
+
+/**
+ * Blend the color with the back color
+ */
+QColor ColorCache::blendBackground(QColor color)
+{
+ // adapt contrast
+ return Rainbow::mix(m_back, color, m_bias);
+}
+
+void ColorCache::recolorAll()
+{
+ auto iter = m_colors.begin();
+ while(iter != m_colors.end())
+ {
+ iter->front = blend(iter->original);
+ iter->back = blendBackground(iter->original);
+ }
+}
diff --git a/launcher/ui/ColorCache.h b/launcher/ui/ColorCache.h
new file mode 100644
index 00000000..a840664d
--- /dev/null
+++ b/launcher/ui/ColorCache.h
@@ -0,0 +1,119 @@
+#pragma once
+#include <QtGui/QColor>
+#include <rainbow.h>
+#include <MessageLevel.h>
+#include <QMap>
+
+class ColorCache
+{
+public:
+ ColorCache(QColor front, QColor back, qreal bias)
+ {
+ m_front = front;
+ m_back = back;
+ m_bias = bias;
+ };
+
+ void addColor(int key, QColor color)
+ {
+ m_colors[key] = {color, blend(color), blendBackground(color)};
+ }
+
+ void setForeground(QColor front)
+ {
+ if(m_front != front)
+ {
+ m_front = front;
+ recolorAll();
+ }
+ }
+
+ void setBackground(QColor back)
+ {
+ if(m_back != back)
+ {
+ m_back = back;
+ recolorAll();
+ }
+ }
+
+ QColor getFront(int key)
+ {
+ auto iter = m_colors.find(key);
+ if(iter == m_colors.end())
+ {
+ return QColor();
+ }
+ return (*iter).front;
+ }
+
+ QColor getBack(int key)
+ {
+ auto iter = m_colors.find(key);
+ if(iter == m_colors.end())
+ {
+ return QColor();
+ }
+ return (*iter).back;
+ }
+
+ /**
+ * Blend the color with the front color, adapting to the back color
+ */
+ QColor blend(QColor color);
+
+ /**
+ * Blend the color with the back color
+ */
+ QColor blendBackground(QColor color);
+
+protected:
+ void recolorAll();
+
+protected:
+ struct ColorEntry
+ {
+ QColor original;
+ QColor front;
+ QColor back;
+ };
+
+protected:
+ qreal m_bias;
+ QColor m_front;
+ QColor m_back;
+ QMap<int, ColorEntry> m_colors;
+};
+
+class LogColorCache : public ColorCache
+{
+public:
+ LogColorCache(QColor front, QColor back)
+ : ColorCache(front, back, 1.0)
+ {
+ addColor((int)MessageLevel::Launcher, QColor("purple"));
+ addColor((int)MessageLevel::Debug, QColor("green"));
+ addColor((int)MessageLevel::Warning, QColor("orange"));
+ addColor((int)MessageLevel::Error, QColor("red"));
+ addColor((int)MessageLevel::Fatal, QColor("red"));
+ addColor((int)MessageLevel::Message, front);
+ }
+
+ QColor getFront(MessageLevel::Enum level)
+ {
+ if(!m_colors.contains((int) level))
+ {
+ return ColorCache::getFront((int)MessageLevel::Message);
+ }
+ return ColorCache::getFront((int)level);
+ }
+
+ QColor getBack(MessageLevel::Enum level)
+ {
+ if(level == MessageLevel::Fatal)
+ {
+ return QColor(Qt::black);
+ }
+ return QColor(Qt::transparent);
+ }
+};
diff --git a/launcher/ui/GuiUtil.cpp b/launcher/ui/GuiUtil.cpp
new file mode 100644
index 00000000..efb1a4df
--- /dev/null
+++ b/launcher/ui/GuiUtil.cpp
@@ -0,0 +1,136 @@
+#include "GuiUtil.h"
+
+#include <QClipboard>
+#include <QApplication>
+#include <QFileDialog>
+
+#include "ui/dialogs/ProgressDialog.h"
+#include "ui/dialogs/CustomMessageBox.h"
+#include "net/PasteUpload.h"
+
+#include "Application.h"
+#include <settings/SettingsObject.h>
+#include <DesktopServices.h>
+#include <BuildConfig.h>
+
+QString GuiUtil::uploadPaste(const QString &text, QWidget *parentWidget)
+{
+ ProgressDialog dialog(parentWidget);
+ auto APIKeySetting = APPLICATION->settings()->get("PasteEEAPIKey").toString();
+ if(APIKeySetting == "multimc")
+ {
+ APIKeySetting = BuildConfig.PASTE_EE_KEY;
+ }
+ std::unique_ptr<PasteUpload> paste(new PasteUpload(parentWidget, text, APIKeySetting));
+
+ if (!paste->validateText())
+ {
+ CustomMessageBox::selectable(
+ parentWidget, QObject::tr("Upload failed"),
+ QObject::tr("The log file is too big. You'll have to upload it manually."),
+ QMessageBox::Warning)->exec();
+ return QString();
+ }
+
+ dialog.execWithTask(paste.get());
+ if (!paste->wasSuccessful())
+ {
+ CustomMessageBox::selectable(
+ parentWidget,
+ QObject::tr("Upload failed"),
+ paste->failReason(),
+ QMessageBox::Critical
+ )->exec();
+ return QString();
+ }
+ else
+ {
+ const QString link = paste->pasteLink();
+ setClipboardText(link);
+ CustomMessageBox::selectable(
+ parentWidget, QObject::tr("Upload finished"),
+ QObject::tr("The <a href=\"%1\">link to the uploaded log</a> has been placed in your clipboard.").arg(link),
+ QMessageBox::Information)->exec();
+ return link;
+ }
+}
+
+void GuiUtil::setClipboardText(const QString &text)
+{
+ QApplication::clipboard()->setText(text);
+}
+
+static QStringList BrowseForFileInternal(QString context, QString caption, QString filter, QString defaultPath, QWidget *parentWidget, bool single)
+{
+ static QMap<QString, QString> savedPaths;
+
+ QFileDialog w(parentWidget, caption);
+ QSet<QString> locations;
+ auto f = [&](QStandardPaths::StandardLocation l)
+ {
+ QString location = QStandardPaths::writableLocation(l);
+ QFileInfo finfo(location);
+ if (!finfo.exists()) {
+ return;
+ }
+ locations.insert(location);
+ };
+ f(QStandardPaths::DesktopLocation);
+ f(QStandardPaths::DocumentsLocation);
+ f(QStandardPaths::DownloadLocation);
+ f(QStandardPaths::HomeLocation);
+ QList<QUrl> urls;
+ for (auto location : locations)
+ {
+ urls.append(QUrl::fromLocalFile(location));
+ }
+ urls.append(QUrl::fromLocalFile(defaultPath));
+
+ w.setFileMode(single ? QFileDialog::ExistingFile : QFileDialog::ExistingFiles);
+ w.setAcceptMode(QFileDialog::AcceptOpen);
+ w.setNameFilter(filter);
+
+ QString pathToOpen;
+ if(savedPaths.contains(context))
+ {
+ pathToOpen = savedPaths[context];
+ }
+ else
+ {
+ pathToOpen = defaultPath;
+ }
+ if(!pathToOpen.isEmpty())
+ {
+ QFileInfo finfo(pathToOpen);
+ if(finfo.exists() && finfo.isDir())
+ {
+ w.setDirectory(finfo.absoluteFilePath());
+ }
+ }
+
+ w.setSidebarUrls(urls);
+
+ if (w.exec())
+ {
+ savedPaths[context] = w.directory().absolutePath();
+ return w.selectedFiles();
+ }
+ savedPaths[context] = w.directory().absolutePath();
+ return {};
+}
+
+QString GuiUtil::BrowseForFile(QString context, QString caption, QString filter, QString defaultPath, QWidget *parentWidget)
+{
+ auto resultList = BrowseForFileInternal(context, caption, filter, defaultPath, parentWidget, true);
+ if(resultList.size())
+ {
+ return resultList[0];
+ }
+ return QString();
+}
+
+
+QStringList GuiUtil::BrowseForFiles(QString context, QString caption, QString filter, QString defaultPath, QWidget *parentWidget)
+{
+ return BrowseForFileInternal(context, caption, filter, defaultPath, parentWidget, false);
+}
diff --git a/launcher/ui/GuiUtil.h b/launcher/ui/GuiUtil.h
new file mode 100644
index 00000000..5e109383
--- /dev/null
+++ b/launcher/ui/GuiUtil.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include <QWidget>
+
+namespace GuiUtil
+{
+QString uploadPaste(const QString &text, QWidget *parentWidget);
+void setClipboardText(const QString &text);
+QStringList BrowseForFiles(QString context, QString caption, QString filter, QString defaultPath, QWidget *parentWidget);
+QString BrowseForFile(QString context, QString caption, QString filter, QString defaultPath, QWidget *parentWidget);
+}
diff --git a/launcher/ui/InstanceWindow.cpp b/launcher/ui/InstanceWindow.cpp
new file mode 100644
index 00000000..ae765c3c
--- /dev/null
+++ b/launcher/ui/InstanceWindow.cpp
@@ -0,0 +1,237 @@
+/* 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 "InstanceWindow.h"
+#include "Application.h"
+
+#include <QScrollBar>
+#include <QMessageBox>
+#include <QHBoxLayout>
+#include <QPushButton>
+#include <qlayoutitem.h>
+#include <QCloseEvent>
+
+#include "ui/dialogs/CustomMessageBox.h"
+#include "ui/dialogs/ProgressDialog.h"
+#include "ui/widgets/PageContainer.h"
+
+#include "InstancePageProvider.h"
+
+#include "icons/IconList.h"
+
+InstanceWindow::InstanceWindow(InstancePtr instance, QWidget *parent)
+ : QMainWindow(parent), m_instance(instance)
+{
+ setAttribute(Qt::WA_DeleteOnClose);
+
+ auto icon = APPLICATION->icons()->getIcon(m_instance->iconKey());
+ QString windowTitle = tr("Console window for ") + m_instance->name();
+
+ // Set window properties
+ {
+ setWindowIcon(icon);
+ setWindowTitle(windowTitle);
+ }
+
+ // Add page container
+ {
+ auto provider = std::make_shared<InstancePageProvider>(m_instance);
+ m_container = new PageContainer(provider.get(), "console", this);
+ m_container->setParentContainer(this);
+ setCentralWidget(m_container);
+ setContentsMargins(0, 0, 0, 0);
+ }
+
+ // Add custom buttons to the page container layout.
+ {
+ auto horizontalLayout = new QHBoxLayout();
+ horizontalLayout->setObjectName(QStringLiteral("horizontalLayout"));
+ horizontalLayout->setContentsMargins(6, -1, 6, -1);
+
+ auto btnHelp = new QPushButton();
+ btnHelp->setText(tr("Help"));
+ horizontalLayout->addWidget(btnHelp);
+ connect(btnHelp, SIGNAL(clicked(bool)), m_container, SLOT(help()));
+
+ auto spacer = new QSpacerItem(40, 20, QSizePolicy::Expanding, QSizePolicy::Minimum);
+ horizontalLayout->addSpacerItem(spacer);
+
+ m_killButton = new QPushButton();
+ horizontalLayout->addWidget(m_killButton);
+ connect(m_killButton, SIGNAL(clicked(bool)), SLOT(on_btnKillMinecraft_clicked()));
+
+ m_launchOfflineButton = new QPushButton();
+ horizontalLayout->addWidget(m_launchOfflineButton);
+ m_launchOfflineButton->setText(tr("Launch Offline"));
+ updateLaunchButtons();
+ connect(m_launchOfflineButton, SIGNAL(clicked(bool)), SLOT(on_btnLaunchMinecraftOffline_clicked()));
+
+ m_closeButton = new QPushButton();
+ m_closeButton->setText(tr("Close"));
+ horizontalLayout->addWidget(m_closeButton);
+ connect(m_closeButton, SIGNAL(clicked(bool)), SLOT(on_closeButton_clicked()));
+
+ m_container->addButtons(horizontalLayout);
+ }
+
+ // restore window state
+ {
+ auto base64State = APPLICATION->settings()->get("ConsoleWindowState").toByteArray();
+ restoreState(QByteArray::fromBase64(base64State));
+ auto base64Geometry = APPLICATION->settings()->get("ConsoleWindowGeometry").toByteArray();
+ restoreGeometry(QByteArray::fromBase64(base64Geometry));
+ }
+
+ // set up instance and launch process recognition
+ {
+ auto launchTask = m_instance->getLaunchTask();
+ on_InstanceLaunchTask_changed(launchTask);
+ connect(m_instance.get(), &BaseInstance::launchTaskChanged, this, &InstanceWindow::on_InstanceLaunchTask_changed);
+ connect(m_instance.get(), &BaseInstance::runningStatusChanged, this, &InstanceWindow::on_RunningState_changed);
+ }
+
+ // set up instance destruction detection
+ {
+ connect(m_instance.get(), &BaseInstance::statusChanged, this, &InstanceWindow::on_instanceStatusChanged);
+ }
+ show();
+}
+
+void InstanceWindow::on_instanceStatusChanged(BaseInstance::Status, BaseInstance::Status newStatus)
+{
+ if(newStatus == BaseInstance::Status::Gone)
+ {
+ m_doNotSave = true;
+ close();
+ }
+}
+
+void InstanceWindow::updateLaunchButtons()
+{
+ if(m_instance->isRunning())
+ {
+ m_launchOfflineButton->setEnabled(false);
+ m_killButton->setText(tr("Kill"));
+ m_killButton->setObjectName("killButton");
+ m_killButton->setToolTip(tr("Kill the running instance"));
+ }
+ else if(!m_instance->canLaunch())
+ {
+ m_launchOfflineButton->setEnabled(false);
+ m_killButton->setText(tr("Launch"));
+ m_killButton->setObjectName("launchButton");
+ m_killButton->setToolTip(tr("Launch the instance"));
+ m_killButton->setEnabled(false);
+ }
+ else
+ {
+ m_launchOfflineButton->setEnabled(true);
+ m_killButton->setText(tr("Launch"));
+ m_killButton->setObjectName("launchButton");
+ m_killButton->setToolTip(tr("Launch the instance"));
+ }
+ // NOTE: this is a hack to force the button to recalculate its style
+ m_killButton->setStyleSheet("/* */");
+ m_killButton->setStyleSheet(QString());
+}
+
+void InstanceWindow::on_btnLaunchMinecraftOffline_clicked()
+{
+ APPLICATION->launch(m_instance, false, nullptr);
+}
+
+void InstanceWindow::on_InstanceLaunchTask_changed(shared_qobject_ptr<LaunchTask> proc)
+{
+ m_proc = proc;
+}
+
+void InstanceWindow::on_RunningState_changed(bool running)
+{
+ updateLaunchButtons();
+ m_container->refreshContainer();
+ if(running) {
+ selectPage("log");
+ }
+}
+
+void InstanceWindow::on_closeButton_clicked()
+{
+ close();
+}
+
+void InstanceWindow::closeEvent(QCloseEvent *event)
+{
+ bool proceed = true;
+ if(!m_doNotSave)
+ {
+ proceed &= m_container->prepareToClose();
+ }
+
+ if(!proceed)
+ {
+ return;
+ }
+
+ APPLICATION->settings()->set("ConsoleWindowState", saveState().toBase64());
+ APPLICATION->settings()->set("ConsoleWindowGeometry", saveGeometry().toBase64());
+ emit isClosing();
+ event->accept();
+}
+
+bool InstanceWindow::saveAll()
+{
+ return m_container->saveAll();
+}
+
+void InstanceWindow::on_btnKillMinecraft_clicked()
+{
+ if(m_instance->isRunning())
+ {
+ APPLICATION->kill(m_instance);
+ }
+ else
+ {
+ APPLICATION->launch(m_instance, true, nullptr);
+ }
+}
+
+QString InstanceWindow::instanceId()
+{
+ return m_instance->id();
+}
+
+bool InstanceWindow::selectPage(QString pageId)
+{
+ return m_container->selectPage(pageId);
+}
+
+void InstanceWindow::refreshContainer()
+{
+ m_container->refreshContainer();
+}
+
+InstanceWindow::~InstanceWindow()
+{
+}
+
+bool InstanceWindow::requestClose()
+{
+ if(m_container->prepareToClose())
+ {
+ close();
+ return true;
+ }
+ return false;
+}
diff --git a/launcher/ui/InstanceWindow.h b/launcher/ui/InstanceWindow.h
new file mode 100644
index 00000000..1acf684e
--- /dev/null
+++ b/launcher/ui/InstanceWindow.h
@@ -0,0 +1,76 @@
+/* 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 <QMainWindow>
+#include <QSystemTrayIcon>
+
+#include "LaunchController.h"
+#include "launch/LaunchTask.h"
+
+#include "ui/pages/BasePageContainer.h"
+
+#include "QObjectPtr.h"
+
+class QPushButton;
+class PageContainer;
+class InstanceWindow : public QMainWindow, public BasePageContainer
+{
+ Q_OBJECT
+
+public:
+ explicit InstanceWindow(InstancePtr proc, QWidget *parent = 0);
+ virtual ~InstanceWindow();
+
+ bool selectPage(QString pageId) override;
+ void refreshContainer() override;
+
+ QString instanceId();
+
+ // save all settings and changes (prepare for launch)
+ bool saveAll();
+
+ // request closing the window (from a page)
+ bool requestClose() override;
+
+signals:
+ void isClosing();
+
+private
+slots:
+ void on_closeButton_clicked();
+ void on_btnKillMinecraft_clicked();
+ void on_btnLaunchMinecraftOffline_clicked();
+
+ void on_InstanceLaunchTask_changed(shared_qobject_ptr<LaunchTask> proc);
+ void on_RunningState_changed(bool running);
+ void on_instanceStatusChanged(BaseInstance::Status, BaseInstance::Status newStatus);
+
+protected:
+ void closeEvent(QCloseEvent *) override;
+
+private:
+ void updateLaunchButtons();
+
+private:
+ shared_qobject_ptr<LaunchTask> m_proc;
+ InstancePtr m_instance;
+ bool m_doNotSave = false;
+ PageContainer *m_container = nullptr;
+ QPushButton *m_closeButton = nullptr;
+ QPushButton *m_killButton = nullptr;
+ QPushButton *m_launchOfflineButton = nullptr;
+};
diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp
new file mode 100644
index 00000000..95d9ae5d
--- /dev/null
+++ b/launcher/ui/MainWindow.cpp
@@ -0,0 +1,1949 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Authors: Andrew Okin
+ * Peterix
+ * Orochimarufan <orochimarufan.x3@gmail.com>
+ *
+ * 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 "Application.h"
+#include "BuildConfig.h"
+
+#include "MainWindow.h"
+
+#include <QtCore/QVariant>
+#include <QtCore/QUrl>
+#include <QtCore/QDir>
+#include <QtCore/QFileInfo>
+
+#include <QtGui/QKeyEvent>
+
+#include <QtWidgets/QAction>
+#include <QtWidgets/QApplication>
+#include <QtWidgets/QButtonGroup>
+#include <QtWidgets/QHBoxLayout>
+#include <QtWidgets/QHeaderView>
+#include <QtWidgets/QMainWindow>
+#include <QtWidgets/QStatusBar>
+#include <QtWidgets/QToolBar>
+#include <QtWidgets/QWidget>
+#include <QtWidgets/QMenu>
+#include <QtWidgets/QMessageBox>
+#include <QtWidgets/QInputDialog>
+#include <QtWidgets/QLabel>
+#include <QtWidgets/QToolButton>
+#include <QtWidgets/QWidgetAction>
+#include <QtWidgets/QProgressDialog>
+#include <QtWidgets/QShortcut>
+
+#include <BaseInstance.h>
+#include <InstanceList.h>
+#include <MMCZip.h>
+#include <icons/IconList.h>
+#include <java/JavaUtils.h>
+#include <java/JavaInstallList.h>
+#include <launch/LaunchTask.h>
+#include <minecraft/auth/AccountList.h>
+#include <SkinUtils.h>
+#include <BuildConfig.h>
+#include <net/NetJob.h>
+#include <net/Download.h>
+#include <news/NewsChecker.h>
+#include <notifications/NotificationChecker.h>
+#include <tools/BaseProfiler.h>
+#include <updater/DownloadTask.h>
+#include <updater/UpdateChecker.h>
+#include <DesktopServices.h>
+#include "InstanceWindow.h"
+#include "InstancePageProvider.h"
+#include "JavaCommon.h"
+#include "LaunchController.h"
+
+#include "ui/instanceview/InstanceProxyModel.h"
+#include "ui/instanceview/InstanceView.h"
+#include "ui/instanceview/InstanceDelegate.h"
+#include "ui/widgets/LabeledToolButton.h"
+#include "ui/dialogs/NewInstanceDialog.h"
+#include "ui/dialogs/ProgressDialog.h"
+#include "ui/dialogs/AboutDialog.h"
+#include "ui/dialogs/VersionSelectDialog.h"
+#include "ui/dialogs/CustomMessageBox.h"
+#include "ui/dialogs/IconPickerDialog.h"
+#include "ui/dialogs/CopyInstanceDialog.h"
+#include "ui/dialogs/UpdateDialog.h"
+#include "ui/dialogs/EditAccountDialog.h"
+#include "ui/dialogs/NotificationDialog.h"
+#include "ui/dialogs/ExportInstanceDialog.h"
+
+#include "UpdateController.h"
+#include "KonamiCode.h"
+
+#include "InstanceImportTask.h"
+#include "InstanceCopyTask.h"
+
+#include "MMCTime.h"
+
+namespace {
+QString profileInUseFilter(const QString & profile, bool used)
+{
+ if(used)
+ {
+ return QObject::tr("%1 (in use)").arg(profile);
+ }
+ else
+ {
+ return profile;
+ }
+}
+}
+
+// WHY: to hold the pre-translation strings together with the T pointer, so it can be retranslated without a lot of ugly code
+template <typename T>
+class Translated
+{
+public:
+ Translated(){}
+ Translated(QWidget *parent)
+ {
+ m_contained = new T(parent);
+ }
+ void setTooltipId(const char * tooltip)
+ {
+ m_tooltip = tooltip;
+ }
+ void setTextId(const char * text)
+ {
+ m_text = text;
+ }
+ operator T*()
+ {
+ return m_contained;
+ }
+ T * operator->()
+ {
+ return m_contained;
+ }
+ void retranslate()
+ {
+ if(m_text)
+ {
+ QString result;
+ result = QApplication::translate("MainWindow", m_text);
+ if(result.contains("%1")) {
+ result = result.arg(BuildConfig.LAUNCHER_NAME);
+ }
+ m_contained->setText(result);
+ }
+ if(m_tooltip)
+ {
+ QString result;
+ result = QApplication::translate("MainWindow", m_tooltip);
+ if(result.contains("%1")) {
+ result = result.arg(BuildConfig.LAUNCHER_NAME);
+ }
+ m_contained->setToolTip(result);
+ }
+ }
+private:
+ T * m_contained = nullptr;
+ const char * m_text = nullptr;
+ const char * m_tooltip = nullptr;
+};
+using TranslatedAction = Translated<QAction>;
+using TranslatedToolButton = Translated<QToolButton>;
+
+class TranslatedToolbar
+{
+public:
+ TranslatedToolbar(){}
+ TranslatedToolbar(QWidget *parent)
+ {
+ m_contained = new QToolBar(parent);
+ }
+ void setWindowTitleId(const char * title)
+ {
+ m_title = title;
+ }
+ operator QToolBar*()
+ {
+ return m_contained;
+ }
+ QToolBar * operator->()
+ {
+ return m_contained;
+ }
+ void retranslate()
+ {
+ if(m_title)
+ {
+ m_contained->setWindowTitle(QApplication::translate("MainWindow", m_title));
+ }
+ }
+private:
+ QToolBar * m_contained = nullptr;
+ const char * m_title = nullptr;
+};
+
+class MainWindow::Ui
+{
+public:
+ TranslatedAction actionAddInstance;
+ //TranslatedAction actionRefresh;
+ TranslatedAction actionCheckUpdate;
+ TranslatedAction actionSettings;
+ TranslatedAction actionPatreon;
+ TranslatedAction actionMoreNews;
+ TranslatedAction actionManageAccounts;
+ TranslatedAction actionLaunchInstance;
+ TranslatedAction actionRenameInstance;
+ TranslatedAction actionChangeInstGroup;
+ TranslatedAction actionChangeInstIcon;
+ TranslatedAction actionEditInstNotes;
+ TranslatedAction actionEditInstance;
+ TranslatedAction actionWorlds;
+ TranslatedAction actionViewSelectedInstFolder;
+ TranslatedAction actionViewSelectedMCFolder;
+ TranslatedAction actionDeleteInstance;
+ TranslatedAction actionConfig_Folder;
+ TranslatedAction actionCAT;
+ TranslatedAction actionCopyInstance;
+ TranslatedAction actionLaunchInstanceOffline;
+ TranslatedAction actionScreenshots;
+ TranslatedAction actionExportInstance;
+ QVector<TranslatedAction *> all_actions;
+
+ LabeledToolButton *renameButton = nullptr;
+ LabeledToolButton *changeIconButton = nullptr;
+
+ QMenu * foldersMenu = nullptr;
+ TranslatedToolButton foldersMenuButton;
+ TranslatedAction actionViewInstanceFolder;
+ TranslatedAction actionViewCentralModsFolder;
+
+ QMenu * helpMenu = nullptr;
+ TranslatedToolButton helpMenuButton;
+ TranslatedAction actionReportBug;
+ TranslatedAction actionDISCORD;
+ TranslatedAction actionREDDIT;
+ TranslatedAction actionAbout;
+
+ QVector<TranslatedToolButton *> all_toolbuttons;
+
+ QWidget *centralWidget = nullptr;
+ QHBoxLayout *horizontalLayout = nullptr;
+ QStatusBar *statusBar = nullptr;
+
+ TranslatedToolbar mainToolBar;
+ TranslatedToolbar instanceToolBar;
+ TranslatedToolbar newsToolBar;
+ QVector<TranslatedToolbar *> all_toolbars;
+ bool m_kill = false;
+
+ void updateLaunchAction()
+ {
+ if(m_kill)
+ {
+ actionLaunchInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Kill"));
+ actionLaunchInstance.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Kill the running instance"));
+ }
+ else
+ {
+ actionLaunchInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Launch"));
+ actionLaunchInstance.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Launch the selected instance."));
+ }
+ actionLaunchInstance.retranslate();
+ }
+ void setLaunchAction(bool kill)
+ {
+ m_kill = kill;
+ updateLaunchAction();
+ }
+
+ void createMainToolbar(QMainWindow *MainWindow)
+ {
+ mainToolBar = TranslatedToolbar(MainWindow);
+ mainToolBar->setObjectName(QStringLiteral("mainToolBar"));
+ mainToolBar->setMovable(false);
+ mainToolBar->setAllowedAreas(Qt::TopToolBarArea);
+ mainToolBar->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
+ mainToolBar->setFloatable(false);
+ mainToolBar.setWindowTitleId(QT_TRANSLATE_NOOP("MainWindow", "Main Toolbar"));
+
+ actionAddInstance = TranslatedAction(MainWindow);
+ actionAddInstance->setObjectName(QStringLiteral("actionAddInstance"));
+ actionAddInstance->setIcon(APPLICATION->getThemedIcon("new"));
+ actionAddInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Add Instance"));
+ actionAddInstance.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Add a new instance."));
+ all_actions.append(&actionAddInstance);
+ mainToolBar->addAction(actionAddInstance);
+
+ mainToolBar->addSeparator();
+
+ foldersMenu = new QMenu(MainWindow);
+ foldersMenu->setToolTipsVisible(true);
+
+ actionViewInstanceFolder = TranslatedAction(MainWindow);
+ actionViewInstanceFolder->setObjectName(QStringLiteral("actionViewInstanceFolder"));
+ actionViewInstanceFolder->setIcon(APPLICATION->getThemedIcon("viewfolder"));
+ actionViewInstanceFolder.setTextId(QT_TRANSLATE_NOOP("MainWindow", "View Instance Folder"));
+ actionViewInstanceFolder.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open the instance folder in a file browser."));
+ all_actions.append(&actionViewInstanceFolder);
+ foldersMenu->addAction(actionViewInstanceFolder);
+
+ actionViewCentralModsFolder = TranslatedAction(MainWindow);
+ actionViewCentralModsFolder->setObjectName(QStringLiteral("actionViewCentralModsFolder"));
+ actionViewCentralModsFolder->setIcon(APPLICATION->getThemedIcon("centralmods"));
+ actionViewCentralModsFolder.setTextId(QT_TRANSLATE_NOOP("MainWindow", "View Central Mods Folder"));
+ actionViewCentralModsFolder.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open the central mods folder in a file browser."));
+ all_actions.append(&actionViewCentralModsFolder);
+ foldersMenu->addAction(actionViewCentralModsFolder);
+
+ foldersMenuButton = TranslatedToolButton(MainWindow);
+ foldersMenuButton.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Folders"));
+ foldersMenuButton.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open one of the folders shared between instances."));
+ foldersMenuButton->setMenu(foldersMenu);
+ foldersMenuButton->setPopupMode(QToolButton::InstantPopup);
+ foldersMenuButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
+ foldersMenuButton->setIcon(APPLICATION->getThemedIcon("viewfolder"));
+ foldersMenuButton->setFocusPolicy(Qt::NoFocus);
+ all_toolbuttons.append(&foldersMenuButton);
+ QWidgetAction* foldersButtonAction = new QWidgetAction(MainWindow);
+ foldersButtonAction->setDefaultWidget(foldersMenuButton);
+ mainToolBar->addAction(foldersButtonAction);
+
+ actionSettings = TranslatedAction(MainWindow);
+ actionSettings->setObjectName(QStringLiteral("actionSettings"));
+ actionSettings->setIcon(APPLICATION->getThemedIcon("settings"));
+ actionSettings->setMenuRole(QAction::PreferencesRole);
+ actionSettings.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Settings"));
+ actionSettings.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Change settings."));
+ all_actions.append(&actionSettings);
+ mainToolBar->addAction(actionSettings);
+
+ helpMenu = new QMenu(MainWindow);
+ helpMenu->setToolTipsVisible(true);
+
+ if (!BuildConfig.BUG_TRACKER_URL.isEmpty()) {
+ actionReportBug = TranslatedAction(MainWindow);
+ actionReportBug->setObjectName(QStringLiteral("actionReportBug"));
+ actionReportBug->setIcon(APPLICATION->getThemedIcon("bug"));
+ actionReportBug.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Report a Bug"));
+ actionReportBug.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open the bug tracker to report a bug with %1."));
+ all_actions.append(&actionReportBug);
+ helpMenu->addAction(actionReportBug);
+ }
+
+ if (!BuildConfig.DISCORD_URL.isEmpty()) {
+ actionDISCORD = TranslatedAction(MainWindow);
+ actionDISCORD->setObjectName(QStringLiteral("actionDISCORD"));
+ actionDISCORD->setIcon(APPLICATION->getThemedIcon("discord"));
+ actionDISCORD.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Discord"));
+ actionDISCORD.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open %1 discord voice chat."));
+ all_actions.append(&actionDISCORD);
+ helpMenu->addAction(actionDISCORD);
+ }
+
+ if (!BuildConfig.SUBREDDIT_URL.isEmpty()) {
+ actionREDDIT = TranslatedAction(MainWindow);
+ actionREDDIT->setObjectName(QStringLiteral("actionREDDIT"));
+ actionREDDIT->setIcon(APPLICATION->getThemedIcon("reddit-alien"));
+ actionREDDIT.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Reddit"));
+ actionREDDIT.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open %1 subreddit."));
+ all_actions.append(&actionREDDIT);
+ helpMenu->addAction(actionREDDIT);
+ }
+
+ actionAbout = TranslatedAction(MainWindow);
+ actionAbout->setObjectName(QStringLiteral("actionAbout"));
+ actionAbout->setIcon(APPLICATION->getThemedIcon("about"));
+ actionAbout->setMenuRole(QAction::AboutRole);
+ actionAbout.setTextId(QT_TRANSLATE_NOOP("MainWindow", "About %1"));
+ actionAbout.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "View information about %1."));
+ all_actions.append(&actionAbout);
+ helpMenu->addAction(actionAbout);
+
+ helpMenuButton = TranslatedToolButton(MainWindow);
+ helpMenuButton.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Help"));
+ helpMenuButton.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Get help with %1 or Minecraft."));
+ helpMenuButton->setMenu(helpMenu);
+ helpMenuButton->setPopupMode(QToolButton::InstantPopup);
+ helpMenuButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
+ helpMenuButton->setIcon(APPLICATION->getThemedIcon("help"));
+ helpMenuButton->setFocusPolicy(Qt::NoFocus);
+ all_toolbuttons.append(&helpMenuButton);
+ QWidgetAction* helpButtonAction = new QWidgetAction(MainWindow);
+ helpButtonAction->setDefaultWidget(helpMenuButton);
+ mainToolBar->addAction(helpButtonAction);
+
+ if(BuildConfig.UPDATER_ENABLED)
+ {
+ actionCheckUpdate = TranslatedAction(MainWindow);
+ actionCheckUpdate->setObjectName(QStringLiteral("actionCheckUpdate"));
+ actionCheckUpdate->setIcon(APPLICATION->getThemedIcon("checkupdate"));
+ actionCheckUpdate.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Update"));
+ actionCheckUpdate.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Check for new updates for %1."));
+ all_actions.append(&actionCheckUpdate);
+ mainToolBar->addAction(actionCheckUpdate);
+ }
+
+ mainToolBar->addSeparator();
+
+ actionPatreon = TranslatedAction(MainWindow);
+ actionPatreon->setObjectName(QStringLiteral("actionPatreon"));
+ actionPatreon->setIcon(APPLICATION->getThemedIcon("patreon"));
+ actionPatreon.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Support %1"));
+ actionPatreon.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open the %1 Patreon page."));
+ all_actions.append(&actionPatreon);
+ mainToolBar->addAction(actionPatreon);
+
+ actionCAT = TranslatedAction(MainWindow);
+ actionCAT->setObjectName(QStringLiteral("actionCAT"));
+ actionCAT->setCheckable(true);
+ actionCAT->setIcon(APPLICATION->getThemedIcon("cat"));
+ actionCAT.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Meow"));
+ actionCAT.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "It's a fluffy kitty :3"));
+ actionCAT->setPriority(QAction::LowPriority);
+ all_actions.append(&actionCAT);
+ mainToolBar->addAction(actionCAT);
+
+ // profile menu and its actions
+ actionManageAccounts = TranslatedAction(MainWindow);
+ actionManageAccounts->setObjectName(QStringLiteral("actionManageAccounts"));
+ actionManageAccounts.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Manage Accounts"));
+ // FIXME: no tooltip!
+ actionManageAccounts->setCheckable(false);
+ actionManageAccounts->setIcon(APPLICATION->getThemedIcon("accounts"));
+ all_actions.append(&actionManageAccounts);
+
+ all_toolbars.append(&mainToolBar);
+ MainWindow->addToolBar(Qt::TopToolBarArea, mainToolBar);
+ }
+
+ void createStatusBar(QMainWindow *MainWindow)
+ {
+ statusBar = new QStatusBar(MainWindow);
+ statusBar->setObjectName(QStringLiteral("statusBar"));
+ MainWindow->setStatusBar(statusBar);
+ }
+
+ void createNewsToolbar(QMainWindow *MainWindow)
+ {
+ newsToolBar = TranslatedToolbar(MainWindow);
+ newsToolBar->setObjectName(QStringLiteral("newsToolBar"));
+ newsToolBar->setMovable(false);
+ newsToolBar->setAllowedAreas(Qt::BottomToolBarArea);
+ newsToolBar->setIconSize(QSize(16, 16));
+ newsToolBar->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
+ newsToolBar->setFloatable(false);
+ newsToolBar->setWindowTitle(QT_TRANSLATE_NOOP("MainWindow", "News Toolbar"));
+
+ actionMoreNews = TranslatedAction(MainWindow);
+ actionMoreNews->setObjectName(QStringLiteral("actionMoreNews"));
+ actionMoreNews->setIcon(APPLICATION->getThemedIcon("news"));
+ actionMoreNews.setTextId(QT_TRANSLATE_NOOP("MainWindow", "More news..."));
+ actionMoreNews.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open the development blog to read more news about %1."));
+ all_actions.append(&actionMoreNews);
+ newsToolBar->addAction(actionMoreNews);
+
+ all_toolbars.append(&newsToolBar);
+ MainWindow->addToolBar(Qt::BottomToolBarArea, newsToolBar);
+ }
+
+ void createInstanceToolbar(QMainWindow *MainWindow)
+ {
+ instanceToolBar = TranslatedToolbar(MainWindow);
+ instanceToolBar->setObjectName(QStringLiteral("instanceToolBar"));
+ // disabled until we have an instance selected
+ instanceToolBar->setEnabled(false);
+ instanceToolBar->setAllowedAreas(Qt::LeftToolBarArea | Qt::RightToolBarArea);
+ instanceToolBar->setToolButtonStyle(Qt::ToolButtonTextOnly);
+ instanceToolBar->setFloatable(false);
+ instanceToolBar->setWindowTitle(QT_TRANSLATE_NOOP("MainWindow", "Instance Toolbar"));
+
+ // NOTE: not added to toolbar, but used for instance context menu (right click)
+ actionChangeInstIcon = TranslatedAction(MainWindow);
+ actionChangeInstIcon->setObjectName(QStringLiteral("actionChangeInstIcon"));
+ actionChangeInstIcon->setIcon(QIcon(":/icons/instances/grass"));
+ actionChangeInstIcon->setIconVisibleInMenu(true);
+ actionChangeInstIcon.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Change Icon"));
+ actionChangeInstIcon.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Change the selected instance's icon."));
+ all_actions.append(&actionChangeInstIcon);
+
+ changeIconButton = new LabeledToolButton(MainWindow);
+ changeIconButton->setObjectName(QStringLiteral("changeIconButton"));
+ changeIconButton->setIcon(APPLICATION->getThemedIcon("news"));
+ changeIconButton->setToolTip(actionChangeInstIcon->toolTip());
+ changeIconButton->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
+ instanceToolBar->addWidget(changeIconButton);
+
+ // NOTE: not added to toolbar, but used for instance context menu (right click)
+ actionRenameInstance = TranslatedAction(MainWindow);
+ actionRenameInstance->setObjectName(QStringLiteral("actionRenameInstance"));
+ actionRenameInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Rename"));
+ actionRenameInstance.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Rename the selected instance."));
+ all_actions.append(&actionRenameInstance);
+
+ // the rename label is inside the rename tool button
+ renameButton = new LabeledToolButton(MainWindow);
+ renameButton->setObjectName(QStringLiteral("renameButton"));
+ renameButton->setToolTip(actionRenameInstance->toolTip());
+ renameButton->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
+ instanceToolBar->addWidget(renameButton);
+
+ instanceToolBar->addSeparator();
+
+ actionLaunchInstance = TranslatedAction(MainWindow);
+ actionLaunchInstance->setObjectName(QStringLiteral("actionLaunchInstance"));
+ all_actions.append(&actionLaunchInstance);
+ instanceToolBar->addAction(actionLaunchInstance);
+
+ actionLaunchInstanceOffline = TranslatedAction(MainWindow);
+ actionLaunchInstanceOffline->setObjectName(QStringLiteral("actionLaunchInstanceOffline"));
+ actionLaunchInstanceOffline.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Launch Offline"));
+ actionLaunchInstanceOffline.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Launch the selected instance in offline mode."));
+ all_actions.append(&actionLaunchInstanceOffline);
+ instanceToolBar->addAction(actionLaunchInstanceOffline);
+
+ instanceToolBar->addSeparator();
+
+ actionEditInstance = TranslatedAction(MainWindow);
+ actionEditInstance->setObjectName(QStringLiteral("actionEditInstance"));
+ actionEditInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Edit Instance"));
+ actionEditInstance.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Change the instance settings, mods and versions."));
+ all_actions.append(&actionEditInstance);
+ instanceToolBar->addAction(actionEditInstance);
+
+ actionEditInstNotes = TranslatedAction(MainWindow);
+ actionEditInstNotes->setObjectName(QStringLiteral("actionEditInstNotes"));
+ actionEditInstNotes.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Edit Notes"));
+ actionEditInstNotes.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Edit the notes for the selected instance."));
+ all_actions.append(&actionEditInstNotes);
+ instanceToolBar->addAction(actionEditInstNotes);
+
+ actionWorlds = TranslatedAction(MainWindow);
+ actionWorlds->setObjectName(QStringLiteral("actionWorlds"));
+ actionWorlds.setTextId(QT_TRANSLATE_NOOP("MainWindow", "View Worlds"));
+ actionWorlds.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "View the worlds of this instance."));
+ all_actions.append(&actionWorlds);
+ instanceToolBar->addAction(actionWorlds);
+
+ actionScreenshots = TranslatedAction(MainWindow);
+ actionScreenshots->setObjectName(QStringLiteral("actionScreenshots"));
+ actionScreenshots.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Manage Screenshots"));
+ actionScreenshots.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "View and upload screenshots for this instance."));
+ all_actions.append(&actionScreenshots);
+ instanceToolBar->addAction(actionScreenshots);
+
+ actionChangeInstGroup = TranslatedAction(MainWindow);
+ actionChangeInstGroup->setObjectName(QStringLiteral("actionChangeInstGroup"));
+ actionChangeInstGroup.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Change Group"));
+ actionChangeInstGroup.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Change the selected instance's group."));
+ all_actions.append(&actionChangeInstGroup);
+ instanceToolBar->addAction(actionChangeInstGroup);
+
+ instanceToolBar->addSeparator();
+
+ actionViewSelectedMCFolder = TranslatedAction(MainWindow);
+ actionViewSelectedMCFolder->setObjectName(QStringLiteral("actionViewSelectedMCFolder"));
+ actionViewSelectedMCFolder.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Minecraft Folder"));
+ actionViewSelectedMCFolder.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open the selected instance's minecraft folder in a file browser."));
+ all_actions.append(&actionViewSelectedMCFolder);
+ instanceToolBar->addAction(actionViewSelectedMCFolder);
+
+ actionConfig_Folder = TranslatedAction(MainWindow);
+ actionConfig_Folder->setObjectName(QStringLiteral("actionConfig_Folder"));
+ actionConfig_Folder.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Config Folder"));
+ actionConfig_Folder.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open the instance's config folder."));
+ all_actions.append(&actionConfig_Folder);
+ instanceToolBar->addAction(actionConfig_Folder);
+
+ actionViewSelectedInstFolder = TranslatedAction(MainWindow);
+ actionViewSelectedInstFolder->setObjectName(QStringLiteral("actionViewSelectedInstFolder"));
+ actionViewSelectedInstFolder.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Instance Folder"));
+ actionViewSelectedInstFolder.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open the selected instance's root folder in a file browser."));
+ all_actions.append(&actionViewSelectedInstFolder);
+ instanceToolBar->addAction(actionViewSelectedInstFolder);
+
+ instanceToolBar->addSeparator();
+
+ actionExportInstance = TranslatedAction(MainWindow);
+ actionExportInstance->setObjectName(QStringLiteral("actionExportInstance"));
+ actionExportInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Export Instance"));
+ // FIXME: missing tooltip
+ all_actions.append(&actionExportInstance);
+ instanceToolBar->addAction(actionExportInstance);
+
+ actionDeleteInstance = TranslatedAction(MainWindow);
+ actionDeleteInstance->setObjectName(QStringLiteral("actionDeleteInstance"));
+ actionDeleteInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Delete"));
+ actionDeleteInstance.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Delete the selected instance."));
+ all_actions.append(&actionDeleteInstance);
+ instanceToolBar->addAction(actionDeleteInstance);
+
+ actionCopyInstance = TranslatedAction(MainWindow);
+ actionCopyInstance->setObjectName(QStringLiteral("actionCopyInstance"));
+ actionCopyInstance->setIcon(APPLICATION->getThemedIcon("copy"));
+ actionCopyInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Copy Instance"));
+ actionCopyInstance.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Copy the selected instance."));
+ all_actions.append(&actionCopyInstance);
+ instanceToolBar->addAction(actionCopyInstance);
+
+ all_toolbars.append(&instanceToolBar);
+ MainWindow->addToolBar(Qt::RightToolBarArea, instanceToolBar);
+ }
+
+ void setupUi(QMainWindow *MainWindow)
+ {
+ if (MainWindow->objectName().isEmpty())
+ {
+ MainWindow->setObjectName(QStringLiteral("MainWindow"));
+ }
+ MainWindow->resize(800, 600);
+ MainWindow->setWindowIcon(APPLICATION->getThemedIcon("logo"));
+ MainWindow->setWindowTitle(BuildConfig.LAUNCHER_DISPLAYNAME);
+#ifndef QT_NO_ACCESSIBILITY
+ MainWindow->setAccessibleName(BuildConfig.LAUNCHER_NAME);
+#endif
+
+ createMainToolbar(MainWindow);
+
+ centralWidget = new QWidget(MainWindow);
+ centralWidget->setObjectName(QStringLiteral("centralWidget"));
+ horizontalLayout = new QHBoxLayout(centralWidget);
+ horizontalLayout->setSpacing(0);
+ horizontalLayout->setObjectName(QStringLiteral("horizontalLayout"));
+ horizontalLayout->setSizeConstraint(QLayout::SetDefaultConstraint);
+ horizontalLayout->setContentsMargins(0, 0, 0, 0);
+ MainWindow->setCentralWidget(centralWidget);
+
+ createStatusBar(MainWindow);
+ createNewsToolbar(MainWindow);
+ createInstanceToolbar(MainWindow);
+
+ retranslateUi(MainWindow);
+
+ QMetaObject::connectSlotsByName(MainWindow);
+ } // setupUi
+
+ void retranslateUi(QMainWindow *MainWindow)
+ {
+ QString winTitle = tr("%1 - Version %2", "Launcher - Version X").arg(BuildConfig.LAUNCHER_DISPLAYNAME, BuildConfig.printableVersionString());
+ if (!BuildConfig.BUILD_PLATFORM.isEmpty())
+ {
+ winTitle += tr(" on %1", "on platform, as in operating system").arg(BuildConfig.BUILD_PLATFORM);
+ }
+ MainWindow->setWindowTitle(winTitle);
+ // all the actions
+ for(auto * item: all_actions)
+ {
+ item->retranslate();
+ }
+ for(auto * item: all_toolbars)
+ {
+ item->retranslate();
+ }
+ for(auto * item: all_toolbuttons)
+ {
+ item->retranslate();
+ }
+ // submenu buttons
+ foldersMenuButton->setText(tr("Folders"));
+ helpMenuButton->setText(tr("Help"));
+ } // retranslateUi
+};
+
+MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new MainWindow::Ui)
+{
+ ui->setupUi(this);
+
+ // OSX magic.
+ setUnifiedTitleAndToolBarOnMac(true);
+
+ // Global shortcuts
+ {
+ // FIXME: This is kinda weird. and bad. We need some kind of managed shutdown.
+ auto q = new QShortcut(QKeySequence::Quit, this);
+ connect(q, SIGNAL(activated()), qApp, SLOT(quit()));
+ }
+
+ // Konami Code
+ {
+ secretEventFilter = new KonamiCode(this);
+ connect(secretEventFilter, &KonamiCode::triggered, this, &MainWindow::konamiTriggered);
+ }
+
+ // Add the news label to the news toolbar.
+ {
+ m_newsChecker.reset(new NewsChecker(APPLICATION->network(), BuildConfig.NEWS_RSS_URL));
+ newsLabel = new QToolButton();
+ newsLabel->setIcon(APPLICATION->getThemedIcon("news"));
+ newsLabel->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
+ newsLabel->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
+ newsLabel->setFocusPolicy(Qt::NoFocus);
+ ui->newsToolBar->insertWidget(ui->actionMoreNews, newsLabel);
+ QObject::connect(newsLabel, &QAbstractButton::clicked, this, &MainWindow::newsButtonClicked);
+ QObject::connect(m_newsChecker.get(), &NewsChecker::newsLoaded, this, &MainWindow::updateNewsLabel);
+ updateNewsLabel();
+ }
+
+ // Create the instance list widget
+ {
+ view = new InstanceView(ui->centralWidget);
+
+ view->setSelectionMode(QAbstractItemView::SingleSelection);
+ // FIXME: leaks ListViewDelegate
+ view->setItemDelegate(new ListViewDelegate(this));
+ view->setFrameShape(QFrame::NoFrame);
+ // do not show ugly blue border on the mac
+ view->setAttribute(Qt::WA_MacShowFocusRect, false);
+
+ view->installEventFilter(this);
+ view->setContextMenuPolicy(Qt::CustomContextMenu);
+ connect(view, &QWidget::customContextMenuRequested, this, &MainWindow::showInstanceContextMenu);
+ connect(view, &InstanceView::droppedURLs, this, &MainWindow::droppedURLs, Qt::QueuedConnection);
+
+ proxymodel = new InstanceProxyModel(this);
+ proxymodel->setSourceModel(APPLICATION->instances().get());
+ proxymodel->sort(0);
+ connect(proxymodel, &InstanceProxyModel::dataChanged, this, &MainWindow::instanceDataChanged);
+
+ view->setModel(proxymodel);
+ view->setSourceOfGroupCollapseStatus([](const QString & groupName)->bool {
+ return APPLICATION->instances()->isGroupCollapsed(groupName);
+ });
+ connect(view, &InstanceView::groupStateChanged, APPLICATION->instances().get(), &InstanceList::on_GroupStateChanged);
+ ui->horizontalLayout->addWidget(view);
+ }
+ // The cat background
+ {
+ bool cat_enable = APPLICATION->settings()->get("TheCat").toBool();
+ ui->actionCAT->setChecked(cat_enable);
+ // NOTE: calling the operator like that is an ugly hack to appease ancient gcc...
+ connect(ui->actionCAT.operator->(), SIGNAL(toggled(bool)), SLOT(onCatToggled(bool)));
+ setCatBackground(cat_enable);
+ }
+ // start instance when double-clicked
+ connect(view, &InstanceView::activated, this, &MainWindow::instanceActivated);
+
+ // track the selection -- update the instance toolbar
+ connect(view->selectionModel(), &QItemSelectionModel::currentChanged, this, &MainWindow::instanceChanged);
+
+ // track icon changes and update the toolbar!
+ connect(APPLICATION->icons().get(), &IconList::iconUpdated, this, &MainWindow::iconUpdated);
+
+ // model reset -> selection is invalid. All the instance pointers are wrong.
+ connect(APPLICATION->instances().get(), &InstanceList::dataIsInvalid, this, &MainWindow::selectionBad);
+
+ // handle newly added instances
+ connect(APPLICATION->instances().get(), &InstanceList::instanceSelectRequest, this, &MainWindow::instanceSelectRequest);
+
+ // When the global settings page closes, we want to know about it and update our state
+ connect(APPLICATION, &Application::globalSettingsClosed, this, &MainWindow::globalSettingsClosed);
+
+ m_statusLeft = new QLabel(tr("No instance selected"), this);
+ m_statusCenter = new QLabel(tr("Total playtime: 0s"), this);
+ statusBar()->addPermanentWidget(m_statusLeft, 1);
+ statusBar()->addPermanentWidget(m_statusCenter, 0);
+
+ // Add "manage accounts" button, right align
+ QWidget *spacer = new QWidget();
+ spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
+ ui->mainToolBar->addWidget(spacer);
+
+ accountMenu = new QMenu(this);
+
+ repopulateAccountsMenu();
+
+ accountMenuButton = new QToolButton(this);
+ accountMenuButton->setMenu(accountMenu);
+ accountMenuButton->setPopupMode(QToolButton::InstantPopup);
+ accountMenuButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
+ accountMenuButton->setIcon(APPLICATION->getThemedIcon("noaccount"));
+
+ QWidgetAction *accountMenuButtonAction = new QWidgetAction(this);
+ accountMenuButtonAction->setDefaultWidget(accountMenuButton);
+
+ ui->mainToolBar->addAction(accountMenuButtonAction);
+
+ // Update the menu when the active account changes.
+ // Shouldn't have to use lambdas here like this, but if I don't, the compiler throws a fit.
+ // Template hell sucks...
+ connect(
+ APPLICATION->accounts().get(),
+ &AccountList::defaultAccountChanged,
+ [this] {
+ defaultAccountChanged();
+ }
+ );
+ connect(
+ APPLICATION->accounts().get(),
+ &AccountList::listChanged,
+ [this]
+ {
+ repopulateAccountsMenu();
+ }
+ );
+
+ // Show initial account
+ defaultAccountChanged();
+
+ // TODO: refresh accounts here?
+ // auto accounts = APPLICATION->accounts();
+
+ // load the news
+ {
+ m_newsChecker->reloadNews();
+ updateNewsLabel();
+ }
+
+
+ if(BuildConfig.UPDATER_ENABLED)
+ {
+ bool updatesAllowed = APPLICATION->updatesAreAllowed();
+ updatesAllowedChanged(updatesAllowed);
+
+ // NOTE: calling the operator like that is an ugly hack to appease ancient gcc...
+ connect(ui->actionCheckUpdate.operator->(), &QAction::triggered, this, &MainWindow::checkForUpdates);
+
+ // set up the updater object.
+ auto updater = APPLICATION->updateChecker();
+ connect(updater.get(), &UpdateChecker::updateAvailable, this, &MainWindow::updateAvailable);
+ connect(updater.get(), &UpdateChecker::noUpdateFound, this, &MainWindow::updateNotAvailable);
+ // if automatic update checks are allowed, start one.
+ if (APPLICATION->settings()->get("AutoUpdate").toBool() && updatesAllowed)
+ {
+ updater->checkForUpdate(APPLICATION->settings()->get("UpdateChannel").toString(), false);
+ }
+ }
+
+ {
+ auto checker = new NotificationChecker();
+ checker->setNotificationsUrl(QUrl(BuildConfig.NOTIFICATION_URL));
+ checker->setApplicationChannel(BuildConfig.VERSION_CHANNEL);
+ checker->setApplicationPlatform(BuildConfig.BUILD_PLATFORM);
+ checker->setApplicationFullVersion(BuildConfig.FULL_VERSION_STR);
+ m_notificationChecker.reset(checker);
+ connect(m_notificationChecker.get(), &NotificationChecker::notificationCheckFinished, this, &MainWindow::notificationsChanged);
+ checker->checkForNotifications();
+ }
+
+ setSelectedInstanceById(APPLICATION->settings()->get("SelectedInstance").toString());
+
+ // removing this looks stupid
+ view->setFocus();
+
+ retranslateUi();
+}
+
+void MainWindow::retranslateUi()
+{
+ auto accounts = APPLICATION->accounts();
+ MinecraftAccountPtr defaultAccount = accounts->defaultAccount();
+ if(defaultAccount) {
+ auto profileLabel = profileInUseFilter(defaultAccount->profileName(), defaultAccount->isInUse());
+ accountMenuButton->setText(profileLabel);
+ }
+ else {
+ accountMenuButton->setText(tr("Profiles"));
+ }
+
+ if (m_selectedInstance) {
+ m_statusLeft->setText(m_selectedInstance->getStatusbarDescription());
+ } else {
+ m_statusLeft->setText(tr("No instance selected"));
+ }
+
+ ui->retranslateUi(this);
+}
+
+MainWindow::~MainWindow()
+{
+}
+
+QMenu * MainWindow::createPopupMenu()
+{
+ QMenu* filteredMenu = QMainWindow::createPopupMenu();
+ filteredMenu->removeAction( ui->mainToolBar->toggleViewAction() );
+ return filteredMenu;
+}
+
+void MainWindow::konamiTriggered()
+{
+ qDebug() << "Super Secret Mode ACTIVATED!";
+}
+
+void MainWindow::showInstanceContextMenu(const QPoint &pos)
+{
+ QList<QAction *> actions;
+
+ QAction *actionSep = new QAction("", this);
+ actionSep->setSeparator(true);
+
+ bool onInstance = view->indexAt(pos).isValid();
+ if (onInstance)
+ {
+ actions = ui->instanceToolBar->actions();
+
+ // replace the change icon widget with an actual action
+ actions.replace(0, ui->actionChangeInstIcon);
+
+ // replace the rename widget with an actual action
+ actions.replace(1, ui->actionRenameInstance);
+
+ // add header
+ actions.prepend(actionSep);
+ QAction *actionVoid = new QAction(m_selectedInstance->name(), this);
+ actionVoid->setEnabled(false);
+ actions.prepend(actionVoid);
+ }
+ else
+ {
+ auto group = view->groupNameAt(pos);
+
+ QAction *actionVoid = new QAction(BuildConfig.LAUNCHER_NAME, this);
+ actionVoid->setEnabled(false);
+
+ QAction *actionCreateInstance = new QAction(tr("Create instance"), this);
+ actionCreateInstance->setToolTip(ui->actionAddInstance->toolTip());
+ if(!group.isNull())
+ {
+ QVariantMap data;
+ data["group"] = group;
+ actionCreateInstance->setData(data);
+ }
+
+ connect(actionCreateInstance, SIGNAL(triggered(bool)), SLOT(on_actionAddInstance_triggered()));
+
+ actions.prepend(actionSep);
+ actions.prepend(actionVoid);
+ actions.append(actionCreateInstance);
+ if(!group.isNull())
+ {
+ QAction *actionDeleteGroup = new QAction(tr("Delete group '%1'").arg(group), this);
+ QVariantMap data;
+ data["group"] = group;
+ actionDeleteGroup->setData(data);
+ connect(actionDeleteGroup, SIGNAL(triggered(bool)), SLOT(deleteGroup()));
+ actions.append(actionDeleteGroup);
+ }
+ }
+ QMenu myMenu;
+ myMenu.addActions(actions);
+ /*
+ if (onInstance)
+ myMenu.setEnabled(m_selectedInstance->canLaunch());
+ */
+ myMenu.exec(view->mapToGlobal(pos));
+}
+
+void MainWindow::updateToolsMenu()
+{
+ QToolButton *launchButton = dynamic_cast<QToolButton*>(ui->instanceToolBar->widgetForAction(ui->actionLaunchInstance));
+ QToolButton *launchOfflineButton = dynamic_cast<QToolButton*>(ui->instanceToolBar->widgetForAction(ui->actionLaunchInstanceOffline));
+
+ if(!m_selectedInstance || m_selectedInstance->isRunning())
+ {
+ ui->actionLaunchInstance->setMenu(nullptr);
+ ui->actionLaunchInstanceOffline->setMenu(nullptr);
+ launchButton->setPopupMode(QToolButton::InstantPopup);
+ launchOfflineButton->setPopupMode(QToolButton::InstantPopup);
+ return;
+ }
+
+ QMenu *launchMenu = ui->actionLaunchInstance->menu();
+ QMenu *launchOfflineMenu = ui->actionLaunchInstanceOffline->menu();
+ launchButton->setPopupMode(QToolButton::MenuButtonPopup);
+ launchOfflineButton->setPopupMode(QToolButton::MenuButtonPopup);
+ if (launchMenu)
+ {
+ launchMenu->clear();
+ }
+ else
+ {
+ launchMenu = new QMenu(this);
+ }
+ if (launchOfflineMenu) {
+ launchOfflineMenu->clear();
+ }
+ else
+ {
+ launchOfflineMenu = new QMenu(this);
+ }
+
+ QAction *normalLaunch = launchMenu->addAction(tr("Launch"));
+ QAction *normalLaunchOffline = launchOfflineMenu->addAction(tr("Launch Offline"));
+ connect(normalLaunch, &QAction::triggered, [this]()
+ {
+ APPLICATION->launch(m_selectedInstance, true);
+ });
+ connect(normalLaunchOffline, &QAction::triggered, [this]()
+ {
+ APPLICATION->launch(m_selectedInstance, false);
+ });
+ QString profilersTitle = tr("Profilers");
+ launchMenu->addSeparator()->setText(profilersTitle);
+ launchOfflineMenu->addSeparator()->setText(profilersTitle);
+ for (auto profiler : APPLICATION->profilers().values())
+ {
+ QAction *profilerAction = launchMenu->addAction(profiler->name());
+ QAction *profilerOfflineAction = launchOfflineMenu->addAction(profiler->name());
+ QString error;
+ if (!profiler->check(&error))
+ {
+ profilerAction->setDisabled(true);
+ profilerOfflineAction->setDisabled(true);
+ QString profilerToolTip = tr("Profiler not setup correctly. Go into settings, \"External Tools\".");
+ profilerAction->setToolTip(profilerToolTip);
+ profilerOfflineAction->setToolTip(profilerToolTip);
+ }
+ else
+ {
+ connect(profilerAction, &QAction::triggered, [this, profiler]()
+ {
+ APPLICATION->launch(m_selectedInstance, true, profiler.get());
+ });
+ connect(profilerOfflineAction, &QAction::triggered, [this, profiler]()
+ {
+ APPLICATION->launch(m_selectedInstance, false, profiler.get());
+ });
+ }
+ }
+ ui->actionLaunchInstance->setMenu(launchMenu);
+ ui->actionLaunchInstanceOffline->setMenu(launchOfflineMenu);
+}
+
+void MainWindow::repopulateAccountsMenu()
+{
+ accountMenu->clear();
+
+ auto accounts = APPLICATION->accounts();
+ MinecraftAccountPtr defaultAccount = accounts->defaultAccount();
+
+ QString active_profileId = "";
+ if (defaultAccount)
+ {
+ // this can be called before accountMenuButton exists
+ if (accountMenuButton)
+ {
+ auto profileLabel = profileInUseFilter(defaultAccount->profileName(), defaultAccount->isInUse());
+ accountMenuButton->setText(profileLabel);
+ }
+ }
+
+ if (accounts->count() <= 0)
+ {
+ QAction *action = new QAction(tr("No accounts added!"), this);
+ action->setEnabled(false);
+ accountMenu->addAction(action);
+ }
+ else
+ {
+ // TODO: Nicer way to iterate?
+ for (int i = 0; i < accounts->count(); i++)
+ {
+ MinecraftAccountPtr account = accounts->at(i);
+ auto profileLabel = profileInUseFilter(account->profileName(), account->isInUse());
+ QAction *action = new QAction(profileLabel, this);
+ action->setData(i);
+ action->setCheckable(true);
+ if (defaultAccount == account)
+ {
+ action->setChecked(true);
+ }
+
+ auto face = account->getFace();
+ if(!face.isNull()) {
+ action->setIcon(face);
+ }
+ else {
+ action->setIcon(APPLICATION->getThemedIcon("noaccount"));
+ }
+ accountMenu->addAction(action);
+ connect(action, SIGNAL(triggered(bool)), SLOT(changeActiveAccount()));
+ }
+ }
+
+ accountMenu->addSeparator();
+
+ QAction *action = new QAction(tr("No Default Account"), this);
+ action->setCheckable(true);
+ action->setIcon(APPLICATION->getThemedIcon("noaccount"));
+ action->setData(-1);
+ if (!defaultAccount) {
+ action->setChecked(true);
+ }
+
+ accountMenu->addAction(action);
+ connect(action, SIGNAL(triggered(bool)), SLOT(changeActiveAccount()));
+
+ accountMenu->addSeparator();
+ accountMenu->addAction(ui->actionManageAccounts);
+}
+
+void MainWindow::updatesAllowedChanged(bool allowed)
+{
+ if(!BuildConfig.UPDATER_ENABLED)
+ {
+ return;
+ }
+ ui->actionCheckUpdate->setEnabled(allowed);
+}
+
+/*
+ * Assumes the sender is a QAction
+ */
+void MainWindow::changeActiveAccount()
+{
+ QAction *sAction = (QAction *)sender();
+
+ // Profile's associated Mojang username
+ if (sAction->data().type() != QVariant::Type::Int)
+ return;
+
+ QVariant data = sAction->data();
+ bool valid = false;
+ int index = data.toInt(&valid);
+ if(!valid) {
+ index = -1;
+ }
+ auto accounts = APPLICATION->accounts();
+ accounts->setDefaultAccount(index == -1 ? nullptr : accounts->at(index));
+ defaultAccountChanged();
+}
+
+void MainWindow::defaultAccountChanged()
+{
+ repopulateAccountsMenu();
+
+ MinecraftAccountPtr account = APPLICATION->accounts()->defaultAccount();
+
+ // FIXME: this needs adjustment for MSA
+ if (account && account->profileName() != "")
+ {
+ auto profileLabel = profileInUseFilter(account->profileName(), account->isInUse());
+ accountMenuButton->setText(profileLabel);
+ auto face = account->getFace();
+ if(face.isNull()) {
+ accountMenuButton->setIcon(APPLICATION->getThemedIcon("noaccount"));
+ }
+ else {
+ accountMenuButton->setIcon(face);
+ }
+ return;
+ }
+
+ // Set the icon to the "no account" icon.
+ accountMenuButton->setIcon(APPLICATION->getThemedIcon("noaccount"));
+ accountMenuButton->setText(tr("Profiles"));
+}
+
+bool MainWindow::eventFilter(QObject *obj, QEvent *ev)
+{
+ if (obj == view)
+ {
+ if (ev->type() == QEvent::KeyPress)
+ {
+ secretEventFilter->input(ev);
+ QKeyEvent *keyEvent = static_cast<QKeyEvent *>(ev);
+ switch (keyEvent->key())
+ {
+ /*
+ case Qt::Key_Enter:
+ case Qt::Key_Return:
+ activateInstance(m_selectedInstance);
+ return true;
+ */
+ case Qt::Key_Delete:
+ on_actionDeleteInstance_triggered();
+ return true;
+ case Qt::Key_F5:
+ refreshInstances();
+ return true;
+ case Qt::Key_F2:
+ on_actionRenameInstance_triggered();
+ return true;
+ default:
+ break;
+ }
+ }
+ }
+ return QMainWindow::eventFilter(obj, ev);
+}
+
+void MainWindow::updateNewsLabel()
+{
+ if (m_newsChecker->isLoadingNews())
+ {
+ newsLabel->setText(tr("Loading news..."));
+ newsLabel->setEnabled(false);
+ }
+ else
+ {
+ QList<NewsEntryPtr> entries = m_newsChecker->getNewsEntries();
+ if (entries.length() > 0)
+ {
+ newsLabel->setText(entries[0]->title);
+ newsLabel->setEnabled(true);
+ }
+ else
+ {
+ newsLabel->setText(tr("No news available."));
+ newsLabel->setEnabled(false);
+ }
+ }
+}
+
+void MainWindow::updateAvailable(GoUpdate::Status status)
+{
+ if(!APPLICATION->updatesAreAllowed())
+ {
+ updateNotAvailable();
+ return;
+ }
+ UpdateDialog dlg(true, this);
+ UpdateAction action = (UpdateAction)dlg.exec();
+ switch (action)
+ {
+ case UPDATE_LATER:
+ qDebug() << "Update will be installed later.";
+ break;
+ case UPDATE_NOW:
+ downloadUpdates(status);
+ break;
+ }
+}
+
+void MainWindow::updateNotAvailable()
+{
+ UpdateDialog dlg(false, this);
+ dlg.exec();
+}
+
+QList<int> stringToIntList(const QString &string)
+{
+ QStringList split = string.split(',', QString::SkipEmptyParts);
+ QList<int> out;
+ for (int i = 0; i < split.size(); ++i)
+ {
+ out.append(split.at(i).toInt());
+ }
+ return out;
+}
+QString intListToString(const QList<int> &list)
+{
+ QStringList slist;
+ for (int i = 0; i < list.size(); ++i)
+ {
+ slist.append(QString::number(list.at(i)));
+ }
+ return slist.join(',');
+}
+void MainWindow::notificationsChanged()
+{
+ QList<NotificationChecker::NotificationEntry> entries = m_notificationChecker->notificationEntries();
+ QList<int> shownNotifications = stringToIntList(APPLICATION->settings()->get("ShownNotifications").toString());
+ for (auto it = entries.begin(); it != entries.end(); ++it)
+ {
+ NotificationChecker::NotificationEntry entry = *it;
+ if (!shownNotifications.contains(entry.id))
+ {
+ NotificationDialog dialog(entry, this);
+ if (dialog.exec() == NotificationDialog::DontShowAgain)
+ {
+ shownNotifications.append(entry.id);
+ }
+ }
+ }
+ APPLICATION->settings()->set("ShownNotifications", intListToString(shownNotifications));
+}
+
+void MainWindow::downloadUpdates(GoUpdate::Status status)
+{
+ if(!APPLICATION->updatesAreAllowed())
+ {
+ return;
+ }
+ qDebug() << "Downloading updates.";
+ ProgressDialog updateDlg(this);
+ status.rootPath = APPLICATION->root();
+
+ auto dlPath = FS::PathCombine(APPLICATION->root(), "update", "XXXXXX");
+ if (!FS::ensureFilePathExists(dlPath))
+ {
+ CustomMessageBox::selectable(this, tr("Error"), tr("Couldn't create folder for update downloads:\n%1").arg(dlPath), QMessageBox::Warning)->show();
+ }
+ GoUpdate::DownloadTask updateTask(APPLICATION->network(), status, dlPath, &updateDlg);
+ // If the task succeeds, install the updates.
+ if (updateDlg.execWithTask(&updateTask))
+ {
+ /**
+ * NOTE: This disables launching instances until the update either succeeds (and this process exits)
+ * or the update fails (and the control leaves this scope).
+ */
+ APPLICATION->updateIsRunning(true);
+ UpdateController update(this, APPLICATION->root(), updateTask.updateFilesDir(), updateTask.operations());
+ update.installUpdates();
+ APPLICATION->updateIsRunning(false);
+ }
+ else
+ {
+ CustomMessageBox::selectable(this, tr("Error"), updateTask.failReason(), QMessageBox::Warning)->show();
+ }
+}
+
+void MainWindow::onCatToggled(bool state)
+{
+ setCatBackground(state);
+ APPLICATION->settings()->set("TheCat", state);
+}
+
+namespace {
+template <typename T>
+T non_stupid_abs(T in)
+{
+ if (in < 0)
+ return -in;
+ return in;
+}
+}
+
+void MainWindow::setCatBackground(bool enabled)
+{
+ if (enabled)
+ {
+ QDateTime now = QDateTime::currentDateTime();
+ QDateTime xmas(QDate(now.date().year(), 12, 25), QTime(0, 0));
+ QString cat = (non_stupid_abs(now.daysTo(xmas)) <= 4) ? "catmas" : "kitteh";
+ view->setStyleSheet(QString(R"(
+InstanceView
+{
+ background-image: url(:/backgrounds/%1);
+ background-attachment: fixed;
+ background-clip: padding;
+ background-position: top right;
+ background-repeat: none;
+ background-color:palette(base);
+})").arg(cat));
+ }
+ else
+ {
+ view->setStyleSheet(QString());
+ }
+}
+
+void MainWindow::runModalTask(Task *task)
+{
+ connect(task, &Task::failed, [this](QString reason)
+ {
+ CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show();
+ });
+ connect(task, &Task::succeeded, [this, task]()
+ {
+ QStringList warnings = task->warnings();
+ if(warnings.count())
+ {
+ CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->show();
+ }
+ });
+ ProgressDialog loadDialog(this);
+ loadDialog.setSkipButton(true, tr("Abort"));
+ loadDialog.execWithTask(task);
+}
+
+void MainWindow::instanceFromInstanceTask(InstanceTask *rawTask)
+{
+ unique_qobject_ptr<Task> task(APPLICATION->instances()->wrapInstanceTask(rawTask));
+ runModalTask(task.get());
+}
+
+void MainWindow::on_actionCopyInstance_triggered()
+{
+ if (!m_selectedInstance)
+ return;
+
+ CopyInstanceDialog copyInstDlg(m_selectedInstance, this);
+ if (!copyInstDlg.exec())
+ return;
+
+ auto copyTask = new InstanceCopyTask(m_selectedInstance, copyInstDlg.shouldCopySaves(), copyInstDlg.shouldKeepPlaytime());
+ copyTask->setName(copyInstDlg.instName());
+ copyTask->setGroup(copyInstDlg.instGroup());
+ copyTask->setIcon(copyInstDlg.iconKey());
+ unique_qobject_ptr<Task> task(APPLICATION->instances()->wrapInstanceTask(copyTask));
+ runModalTask(task.get());
+}
+
+void MainWindow::finalizeInstance(InstancePtr inst)
+{
+ view->updateGeometries();
+ setSelectedInstanceById(inst->id());
+ if (APPLICATION->accounts()->anyAccountIsValid())
+ {
+ ProgressDialog loadDialog(this);
+ auto update = inst->createUpdateTask(Net::Mode::Online);
+ connect(update.get(), &Task::failed, [this](QString reason)
+ {
+ QString error = QString("Instance load failed: %1").arg(reason);
+ CustomMessageBox::selectable(this, tr("Error"), error, QMessageBox::Warning)->show();
+ });
+ if(update)
+ {
+ loadDialog.setSkipButton(true, tr("Abort"));
+ loadDialog.execWithTask(update.get());
+ }
+ }
+ else
+ {
+ CustomMessageBox::selectable(
+ this,
+ tr("Error"),
+ tr("The launcher cannot download Minecraft or update instances unless you have at least "
+ "one account added.\nPlease add your Mojang or Minecraft account."),
+ QMessageBox::Warning
+ )->show();
+ }
+}
+
+void MainWindow::addInstance(QString url)
+{
+ QString groupName;
+ do
+ {
+ QObject* obj = sender();
+ if(!obj)
+ break;
+ QAction *action = qobject_cast<QAction *>(obj);
+ if(!action)
+ break;
+ auto map = action->data().toMap();
+ if(!map.contains("group"))
+ break;
+ groupName = map["group"].toString();
+ } while(0);
+
+ if(groupName.isEmpty())
+ {
+ groupName = APPLICATION->settings()->get("LastUsedGroupForNewInstance").toString();
+ }
+
+ NewInstanceDialog newInstDlg(groupName, url, this);
+ if (!newInstDlg.exec())
+ return;
+
+ APPLICATION->settings()->set("LastUsedGroupForNewInstance", newInstDlg.instGroup());
+
+ InstanceTask * creationTask = newInstDlg.extractTask();
+ if(creationTask)
+ {
+ instanceFromInstanceTask(creationTask);
+ }
+}
+
+void MainWindow::on_actionAddInstance_triggered()
+{
+ addInstance();
+}
+
+void MainWindow::droppedURLs(QList<QUrl> urls)
+{
+ for(auto & url:urls)
+ {
+ if(url.isLocalFile())
+ {
+ addInstance(url.toLocalFile());
+ }
+ else
+ {
+ addInstance(url.toString());
+ }
+ // Only process one dropped file...
+ break;
+ }
+}
+
+void MainWindow::on_actionREDDIT_triggered()
+{
+ DesktopServices::openUrl(QUrl(BuildConfig.SUBREDDIT_URL));
+}
+
+void MainWindow::on_actionDISCORD_triggered()
+{
+ DesktopServices::openUrl(QUrl(BuildConfig.DISCORD_URL));
+}
+
+void MainWindow::on_actionChangeInstIcon_triggered()
+{
+ if (!m_selectedInstance)
+ return;
+
+ IconPickerDialog dlg(this);
+ dlg.execWithSelection(m_selectedInstance->iconKey());
+ if (dlg.result() == QDialog::Accepted)
+ {
+ m_selectedInstance->setIconKey(dlg.selectedIconKey);
+ auto icon = APPLICATION->icons()->getIcon(dlg.selectedIconKey);
+ ui->actionChangeInstIcon->setIcon(icon);
+ ui->changeIconButton->setIcon(icon);
+ }
+}
+
+void MainWindow::iconUpdated(QString icon)
+{
+ if (icon == m_currentInstIcon)
+ {
+ auto icon = APPLICATION->icons()->getIcon(m_currentInstIcon);
+ ui->actionChangeInstIcon->setIcon(icon);
+ ui->changeIconButton->setIcon(icon);
+ }
+}
+
+void MainWindow::updateInstanceToolIcon(QString new_icon)
+{
+ m_currentInstIcon = new_icon;
+ auto icon = APPLICATION->icons()->getIcon(m_currentInstIcon);
+ ui->actionChangeInstIcon->setIcon(icon);
+ ui->changeIconButton->setIcon(icon);
+}
+
+void MainWindow::setSelectedInstanceById(const QString &id)
+{
+ if (id.isNull())
+ return;
+ const QModelIndex index = APPLICATION->instances()->getInstanceIndexById(id);
+ if (index.isValid())
+ {
+ QModelIndex selectionIndex = proxymodel->mapFromSource(index);
+ view->selectionModel()->setCurrentIndex(selectionIndex, QItemSelectionModel::ClearAndSelect);
+ updateStatusCenter();
+ }
+}
+
+void MainWindow::on_actionChangeInstGroup_triggered()
+{
+ if (!m_selectedInstance)
+ return;
+
+ bool ok = false;
+ InstanceId instId = m_selectedInstance->id();
+ QString name(APPLICATION->instances()->getInstanceGroup(instId));
+ auto groups = APPLICATION->instances()->getGroups();
+ groups.insert(0, "");
+ groups.sort(Qt::CaseInsensitive);
+ int foo = groups.indexOf(name);
+
+ name = QInputDialog::getItem(this, tr("Group name"), tr("Enter a new group name."), groups, foo, true, &ok);
+ name = name.simplified();
+ if (ok)
+ {
+ APPLICATION->instances()->setInstanceGroup(instId, name);
+ }
+}
+
+void MainWindow::deleteGroup()
+{
+ QObject* obj = sender();
+ if(!obj)
+ return;
+ QAction *action = qobject_cast<QAction *>(obj);
+ if(!action)
+ return;
+ auto map = action->data().toMap();
+ if(!map.contains("group"))
+ return;
+ QString groupName = map["group"].toString();
+ if(!groupName.isEmpty())
+ {
+ auto reply = QMessageBox::question(this, tr("Delete group"), tr("Are you sure you want to delete the group %1")
+ .arg(groupName), QMessageBox::Yes | QMessageBox::No);
+ if(reply == QMessageBox::Yes)
+ {
+ APPLICATION->instances()->deleteGroup(groupName);
+ }
+ }
+}
+
+void MainWindow::on_actionViewInstanceFolder_triggered()
+{
+ QString str = APPLICATION->settings()->get("InstanceDir").toString();
+ DesktopServices::openDirectory(str);
+}
+
+void MainWindow::refreshInstances()
+{
+ APPLICATION->instances()->loadList();
+}
+
+void MainWindow::on_actionViewCentralModsFolder_triggered()
+{
+ DesktopServices::openDirectory(APPLICATION->settings()->get("CentralModsDir").toString(), true);
+}
+
+void MainWindow::on_actionConfig_Folder_triggered()
+{
+ if (m_selectedInstance)
+ {
+ QString str = m_selectedInstance->instanceConfigFolder();
+ DesktopServices::openDirectory(QDir(str).absolutePath());
+ }
+}
+
+void MainWindow::checkForUpdates()
+{
+ if(BuildConfig.UPDATER_ENABLED)
+ {
+ auto updater = APPLICATION->updateChecker();
+ updater->checkForUpdate(APPLICATION->settings()->get("UpdateChannel").toString(), true);
+ }
+ else
+ {
+ qWarning() << "Updater not set up. Cannot check for updates.";
+ }
+}
+
+void MainWindow::on_actionSettings_triggered()
+{
+ APPLICATION->ShowGlobalSettings(this, "global-settings");
+}
+
+void MainWindow::globalSettingsClosed()
+{
+ // FIXME: quick HACK to make this work. improve, optimize.
+ APPLICATION->instances()->loadList();
+ proxymodel->invalidate();
+ proxymodel->sort(0);
+ updateToolsMenu();
+ updateStatusCenter();
+ update();
+}
+
+void MainWindow::on_actionInstanceSettings_triggered()
+{
+ APPLICATION->showInstanceWindow(m_selectedInstance, "settings");
+}
+
+void MainWindow::on_actionEditInstNotes_triggered()
+{
+ APPLICATION->showInstanceWindow(m_selectedInstance, "notes");
+}
+
+void MainWindow::on_actionWorlds_triggered()
+{
+ APPLICATION->showInstanceWindow(m_selectedInstance, "worlds");
+}
+
+void MainWindow::on_actionEditInstance_triggered()
+{
+ APPLICATION->showInstanceWindow(m_selectedInstance);
+}
+
+void MainWindow::on_actionScreenshots_triggered()
+{
+ APPLICATION->showInstanceWindow(m_selectedInstance, "screenshots");
+}
+
+void MainWindow::on_actionManageAccounts_triggered()
+{
+ APPLICATION->ShowGlobalSettings(this, "accounts");
+}
+
+void MainWindow::on_actionReportBug_triggered()
+{
+ DesktopServices::openUrl(QUrl(BuildConfig.BUG_TRACKER_URL));
+}
+
+void MainWindow::on_actionPatreon_triggered()
+{
+ DesktopServices::openUrl(QUrl("https://www.patreon.com/multimc"));
+}
+
+void MainWindow::on_actionMoreNews_triggered()
+{
+ DesktopServices::openUrl(QUrl("https://multimc.org/posts.html"));
+}
+
+void MainWindow::newsButtonClicked()
+{
+ QList<NewsEntryPtr> entries = m_newsChecker->getNewsEntries();
+ if (entries.count() > 0)
+ {
+ DesktopServices::openUrl(QUrl(entries[0]->link));
+ }
+ else
+ {
+ DesktopServices::openUrl(QUrl("https://multimc.org/posts.html"));
+ }
+}
+
+void MainWindow::on_actionAbout_triggered()
+{
+ AboutDialog dialog(this);
+ dialog.exec();
+}
+
+void MainWindow::on_actionDeleteInstance_triggered()
+{
+ if (!m_selectedInstance)
+ {
+ return;
+ }
+ auto id = m_selectedInstance->id();
+ auto response = CustomMessageBox::selectable(
+ this,
+ tr("CAREFUL!"),
+ tr("About to delete: %1\nThis is permanent and will completely delete the instance.\n\nAre you sure?").arg(m_selectedInstance->name()),
+ QMessageBox::Warning,
+ QMessageBox::Yes | QMessageBox::No,
+ QMessageBox::No
+ )->exec();
+ if (response == QMessageBox::Yes)
+ {
+ APPLICATION->instances()->deleteInstance(id);
+ }
+}
+
+void MainWindow::on_actionExportInstance_triggered()
+{
+ if (m_selectedInstance)
+ {
+ ExportInstanceDialog dlg(m_selectedInstance, this);
+ dlg.exec();
+ }
+}
+
+void MainWindow::on_actionRenameInstance_triggered()
+{
+ if (m_selectedInstance)
+ {
+ view->edit(view->currentIndex());
+ }
+}
+
+void MainWindow::on_actionViewSelectedInstFolder_triggered()
+{
+ if (m_selectedInstance)
+ {
+ QString str = m_selectedInstance->instanceRoot();
+ DesktopServices::openDirectory(QDir(str).absolutePath());
+ }
+}
+
+void MainWindow::on_actionViewSelectedMCFolder_triggered()
+{
+ if (m_selectedInstance)
+ {
+ QString str = m_selectedInstance->gameRoot();
+ if (!FS::ensureFilePathExists(str))
+ {
+ // TODO: report error
+ return;
+ }
+ DesktopServices::openDirectory(QDir(str).absolutePath());
+ }
+}
+
+
+void MainWindow::closeEvent(QCloseEvent *event)
+{
+ // Save the window state and geometry.
+ APPLICATION->settings()->set("MainWindowState", saveState().toBase64());
+ APPLICATION->settings()->set("MainWindowGeometry", saveGeometry().toBase64());
+ event->accept();
+ emit isClosing();
+}
+
+void MainWindow::changeEvent(QEvent* event)
+{
+ if (event->type() == QEvent::LanguageChange)
+ {
+ retranslateUi();
+ }
+ QMainWindow::changeEvent(event);
+}
+
+void MainWindow::instanceActivated(QModelIndex index)
+{
+ if (!index.isValid())
+ return;
+ QString id = index.data(InstanceList::InstanceIDRole).toString();
+ InstancePtr inst = APPLICATION->instances()->getInstanceById(id);
+ if (!inst)
+ return;
+
+ activateInstance(inst);
+}
+
+void MainWindow::on_actionLaunchInstance_triggered()
+{
+ if (!m_selectedInstance)
+ {
+ return;
+ }
+ if(m_selectedInstance->isRunning())
+ {
+ APPLICATION->kill(m_selectedInstance);
+ }
+ else
+ {
+ APPLICATION->launch(m_selectedInstance);
+ }
+}
+
+void MainWindow::activateInstance(InstancePtr instance)
+{
+ APPLICATION->launch(instance);
+}
+
+void MainWindow::on_actionLaunchInstanceOffline_triggered()
+{
+ if (m_selectedInstance)
+ {
+ APPLICATION->launch(m_selectedInstance, false);
+ }
+}
+
+void MainWindow::taskEnd()
+{
+ QObject *sender = QObject::sender();
+ if (sender == m_versionLoadTask)
+ m_versionLoadTask = NULL;
+
+ sender->deleteLater();
+}
+
+void MainWindow::startTask(Task *task)
+{
+ connect(task, SIGNAL(succeeded()), SLOT(taskEnd()));
+ connect(task, SIGNAL(failed(QString)), SLOT(taskEnd()));
+ task->start();
+}
+
+void MainWindow::instanceChanged(const QModelIndex &current, const QModelIndex &previous)
+{
+ if (!current.isValid())
+ {
+ APPLICATION->settings()->set("SelectedInstance", QString());
+ selectionBad();
+ return;
+ }
+ QString id = current.data(InstanceList::InstanceIDRole).toString();
+ m_selectedInstance = APPLICATION->instances()->getInstanceById(id);
+ if (m_selectedInstance)
+ {
+ ui->instanceToolBar->setEnabled(true);
+ if(m_selectedInstance->isRunning())
+ {
+ ui->actionLaunchInstance->setEnabled(true);
+ ui->setLaunchAction(true);
+ }
+ else
+ {
+ ui->actionLaunchInstance->setEnabled(m_selectedInstance->canLaunch());
+ ui->setLaunchAction(false);
+ }
+ ui->actionLaunchInstanceOffline->setEnabled(m_selectedInstance->canLaunch());
+ ui->actionExportInstance->setEnabled(m_selectedInstance->canExport());
+ ui->renameButton->setText(m_selectedInstance->name());
+ m_statusLeft->setText(m_selectedInstance->getStatusbarDescription());
+ updateStatusCenter();
+ updateInstanceToolIcon(m_selectedInstance->iconKey());
+
+ updateToolsMenu();
+
+ APPLICATION->settings()->set("SelectedInstance", m_selectedInstance->id());
+ }
+ else
+ {
+ ui->instanceToolBar->setEnabled(false);
+ APPLICATION->settings()->set("SelectedInstance", QString());
+ selectionBad();
+ return;
+ }
+}
+
+void MainWindow::instanceSelectRequest(QString id)
+{
+ setSelectedInstanceById(id);
+}
+
+void MainWindow::instanceDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight)
+{
+ auto current = view->selectionModel()->currentIndex();
+ QItemSelection test(topLeft, bottomRight);
+ if (test.contains(current))
+ {
+ instanceChanged(current, current);
+ }
+}
+
+void MainWindow::selectionBad()
+{
+ // start by reseting everything...
+ m_selectedInstance = nullptr;
+
+ statusBar()->clearMessage();
+ ui->instanceToolBar->setEnabled(false);
+ ui->renameButton->setText(tr("Rename Instance"));
+ updateInstanceToolIcon("grass");
+
+ // ...and then see if we can enable the previously selected instance
+ setSelectedInstanceById(APPLICATION->settings()->get("SelectedInstance").toString());
+}
+
+void MainWindow::checkInstancePathForProblems()
+{
+ QString instanceFolder = APPLICATION->settings()->get("InstanceDir").toString();
+ if (FS::checkProblemticPathJava(QDir(instanceFolder)))
+ {
+ QMessageBox warning(this);
+ warning.setText(tr("Your instance folder contains \'!\' and this is known to cause Java problems!"));
+ warning.setInformativeText(
+ tr(
+ "You have now two options: <br/>"
+ " - change the instance folder in the settings <br/>"
+ " - move this installation of %1 to a different folder"
+ ).arg(BuildConfig.LAUNCHER_NAME)
+ );
+ warning.setDefaultButton(QMessageBox::Ok);
+ warning.exec();
+ }
+ auto tempFolderText = tr("This is a problem: <br/>"
+ " - The launcher will likely be deleted without warning by the operating system <br/>"
+ " - close the launcher now and extract it to a real location, not a temporary folder");
+ QString pathfoldername = QDir(instanceFolder).absolutePath();
+ if (pathfoldername.contains("Rar$", Qt::CaseInsensitive))
+ {
+ QMessageBox warning(this);
+ warning.setText(tr("Your instance folder contains \'Rar$\' - that means you haven't extracted the launcher archive!"));
+ warning.setInformativeText(tempFolderText);
+ warning.setDefaultButton(QMessageBox::Ok);
+ warning.exec();
+ }
+ else if (pathfoldername.startsWith(QDir::tempPath()) || pathfoldername.contains("/TempState/"))
+ {
+ QMessageBox warning(this);
+ warning.setText(tr("Your instance folder is in a temporary folder: \'%1\'!").arg(QDir::tempPath()));
+ warning.setInformativeText(tempFolderText);
+ warning.setDefaultButton(QMessageBox::Ok);
+ warning.exec();
+ }
+}
+
+void MainWindow::updateStatusCenter()
+{
+ m_statusCenter->setVisible(APPLICATION->settings()->get("ShowGlobalGameTime").toBool());
+
+ int timePlayed = APPLICATION->instances()->getTotalPlayTime();
+ if (timePlayed > 0) {
+ m_statusCenter->setText(tr("Total playtime: %1").arg(Time::prettifyDuration(timePlayed)));
+ }
+}
diff --git a/launcher/ui/MainWindow.h b/launcher/ui/MainWindow.h
new file mode 100644
index 00000000..7e1256cc
--- /dev/null
+++ b/launcher/ui/MainWindow.h
@@ -0,0 +1,221 @@
+/* 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 <memory>
+
+#include <QMainWindow>
+#include <QProcess>
+#include <QTimer>
+
+#include "BaseInstance.h"
+#include "minecraft/auth/MinecraftAccount.h"
+#include "net/NetJob.h"
+#include "updater/GoUpdate.h"
+
+class LaunchController;
+class NewsChecker;
+class NotificationChecker;
+class QToolButton;
+class InstanceProxyModel;
+class LabeledToolButton;
+class QLabel;
+class MinecraftLauncher;
+class BaseProfilerFactory;
+class InstanceView;
+class KonamiCode;
+class InstanceTask;
+
+class MainWindow : public QMainWindow
+{
+ Q_OBJECT
+
+ class Ui;
+
+public:
+ explicit MainWindow(QWidget *parent = 0);
+ ~MainWindow();
+
+ bool eventFilter(QObject *obj, QEvent *ev) override;
+ void closeEvent(QCloseEvent *event) override;
+ void changeEvent(QEvent * event) override;
+
+ void checkInstancePathForProblems();
+
+ void updatesAllowedChanged(bool allowed);
+
+ void droppedURLs(QList<QUrl> urls);
+signals:
+ void isClosing();
+
+protected:
+ QMenu * createPopupMenu() override;
+
+private slots:
+ void onCatToggled(bool);
+
+ void on_actionAbout_triggered();
+
+ void on_actionAddInstance_triggered();
+
+ void on_actionREDDIT_triggered();
+
+ void on_actionDISCORD_triggered();
+
+ void on_actionCopyInstance_triggered();
+
+ void on_actionChangeInstGroup_triggered();
+
+ void on_actionChangeInstIcon_triggered();
+ void on_changeIconButton_clicked(bool)
+ {
+ on_actionChangeInstIcon_triggered();
+ }
+
+ void on_actionViewInstanceFolder_triggered();
+
+ void on_actionConfig_Folder_triggered();
+
+ void on_actionViewSelectedInstFolder_triggered();
+
+ void on_actionViewSelectedMCFolder_triggered();
+
+ void refreshInstances();
+
+ void on_actionViewCentralModsFolder_triggered();
+
+ void checkForUpdates();
+
+ void on_actionSettings_triggered();
+
+ void on_actionInstanceSettings_triggered();
+
+ void on_actionManageAccounts_triggered();
+
+ void on_actionReportBug_triggered();
+
+ void on_actionPatreon_triggered();
+
+ void on_actionMoreNews_triggered();
+
+ void newsButtonClicked();
+
+ void on_actionLaunchInstance_triggered();
+
+ void on_actionLaunchInstanceOffline_triggered();
+
+ void on_actionDeleteInstance_triggered();
+
+ void deleteGroup();
+
+ void on_actionExportInstance_triggered();
+
+ void on_actionRenameInstance_triggered();
+ void on_renameButton_clicked(bool)
+ {
+ on_actionRenameInstance_triggered();
+ }
+
+ void on_actionEditInstance_triggered();
+
+ void on_actionEditInstNotes_triggered();
+
+ void on_actionWorlds_triggered();
+
+ void on_actionScreenshots_triggered();
+
+ void taskEnd();
+
+ /**
+ * called when an icon is changed in the icon model.
+ */
+ void iconUpdated(QString);
+
+ void showInstanceContextMenu(const QPoint &);
+
+ void updateToolsMenu();
+
+ void instanceActivated(QModelIndex);
+
+ void instanceChanged(const QModelIndex &current, const QModelIndex &previous);
+
+ void instanceSelectRequest(QString id);
+
+ void instanceDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight);
+
+ void selectionBad();
+
+ void startTask(Task *task);
+
+ void updateAvailable(GoUpdate::Status status);
+
+ void updateNotAvailable();
+
+ void notificationsChanged();
+
+ void defaultAccountChanged();
+
+ void changeActiveAccount();
+
+ void repopulateAccountsMenu();
+
+ void updateNewsLabel();
+
+ /*!
+ * Runs the DownloadTask and installs updates.
+ */
+ void downloadUpdates(GoUpdate::Status status);
+
+ void konamiTriggered();
+
+ void globalSettingsClosed();
+
+private:
+ void retranslateUi();
+
+ void addInstance(QString url = QString());
+ void activateInstance(InstancePtr instance);
+ void setCatBackground(bool enabled);
+ void updateInstanceToolIcon(QString new_icon);
+ void setSelectedInstanceById(const QString &id);
+ void updateStatusCenter();
+
+ void runModalTask(Task *task);
+ void instanceFromInstanceTask(InstanceTask *task);
+ void finalizeInstance(InstancePtr inst);
+
+private:
+ std::unique_ptr<Ui> ui;
+
+ // these are managed by Qt's memory management model!
+ InstanceView *view = nullptr;
+ InstanceProxyModel *proxymodel = nullptr;
+ QToolButton *newsLabel = nullptr;
+ QLabel *m_statusLeft = nullptr;
+ QLabel *m_statusCenter = nullptr;
+ QMenu *accountMenu = nullptr;
+ QToolButton *accountMenuButton = nullptr;
+ KonamiCode * secretEventFilter = nullptr;
+
+ unique_qobject_ptr<NewsChecker> m_newsChecker;
+ unique_qobject_ptr<NotificationChecker> m_notificationChecker;
+
+ InstancePtr m_selectedInstance;
+ QString m_currentInstIcon;
+
+ // managed by the application object
+ Task *m_versionLoadTask = nullptr;
+};
diff --git a/launcher/ui/dialogs/AboutDialog.cpp b/launcher/ui/dialogs/AboutDialog.cpp
new file mode 100644
index 00000000..501156d0
--- /dev/null
+++ b/launcher/ui/dialogs/AboutDialog.cpp
@@ -0,0 +1,149 @@
+/* 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 "AboutDialog.h"
+#include "ui_AboutDialog.h"
+#include <QIcon>
+#include "Application.h"
+#include "BuildConfig.h"
+
+#include <net/NetJob.h>
+
+#include "HoeDown.h"
+
+namespace {
+// Credits
+// This is a hack, but I can't think of a better way to do this easily without screwing with QTextDocument...
+QString getCreditsHtml(QStringList patrons)
+{
+ QString patronsHeading = QObject::tr("Patrons", "About Credits");
+ QString output;
+ QTextStream stream(&output);
+ stream.setCodec(QTextCodec::codecForName("UTF-8"));
+ stream << "<center>\n";
+ // TODO: possibly retrieve from git history at build time?
+ stream << "<h3>" << QObject::tr("Developers", "About Credits") << "</h3>\n";
+ stream << "<p>Andrew Okin &lt;<a href='mailto:forkk@forkk.net'>forkk@forkk.net</a>&gt;</p>\n";
+ stream << "<p>Petr Mrázek &lt;<a href='mailto:peterix@gmail.com'>peterix@gmail.com</a>&gt;</p>\n";
+ stream << "<p>Sky Welch &lt;<a href='mailto:multimc@bunnies.io'>multimc@bunnies.io</a>&gt;</p>\n";
+ stream << "<p>Jan (02JanDal) &lt;<a href='mailto:02jandal@gmail.com'>02jandal@gmail.com</a>&gt;</p>\n";
+ stream << "<p>RoboSky &lt;<a href='https://twitter.com/RoboSky_'>@RoboSky_</a>&gt;</p>\n";
+ stream << "<br />\n";
+
+ stream << "<h3>" << QObject::tr("With thanks to", "About Credits") << "</h3>\n";
+ stream << "<p>Orochimarufan &lt;<a href='mailto:orochimarufan.x3@gmail.com'>orochimarufan.x3@gmail.com</a>&gt;</p>\n";
+ stream << "<p>TakSuyu &lt;<a href='mailto:taksuyu@gmail.com'>taksuyu@gmail.com</a>&gt;</p>\n";
+ stream << "<p>Kilobyte &lt;<a href='mailto:stiepen22@gmx.de'>stiepen22@gmx.de</a>&gt;</p>\n";
+ stream << "<p>Rootbear75 &lt;<a href='https://twitter.com/rootbear75'>@rootbear75</a>&gt;</p>\n";
+ stream << "<p>Zeker Zhayard &lt;<a href='https://twitter.com/zeker_zhayard'>@Zeker_Zhayard</a>&gt;</p>\n";
+ stream << "<br />\n";
+
+ if(!patrons.isEmpty()) {
+ stream << "<h3>" << QObject::tr("Patrons", "About Credits") << "</h3>\n";
+ for (QString patron : patrons)
+ {
+ stream << "<p>" << patron << "</p>\n";
+ }
+ }
+ stream << "</center>\n";
+ return output;
+}
+
+QString getLicenseHtml()
+{
+ HoeDown hoedown;
+ QFile dataFile(":/documents/COPYING.md");
+ dataFile.open(QIODevice::ReadOnly);
+ QString output = hoedown.process(dataFile.readAll());
+ return output;
+}
+
+}
+
+AboutDialog::AboutDialog(QWidget *parent) : QDialog(parent), ui(new Ui::AboutDialog)
+{
+ ui->setupUi(this);
+
+ QString launcherName = BuildConfig.LAUNCHER_NAME;
+
+ setWindowTitle(tr("About %1").arg(launcherName));
+
+ QString chtml = getCreditsHtml(QStringList());
+ ui->creditsText->setHtml(chtml);
+
+ QString lhtml = getLicenseHtml();
+ ui->licenseText->setHtml(lhtml);
+
+ ui->urlLabel->setOpenExternalLinks(true);
+
+ ui->icon->setPixmap(APPLICATION->getThemedIcon("logo").pixmap(64));
+ ui->title->setText(launcherName);
+
+ ui->versionLabel->setText(tr("Version") +": " + BuildConfig.printableVersionString());
+ ui->platformLabel->setText(tr("Platform") +": " + BuildConfig.BUILD_PLATFORM);
+
+ if (BuildConfig.VERSION_BUILD >= 0)
+ ui->buildNumLabel->setText(tr("Build Number") +": " + QString::number(BuildConfig.VERSION_BUILD));
+ else
+ ui->buildNumLabel->setVisible(false);
+
+ if (!BuildConfig.VERSION_CHANNEL.isEmpty())
+ ui->channelLabel->setText(tr("Channel") +": " + BuildConfig.VERSION_CHANNEL);
+ else
+ ui->channelLabel->setVisible(false);
+
+ ui->redistributionText->setHtml(tr(
+"<p>We keep MultiMC open source because we think it's important to be able to see the source code for a project like this, and we do so using the Apache license.</p>\n"
+"<p>Part of the reason for using the Apache license is we don't want people using the &quot;MultiMC&quot; name when redistributing the project. "
+"This means people must take the time to go through the source code and remove all references to &quot;MultiMC&quot;, including but not limited to the project "
+"icon and the title of windows, (no <b>MultiMC-fork</b> in the title).</p>\n"
+"<p>The Apache license covers reasonable use for the name - a mention of the project's origins in the About dialog and the license is acceptable. "
+"However, it should be abundantly clear that the project is a fork <b>without</b> implying that you have our blessing.</p>"
+ ));
+
+ QString urlText("<html><head/><body><p><a href=\"%1\">%1</a></p></body></html>");
+ ui->urlLabel->setText(urlText.arg(BuildConfig.LAUNCHER_GIT));
+
+ QString copyText("© 2012-2021 %1");
+ ui->copyLabel->setText(copyText.arg(BuildConfig.LAUNCHER_COPYRIGHT));
+
+ connect(ui->closeButton, SIGNAL(clicked()), SLOT(close()));
+
+ connect(ui->aboutQt, &QPushButton::clicked, &QApplication::aboutQt);
+
+ loadPatronList();
+}
+
+AboutDialog::~AboutDialog()
+{
+ delete ui;
+}
+
+void AboutDialog::loadPatronList()
+{
+ netJob = new NetJob("Patreon Patron List");
+ netJob->addNetAction(Net::Download::makeByteArray(QUrl("https://files.multimc.org/patrons.txt"), &dataSink));
+ connect(netJob.get(), &NetJob::succeeded, this, &AboutDialog::patronListLoaded);
+ netJob->start(APPLICATION->network());
+}
+
+void AboutDialog::patronListLoaded()
+{
+ QString patronListStr(dataSink);
+ dataSink.clear();
+ QString html = getCreditsHtml(patronListStr.split("\n", QString::SkipEmptyParts));
+ ui->creditsText->setHtml(html);
+}
+
diff --git a/launcher/ui/dialogs/AboutDialog.h b/launcher/ui/dialogs/AboutDialog.h
new file mode 100644
index 00000000..cc4b8850
--- /dev/null
+++ b/launcher/ui/dialogs/AboutDialog.h
@@ -0,0 +1,47 @@
+/* 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 <QDialog>
+#include <net/NetJob.h>
+
+namespace Ui
+{
+class AboutDialog;
+}
+
+class AboutDialog : public QDialog
+{
+ Q_OBJECT
+
+public:
+ explicit AboutDialog(QWidget *parent = 0);
+ ~AboutDialog();
+
+public
+slots:
+ /// Starts loading a list of Patreon patrons.
+ void loadPatronList();
+
+ /// Slot for when the patron list loads successfully.
+ void patronListLoaded();
+
+private:
+ Ui::AboutDialog *ui;
+
+ NetJob::Ptr netJob;
+ QByteArray dataSink;
+};
diff --git a/launcher/ui/dialogs/AboutDialog.ui b/launcher/ui/dialogs/AboutDialog.ui
new file mode 100644
index 00000000..422e877b
--- /dev/null
+++ b/launcher/ui/dialogs/AboutDialog.ui
@@ -0,0 +1,316 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>AboutDialog</class>
+ <widget class="QDialog" name="AboutDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>783</width>
+ <height>843</height>
+ </rect>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>450</width>
+ <height>400</height>
+ </size>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <item>
+ <spacer name="horizontalSpacer">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QLabel" name="icon">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>64</width>
+ <height>64</height>
+ </size>
+ </property>
+ <property name="baseSize">
+ <size>
+ <width>64</width>
+ <height>64</height>
+ </size>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_2">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QLabel" name="title">
+ <property name="font">
+ <font>
+ <pointsize>15</pointsize>
+ </font>
+ </property>
+ <property name="text">
+ <string notr="true">MultiMC 5</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QTabWidget" name="tabWidget">
+ <property name="currentIndex">
+ <number>0</number>
+ </property>
+ <widget class="QWidget" name="aboutTab">
+ <attribute name="title">
+ <string>About</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_5">
+ <item>
+ <widget class="QLabel" name="aboutLabel">
+ <property name="enabled">
+ <bool>true</bool>
+ </property>
+ <property name="text">
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;A custom launcher that makes managing Minecraft easier by allowing you to have multiple instances of Minecraft at once.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignCenter</set>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="urlLabel">
+ <property name="font">
+ <font>
+ <pointsize>10</pointsize>
+ </font>
+ </property>
+ <property name="text">
+ <string notr="true">GIT URL</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="copyLabel">
+ <property name="font">
+ <font>
+ <pointsize>8</pointsize>
+ <kerning>true</kerning>
+ </font>
+ </property>
+ <property name="text">
+ <string notr="true">COPYRIGHT</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="Line" name="line">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="versionLabel">
+ <property name="text">
+ <string>Version:</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="platformLabel">
+ <property name="text">
+ <string>Platform:</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="buildNumLabel">
+ <property name="text">
+ <string>Build Number:</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="channelLabel">
+ <property name="text">
+ <string>Channel:</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="verticalSpacer">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>212</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QWidget" name="creditsTab">
+ <attribute name="title">
+ <string>Credits</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_2">
+ <item>
+ <widget class="QTextEdit" name="creditsText">
+ <property name="readOnly">
+ <bool>true</bool>
+ </property>
+ <property name="textInteractionFlags">
+ <set>Qt::TextBrowserInteraction</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QWidget" name="licenseTab">
+ <attribute name="title">
+ <string>License</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_3">
+ <item>
+ <widget class="QTextEdit" name="licenseText">
+ <property name="minimumSize">
+ <size>
+ <width>0</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="font">
+ <font>
+ <family>DejaVu Sans Mono</family>
+ </font>
+ </property>
+ <property name="readOnly">
+ <bool>true</bool>
+ </property>
+ <property name="textInteractionFlags">
+ <set>Qt::TextBrowserInteraction</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QWidget" name="forkingTab">
+ <attribute name="title">
+ <string>Forking/Redistribution</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_4">
+ <item>
+ <widget class="QTextEdit" name="redistributionText">
+ <property name="textInteractionFlags">
+ <set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </widget>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_2">
+ <item>
+ <widget class="QPushButton" name="aboutQt">
+ <property name="autoFillBackground">
+ <bool>false</bool>
+ </property>
+ <property name="text">
+ <string>About Qt</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_3">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QPushButton" name="closeButton">
+ <property name="text">
+ <string>Close</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ <tabstops>
+ <tabstop>tabWidget</tabstop>
+ <tabstop>creditsText</tabstop>
+ <tabstop>licenseText</tabstop>
+ <tabstop>redistributionText</tabstop>
+ <tabstop>aboutQt</tabstop>
+ <tabstop>closeButton</tabstop>
+ </tabstops>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/launcher/ui/dialogs/CopyInstanceDialog.cpp b/launcher/ui/dialogs/CopyInstanceDialog.cpp
new file mode 100644
index 00000000..e5113981
--- /dev/null
+++ b/launcher/ui/dialogs/CopyInstanceDialog.cpp
@@ -0,0 +1,144 @@
+/* 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 <QLayout>
+#include <QPushButton>
+
+#include "Application.h"
+#include "CopyInstanceDialog.h"
+#include "ui_CopyInstanceDialog.h"
+
+#include "ui/dialogs/IconPickerDialog.h"
+
+#include "BaseVersion.h"
+#include "icons/IconList.h"
+#include "tasks/Task.h"
+#include "BaseInstance.h"
+#include "InstanceList.h"
+
+CopyInstanceDialog::CopyInstanceDialog(InstancePtr original, QWidget *parent)
+ :QDialog(parent), ui(new Ui::CopyInstanceDialog), m_original(original)
+{
+ ui->setupUi(this);
+ resize(minimumSizeHint());
+ layout()->setSizeConstraint(QLayout::SetFixedSize);
+
+ InstIconKey = original->iconKey();
+ ui->iconButton->setIcon(APPLICATION->icons()->getIcon(InstIconKey));
+ ui->instNameTextBox->setText(original->name());
+ ui->instNameTextBox->setFocus();
+ auto groups = APPLICATION->instances()->getGroups().toSet();
+ auto groupList = QStringList(groups.toList());
+ groupList.sort(Qt::CaseInsensitive);
+ groupList.removeOne("");
+ groupList.push_front("");
+ ui->groupBox->addItems(groupList);
+ int index = groupList.indexOf(APPLICATION->instances()->getInstanceGroup(m_original->id()));
+ if(index == -1)
+ {
+ index = 0;
+ }
+ ui->groupBox->setCurrentIndex(index);
+ ui->groupBox->lineEdit()->setPlaceholderText(tr("No group"));
+ ui->copySavesCheckbox->setChecked(m_copySaves);
+ ui->keepPlaytimeCheckbox->setChecked(m_keepPlaytime);
+}
+
+CopyInstanceDialog::~CopyInstanceDialog()
+{
+ delete ui;
+}
+
+void CopyInstanceDialog::updateDialogState()
+{
+ auto allowOK = !instName().isEmpty();
+ auto OkButton = ui->buttonBox->button(QDialogButtonBox::Ok);
+ if(OkButton->isEnabled() != allowOK)
+ {
+ OkButton->setEnabled(allowOK);
+ }
+}
+
+QString CopyInstanceDialog::instName() const
+{
+ auto result = ui->instNameTextBox->text().trimmed();
+ if(result.size())
+ {
+ return result;
+ }
+ return QString();
+}
+
+QString CopyInstanceDialog::iconKey() const
+{
+ return InstIconKey;
+}
+
+QString CopyInstanceDialog::instGroup() const
+{
+ return ui->groupBox->currentText();
+}
+
+void CopyInstanceDialog::on_iconButton_clicked()
+{
+ IconPickerDialog dlg(this);
+ dlg.execWithSelection(InstIconKey);
+
+ if (dlg.result() == QDialog::Accepted)
+ {
+ InstIconKey = dlg.selectedIconKey;
+ ui->iconButton->setIcon(APPLICATION->icons()->getIcon(InstIconKey));
+ }
+}
+
+void CopyInstanceDialog::on_instNameTextBox_textChanged(const QString &arg1)
+{
+ updateDialogState();
+}
+
+bool CopyInstanceDialog::shouldCopySaves() const
+{
+ return m_copySaves;
+}
+
+void CopyInstanceDialog::on_copySavesCheckbox_stateChanged(int state)
+{
+ if(state == Qt::Unchecked)
+ {
+ m_copySaves = false;
+ }
+ else if(state == Qt::Checked)
+ {
+ m_copySaves = true;
+ }
+}
+
+bool CopyInstanceDialog::shouldKeepPlaytime() const
+{
+ return m_keepPlaytime;
+}
+
+
+void CopyInstanceDialog::on_keepPlaytimeCheckbox_stateChanged(int state)
+{
+ if(state == Qt::Unchecked)
+ {
+ m_keepPlaytime = false;
+ }
+ else if(state == Qt::Checked)
+ {
+ m_keepPlaytime = true;
+ }
+}
diff --git a/launcher/ui/dialogs/CopyInstanceDialog.h b/launcher/ui/dialogs/CopyInstanceDialog.h
new file mode 100644
index 00000000..bf3cd920
--- /dev/null
+++ b/launcher/ui/dialogs/CopyInstanceDialog.h
@@ -0,0 +1,58 @@
+/* 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 <QDialog>
+#include "BaseVersion.h"
+#include <BaseInstance.h>
+
+class BaseInstance;
+
+namespace Ui
+{
+class CopyInstanceDialog;
+}
+
+class CopyInstanceDialog : public QDialog
+{
+ Q_OBJECT
+
+public:
+ explicit CopyInstanceDialog(InstancePtr original, QWidget *parent = 0);
+ ~CopyInstanceDialog();
+
+ void updateDialogState();
+
+ QString instName() const;
+ QString instGroup() const;
+ QString iconKey() const;
+ bool shouldCopySaves() const;
+ bool shouldKeepPlaytime() const;
+
+private
+slots:
+ void on_iconButton_clicked();
+ void on_instNameTextBox_textChanged(const QString &arg1);
+ void on_copySavesCheckbox_stateChanged(int state);
+ void on_keepPlaytimeCheckbox_stateChanged(int state);
+
+private:
+ Ui::CopyInstanceDialog *ui;
+ QString InstIconKey;
+ InstancePtr m_original;
+ bool m_copySaves = true;
+ bool m_keepPlaytime = true;
+};
diff --git a/launcher/ui/dialogs/CopyInstanceDialog.ui b/launcher/ui/dialogs/CopyInstanceDialog.ui
new file mode 100644
index 00000000..f4b191e2
--- /dev/null
+++ b/launcher/ui/dialogs/CopyInstanceDialog.ui
@@ -0,0 +1,182 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>CopyInstanceDialog</class>
+ <widget class="QDialog" name="CopyInstanceDialog">
+ <property name="windowModality">
+ <enum>Qt::ApplicationModal</enum>
+ </property>
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>345</width>
+ <height>323</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Copy Instance</string>
+ </property>
+ <property name="windowIcon">
+ <iconset>
+ <normaloff>:/icons/toolbar/copy</normaloff>:/icons/toolbar/copy</iconset>
+ </property>
+ <property name="modal">
+ <bool>true</bool>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <layout class="QHBoxLayout" name="iconBtnLayout">
+ <item>
+ <spacer name="iconBtnLeftSpacer">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QToolButton" name="iconButton">
+ <property name="icon">
+ <iconset>
+ <normaloff>:/icons/instances/grass</normaloff>:/icons/instances/grass</iconset>
+ </property>
+ <property name="iconSize">
+ <size>
+ <width>80</width>
+ <height>80</height>
+ </size>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="iconBtnRightSpacer">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QLineEdit" name="instNameTextBox">
+ <property name="placeholderText">
+ <string>Name</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="Line" name="line">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <layout class="QGridLayout" name="gridLayout">
+ <item row="0" column="0">
+ <widget class="QLabel" name="labelVersion_3">
+ <property name="text">
+ <string>&amp;Group</string>
+ </property>
+ <property name="buddy">
+ <cstring>groupBox</cstring>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1" colspan="2">
+ <widget class="QComboBox" name="groupBox">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="editable">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="copySavesCheckbox">
+ <property name="text">
+ <string>Copy saves</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="keepPlaytimeCheckbox">
+ <property name="text">
+ <string>Keep play time</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QDialogButtonBox" name="buttonBox">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="standardButtons">
+ <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <tabstops>
+ <tabstop>iconButton</tabstop>
+ <tabstop>instNameTextBox</tabstop>
+ <tabstop>groupBox</tabstop>
+ <tabstop>copySavesCheckbox</tabstop>
+ <tabstop>keepPlaytimeCheckbox</tabstop>
+ </tabstops>
+ <resources>
+ <include location="../../graphics.qrc"/>
+ </resources>
+ <connections>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>accepted()</signal>
+ <receiver>CopyInstanceDialog</receiver>
+ <slot>accept()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>248</x>
+ <y>254</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>157</x>
+ <y>274</y>
+ </hint>
+ </hints>
+ </connection>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>rejected()</signal>
+ <receiver>CopyInstanceDialog</receiver>
+ <slot>reject()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>316</x>
+ <y>260</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>286</x>
+ <y>274</y>
+ </hint>
+ </hints>
+ </connection>
+ </connections>
+</ui>
diff --git a/launcher/ui/dialogs/CustomMessageBox.cpp b/launcher/ui/dialogs/CustomMessageBox.cpp
new file mode 100644
index 00000000..19029f68
--- /dev/null
+++ b/launcher/ui/dialogs/CustomMessageBox.cpp
@@ -0,0 +1,35 @@
+/* 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 "CustomMessageBox.h"
+
+namespace CustomMessageBox
+{
+QMessageBox *selectable(QWidget *parent, const QString &title, const QString &text,
+ QMessageBox::Icon icon, QMessageBox::StandardButtons buttons,
+ QMessageBox::StandardButton defaultButton)
+{
+ QMessageBox *messageBox = new QMessageBox(parent);
+ messageBox->setWindowTitle(title);
+ messageBox->setText(text);
+ messageBox->setStandardButtons(buttons);
+ messageBox->setDefaultButton(defaultButton);
+ messageBox->setTextInteractionFlags(Qt::TextSelectableByMouse);
+ messageBox->setIcon(icon);
+ messageBox->setTextInteractionFlags(Qt::TextBrowserInteraction);
+
+ return messageBox;
+}
+}
diff --git a/launcher/ui/dialogs/CustomMessageBox.h b/launcher/ui/dialogs/CustomMessageBox.h
new file mode 100644
index 00000000..712c6518
--- /dev/null
+++ b/launcher/ui/dialogs/CustomMessageBox.h
@@ -0,0 +1,26 @@
+/* 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 <QMessageBox>
+
+namespace CustomMessageBox
+{
+QMessageBox *selectable(QWidget *parent, const QString &title, const QString &text,
+ QMessageBox::Icon icon = QMessageBox::NoIcon,
+ QMessageBox::StandardButtons buttons = QMessageBox::Ok,
+ QMessageBox::StandardButton defaultButton = QMessageBox::NoButton);
+}
diff --git a/launcher/ui/dialogs/EditAccountDialog.cpp b/launcher/ui/dialogs/EditAccountDialog.cpp
new file mode 100644
index 00000000..002c064b
--- /dev/null
+++ b/launcher/ui/dialogs/EditAccountDialog.cpp
@@ -0,0 +1,61 @@
+/* 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 "EditAccountDialog.h"
+#include "ui_EditAccountDialog.h"
+#include <DesktopServices.h>
+#include <QUrl>
+
+EditAccountDialog::EditAccountDialog(const QString &text, QWidget *parent, int flags)
+ : QDialog(parent), ui(new Ui::EditAccountDialog)
+{
+ ui->setupUi(this);
+
+ ui->label->setText(text);
+ ui->label->setVisible(!text.isEmpty());
+
+ ui->userTextBox->setEnabled(flags & UsernameField);
+ ui->passTextBox->setEnabled(flags & PasswordField);
+}
+
+EditAccountDialog::~EditAccountDialog()
+{
+ delete ui;
+}
+
+void EditAccountDialog::on_label_linkActivated(const QString &link)
+{
+ DesktopServices::openUrl(QUrl(link));
+}
+
+void EditAccountDialog::setUsername(const QString & user) const
+{
+ ui->userTextBox->setText(user);
+}
+
+QString EditAccountDialog::username() const
+{
+ return ui->userTextBox->text();
+}
+
+void EditAccountDialog::setPassword(const QString & pass) const
+{
+ ui->passTextBox->setText(pass);
+}
+
+QString EditAccountDialog::password() const
+{
+ return ui->passTextBox->text();
+}
diff --git a/launcher/ui/dialogs/EditAccountDialog.h b/launcher/ui/dialogs/EditAccountDialog.h
new file mode 100644
index 00000000..6b5eb4aa
--- /dev/null
+++ b/launcher/ui/dialogs/EditAccountDialog.h
@@ -0,0 +1,56 @@
+/* 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 <QDialog>
+
+namespace Ui
+{
+class EditAccountDialog;
+}
+
+class EditAccountDialog : public QDialog
+{
+ Q_OBJECT
+
+public:
+ explicit EditAccountDialog(const QString &text = "", QWidget *parent = 0,
+ int flags = UsernameField | PasswordField);
+ ~EditAccountDialog();
+
+ void setUsername(const QString & user) const;
+ void setPassword(const QString & pass) const;
+
+ QString username() const;
+ QString password() const;
+
+ enum Flags
+ {
+ NoFlags = 0,
+
+ //! Specifies that the dialog should have a username field.
+ UsernameField,
+
+ //! Specifies that the dialog should have a password field.
+ PasswordField,
+ };
+
+private slots:
+ void on_label_linkActivated(const QString &link);
+
+private:
+ Ui::EditAccountDialog *ui;
+};
diff --git a/launcher/ui/dialogs/EditAccountDialog.ui b/launcher/ui/dialogs/EditAccountDialog.ui
new file mode 100644
index 00000000..e87509bc
--- /dev/null
+++ b/launcher/ui/dialogs/EditAccountDialog.ui
@@ -0,0 +1,94 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>EditAccountDialog</class>
+ <widget class="QDialog" name="EditAccountDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>400</width>
+ <height>148</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Login</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <widget class="QLabel" name="label">
+ <property name="text">
+ <string notr="true">Message label placeholder.</string>
+ </property>
+ <property name="textFormat">
+ <enum>Qt::RichText</enum>
+ </property>
+ <property name="textInteractionFlags">
+ <set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLineEdit" name="userTextBox">
+ <property name="placeholderText">
+ <string>Email</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLineEdit" name="passTextBox">
+ <property name="echoMode">
+ <enum>QLineEdit::Password</enum>
+ </property>
+ <property name="placeholderText">
+ <string>Password</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QDialogButtonBox" name="buttonBox">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="standardButtons">
+ <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>accepted()</signal>
+ <receiver>EditAccountDialog</receiver>
+ <slot>accept()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>248</x>
+ <y>254</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>157</x>
+ <y>274</y>
+ </hint>
+ </hints>
+ </connection>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>rejected()</signal>
+ <receiver>EditAccountDialog</receiver>
+ <slot>reject()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>316</x>
+ <y>260</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>286</x>
+ <y>274</y>
+ </hint>
+ </hints>
+ </connection>
+ </connections>
+</ui>
diff --git a/launcher/ui/dialogs/ExportInstanceDialog.cpp b/launcher/ui/dialogs/ExportInstanceDialog.cpp
new file mode 100644
index 00000000..1a164875
--- /dev/null
+++ b/launcher/ui/dialogs/ExportInstanceDialog.cpp
@@ -0,0 +1,482 @@
+/* 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 "ExportInstanceDialog.h"
+#include "ui_ExportInstanceDialog.h"
+#include <BaseInstance.h>
+#include <MMCZip.h>
+#include <QFileDialog>
+#include <QMessageBox>
+#include <qfilesystemmodel.h>
+
+#include <QSortFilterProxyModel>
+#include <QDebug>
+#include <qstack.h>
+#include <QSaveFile>
+#include "MMCStrings.h"
+#include "SeparatorPrefixTree.h"
+#include "Application.h"
+#include <icons/IconList.h>
+#include <FileSystem.h>
+
+class PackIgnoreProxy : public QSortFilterProxyModel
+{
+ Q_OBJECT
+
+public:
+ PackIgnoreProxy(InstancePtr instance, QObject *parent) : QSortFilterProxyModel(parent)
+ {
+ m_instance = instance;
+ }
+ // NOTE: Sadly, we have to do sorting ourselves.
+ bool lessThan(const QModelIndex &left, const QModelIndex &right) const
+ {
+ QFileSystemModel *fsm = qobject_cast<QFileSystemModel *>(sourceModel());
+ if (!fsm)
+ {
+ return QSortFilterProxyModel::lessThan(left, right);
+ }
+ bool asc = sortOrder() == Qt::AscendingOrder ? true : false;
+
+ QFileInfo leftFileInfo = fsm->fileInfo(left);
+ QFileInfo rightFileInfo = fsm->fileInfo(right);
+
+ if (!leftFileInfo.isDir() && rightFileInfo.isDir())
+ {
+ return !asc;
+ }
+ if (leftFileInfo.isDir() && !rightFileInfo.isDir())
+ {
+ return asc;
+ }
+
+ // sort and proxy model breaks the original model...
+ if (sortColumn() == 0)
+ {
+ return Strings::naturalCompare(leftFileInfo.fileName(), rightFileInfo.fileName(),
+ Qt::CaseInsensitive) < 0;
+ }
+ if (sortColumn() == 1)
+ {
+ auto leftSize = leftFileInfo.size();
+ auto rightSize = rightFileInfo.size();
+ if ((leftSize == rightSize) || (leftFileInfo.isDir() && rightFileInfo.isDir()))
+ {
+ return Strings::naturalCompare(leftFileInfo.fileName(),
+ rightFileInfo.fileName(),
+ Qt::CaseInsensitive) < 0
+ ? asc
+ : !asc;
+ }
+ return leftSize < rightSize;
+ }
+ return QSortFilterProxyModel::lessThan(left, right);
+ }
+
+ virtual Qt::ItemFlags flags(const QModelIndex &index) const
+ {
+ if (!index.isValid())
+ return Qt::NoItemFlags;
+
+ auto sourceIndex = mapToSource(index);
+ Qt::ItemFlags flags = sourceIndex.flags();
+ if (index.column() == 0)
+ {
+ flags |= Qt::ItemIsUserCheckable;
+ if (sourceIndex.model()->hasChildren(sourceIndex))
+ {
+ flags |= Qt::ItemIsTristate;
+ }
+ }
+
+ return flags;
+ }
+
+ virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const
+ {
+ QModelIndex sourceIndex = mapToSource(index);
+
+ if (index.column() == 0 && role == Qt::CheckStateRole)
+ {
+ QFileSystemModel *fsm = qobject_cast<QFileSystemModel *>(sourceModel());
+ auto blockedPath = relPath(fsm->filePath(sourceIndex));
+ auto cover = blocked.cover(blockedPath);
+ if (!cover.isNull())
+ {
+ return QVariant(Qt::Unchecked);
+ }
+ else if (blocked.exists(blockedPath))
+ {
+ return QVariant(Qt::PartiallyChecked);
+ }
+ else
+ {
+ return QVariant(Qt::Checked);
+ }
+ }
+
+ return sourceIndex.data(role);
+ }
+
+ virtual bool setData(const QModelIndex &index, const QVariant &value,
+ int role = Qt::EditRole)
+ {
+ if (index.column() == 0 && role == Qt::CheckStateRole)
+ {
+ Qt::CheckState state = static_cast<Qt::CheckState>(value.toInt());
+ return setFilterState(index, state);
+ }
+
+ QModelIndex sourceIndex = mapToSource(index);
+ return QSortFilterProxyModel::sourceModel()->setData(sourceIndex, value, role);
+ }
+
+ QString relPath(const QString &path) const
+ {
+ QString prefix = QDir().absoluteFilePath(m_instance->instanceRoot());
+ prefix += '/';
+ if (!path.startsWith(prefix))
+ {
+ return QString();
+ }
+ return path.mid(prefix.size());
+ }
+
+ bool setFilterState(QModelIndex index, Qt::CheckState state)
+ {
+ QFileSystemModel *fsm = qobject_cast<QFileSystemModel *>(sourceModel());
+
+ if (!fsm)
+ {
+ return false;
+ }
+
+ QModelIndex sourceIndex = mapToSource(index);
+ auto blockedPath = relPath(fsm->filePath(sourceIndex));
+ bool changed = false;
+ if (state == Qt::Unchecked)
+ {
+ // blocking a path
+ auto &node = blocked.insert(blockedPath);
+ // get rid of all blocked nodes below
+ node.clear();
+ changed = true;
+ }
+ else if (state == Qt::Checked || state == Qt::PartiallyChecked)
+ {
+ if (!blocked.remove(blockedPath))
+ {
+ auto cover = blocked.cover(blockedPath);
+ qDebug() << "Blocked by cover" << cover;
+ // uncover
+ blocked.remove(cover);
+ // block all contents, except for any cover
+ QModelIndex rootIndex =
+ fsm->index(FS::PathCombine(m_instance->instanceRoot(), cover));
+ QModelIndex doing = rootIndex;
+ int row = 0;
+ QStack<QModelIndex> todo;
+ while (1)
+ {
+ auto node = doing.child(row, 0);
+ if (!node.isValid())
+ {
+ if (!todo.size())
+ {
+ break;
+ }
+ else
+ {
+ doing = todo.pop();
+ row = 0;
+ continue;
+ }
+ }
+ auto relpath = relPath(fsm->filePath(node));
+ if (blockedPath.startsWith(relpath)) // cover found?
+ {
+ // continue processing cover later
+ todo.push(node);
+ }
+ else
+ {
+ // or just block this one.
+ blocked.insert(relpath);
+ }
+ row++;
+ }
+ }
+ changed = true;
+ }
+ if (changed)
+ {
+ // update the thing
+ emit dataChanged(index, index, {Qt::CheckStateRole});
+ // update everything above index
+ QModelIndex up = index.parent();
+ while (1)
+ {
+ if (!up.isValid())
+ break;
+ emit dataChanged(up, up, {Qt::CheckStateRole});
+ up = up.parent();
+ }
+ // and everything below the index
+ QModelIndex doing = index;
+ int row = 0;
+ QStack<QModelIndex> todo;
+ while (1)
+ {
+ auto node = doing.child(row, 0);
+ if (!node.isValid())
+ {
+ if (!todo.size())
+ {
+ break;
+ }
+ else
+ {
+ doing = todo.pop();
+ row = 0;
+ continue;
+ }
+ }
+ emit dataChanged(node, node, {Qt::CheckStateRole});
+ todo.push(node);
+ row++;
+ }
+ // siblings and unrelated nodes are ignored
+ }
+ return true;
+ }
+
+ bool shouldExpand(QModelIndex index)
+ {
+ QModelIndex sourceIndex = mapToSource(index);
+ QFileSystemModel *fsm = qobject_cast<QFileSystemModel *>(sourceModel());
+ if (!fsm)
+ {
+ return false;
+ }
+ auto blockedPath = relPath(fsm->filePath(sourceIndex));
+ auto found = blocked.find(blockedPath);
+ if(found)
+ {
+ return !found->leaf();
+ }
+ return false;
+ }
+
+ void setBlockedPaths(QStringList paths)
+ {
+ beginResetModel();
+ blocked.clear();
+ blocked.insert(paths);
+ endResetModel();
+ }
+
+ const SeparatorPrefixTree<'/'> & blockedPaths() const
+ {
+ return blocked;
+ }
+
+protected:
+ bool filterAcceptsColumn(int source_column, const QModelIndex &source_parent) const
+ {
+ Q_UNUSED(source_parent)
+
+ // adjust the columns you want to filter out here
+ // return false for those that will be hidden
+ if (source_column == 2 || source_column == 3)
+ return false;
+
+ return true;
+ }
+
+private:
+ InstancePtr m_instance;
+ SeparatorPrefixTree<'/'> blocked;
+};
+
+ExportInstanceDialog::ExportInstanceDialog(InstancePtr instance, QWidget *parent)
+ : QDialog(parent), ui(new Ui::ExportInstanceDialog), m_instance(instance)
+{
+ ui->setupUi(this);
+ auto model = new QFileSystemModel(this);
+ proxyModel = new PackIgnoreProxy(m_instance, this);
+ loadPackIgnore();
+ proxyModel->setSourceModel(model);
+ auto root = instance->instanceRoot();
+ ui->treeView->setModel(proxyModel);
+ ui->treeView->setRootIndex(proxyModel->mapFromSource(model->index(root)));
+ ui->treeView->sortByColumn(0, Qt::AscendingOrder);
+
+ connect(proxyModel, SIGNAL(rowsInserted(QModelIndex,int,int)), SLOT(rowsInserted(QModelIndex,int,int)));
+
+ model->setFilter(QDir::AllEntries | QDir::NoDotAndDotDot | QDir::AllDirs | QDir::Hidden);
+ model->setRootPath(root);
+ auto headerView = ui->treeView->header();
+ headerView->setSectionResizeMode(QHeaderView::ResizeToContents);
+ headerView->setSectionResizeMode(0, QHeaderView::Stretch);
+}
+
+ExportInstanceDialog::~ExportInstanceDialog()
+{
+ delete ui;
+}
+
+/// Save icon to instance's folder is needed
+void SaveIcon(InstancePtr m_instance)
+{
+ auto iconKey = m_instance->iconKey();
+ auto iconList = APPLICATION->icons();
+ auto mmcIcon = iconList->icon(iconKey);
+ if(!mmcIcon || mmcIcon->isBuiltIn()) {
+ return;
+ }
+ auto path = mmcIcon->getFilePath();
+ if(!path.isNull()) {
+ QFileInfo inInfo (path);
+ FS::copy(path, FS::PathCombine(m_instance->instanceRoot(), inInfo.fileName())) ();
+ return;
+ }
+ auto & image = mmcIcon->m_images[mmcIcon->type()];
+ auto & icon = image.icon;
+ auto sizes = icon.availableSizes();
+ if(sizes.size() == 0)
+ {
+ return;
+ }
+ auto areaOf = [](QSize size)
+ {
+ return size.width() * size.height();
+ };
+ QSize largest = sizes[0];
+ // find variant with largest area
+ for(auto size: sizes)
+ {
+ if(areaOf(largest) < areaOf(size))
+ {
+ largest = size;
+ }
+ }
+ auto pixmap = icon.pixmap(largest);
+ pixmap.save(FS::PathCombine(m_instance->instanceRoot(), iconKey + ".png"));
+}
+
+bool ExportInstanceDialog::doExport()
+{
+ auto name = FS::RemoveInvalidFilenameChars(m_instance->name());
+
+ const QString output = QFileDialog::getSaveFileName(
+ this, tr("Export %1").arg(m_instance->name()),
+ FS::PathCombine(QDir::homePath(), name + ".zip"), "Zip (*.zip)", nullptr, QFileDialog::DontConfirmOverwrite);
+ if (output.isEmpty())
+ {
+ return false;
+ }
+ if (QFile::exists(output))
+ {
+ int ret =
+ QMessageBox::question(this, tr("Overwrite?"),
+ tr("This file already exists. Do you want to overwrite it?"),
+ QMessageBox::No, QMessageBox::Yes);
+ if (ret == QMessageBox::No)
+ {
+ return false;
+ }
+ }
+
+ SaveIcon(m_instance);
+
+ auto & blocked = proxyModel->blockedPaths();
+ using std::placeholders::_1;
+ if (!JlCompress::compressDir(output, m_instance->instanceRoot(), name, std::bind(&SeparatorPrefixTree<'/'>::covers, blocked, _1)))
+ {
+ QMessageBox::warning(this, tr("Error"), tr("Unable to export instance"));
+ return false;
+ }
+ return true;
+}
+
+void ExportInstanceDialog::done(int result)
+{
+ savePackIgnore();
+ if (result == QDialog::Accepted)
+ {
+ if (doExport())
+ {
+ QDialog::done(QDialog::Accepted);
+ return;
+ }
+ else
+ {
+ return;
+ }
+ }
+ QDialog::done(result);
+}
+
+void ExportInstanceDialog::rowsInserted(QModelIndex parent, int top, int bottom)
+{
+ //WARNING: possible off-by-one?
+ for(int i = top; i < bottom; i++)
+ {
+ auto node = parent.child(i, 0);
+ if(proxyModel->shouldExpand(node))
+ {
+ auto expNode = node.parent();
+ if(!expNode.isValid())
+ {
+ continue;
+ }
+ ui->treeView->expand(node);
+ }
+ }
+}
+
+QString ExportInstanceDialog::ignoreFileName()
+{
+ return FS::PathCombine(m_instance->instanceRoot(), ".packignore");
+}
+
+void ExportInstanceDialog::loadPackIgnore()
+{
+ auto filename = ignoreFileName();
+ QFile ignoreFile(filename);
+ if(!ignoreFile.open(QIODevice::ReadOnly))
+ {
+ return;
+ }
+ auto data = ignoreFile.readAll();
+ auto string = QString::fromUtf8(data);
+ proxyModel->setBlockedPaths(string.split('\n', QString::SkipEmptyParts));
+}
+
+void ExportInstanceDialog::savePackIgnore()
+{
+ auto data = proxyModel->blockedPaths().toStringList().join('\n').toUtf8();
+ auto filename = ignoreFileName();
+ try
+ {
+ FS::write(filename, data);
+ }
+ catch (const Exception &e)
+ {
+ qWarning() << e.cause();
+ }
+}
+
+#include "ExportInstanceDialog.moc"
diff --git a/launcher/ui/dialogs/ExportInstanceDialog.h b/launcher/ui/dialogs/ExportInstanceDialog.h
new file mode 100644
index 00000000..dea02d1b
--- /dev/null
+++ b/launcher/ui/dialogs/ExportInstanceDialog.h
@@ -0,0 +1,54 @@
+/* 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 <QDialog>
+#include <QModelIndex>
+#include <memory>
+
+class BaseInstance;
+class PackIgnoreProxy;
+typedef std::shared_ptr<BaseInstance> InstancePtr;
+
+namespace Ui
+{
+class ExportInstanceDialog;
+}
+
+class ExportInstanceDialog : public QDialog
+{
+ Q_OBJECT
+
+public:
+ explicit ExportInstanceDialog(InstancePtr instance, QWidget *parent = 0);
+ ~ExportInstanceDialog();
+
+ virtual void done(int result);
+
+private:
+ bool doExport();
+ void loadPackIgnore();
+ void savePackIgnore();
+ QString ignoreFileName();
+
+private:
+ Ui::ExportInstanceDialog *ui;
+ InstancePtr m_instance;
+ PackIgnoreProxy * proxyModel;
+
+private slots:
+ void rowsInserted(QModelIndex parent, int top, int bottom);
+};
diff --git a/launcher/ui/dialogs/ExportInstanceDialog.ui b/launcher/ui/dialogs/ExportInstanceDialog.ui
new file mode 100644
index 00000000..bcd4e84a
--- /dev/null
+++ b/launcher/ui/dialogs/ExportInstanceDialog.ui
@@ -0,0 +1,83 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>ExportInstanceDialog</class>
+ <widget class="QDialog" name="ExportInstanceDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>720</width>
+ <height>625</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Export Instance</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <widget class="QTreeView" name="treeView">
+ <property name="alternatingRowColors">
+ <bool>true</bool>
+ </property>
+ <property name="selectionMode">
+ <enum>QAbstractItemView::ExtendedSelection</enum>
+ </property>
+ <property name="sortingEnabled">
+ <bool>true</bool>
+ </property>
+ <attribute name="headerStretchLastSection">
+ <bool>false</bool>
+ </attribute>
+ </widget>
+ </item>
+ <item>
+ <widget class="QDialogButtonBox" name="buttonBox">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="standardButtons">
+ <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <tabstops>
+ <tabstop>treeView</tabstop>
+ </tabstops>
+ <resources/>
+ <connections>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>accepted()</signal>
+ <receiver>ExportInstanceDialog</receiver>
+ <slot>accept()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>248</x>
+ <y>254</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>157</x>
+ <y>274</y>
+ </hint>
+ </hints>
+ </connection>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>rejected()</signal>
+ <receiver>ExportInstanceDialog</receiver>
+ <slot>reject()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>316</x>
+ <y>260</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>286</x>
+ <y>274</y>
+ </hint>
+ </hints>
+ </connection>
+ </connections>
+</ui>
diff --git a/launcher/ui/dialogs/IconPickerDialog.cpp b/launcher/ui/dialogs/IconPickerDialog.cpp
new file mode 100644
index 00000000..fcb645db
--- /dev/null
+++ b/launcher/ui/dialogs/IconPickerDialog.cpp
@@ -0,0 +1,163 @@
+/* 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 <QKeyEvent>
+#include <QPushButton>
+#include <QFileDialog>
+
+#include "Application.h"
+
+#include "IconPickerDialog.h"
+#include "ui_IconPickerDialog.h"
+
+#include "ui/instanceview/InstanceDelegate.h"
+
+#include "icons/IconList.h"
+#include "icons/IconUtils.h"
+#include <DesktopServices.h>
+
+IconPickerDialog::IconPickerDialog(QWidget *parent)
+ : QDialog(parent), ui(new Ui::IconPickerDialog)
+{
+ ui->setupUi(this);
+ setWindowModality(Qt::WindowModal);
+
+ auto contentsWidget = ui->iconView;
+ contentsWidget->setViewMode(QListView::IconMode);
+ contentsWidget->setFlow(QListView::LeftToRight);
+ contentsWidget->setIconSize(QSize(48, 48));
+ contentsWidget->setMovement(QListView::Static);
+ contentsWidget->setResizeMode(QListView::Adjust);
+ contentsWidget->setSelectionMode(QAbstractItemView::SingleSelection);
+ contentsWidget->setSpacing(5);
+ contentsWidget->setWordWrap(false);
+ contentsWidget->setWrapping(true);
+ contentsWidget->setUniformItemSizes(true);
+ contentsWidget->setTextElideMode(Qt::ElideRight);
+ contentsWidget->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
+ contentsWidget->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
+ contentsWidget->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+ contentsWidget->setItemDelegate(new ListViewDelegate());
+
+ // contentsWidget->setAcceptDrops(true);
+ contentsWidget->setDropIndicatorShown(true);
+ contentsWidget->viewport()->setAcceptDrops(true);
+ contentsWidget->setDragDropMode(QAbstractItemView::DropOnly);
+ contentsWidget->setDefaultDropAction(Qt::CopyAction);
+
+ contentsWidget->installEventFilter(this);
+
+ contentsWidget->setModel(APPLICATION->icons().get());
+
+ // NOTE: ResetRole forces the button to be on the left, while the OK/Cancel ones are on the right. We win.
+ auto buttonAdd = ui->buttonBox->addButton(tr("Add Icon"), QDialogButtonBox::ResetRole);
+ auto buttonRemove = ui->buttonBox->addButton(tr("Remove Icon"), QDialogButtonBox::ResetRole);
+
+ connect(buttonAdd, SIGNAL(clicked(bool)), SLOT(addNewIcon()));
+ connect(buttonRemove, SIGNAL(clicked(bool)), SLOT(removeSelectedIcon()));
+
+ connect(contentsWidget, SIGNAL(doubleClicked(QModelIndex)), SLOT(activated(QModelIndex)));
+
+ connect(contentsWidget->selectionModel(), SIGNAL(selectionChanged(QItemSelection, QItemSelection)), SLOT(selectionChanged(QItemSelection, QItemSelection)));
+
+ auto buttonFolder = ui->buttonBox->addButton(tr("Open Folder"), QDialogButtonBox::ResetRole);
+ connect(buttonFolder, &QPushButton::clicked, this, &IconPickerDialog::openFolder);
+}
+
+bool IconPickerDialog::eventFilter(QObject *obj, QEvent *evt)
+{
+ if (obj != ui->iconView)
+ return QDialog::eventFilter(obj, evt);
+ if (evt->type() != QEvent::KeyPress)
+ {
+ return QDialog::eventFilter(obj, evt);
+ }
+ QKeyEvent *keyEvent = static_cast<QKeyEvent *>(evt);
+ switch (keyEvent->key())
+ {
+ case Qt::Key_Delete:
+ removeSelectedIcon();
+ return true;
+ case Qt::Key_Plus:
+ addNewIcon();
+ return true;
+ default:
+ break;
+ }
+ return QDialog::eventFilter(obj, evt);
+}
+
+void IconPickerDialog::addNewIcon()
+{
+ //: The title of the select icons open file dialog
+ QString selectIcons = tr("Select Icons");
+ //: The type of icon files
+ auto filter = IconUtils::getIconFilter();
+ QStringList fileNames = QFileDialog::getOpenFileNames(this, selectIcons, QString(), tr("Icons %1").arg(filter));
+ APPLICATION->icons()->installIcons(fileNames);
+}
+
+void IconPickerDialog::removeSelectedIcon()
+{
+ APPLICATION->icons()->deleteIcon(selectedIconKey);
+}
+
+void IconPickerDialog::activated(QModelIndex index)
+{
+ selectedIconKey = index.data(Qt::UserRole).toString();
+ accept();
+}
+
+void IconPickerDialog::selectionChanged(QItemSelection selected, QItemSelection deselected)
+{
+ if (selected.empty())
+ return;
+
+ QString key = selected.first().indexes().first().data(Qt::UserRole).toString();
+ if (!key.isEmpty()) {
+ selectedIconKey = key;
+ }
+}
+
+int IconPickerDialog::execWithSelection(QString selection)
+{
+ auto list = APPLICATION->icons();
+ auto contentsWidget = ui->iconView;
+ selectedIconKey = selection;
+
+ int index_nr = list->getIconIndex(selection);
+ auto model_index = list->index(index_nr);
+ contentsWidget->selectionModel()->select(
+ model_index, QItemSelectionModel::Current | QItemSelectionModel::Select);
+
+ QMetaObject::invokeMethod(this, "delayed_scroll", Qt::QueuedConnection, Q_ARG(QModelIndex, model_index));
+ return QDialog::exec();
+}
+
+void IconPickerDialog::delayed_scroll(QModelIndex model_index)
+{
+ auto contentsWidget = ui->iconView;
+ contentsWidget->scrollTo(model_index);
+}
+
+IconPickerDialog::~IconPickerDialog()
+{
+ delete ui;
+}
+
+void IconPickerDialog::openFolder()
+{
+ DesktopServices::openDirectory(APPLICATION->icons()->getDirectory(), true);
+}
diff --git a/launcher/ui/dialogs/IconPickerDialog.h b/launcher/ui/dialogs/IconPickerDialog.h
new file mode 100644
index 00000000..9af6a678
--- /dev/null
+++ b/launcher/ui/dialogs/IconPickerDialog.h
@@ -0,0 +1,49 @@
+/* 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 <QDialog>
+#include <QItemSelection>
+
+namespace Ui
+{
+class IconPickerDialog;
+}
+
+class IconPickerDialog : public QDialog
+{
+ Q_OBJECT
+
+public:
+ explicit IconPickerDialog(QWidget *parent = 0);
+ ~IconPickerDialog();
+ int execWithSelection(QString selection);
+ QString selectedIconKey;
+
+protected:
+ virtual bool eventFilter(QObject *, QEvent *);
+
+private:
+ Ui::IconPickerDialog *ui;
+
+private
+slots:
+ void selectionChanged(QItemSelection, QItemSelection);
+ void activated(QModelIndex);
+ void delayed_scroll(QModelIndex);
+ void addNewIcon();
+ void removeSelectedIcon();
+ void openFolder();
+};
diff --git a/launcher/ui/dialogs/IconPickerDialog.ui b/launcher/ui/dialogs/IconPickerDialog.ui
new file mode 100644
index 00000000..c548edfb
--- /dev/null
+++ b/launcher/ui/dialogs/IconPickerDialog.ui
@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>IconPickerDialog</class>
+ <widget class="QDialog" name="IconPickerDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>676</width>
+ <height>555</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Pick icon</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <widget class="QListView" name="iconView"/>
+ </item>
+ <item>
+ <widget class="QDialogButtonBox" name="buttonBox">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="standardButtons">
+ <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>accepted()</signal>
+ <receiver>IconPickerDialog</receiver>
+ <slot>accept()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>248</x>
+ <y>254</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>157</x>
+ <y>274</y>
+ </hint>
+ </hints>
+ </connection>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>rejected()</signal>
+ <receiver>IconPickerDialog</receiver>
+ <slot>reject()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>316</x>
+ <y>260</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>286</x>
+ <y>274</y>
+ </hint>
+ </hints>
+ </connection>
+ </connections>
+</ui>
diff --git a/launcher/ui/dialogs/LoginDialog.cpp b/launcher/ui/dialogs/LoginDialog.cpp
new file mode 100644
index 00000000..bf0806e1
--- /dev/null
+++ b/launcher/ui/dialogs/LoginDialog.cpp
@@ -0,0 +1,119 @@
+/* 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 "LoginDialog.h"
+#include "ui_LoginDialog.h"
+
+#include "minecraft/auth/AccountTask.h"
+
+#include <QtWidgets/QPushButton>
+
+LoginDialog::LoginDialog(QWidget *parent) : QDialog(parent), ui(new Ui::LoginDialog)
+{
+ ui->setupUi(this);
+ ui->progressBar->setVisible(false);
+ ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
+
+ connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
+ connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
+}
+
+LoginDialog::~LoginDialog()
+{
+ delete ui;
+}
+
+// Stage 1: User interaction
+void LoginDialog::accept()
+{
+ setUserInputsEnabled(false);
+ ui->progressBar->setVisible(true);
+
+ // Setup the login task and start it
+ m_account = MinecraftAccount::createFromUsername(ui->userTextBox->text());
+ m_loginTask = m_account->login(nullptr, ui->passTextBox->text());
+ connect(m_loginTask.get(), &Task::failed, this, &LoginDialog::onTaskFailed);
+ connect(m_loginTask.get(), &Task::succeeded, this, &LoginDialog::onTaskSucceeded);
+ connect(m_loginTask.get(), &Task::status, this, &LoginDialog::onTaskStatus);
+ connect(m_loginTask.get(), &Task::progress, this, &LoginDialog::onTaskProgress);
+ m_loginTask->start();
+}
+
+void LoginDialog::setUserInputsEnabled(bool enable)
+{
+ ui->userTextBox->setEnabled(enable);
+ ui->passTextBox->setEnabled(enable);
+ ui->buttonBox->setEnabled(enable);
+}
+
+// Enable the OK button only when both textboxes contain something.
+void LoginDialog::on_userTextBox_textEdited(const QString &newText)
+{
+ ui->buttonBox->button(QDialogButtonBox::Ok)
+ ->setEnabled(!newText.isEmpty() && !ui->passTextBox->text().isEmpty());
+}
+void LoginDialog::on_passTextBox_textEdited(const QString &newText)
+{
+ ui->buttonBox->button(QDialogButtonBox::Ok)
+ ->setEnabled(!newText.isEmpty() && !ui->userTextBox->text().isEmpty());
+}
+
+void LoginDialog::onTaskFailed(const QString &reason)
+{
+ // Set message
+ auto lines = reason.split('\n');
+ QString processed;
+ for(auto line: lines) {
+ if(line.size()) {
+ processed += "<font color='red'>" + line + "</font><br />";
+ }
+ else {
+ processed += "<br />";
+ }
+ }
+ ui->label->setText(processed);
+
+ // Re-enable user-interaction
+ setUserInputsEnabled(true);
+ ui->progressBar->setVisible(false);
+}
+
+void LoginDialog::onTaskSucceeded()
+{
+ QDialog::accept();
+}
+
+void LoginDialog::onTaskStatus(const QString &status)
+{
+ ui->label->setText(status);
+}
+
+void LoginDialog::onTaskProgress(qint64 current, qint64 total)
+{
+ ui->progressBar->setMaximum(total);
+ ui->progressBar->setValue(current);
+}
+
+// Public interface
+MinecraftAccountPtr LoginDialog::newAccount(QWidget *parent, QString msg)
+{
+ LoginDialog dlg(parent);
+ dlg.ui->label->setText(msg);
+ if (dlg.exec() == QDialog::Accepted)
+ {
+ return dlg.m_account;
+ }
+ return 0;
+}
diff --git a/launcher/ui/dialogs/LoginDialog.h b/launcher/ui/dialogs/LoginDialog.h
new file mode 100644
index 00000000..f8101ff5
--- /dev/null
+++ b/launcher/ui/dialogs/LoginDialog.h
@@ -0,0 +1,59 @@
+/* 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 <QtWidgets/QDialog>
+#include <QtCore/QEventLoop>
+
+#include "minecraft/auth/MinecraftAccount.h"
+#include "tasks/Task.h"
+
+namespace Ui
+{
+class LoginDialog;
+}
+
+class LoginDialog : public QDialog
+{
+ Q_OBJECT
+
+public:
+ ~LoginDialog();
+
+ static MinecraftAccountPtr newAccount(QWidget *parent, QString message);
+
+private:
+ explicit LoginDialog(QWidget *parent = 0);
+
+ void setUserInputsEnabled(bool enable);
+
+protected
+slots:
+ void accept();
+
+ void onTaskFailed(const QString &reason);
+ void onTaskSucceeded();
+ void onTaskStatus(const QString &status);
+ void onTaskProgress(qint64 current, qint64 total);
+
+ void on_userTextBox_textEdited(const QString &newText);
+ void on_passTextBox_textEdited(const QString &newText);
+
+private:
+ Ui::LoginDialog *ui;
+ MinecraftAccountPtr m_account;
+ Task::Ptr m_loginTask;
+};
diff --git a/launcher/ui/dialogs/LoginDialog.ui b/launcher/ui/dialogs/LoginDialog.ui
new file mode 100644
index 00000000..8fa4a45d
--- /dev/null
+++ b/launcher/ui/dialogs/LoginDialog.ui
@@ -0,0 +1,77 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>LoginDialog</class>
+ <widget class="QDialog" name="LoginDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>421</width>
+ <height>198</height>
+ </rect>
+ </property>
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="windowTitle">
+ <string>Add Account</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <widget class="QLabel" name="label">
+ <property name="text">
+ <string notr="true">Message label placeholder.</string>
+ </property>
+ <property name="textFormat">
+ <enum>Qt::RichText</enum>
+ </property>
+ <property name="textInteractionFlags">
+ <set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLineEdit" name="userTextBox">
+ <property name="placeholderText">
+ <string>Email</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLineEdit" name="passTextBox">
+ <property name="echoMode">
+ <enum>QLineEdit::Password</enum>
+ </property>
+ <property name="placeholderText">
+ <string>Password</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QProgressBar" name="progressBar">
+ <property name="value">
+ <number>24</number>
+ </property>
+ <property name="textVisible">
+ <bool>false</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QDialogButtonBox" name="buttonBox">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="standardButtons">
+ <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/launcher/ui/dialogs/MSALoginDialog.cpp b/launcher/ui/dialogs/MSALoginDialog.cpp
new file mode 100644
index 00000000..15c04061
--- /dev/null
+++ b/launcher/ui/dialogs/MSALoginDialog.cpp
@@ -0,0 +1,141 @@
+/* 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 "MSALoginDialog.h"
+#include "ui_MSALoginDialog.h"
+
+#include "minecraft/auth/AccountTask.h"
+
+#include <QtWidgets/QPushButton>
+#include <QUrl>
+
+MSALoginDialog::MSALoginDialog(QWidget *parent) : QDialog(parent), ui(new Ui::MSALoginDialog)
+{
+ ui->setupUi(this);
+ ui->progressBar->setVisible(false);
+ // ui->buttonBox->button(QDialogButtonBox::Cancel)->setEnabled(false);
+
+ connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
+ connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
+}
+
+int MSALoginDialog::exec() {
+ setUserInputsEnabled(false);
+ ui->progressBar->setVisible(true);
+
+ // Setup the login task and start it
+ m_account = MinecraftAccount::createBlankMSA();
+ m_loginTask = m_account->loginMSA(nullptr);
+ connect(m_loginTask.get(), &Task::failed, this, &MSALoginDialog::onTaskFailed);
+ connect(m_loginTask.get(), &Task::succeeded, this, &MSALoginDialog::onTaskSucceeded);
+ connect(m_loginTask.get(), &Task::status, this, &MSALoginDialog::onTaskStatus);
+ connect(m_loginTask.get(), &Task::progress, this, &MSALoginDialog::onTaskProgress);
+ connect(m_loginTask.get(), &AccountTask::showVerificationUriAndCode, this, &MSALoginDialog::showVerificationUriAndCode);
+ connect(m_loginTask.get(), &AccountTask::hideVerificationUriAndCode, this, &MSALoginDialog::hideVerificationUriAndCode);
+ connect(&m_externalLoginTimer, &QTimer::timeout, this, &MSALoginDialog::externalLoginTick);
+ m_loginTask->start();
+
+ return QDialog::exec();
+}
+
+
+MSALoginDialog::~MSALoginDialog()
+{
+ delete ui;
+}
+
+void MSALoginDialog::externalLoginTick() {
+ m_externalLoginElapsed++;
+ ui->progressBar->setValue(m_externalLoginElapsed);
+ ui->progressBar->repaint();
+
+ if(m_externalLoginElapsed >= m_externalLoginTimeout) {
+ m_externalLoginTimer.stop();
+ }
+}
+
+
+void MSALoginDialog::showVerificationUriAndCode(const QUrl& uri, const QString& code, int expiresIn) {
+ m_externalLoginElapsed = 0;
+ m_externalLoginTimeout = expiresIn;
+
+ m_externalLoginTimer.setInterval(1000);
+ m_externalLoginTimer.setSingleShot(false);
+ m_externalLoginTimer.start();
+
+ ui->progressBar->setMaximum(expiresIn);
+ ui->progressBar->setValue(m_externalLoginElapsed);
+
+ QString urlString = uri.toString();
+ QString linkString = QString("<a href=\"%1\">%2</a>").arg(urlString, urlString);
+ ui->label->setText(tr("<p>Please open up %1 in a browser and put in the code <b>%2</b> to proceed with login.</p>").arg(linkString, code));
+}
+
+void MSALoginDialog::hideVerificationUriAndCode() {
+ m_externalLoginTimer.stop();
+}
+
+void MSALoginDialog::setUserInputsEnabled(bool enable)
+{
+ ui->buttonBox->setEnabled(enable);
+}
+
+void MSALoginDialog::onTaskFailed(const QString &reason)
+{
+ // Set message
+ auto lines = reason.split('\n');
+ QString processed;
+ for(auto line: lines) {
+ if(line.size()) {
+ processed += "<font color='red'>" + line + "</font><br />";
+ }
+ else {
+ processed += "<br />";
+ }
+ }
+ ui->label->setText(processed);
+
+ // Re-enable user-interaction
+ setUserInputsEnabled(true);
+ ui->progressBar->setVisible(false);
+}
+
+void MSALoginDialog::onTaskSucceeded()
+{
+ QDialog::accept();
+}
+
+void MSALoginDialog::onTaskStatus(const QString &status)
+{
+ ui->label->setText(status);
+}
+
+void MSALoginDialog::onTaskProgress(qint64 current, qint64 total)
+{
+ ui->progressBar->setMaximum(total);
+ ui->progressBar->setValue(current);
+}
+
+// Public interface
+MinecraftAccountPtr MSALoginDialog::newAccount(QWidget *parent, QString msg)
+{
+ MSALoginDialog dlg(parent);
+ dlg.ui->label->setText(msg);
+ if (dlg.exec() == QDialog::Accepted)
+ {
+ return dlg.m_account;
+ }
+ return 0;
+}
diff --git a/launcher/ui/dialogs/MSALoginDialog.h b/launcher/ui/dialogs/MSALoginDialog.h
new file mode 100644
index 00000000..4cf146ab
--- /dev/null
+++ b/launcher/ui/dialogs/MSALoginDialog.h
@@ -0,0 +1,63 @@
+/* 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 <QtWidgets/QDialog>
+#include <QtCore/QEventLoop>
+#include <QTimer>
+
+#include "minecraft/auth/MinecraftAccount.h"
+
+namespace Ui
+{
+class MSALoginDialog;
+}
+
+class MSALoginDialog : public QDialog
+{
+ Q_OBJECT
+
+public:
+ ~MSALoginDialog();
+
+ static MinecraftAccountPtr newAccount(QWidget *parent, QString message);
+ int exec() override;
+
+private:
+ explicit MSALoginDialog(QWidget *parent = 0);
+
+ void setUserInputsEnabled(bool enable);
+
+protected
+slots:
+ void onTaskFailed(const QString &reason);
+ void onTaskSucceeded();
+ void onTaskStatus(const QString &status);
+ void onTaskProgress(qint64 current, qint64 total);
+ void showVerificationUriAndCode(const QUrl &uri, const QString &code, int expiresIn);
+ void hideVerificationUriAndCode();
+
+ void externalLoginTick();
+
+private:
+ Ui::MSALoginDialog *ui;
+ MinecraftAccountPtr m_account;
+ shared_qobject_ptr<AccountTask> m_loginTask;
+ QTimer m_externalLoginTimer;
+ int m_externalLoginElapsed = 0;
+ int m_externalLoginTimeout = 0;
+};
+
diff --git a/launcher/ui/dialogs/MSALoginDialog.ui b/launcher/ui/dialogs/MSALoginDialog.ui
new file mode 100644
index 00000000..78cbfb26
--- /dev/null
+++ b/launcher/ui/dialogs/MSALoginDialog.ui
@@ -0,0 +1,65 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>MSALoginDialog</class>
+ <widget class="QDialog" name="MSALoginDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>491</width>
+ <height>143</height>
+ </rect>
+ </property>
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="windowTitle">
+ <string>Add Microsoft Account</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <widget class="QLabel" name="label">
+ <property name="text">
+ <string notr="true">Message label placeholder.
+
+aaaaa</string>
+ </property>
+ <property name="textFormat">
+ <enum>Qt::RichText</enum>
+ </property>
+ <property name="openExternalLinks">
+ <bool>true</bool>
+ </property>
+ <property name="textInteractionFlags">
+ <set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QProgressBar" name="progressBar">
+ <property name="value">
+ <number>24</number>
+ </property>
+ <property name="textVisible">
+ <bool>false</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QDialogButtonBox" name="buttonBox">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="standardButtons">
+ <set>QDialogButtonBox::Cancel</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/launcher/ui/dialogs/NewComponentDialog.cpp b/launcher/ui/dialogs/NewComponentDialog.cpp
new file mode 100644
index 00000000..1bbafb0c
--- /dev/null
+++ b/launcher/ui/dialogs/NewComponentDialog.cpp
@@ -0,0 +1,106 @@
+/* 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 "Application.h"
+#include "NewComponentDialog.h"
+#include "ui_NewComponentDialog.h"
+
+#include <BaseVersion.h>
+#include <icons/IconList.h>
+#include <tasks/Task.h>
+#include <InstanceList.h>
+
+#include "VersionSelectDialog.h"
+#include "ProgressDialog.h"
+#include "IconPickerDialog.h"
+
+#include <QLayout>
+#include <QPushButton>
+#include <QFileDialog>
+#include <QValidator>
+
+#include <meta/Index.h>
+#include <meta/VersionList.h>
+
+NewComponentDialog::NewComponentDialog(const QString & initialName, const QString & initialUid, QWidget *parent)
+ : QDialog(parent), ui(new Ui::NewComponentDialog)
+{
+ ui->setupUi(this);
+ resize(minimumSizeHint());
+
+ ui->nameTextBox->setText(initialName);
+ ui->uidTextBox->setText(initialUid);
+
+ connect(ui->nameTextBox, &QLineEdit::textChanged, this, &NewComponentDialog::updateDialogState);
+ connect(ui->uidTextBox, &QLineEdit::textChanged, this, &NewComponentDialog::updateDialogState);
+
+ auto groups = APPLICATION->instances()->getGroups().toSet();
+ ui->nameTextBox->setFocus();
+
+ originalPlaceholderText = ui->uidTextBox->placeholderText();
+ updateDialogState();
+}
+
+NewComponentDialog::~NewComponentDialog()
+{
+ delete ui;
+}
+
+void NewComponentDialog::updateDialogState()
+{
+ auto protoUid = ui->nameTextBox->text().toLower();
+ protoUid.remove(QRegularExpression("[^a-z]"));
+ if(protoUid.isEmpty())
+ {
+ ui->uidTextBox->setPlaceholderText(originalPlaceholderText);
+ }
+ else
+ {
+ QString suggestedUid = "org.multimc.custom." + protoUid;
+ ui->uidTextBox->setPlaceholderText(suggestedUid);
+ }
+ bool allowOK = !name().isEmpty() && !uid().isEmpty() && !uidBlacklist.contains(uid());
+ ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(allowOK);
+}
+
+QString NewComponentDialog::name() const
+{
+ auto result = ui->nameTextBox->text();
+ if(result.size())
+ {
+ return result.trimmed();
+ }
+ return QString();
+}
+
+QString NewComponentDialog::uid() const
+{
+ auto result = ui->uidTextBox->text();
+ if(result.size())
+ {
+ return result.trimmed();
+ }
+ result = ui->uidTextBox->placeholderText();
+ if(result.size() && result != originalPlaceholderText)
+ {
+ return result.trimmed();
+ }
+ return QString();
+}
+
+void NewComponentDialog::setBlacklist(QStringList badUids)
+{
+ uidBlacklist = badUids;
+}
diff --git a/launcher/ui/dialogs/NewComponentDialog.h b/launcher/ui/dialogs/NewComponentDialog.h
new file mode 100644
index 00000000..8c790beb
--- /dev/null
+++ b/launcher/ui/dialogs/NewComponentDialog.h
@@ -0,0 +1,48 @@
+/* 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 <QDialog>
+
+#include <QString>
+#include <QStringList>
+
+namespace Ui
+{
+class NewComponentDialog;
+}
+
+class NewComponentDialog : public QDialog
+{
+ Q_OBJECT
+
+public:
+ explicit NewComponentDialog(const QString & initialName = QString(), const QString & initialUid = QString(), QWidget *parent = 0);
+ virtual ~NewComponentDialog();
+ void setBlacklist(QStringList badUids);
+
+ QString name() const;
+ QString uid() const;
+
+private slots:
+ void updateDialogState();
+
+private:
+ Ui::NewComponentDialog *ui;
+
+ QString originalPlaceholderText;
+ QStringList uidBlacklist;
+};
diff --git a/launcher/ui/dialogs/NewComponentDialog.ui b/launcher/ui/dialogs/NewComponentDialog.ui
new file mode 100644
index 00000000..03b0d222
--- /dev/null
+++ b/launcher/ui/dialogs/NewComponentDialog.ui
@@ -0,0 +1,101 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>NewComponentDialog</class>
+ <widget class="QDialog" name="NewComponentDialog">
+ <property name="windowModality">
+ <enum>Qt::ApplicationModal</enum>
+ </property>
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>345</width>
+ <height>146</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Add Empty Component</string>
+ </property>
+ <property name="windowIcon">
+ <iconset>
+ <normaloff>:/icons/toolbar/copy</normaloff>:/icons/toolbar/copy</iconset>
+ </property>
+ <property name="modal">
+ <bool>true</bool>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <widget class="QLineEdit" name="nameTextBox">
+ <property name="placeholderText">
+ <string>Name</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLineEdit" name="uidTextBox">
+ <property name="placeholderText">
+ <string>uid</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="Line" name="line">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QDialogButtonBox" name="buttonBox">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="standardButtons">
+ <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <tabstops>
+ <tabstop>nameTextBox</tabstop>
+ <tabstop>uidTextBox</tabstop>
+ </tabstops>
+ <resources>
+ <include location="../../graphics.qrc"/>
+ </resources>
+ <connections>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>accepted()</signal>
+ <receiver>NewComponentDialog</receiver>
+ <slot>accept()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>248</x>
+ <y>254</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>157</x>
+ <y>274</y>
+ </hint>
+ </hints>
+ </connection>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>rejected()</signal>
+ <receiver>NewComponentDialog</receiver>
+ <slot>reject()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>316</x>
+ <y>260</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>286</x>
+ <y>274</y>
+ </hint>
+ </hints>
+ </connection>
+ </connections>
+</ui>
diff --git a/launcher/ui/dialogs/NewInstanceDialog.cpp b/launcher/ui/dialogs/NewInstanceDialog.cpp
new file mode 100644
index 00000000..b402839c
--- /dev/null
+++ b/launcher/ui/dialogs/NewInstanceDialog.cpp
@@ -0,0 +1,255 @@
+/* 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 "Application.h"
+#include "NewInstanceDialog.h"
+#include "ui_NewInstanceDialog.h"
+
+#include <BaseVersion.h>
+#include <icons/IconList.h>
+#include <tasks/Task.h>
+#include <InstanceList.h>
+
+#include "VersionSelectDialog.h"
+#include "ProgressDialog.h"
+#include "IconPickerDialog.h"
+
+#include <QLayout>
+#include <QPushButton>
+#include <QFileDialog>
+#include <QValidator>
+#include <QDialogButtonBox>
+
+#include "ui/widgets/PageContainer.h"
+#include "ui/pages/modplatform/VanillaPage.h"
+#include "ui/pages/modplatform/atlauncher/AtlPage.h"
+#include "ui/pages/modplatform/ftb/FtbPage.h"
+#include "ui/pages/modplatform/legacy_ftb/Page.h"
+#include "ui/pages/modplatform/flame/FlamePage.h"
+#include "ui/pages/modplatform/ImportPage.h"
+#include "ui/pages/modplatform/technic/TechnicPage.h"
+
+
+
+NewInstanceDialog::NewInstanceDialog(const QString & initialGroup, const QString & url, QWidget *parent)
+ : QDialog(parent), ui(new Ui::NewInstanceDialog)
+{
+ ui->setupUi(this);
+
+ setWindowIcon(APPLICATION->getThemedIcon("new"));
+
+ InstIconKey = "default";
+ ui->iconButton->setIcon(APPLICATION->icons()->getIcon(InstIconKey));
+
+ auto groups = APPLICATION->instances()->getGroups().toSet();
+ auto groupList = QStringList(groups.toList());
+ groupList.sort(Qt::CaseInsensitive);
+ groupList.removeOne("");
+ groupList.push_front(initialGroup);
+ groupList.push_front("");
+ ui->groupBox->addItems(groupList);
+ int index = groupList.indexOf(initialGroup);
+ if(index == -1)
+ {
+ index = 0;
+ }
+ ui->groupBox->setCurrentIndex(index);
+ ui->groupBox->lineEdit()->setPlaceholderText(tr("No group"));
+
+
+ // NOTE: m_buttons must be initialized before PageContainer, because it indirectly accesses m_buttons through setSuggestedPack! Do not move this below.
+ m_buttons = new QDialogButtonBox(QDialogButtonBox::Help | QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
+
+ m_container = new PageContainer(this);
+ m_container->setSizePolicy(QSizePolicy::Policy::Preferred, QSizePolicy::Policy::Expanding);
+ m_container->layout()->setContentsMargins(0, 0, 0, 0);
+ ui->verticalLayout->insertWidget(2, m_container);
+
+ m_container->addButtons(m_buttons);
+
+ // Bonk Qt over its stupid head and make sure it understands which button is the default one...
+ // See: https://stackoverflow.com/questions/24556831/qbuttonbox-set-default-button
+ auto OkButton = m_buttons->button(QDialogButtonBox::Ok);
+ OkButton->setDefault(true);
+ OkButton->setAutoDefault(true);
+ connect(OkButton, &QPushButton::clicked, this, &NewInstanceDialog::accept);
+
+ auto CancelButton = m_buttons->button(QDialogButtonBox::Cancel);
+ CancelButton->setDefault(false);
+ CancelButton->setAutoDefault(false);
+ connect(CancelButton, &QPushButton::clicked, this, &NewInstanceDialog::reject);
+
+ auto HelpButton = m_buttons->button(QDialogButtonBox::Help);
+ HelpButton->setDefault(false);
+ HelpButton->setAutoDefault(false);
+ connect(HelpButton, &QPushButton::clicked, m_container, &PageContainer::help);
+
+ if(!url.isEmpty())
+ {
+ QUrl actualUrl(url);
+ m_container->selectPage("import");
+ importPage->setUrl(url);
+ }
+
+ updateDialogState();
+
+ restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get("NewInstanceGeometry").toByteArray()));
+}
+
+void NewInstanceDialog::reject()
+{
+ APPLICATION->settings()->set("NewInstanceGeometry", saveGeometry().toBase64());
+ QDialog::reject();
+}
+
+void NewInstanceDialog::accept()
+{
+ APPLICATION->settings()->set("NewInstanceGeometry", saveGeometry().toBase64());
+ importIconNow();
+ QDialog::accept();
+}
+
+QList<BasePage *> NewInstanceDialog::getPages()
+{
+ importPage = new ImportPage(this);
+ flamePage = new FlamePage(this);
+ auto technicPage = new TechnicPage(this);
+ return
+ {
+ new VanillaPage(this),
+ importPage,
+ new AtlPage(this),
+ flamePage,
+ new FtbPage(this),
+ new LegacyFTB::Page(this),
+ technicPage
+ };
+}
+
+QString NewInstanceDialog::dialogTitle()
+{
+ return tr("New Instance");
+}
+
+NewInstanceDialog::~NewInstanceDialog()
+{
+ delete ui;
+}
+
+void NewInstanceDialog::setSuggestedPack(const QString& name, InstanceTask* task)
+{
+ creationTask.reset(task);
+ ui->instNameTextBox->setPlaceholderText(name);
+
+ if(!task)
+ {
+ ui->iconButton->setIcon(APPLICATION->icons()->getIcon("default"));
+ importIcon = false;
+ }
+
+ auto allowOK = task && !instName().isEmpty();
+ m_buttons->button(QDialogButtonBox::Ok)->setEnabled(allowOK);
+}
+
+void NewInstanceDialog::setSuggestedIconFromFile(const QString &path, const QString &name)
+{
+ importIcon = true;
+ importIconPath = path;
+ importIconName = name;
+
+ //Hmm, for some reason they can be to small
+ ui->iconButton->setIcon(QIcon(path));
+}
+
+void NewInstanceDialog::setSuggestedIcon(const QString &key)
+{
+ auto icon = APPLICATION->icons()->getIcon(key);
+ importIcon = false;
+
+ ui->iconButton->setIcon(icon);
+}
+
+InstanceTask * NewInstanceDialog::extractTask()
+{
+ InstanceTask * extracted = creationTask.get();
+ creationTask.release();
+ extracted->setName(instName());
+ extracted->setGroup(instGroup());
+ extracted->setIcon(iconKey());
+ return extracted;
+}
+
+void NewInstanceDialog::updateDialogState()
+{
+ auto allowOK = creationTask && !instName().isEmpty();
+ auto OkButton = m_buttons->button(QDialogButtonBox::Ok);
+ if(OkButton->isEnabled() != allowOK)
+ {
+ OkButton->setEnabled(allowOK);
+ }
+}
+
+QString NewInstanceDialog::instName() const
+{
+ auto result = ui->instNameTextBox->text().trimmed();
+ if(result.size())
+ {
+ return result;
+ }
+ result = ui->instNameTextBox->placeholderText().trimmed();
+ if(result.size())
+ {
+ return result;
+ }
+ return QString();
+}
+
+QString NewInstanceDialog::instGroup() const
+{
+ return ui->groupBox->currentText();
+}
+QString NewInstanceDialog::iconKey() const
+{
+ return InstIconKey;
+}
+
+void NewInstanceDialog::on_iconButton_clicked()
+{
+ importIconNow(); //so the user can switch back
+ IconPickerDialog dlg(this);
+ dlg.execWithSelection(InstIconKey);
+
+ if (dlg.result() == QDialog::Accepted)
+ {
+ InstIconKey = dlg.selectedIconKey;
+ ui->iconButton->setIcon(APPLICATION->icons()->getIcon(InstIconKey));
+ importIcon = false;
+ }
+}
+
+void NewInstanceDialog::on_instNameTextBox_textChanged(const QString &arg1)
+{
+ updateDialogState();
+}
+
+void NewInstanceDialog::importIconNow()
+{
+ if(importIcon) {
+ APPLICATION->icons()->installIcon(importIconPath, importIconName);
+ InstIconKey = importIconName;
+ importIcon = false;
+ }
+ APPLICATION->settings()->set("NewInstanceGeometry", saveGeometry().toBase64());
+}
diff --git a/launcher/ui/dialogs/NewInstanceDialog.h b/launcher/ui/dialogs/NewInstanceDialog.h
new file mode 100644
index 00000000..ef74634e
--- /dev/null
+++ b/launcher/ui/dialogs/NewInstanceDialog.h
@@ -0,0 +1,80 @@
+/* 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 <QDialog>
+
+#include "BaseVersion.h"
+#include "ui/pages/BasePageProvider.h"
+#include "InstanceTask.h"
+
+namespace Ui
+{
+class NewInstanceDialog;
+}
+
+class PageContainer;
+class QDialogButtonBox;
+class ImportPage;
+class FlamePage;
+
+class NewInstanceDialog : public QDialog, public BasePageProvider
+{
+ Q_OBJECT
+
+public:
+ explicit NewInstanceDialog(const QString & initialGroup, const QString & url = QString(), QWidget *parent = 0);
+ ~NewInstanceDialog();
+
+ void updateDialogState();
+
+ void setSuggestedPack(const QString & name = QString(), InstanceTask * task = nullptr);
+ void setSuggestedIconFromFile(const QString &path, const QString &name);
+ void setSuggestedIcon(const QString &key);
+
+ InstanceTask * extractTask();
+
+ QString dialogTitle() override;
+ QList<BasePage *> getPages() override;
+
+ QString instName() const;
+ QString instGroup() const;
+ QString iconKey() const;
+
+public slots:
+ void accept() override;
+ void reject() override;
+
+private slots:
+ void on_iconButton_clicked();
+ void on_instNameTextBox_textChanged(const QString &arg1);
+
+private:
+ Ui::NewInstanceDialog *ui = nullptr;
+ PageContainer * m_container = nullptr;
+ QDialogButtonBox * m_buttons = nullptr;
+
+ QString InstIconKey;
+ ImportPage *importPage = nullptr;
+ FlamePage *flamePage = nullptr;
+ std::unique_ptr<InstanceTask> creationTask;
+
+ bool importIcon = false;
+ QString importIconPath;
+ QString importIconName;
+
+ void importIconNow();
+};
diff --git a/launcher/ui/dialogs/NewInstanceDialog.ui b/launcher/ui/dialogs/NewInstanceDialog.ui
new file mode 100644
index 00000000..7fb19ff5
--- /dev/null
+++ b/launcher/ui/dialogs/NewInstanceDialog.ui
@@ -0,0 +1,87 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>NewInstanceDialog</class>
+ <widget class="QDialog" name="NewInstanceDialog">
+ <property name="windowModality">
+ <enum>Qt::ApplicationModal</enum>
+ </property>
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>730</width>
+ <height>127</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>New Instance</string>
+ </property>
+ <property name="windowIcon">
+ <iconset>
+ <normaloff>:/icons/toolbar/new</normaloff>:/icons/toolbar/new</iconset>
+ </property>
+ <property name="modal">
+ <bool>true</bool>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <layout class="QGridLayout" name="gridLayout_2">
+ <item row="1" column="2">
+ <widget class="QComboBox" name="groupBox">
+ <property name="editable">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QLabel" name="groupLabel">
+ <property name="text">
+ <string>&amp;Group:</string>
+ </property>
+ <property name="buddy">
+ <cstring>groupBox</cstring>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="2">
+ <widget class="QLineEdit" name="instNameTextBox"/>
+ </item>
+ <item row="0" column="1">
+ <widget class="QLabel" name="nameLabel">
+ <property name="text">
+ <string>&amp;Name:</string>
+ </property>
+ <property name="buddy">
+ <cstring>instNameTextBox</cstring>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="0" rowspan="2">
+ <widget class="QToolButton" name="iconButton">
+ <property name="iconSize">
+ <size>
+ <width>80</width>
+ <height>80</height>
+ </size>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="Line" name="line">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <tabstops>
+ <tabstop>iconButton</tabstop>
+ <tabstop>instNameTextBox</tabstop>
+ <tabstop>groupBox</tabstop>
+ </tabstops>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/launcher/ui/dialogs/NotificationDialog.cpp b/launcher/ui/dialogs/NotificationDialog.cpp
new file mode 100644
index 00000000..f2a35ae2
--- /dev/null
+++ b/launcher/ui/dialogs/NotificationDialog.cpp
@@ -0,0 +1,86 @@
+#include "NotificationDialog.h"
+#include "ui_NotificationDialog.h"
+
+#include <QTimerEvent>
+#include <QStyle>
+
+NotificationDialog::NotificationDialog(const NotificationChecker::NotificationEntry &entry, QWidget *parent) :
+ QDialog(parent, Qt::MSWindowsFixedSizeDialogHint | Qt::WindowTitleHint | Qt::CustomizeWindowHint),
+ ui(new Ui::NotificationDialog)
+{
+ ui->setupUi(this);
+
+ QStyle::StandardPixmap icon;
+ switch (entry.type)
+ {
+ case NotificationChecker::NotificationEntry::Critical:
+ icon = QStyle::SP_MessageBoxCritical;
+ break;
+ case NotificationChecker::NotificationEntry::Warning:
+ icon = QStyle::SP_MessageBoxWarning;
+ break;
+ default:
+ case NotificationChecker::NotificationEntry::Information:
+ icon = QStyle::SP_MessageBoxInformation;
+ break;
+ }
+ ui->iconLabel->setPixmap(style()->standardPixmap(icon, 0, this));
+ ui->messageLabel->setText(entry.message);
+
+ m_dontShowAgainText = tr("Don't show again");
+ m_closeText = tr("Close");
+
+ ui->dontShowAgainBtn->setText(m_dontShowAgainText + QString(" (%1)").arg(m_dontShowAgainTime));
+ ui->closeBtn->setText(m_closeText + QString(" (%1)").arg(m_closeTime));
+
+ startTimer(1000);
+}
+
+NotificationDialog::~NotificationDialog()
+{
+ delete ui;
+}
+
+void NotificationDialog::timerEvent(QTimerEvent *event)
+{
+ if (m_dontShowAgainTime > 0)
+ {
+ m_dontShowAgainTime--;
+ if (m_dontShowAgainTime == 0)
+ {
+ ui->dontShowAgainBtn->setText(m_dontShowAgainText);
+ ui->dontShowAgainBtn->setEnabled(true);
+ }
+ else
+ {
+ ui->dontShowAgainBtn->setText(m_dontShowAgainText + QString(" (%1)").arg(m_dontShowAgainTime));
+ }
+ }
+ if (m_closeTime > 0)
+ {
+ m_closeTime--;
+ if (m_closeTime == 0)
+ {
+ ui->closeBtn->setText(m_closeText);
+ ui->closeBtn->setEnabled(true);
+ }
+ else
+ {
+ ui->closeBtn->setText(m_closeText + QString(" (%1)").arg(m_closeTime));
+ }
+ }
+
+ if (m_closeTime == 0 && m_dontShowAgainTime == 0)
+ {
+ killTimer(event->timerId());
+ }
+}
+
+void NotificationDialog::on_dontShowAgainBtn_clicked()
+{
+ done(DontShowAgain);
+}
+void NotificationDialog::on_closeBtn_clicked()
+{
+ done(Normal);
+}
diff --git a/launcher/ui/dialogs/NotificationDialog.h b/launcher/ui/dialogs/NotificationDialog.h
new file mode 100644
index 00000000..e1cbb9fa
--- /dev/null
+++ b/launcher/ui/dialogs/NotificationDialog.h
@@ -0,0 +1,44 @@
+#ifndef NOTIFICATIONDIALOG_H
+#define NOTIFICATIONDIALOG_H
+
+#include <QDialog>
+
+#include "notifications/NotificationChecker.h"
+
+namespace Ui {
+class NotificationDialog;
+}
+
+class NotificationDialog : public QDialog
+{
+ Q_OBJECT
+
+public:
+ explicit NotificationDialog(const NotificationChecker::NotificationEntry &entry, QWidget *parent = 0);
+ ~NotificationDialog();
+
+ enum ExitCode
+ {
+ Normal,
+ DontShowAgain
+ };
+
+protected:
+ void timerEvent(QTimerEvent *event);
+
+private:
+ Ui::NotificationDialog *ui;
+
+ int m_dontShowAgainTime = 10;
+ int m_closeTime = 5;
+
+ QString m_dontShowAgainText;
+ QString m_closeText;
+
+private
+slots:
+ void on_dontShowAgainBtn_clicked();
+ void on_closeBtn_clicked();
+};
+
+#endif // NOTIFICATIONDIALOG_H
diff --git a/launcher/ui/dialogs/NotificationDialog.ui b/launcher/ui/dialogs/NotificationDialog.ui
new file mode 100644
index 00000000..3e6c22bc
--- /dev/null
+++ b/launcher/ui/dialogs/NotificationDialog.ui
@@ -0,0 +1,85 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>NotificationDialog</class>
+ <widget class="QDialog" name="NotificationDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>320</width>
+ <height>240</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Notification</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout" stretch="1,0">
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_2" stretch="0,1">
+ <item>
+ <widget class="QLabel" name="iconLabel">
+ <property name="text">
+ <string notr="true">TextLabel</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="messageLabel">
+ <property name="text">
+ <string notr="true">TextLabel</string>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ <property name="openExternalLinks">
+ <bool>true</bool>
+ </property>
+ <property name="textInteractionFlags">
+ <set>Qt::TextBrowserInteraction</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <item>
+ <spacer name="horizontalSpacer">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QPushButton" name="dontShowAgainBtn">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ <property name="text">
+ <string>Don't show again</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="closeBtn">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ <property name="text">
+ <string>Close</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/launcher/ui/dialogs/ProfileSelectDialog.cpp b/launcher/ui/dialogs/ProfileSelectDialog.cpp
new file mode 100644
index 00000000..7882cf45
--- /dev/null
+++ b/launcher/ui/dialogs/ProfileSelectDialog.cpp
@@ -0,0 +1,115 @@
+/* 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 "ProfileSelectDialog.h"
+#include "ui_ProfileSelectDialog.h"
+
+#include <QItemSelectionModel>
+#include <QDebug>
+
+#include "SkinUtils.h"
+#include "Application.h"
+
+#include "ui/dialogs/ProgressDialog.h"
+
+ProfileSelectDialog::ProfileSelectDialog(const QString &message, int flags, QWidget *parent)
+ : QDialog(parent), ui(new Ui::ProfileSelectDialog)
+{
+ ui->setupUi(this);
+
+ m_accounts = APPLICATION->accounts();
+ auto view = ui->listView;
+ //view->setModel(m_accounts.get());
+ //view->hideColumn(AccountList::ActiveColumn);
+ view->setColumnCount(1);
+ view->setRootIsDecorated(false);
+ // FIXME: use a real model, not this
+ if(QTreeWidgetItem* header = view->headerItem())
+ {
+ header->setText(0, tr("Name"));
+ }
+ else
+ {
+ view->setHeaderLabel(tr("Name"));
+ }
+ QList <QTreeWidgetItem *> items;
+ for (int i = 0; i < m_accounts->count(); i++)
+ {
+ MinecraftAccountPtr account = m_accounts->at(i);
+ QString profileLabel;
+ if(account->isInUse()) {
+ profileLabel = tr("%1 (in use)").arg(account->profileName());
+ }
+ else {
+ profileLabel = account->profileName();
+ }
+ auto item = new QTreeWidgetItem(view);
+ item->setText(0, profileLabel);
+ item->setIcon(0, account->getFace());
+ item->setData(0, AccountList::PointerRole, QVariant::fromValue(account));
+ items.append(item);
+ }
+ view->addTopLevelItems(items);
+
+ // Set the message label.
+ ui->msgLabel->setVisible(!message.isEmpty());
+ ui->msgLabel->setText(message);
+
+ // Flags...
+ ui->globalDefaultCheck->setVisible(flags & GlobalDefaultCheckbox);
+ ui->instDefaultCheck->setVisible(flags & InstanceDefaultCheckbox);
+ qDebug() << flags;
+
+ // Select the first entry in the list.
+ ui->listView->setCurrentIndex(ui->listView->model()->index(0, 0));
+
+ connect(ui->listView, SIGNAL(doubleClicked(QModelIndex)), SLOT(on_buttonBox_accepted()));
+}
+
+ProfileSelectDialog::~ProfileSelectDialog()
+{
+ delete ui;
+}
+
+MinecraftAccountPtr ProfileSelectDialog::selectedAccount() const
+{
+ return m_selected;
+}
+
+bool ProfileSelectDialog::useAsGlobalDefault() const
+{
+ return ui->globalDefaultCheck->isChecked();
+}
+
+bool ProfileSelectDialog::useAsInstDefaullt() const
+{
+ return ui->instDefaultCheck->isChecked();
+}
+
+void ProfileSelectDialog::on_buttonBox_accepted()
+{
+ QModelIndexList selection = ui->listView->selectionModel()->selectedIndexes();
+ if (selection.size() > 0)
+ {
+ QModelIndex selected = selection.first();
+ m_selected = selected.data(AccountList::PointerRole).value<MinecraftAccountPtr>();
+ }
+ close();
+}
+
+void ProfileSelectDialog::on_buttonBox_rejected()
+{
+ close();
+}
diff --git a/launcher/ui/dialogs/ProfileSelectDialog.h b/launcher/ui/dialogs/ProfileSelectDialog.h
new file mode 100644
index 00000000..38aa4249
--- /dev/null
+++ b/launcher/ui/dialogs/ProfileSelectDialog.h
@@ -0,0 +1,90 @@
+/* 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 <QDialog>
+
+#include <memory>
+
+#include "minecraft/auth/AccountList.h"
+
+namespace Ui
+{
+class ProfileSelectDialog;
+}
+
+class ProfileSelectDialog : public QDialog
+{
+ Q_OBJECT
+public:
+ enum Flags
+ {
+ NoFlags = 0,
+
+ /*!
+ * Shows a check box on the dialog that allows the user to specify that the account
+ * they've selected should be used as the global default for all instances.
+ */
+ GlobalDefaultCheckbox,
+
+ /*!
+ * Shows a check box on the dialog that allows the user to specify that the account
+ * they've selected should be used as the default for the instance they are currently launching.
+ * This is not currently implemented.
+ */
+ InstanceDefaultCheckbox,
+ };
+
+ /*!
+ * Constructs a new account select dialog with the given parent and message.
+ * The message will be shown at the top of the dialog. It is an empty string by default.
+ */
+ explicit ProfileSelectDialog(const QString& message="", int flags=0, QWidget *parent = 0);
+ ~ProfileSelectDialog();
+
+ /*!
+ * Gets a pointer to the account that the user selected.
+ * This is null if the user clicked cancel or hasn't clicked OK yet.
+ */
+ MinecraftAccountPtr selectedAccount() const;
+
+ /*!
+ * Returns true if the user checked the "use as global default" checkbox.
+ * If the checkbox wasn't shown, this function returns false.
+ */
+ bool useAsGlobalDefault() const;
+
+ /*!
+ * Returns true if the user checked the "use as instance default" checkbox.
+ * If the checkbox wasn't shown, this function returns false.
+ */
+ bool useAsInstDefaullt() const;
+
+public
+slots:
+ void on_buttonBox_accepted();
+
+ void on_buttonBox_rejected();
+
+protected:
+ shared_qobject_ptr<AccountList> m_accounts;
+
+ //! The account that was selected when the user clicked OK.
+ MinecraftAccountPtr m_selected;
+
+private:
+ Ui::ProfileSelectDialog *ui;
+};
diff --git a/launcher/ui/dialogs/ProfileSelectDialog.ui b/launcher/ui/dialogs/ProfileSelectDialog.ui
new file mode 100644
index 00000000..e779b51b
--- /dev/null
+++ b/launcher/ui/dialogs/ProfileSelectDialog.ui
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>ProfileSelectDialog</class>
+ <widget class="QDialog" name="ProfileSelectDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>465</width>
+ <height>300</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Select an Account</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <widget class="QLabel" name="msgLabel">
+ <property name="text">
+ <string>Select a profile.</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QTreeWidget" name="listView">
+ <column>
+ <property name="text">
+ <string notr="true">1</string>
+ </property>
+ </column>
+ </widget>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <item>
+ <widget class="QCheckBox" name="globalDefaultCheck">
+ <property name="text">
+ <string>Use as default?</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="instDefaultCheck">
+ <property name="text">
+ <string>Use as default for this instance only?</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QDialogButtonBox" name="buttonBox">
+ <property name="standardButtons">
+ <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/launcher/ui/dialogs/ProfileSetupDialog.cpp b/launcher/ui/dialogs/ProfileSetupDialog.cpp
new file mode 100644
index 00000000..d3e2b9a4
--- /dev/null
+++ b/launcher/ui/dialogs/ProfileSetupDialog.cpp
@@ -0,0 +1,250 @@
+/* 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 "ProfileSetupDialog.h"
+#include "ui_ProfileSetupDialog.h"
+
+#include <QPushButton>
+#include <QAction>
+#include <QRegExpValidator>
+#include <QJsonDocument>
+#include <QDebug>
+
+#include "ui/dialogs/ProgressDialog.h"
+
+#include <Application.h>
+#include "minecraft/auth/flows/AuthRequest.h"
+#include "minecraft/auth/flows/Parsers.h"
+
+
+ProfileSetupDialog::ProfileSetupDialog(MinecraftAccountPtr accountToSetup, QWidget *parent)
+ : QDialog(parent), m_accountToSetup(accountToSetup), ui(new Ui::ProfileSetupDialog)
+{
+ ui->setupUi(this);
+ ui->errorLabel->setVisible(false);
+
+ goodIcon = APPLICATION->getThemedIcon("status-good");
+ yellowIcon = APPLICATION->getThemedIcon("status-yellow");
+ badIcon = APPLICATION->getThemedIcon("status-bad");
+
+ QRegExp permittedNames("[a-zA-Z0-9_]{3,16}");
+ auto nameEdit = ui->nameEdit;
+ nameEdit->setValidator(new QRegExpValidator(permittedNames));
+ nameEdit->setClearButtonEnabled(true);
+ validityAction = nameEdit->addAction(yellowIcon, QLineEdit::LeadingPosition);
+ connect(nameEdit, &QLineEdit::textEdited, this, &ProfileSetupDialog::nameEdited);
+
+ checkStartTimer.setSingleShot(true);
+ connect(&checkStartTimer, &QTimer::timeout, this, &ProfileSetupDialog::startCheck);
+
+ setNameStatus(NameStatus::NotSet, QString());
+}
+
+ProfileSetupDialog::~ProfileSetupDialog()
+{
+ delete ui;
+}
+
+void ProfileSetupDialog::on_buttonBox_accepted()
+{
+ setupProfile(currentCheck);
+}
+
+void ProfileSetupDialog::on_buttonBox_rejected()
+{
+ reject();
+}
+
+void ProfileSetupDialog::setNameStatus(ProfileSetupDialog::NameStatus status, QString errorString = QString())
+{
+ nameStatus = status;
+ auto okButton = ui->buttonBox->button(QDialogButtonBox::Ok);
+ switch(nameStatus)
+ {
+ case NameStatus::Available: {
+ validityAction->setIcon(goodIcon);
+ okButton->setEnabled(true);
+ }
+ break;
+ case NameStatus::NotSet:
+ case NameStatus::Pending:
+ validityAction->setIcon(yellowIcon);
+ okButton->setEnabled(false);
+ break;
+ case NameStatus::Exists:
+ case NameStatus::Error:
+ validityAction->setIcon(badIcon);
+ okButton->setEnabled(false);
+ break;
+ }
+ if(!errorString.isEmpty()) {
+ ui->errorLabel->setText(errorString);
+ ui->errorLabel->setVisible(true);
+ }
+ else {
+ ui->errorLabel->setVisible(false);
+ }
+}
+
+void ProfileSetupDialog::nameEdited(const QString& name)
+{
+ if(!ui->nameEdit->hasAcceptableInput()) {
+ setNameStatus(NameStatus::NotSet, tr("Name is too short - must be between 3 and 16 characters long."));
+ return;
+ }
+ scheduleCheck(name);
+}
+
+void ProfileSetupDialog::scheduleCheck(const QString& name) {
+ queuedCheck = name;
+ setNameStatus(NameStatus::Pending);
+ checkStartTimer.start(1000);
+}
+
+void ProfileSetupDialog::startCheck() {
+ if(isChecking) {
+ return;
+ }
+ if(queuedCheck.isNull()) {
+ return;
+ }
+ checkName(queuedCheck);
+}
+
+
+void ProfileSetupDialog::checkName(const QString &name) {
+ if(isChecking) {
+ return;
+ }
+
+ currentCheck = name;
+ isChecking = true;
+
+ auto token = m_accountToSetup->accessToken();
+
+ auto url = QString("https://api.minecraftservices.com/minecraft/profile/name/%1/available").arg(name);
+ QNetworkRequest request = QNetworkRequest(url);
+ request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
+ request.setRawHeader("Accept", "application/json");
+ request.setRawHeader("Authorization", QString("Bearer %1").arg(token).toUtf8());
+
+ AuthRequest *requestor = new AuthRequest(this);
+ connect(requestor, &AuthRequest::finished, this, &ProfileSetupDialog::checkFinished);
+ requestor->get(request);
+}
+
+void ProfileSetupDialog::checkFinished(
+ QNetworkReply::NetworkError error,
+ QByteArray data,
+ QList<QNetworkReply::RawHeaderPair> headers
+) {
+ if(error == QNetworkReply::NoError) {
+ auto doc = QJsonDocument::fromJson(data);
+ auto root = doc.object();
+ auto statusValue = root.value("status").toString("INVALID");
+ if(statusValue == "AVAILABLE") {
+ setNameStatus(NameStatus::Available);
+ }
+ else if (statusValue == "DUPLICATE") {
+ setNameStatus(NameStatus::Exists, tr("Minecraft profile with name %1 already exists.").arg(currentCheck));
+ }
+ else if (statusValue == "NOT_ALLOWED") {
+ setNameStatus(NameStatus::Exists, tr("The name %1 is not allowed.").arg(currentCheck));
+ }
+ else {
+ setNameStatus(NameStatus::Error, tr("Unhandled profile name status: %1").arg(statusValue));
+ }
+ }
+ else {
+ setNameStatus(NameStatus::Error, tr("Failed to check name availability."));
+ }
+ isChecking = false;
+}
+
+void ProfileSetupDialog::setupProfile(const QString &profileName) {
+ if(isWorking) {
+ return;
+ }
+
+ auto token = m_accountToSetup->accessToken();
+
+ auto url = QString("https://api.minecraftservices.com/minecraft/profile");
+ QNetworkRequest request = QNetworkRequest(url);
+ request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
+ request.setRawHeader("Accept", "application/json");
+ request.setRawHeader("Authorization", QString("Bearer %1").arg(token).toUtf8());
+
+ QString payloadTemplate("{\"profileName\":\"%1\"}");
+ auto data = payloadTemplate.arg(profileName).toUtf8();
+
+ AuthRequest *requestor = new AuthRequest(this);
+ connect(requestor, &AuthRequest::finished, this, &ProfileSetupDialog::setupProfileFinished);
+ requestor->post(request, data);
+ isWorking = true;
+
+ auto button = ui->buttonBox->button(QDialogButtonBox::Cancel);
+ button->setEnabled(false);
+}
+
+namespace {
+
+struct MojangError{
+ static MojangError fromJSON(QByteArray data) {
+ MojangError out;
+ out.error = QString::fromUtf8(data);
+ auto doc = QJsonDocument::fromJson(data, &out.parseError);
+ auto object = doc.object();
+
+ out.fullyParsed = true;
+ out.fullyParsed &= Parsers::getString(object.value("path"), out.path);
+ out.fullyParsed &= Parsers::getString(object.value("error"), out.error);
+ out.fullyParsed &= Parsers::getString(object.value("errorMessage"), out.errorMessage);
+
+ return out;
+ }
+
+ QString rawError;
+ QJsonParseError parseError;
+ bool fullyParsed;
+
+ QString path;
+ QString error;
+ QString errorMessage;
+};
+
+}
+
+void ProfileSetupDialog::setupProfileFinished(
+ QNetworkReply::NetworkError error,
+ QByteArray data,
+ QList<QNetworkReply::RawHeaderPair> headers
+) {
+ isWorking = false;
+ if(error == QNetworkReply::NoError) {
+ /*
+ * data contains the profile in the response
+ * ... we could parse it and update the account, but let's just return back to the normal login flow instead...
+ */
+ accept();
+ }
+ else {
+ auto parsedError = MojangError::fromJSON(data);
+ ui->errorLabel->setVisible(true);
+ ui->errorLabel->setText(tr("The server returned the following error:") + "\n\n" + parsedError.errorMessage);
+ qDebug() << parsedError.rawError;
+ auto button = ui->buttonBox->button(QDialogButtonBox::Cancel);
+ button->setEnabled(true);
+ }
+}
diff --git a/launcher/ui/dialogs/ProfileSetupDialog.h b/launcher/ui/dialogs/ProfileSetupDialog.h
new file mode 100644
index 00000000..6f413ebd
--- /dev/null
+++ b/launcher/ui/dialogs/ProfileSetupDialog.h
@@ -0,0 +1,88 @@
+/* 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 <QDialog>
+#include <QIcon>
+#include <QTimer>
+#include <QNetworkReply>
+
+#include <memory>
+#include <minecraft/auth/MinecraftAccount.h>
+
+namespace Ui
+{
+class ProfileSetupDialog;
+}
+
+class ProfileSetupDialog : public QDialog
+{
+ Q_OBJECT
+public:
+
+ explicit ProfileSetupDialog(MinecraftAccountPtr accountToSetup, QWidget *parent = 0);
+ ~ProfileSetupDialog();
+
+ enum class NameStatus
+ {
+ NotSet,
+ Pending,
+ Available,
+ Exists,
+ Error
+ } nameStatus = NameStatus::NotSet;
+
+private slots:
+ void on_buttonBox_accepted();
+ void on_buttonBox_rejected();
+
+ void nameEdited(const QString &name);
+ void checkFinished(
+ QNetworkReply::NetworkError error,
+ QByteArray data,
+ QList<QNetworkReply::RawHeaderPair> headers
+ );
+ void startCheck();
+
+ void setupProfileFinished(
+ QNetworkReply::NetworkError error,
+ QByteArray data,
+ QList<QNetworkReply::RawHeaderPair> headers
+ );
+protected:
+ void scheduleCheck(const QString &name);
+ void checkName(const QString &name);
+ void setNameStatus(NameStatus status, QString errorString);
+
+ void setupProfile(const QString & profileName);
+
+private:
+ MinecraftAccountPtr m_accountToSetup;
+ Ui::ProfileSetupDialog *ui;
+ QIcon goodIcon;
+ QIcon yellowIcon;
+ QIcon badIcon;
+ QAction * validityAction = nullptr;
+
+ QString queuedCheck;
+
+ bool isChecking = false;
+ bool isWorking = false;
+ QString currentCheck;
+
+ QTimer checkStartTimer;
+};
+
diff --git a/launcher/ui/dialogs/ProfileSetupDialog.ui b/launcher/ui/dialogs/ProfileSetupDialog.ui
new file mode 100644
index 00000000..9dbabb4b
--- /dev/null
+++ b/launcher/ui/dialogs/ProfileSetupDialog.ui
@@ -0,0 +1,74 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>ProfileSetupDialog</class>
+ <widget class="QDialog" name="ProfileSetupDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>615</width>
+ <height>208</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Choose Minecraft name</string>
+ </property>
+ <layout class="QGridLayout" name="gridLayout">
+ <item row="0" column="0" colspan="2">
+ <widget class="QLabel" name="descriptionLabel">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Minimum" vsizetype="Minimum">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>You just need to take one more step to be able to play Minecraft on this account.
+
+Choose your name carefully:</string>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ <property name="buddy">
+ <cstring>nameEdit</cstring>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="0">
+ <widget class="QLineEdit" name="nameEdit"/>
+ </item>
+ <item row="4" column="0" colspan="2">
+ <widget class="QDialogButtonBox" name="buttonBox">
+ <property name="standardButtons">
+ <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="0">
+ <widget class="QLabel" name="errorLabel">
+ <property name="enabled">
+ <bool>true</bool>
+ </property>
+ <property name="text">
+ <string notr="true">Errors go here</string>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ <property name="openExternalLinks">
+ <bool>true</bool>
+ </property>
+ <property name="textInteractionFlags">
+ <set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <tabstops>
+ <tabstop>nameEdit</tabstop>
+ </tabstops>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/launcher/ui/dialogs/ProgressDialog.cpp b/launcher/ui/dialogs/ProgressDialog.cpp
new file mode 100644
index 00000000..4b092859
--- /dev/null
+++ b/launcher/ui/dialogs/ProgressDialog.cpp
@@ -0,0 +1,196 @@
+/* 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 "ProgressDialog.h"
+#include "ui_ProgressDialog.h"
+
+#include <QKeyEvent>
+#include <QDebug>
+
+#include "tasks/Task.h"
+
+ProgressDialog::ProgressDialog(QWidget *parent) : QDialog(parent), ui(new Ui::ProgressDialog)
+{
+ ui->setupUi(this);
+ this->setWindowFlags(this->windowFlags() & ~Qt::WindowContextHelpButtonHint);
+ setSkipButton(false);
+ changeProgress(0, 100);
+}
+
+void ProgressDialog::setSkipButton(bool present, QString label)
+{
+ ui->skipButton->setAutoDefault(false);
+ ui->skipButton->setDefault(false);
+ ui->skipButton->setFocusPolicy(Qt::ClickFocus);
+ ui->skipButton->setEnabled(present);
+ ui->skipButton->setVisible(present);
+ ui->skipButton->setText(label);
+ updateSize();
+}
+
+void ProgressDialog::on_skipButton_clicked(bool checked)
+{
+ Q_UNUSED(checked);
+ task->abort();
+}
+
+ProgressDialog::~ProgressDialog()
+{
+ delete ui;
+}
+
+void ProgressDialog::updateSize()
+{
+ QSize qSize = QSize(480, minimumSizeHint().height());
+ resize(qSize);
+ setFixedSize(qSize);
+}
+
+int ProgressDialog::execWithTask(Task *task)
+{
+ this->task = task;
+ QDialog::DialogCode result;
+
+ if(!task)
+ {
+ qDebug() << "Programmer error: progress dialog created with null task.";
+ return Accepted;
+ }
+
+ if(handleImmediateResult(result))
+ {
+ return result;
+ }
+
+ // Connect signals.
+ connect(task, SIGNAL(started()), SLOT(onTaskStarted()));
+ connect(task, SIGNAL(failed(QString)), SLOT(onTaskFailed(QString)));
+ connect(task, SIGNAL(succeeded()), SLOT(onTaskSucceeded()));
+ connect(task, SIGNAL(status(QString)), SLOT(changeStatus(const QString &)));
+ connect(task, SIGNAL(progress(qint64, qint64)), SLOT(changeProgress(qint64, qint64)));
+
+ // if this didn't connect to an already running task, invoke start
+ if(!task->isRunning())
+ {
+ task->start();
+ }
+ if(task->isRunning())
+ {
+ changeProgress(task->getProgress(), task->getTotalProgress());
+ changeStatus(task->getStatus());
+ return QDialog::exec();
+ }
+ else if(handleImmediateResult(result))
+ {
+ return result;
+ }
+ else
+ {
+ return QDialog::Rejected;
+ }
+}
+
+// TODO: only provide the unique_ptr overloads
+int ProgressDialog::execWithTask(std::unique_ptr<Task> &&task)
+{
+ connect(this, &ProgressDialog::destroyed, task.get(), &Task::deleteLater);
+ return execWithTask(task.release());
+}
+int ProgressDialog::execWithTask(std::unique_ptr<Task> &task)
+{
+ connect(this, &ProgressDialog::destroyed, task.get(), &Task::deleteLater);
+ return execWithTask(task.release());
+}
+
+bool ProgressDialog::handleImmediateResult(QDialog::DialogCode &result)
+{
+ if(task->isFinished())
+ {
+ if(task->wasSuccessful())
+ {
+ result = QDialog::Accepted;
+ }
+ else
+ {
+ result = QDialog::Rejected;
+ }
+ return true;
+ }
+ return false;
+}
+
+Task *ProgressDialog::getTask()
+{
+ return task;
+}
+
+void ProgressDialog::onTaskStarted()
+{
+}
+
+void ProgressDialog::onTaskFailed(QString failure)
+{
+ reject();
+}
+
+void ProgressDialog::onTaskSucceeded()
+{
+ accept();
+}
+
+void ProgressDialog::changeStatus(const QString &status)
+{
+ ui->statusLabel->setText(status);
+ updateSize();
+}
+
+void ProgressDialog::changeProgress(qint64 current, qint64 total)
+{
+ ui->taskProgressBar->setMaximum(total);
+ ui->taskProgressBar->setValue(current);
+}
+
+void ProgressDialog::keyPressEvent(QKeyEvent *e)
+{
+ if(ui->skipButton->isVisible())
+ {
+ if (e->key() == Qt::Key_Escape)
+ {
+ on_skipButton_clicked(true);
+ return;
+ }
+ else if(e->key() == Qt::Key_Tab)
+ {
+ ui->skipButton->setFocusPolicy(Qt::StrongFocus);
+ ui->skipButton->setFocus();
+ ui->skipButton->setAutoDefault(true);
+ ui->skipButton->setDefault(true);
+ return;
+ }
+ }
+ QDialog::keyPressEvent(e);
+}
+
+void ProgressDialog::closeEvent(QCloseEvent *e)
+{
+ if (task && task->isRunning())
+ {
+ e->ignore();
+ }
+ else
+ {
+ QDialog::closeEvent(e);
+ }
+}
diff --git a/launcher/ui/dialogs/ProgressDialog.h b/launcher/ui/dialogs/ProgressDialog.h
new file mode 100644
index 00000000..b28ad4fa
--- /dev/null
+++ b/launcher/ui/dialogs/ProgressDialog.h
@@ -0,0 +1,71 @@
+/* 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 <QDialog>
+#include <memory>
+
+class Task;
+
+namespace Ui
+{
+class ProgressDialog;
+}
+
+class ProgressDialog : public QDialog
+{
+ Q_OBJECT
+
+public:
+ explicit ProgressDialog(QWidget *parent = 0);
+ ~ProgressDialog();
+
+ void updateSize();
+
+ int execWithTask(Task *task);
+ int execWithTask(std::unique_ptr<Task> &&task);
+ int execWithTask(std::unique_ptr<Task> &task);
+
+ void setSkipButton(bool present, QString label = QString());
+
+ Task *getTask();
+
+public
+slots:
+ void onTaskStarted();
+ void onTaskFailed(QString failure);
+ void onTaskSucceeded();
+
+ void changeStatus(const QString &status);
+ void changeProgress(qint64 current, qint64 total);
+
+
+private
+slots:
+ void on_skipButton_clicked(bool checked);
+
+protected:
+ virtual void keyPressEvent(QKeyEvent *e);
+ virtual void closeEvent(QCloseEvent *e);
+
+private:
+ bool handleImmediateResult(QDialog::DialogCode &result);
+
+private:
+ Ui::ProgressDialog *ui;
+
+ Task *task;
+};
diff --git a/launcher/ui/dialogs/ProgressDialog.ui b/launcher/ui/dialogs/ProgressDialog.ui
new file mode 100644
index 00000000..04b8fef3
--- /dev/null
+++ b/launcher/ui/dialogs/ProgressDialog.ui
@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>ProgressDialog</class>
+ <widget class="QDialog" name="ProgressDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>400</width>
+ <height>100</height>
+ </rect>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>400</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>600</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="windowTitle">
+ <string>Please wait...</string>
+ </property>
+ <layout class="QGridLayout" name="gridLayout">
+ <item row="0" column="0">
+ <widget class="QLabel" name="statusLabel">
+ <property name="text">
+ <string>Task Status...</string>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0">
+ <widget class="QProgressBar" name="taskProgressBar">
+ <property name="value">
+ <number>24</number>
+ </property>
+ <property name="textVisible">
+ <bool>false</bool>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="0">
+ <widget class="QPushButton" name="skipButton">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Skip</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/launcher/ui/dialogs/SkinUploadDialog.cpp b/launcher/ui/dialogs/SkinUploadDialog.cpp
new file mode 100644
index 00000000..4e6142fa
--- /dev/null
+++ b/launcher/ui/dialogs/SkinUploadDialog.cpp
@@ -0,0 +1,155 @@
+#include <QFileInfo>
+#include <QFileDialog>
+#include <QPainter>
+
+#include <FileSystem.h>
+
+#include <minecraft/services/SkinUpload.h>
+#include <minecraft/services/CapeChange.h>
+#include <tasks/SequentialTask.h>
+
+#include "SkinUploadDialog.h"
+#include "ui_SkinUploadDialog.h"
+#include "ProgressDialog.h"
+#include "CustomMessageBox.h"
+
+void SkinUploadDialog::on_buttonBox_rejected()
+{
+ close();
+}
+
+void SkinUploadDialog::on_buttonBox_accepted()
+{
+ AuthSessionPtr session = std::make_shared<AuthSession>();
+ auto login = m_acct->refresh(session);
+ ProgressDialog prog(this);
+ if (prog.execWithTask((Task*)login.get()) != QDialog::Accepted)
+ {
+ //FIXME: recover with password prompt
+ CustomMessageBox::selectable(this, tr("Skin Upload"), tr("Failed to login!"), QMessageBox::Warning)->exec();
+ close();
+ return;
+ }
+ QString fileName;
+ QString input = ui->skinPathTextBox->text();
+ QRegExp urlPrefixMatcher("^([a-z]+)://.+$");
+ bool isLocalFile = false;
+ // it has an URL prefix -> it is an URL
+ if(urlPrefixMatcher.exactMatch(input))
+ {
+ QUrl fileURL = input;
+ if(fileURL.isValid())
+ {
+ // local?
+ if(fileURL.isLocalFile())
+ {
+ isLocalFile = true;
+ fileName = fileURL.toLocalFile();
+ }
+ else
+ {
+ CustomMessageBox::selectable(
+ this,
+ tr("Skin Upload"),
+ tr("Using remote URLs for setting skins is not implemented yet."),
+ QMessageBox::Warning
+ )->exec();
+ close();
+ return;
+ }
+ }
+ else
+ {
+ CustomMessageBox::selectable(
+ this,
+ tr("Skin Upload"),
+ tr("You cannot use an invalid URL for uploading skins."),
+ QMessageBox::Warning
+ )->exec();
+ close();
+ return;
+ }
+ }
+ else
+ {
+ // just assume it's a path then
+ isLocalFile = true;
+ fileName = ui->skinPathTextBox->text();
+ }
+ if (isLocalFile && !QFile::exists(fileName))
+ {
+ CustomMessageBox::selectable(this, tr("Skin Upload"), tr("Skin file does not exist!"), QMessageBox::Warning)->exec();
+ close();
+ return;
+ }
+ SkinUpload::Model model = SkinUpload::STEVE;
+ if (ui->steveBtn->isChecked())
+ {
+ model = SkinUpload::STEVE;
+ }
+ else if (ui->alexBtn->isChecked())
+ {
+ model = SkinUpload::ALEX;
+ }
+ SequentialTask skinUpload;
+ skinUpload.addTask(shared_qobject_ptr<SkinUpload>(new SkinUpload(this, session, FS::read(fileName), model)));
+ auto selectedCape = ui->capeCombo->currentData().toString();
+ if(selectedCape != m_acct->accountData()->minecraftProfile.currentCape) {
+ skinUpload.addTask(shared_qobject_ptr<CapeChange>(new CapeChange(this, session, selectedCape)));
+ }
+ if (prog.execWithTask(&skinUpload) != QDialog::Accepted)
+ {
+ CustomMessageBox::selectable(this, tr("Skin Upload"), tr("Failed to upload skin!"), QMessageBox::Warning)->exec();
+ close();
+ return;
+ }
+ CustomMessageBox::selectable(this, tr("Skin Upload"), tr("Success"), QMessageBox::Information)->exec();
+ close();
+}
+
+void SkinUploadDialog::on_skinBrowseBtn_clicked()
+{
+ QString raw_path = QFileDialog::getOpenFileName(this, tr("Select Skin Texture"), QString(), "*.png");
+ if (raw_path.isEmpty() || !QFileInfo::exists(raw_path))
+ {
+ return;
+ }
+ QString cooked_path = FS::NormalizePath(raw_path);
+ ui->skinPathTextBox->setText(cooked_path);
+}
+
+SkinUploadDialog::SkinUploadDialog(MinecraftAccountPtr acct, QWidget *parent)
+ :QDialog(parent), m_acct(acct), ui(new Ui::SkinUploadDialog)
+{
+ ui->setupUi(this);
+
+ // FIXME: add a model for this, download/refresh the capes on demand
+ auto &data = *acct->accountData();
+ int index = 0;
+ ui->capeCombo->addItem(tr("No Cape"), QVariant());
+ auto currentCape = data.minecraftProfile.currentCape;
+ if(currentCape.isEmpty()) {
+ ui->capeCombo->setCurrentIndex(index);
+ }
+
+ for(auto & cape: data.minecraftProfile.capes) {
+ index++;
+ if(cape.data.size()) {
+ QPixmap capeImage;
+ if(capeImage.loadFromData(cape.data, "PNG")) {
+ QPixmap preview = QPixmap(10, 16);
+ QPainter painter(&preview);
+ painter.drawPixmap(0, 0, capeImage.copy(1, 1, 10, 16));
+ ui->capeCombo->addItem(capeImage, cape.alias, cape.id);
+ if(currentCape == cape.id) {
+ ui->capeCombo->setCurrentIndex(index);
+ }
+ continue;
+ }
+ }
+ ui->capeCombo->addItem(cape.alias, cape.id);
+ if(currentCape == cape.id) {
+ ui->capeCombo->setCurrentIndex(index);
+ }
+ }
+}
diff --git a/launcher/ui/dialogs/SkinUploadDialog.h b/launcher/ui/dialogs/SkinUploadDialog.h
new file mode 100644
index 00000000..84d17dc6
--- /dev/null
+++ b/launcher/ui/dialogs/SkinUploadDialog.h
@@ -0,0 +1,29 @@
+#pragma once
+
+#include <QDialog>
+#include <minecraft/auth/MinecraftAccount.h>
+
+namespace Ui
+{
+ class SkinUploadDialog;
+}
+
+class SkinUploadDialog : public QDialog {
+ Q_OBJECT
+public:
+ explicit SkinUploadDialog(MinecraftAccountPtr acct, QWidget *parent = 0);
+ virtual ~SkinUploadDialog() {};
+
+public slots:
+ void on_buttonBox_accepted();
+
+ void on_buttonBox_rejected();
+
+ void on_skinBrowseBtn_clicked();
+
+protected:
+ MinecraftAccountPtr m_acct;
+
+private:
+ Ui::SkinUploadDialog *ui;
+};
diff --git a/launcher/ui/dialogs/SkinUploadDialog.ui b/launcher/ui/dialogs/SkinUploadDialog.ui
new file mode 100644
index 00000000..f4b0ed0a
--- /dev/null
+++ b/launcher/ui/dialogs/SkinUploadDialog.ui
@@ -0,0 +1,97 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>SkinUploadDialog</class>
+ <widget class="QDialog" name="SkinUploadDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>394</width>
+ <height>360</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Skin Upload</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <widget class="QGroupBox" name="fileBox">
+ <property name="title">
+ <string>Skin File</string>
+ </property>
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <item>
+ <widget class="QLineEdit" name="skinPathTextBox"/>
+ </item>
+ <item>
+ <widget class="QPushButton" name="skinBrowseBtn">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>28</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="text">
+ <string notr="true">...</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="modelBox">
+ <property name="title">
+ <string>Player Model</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_1">
+ <item>
+ <widget class="QRadioButton" name="steveBtn">
+ <property name="text">
+ <string>Steve Model</string>
+ </property>
+ <property name="checked">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QRadioButton" name="alexBtn">
+ <property name="text">
+ <string>Alex Model</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="capeBox">
+ <property name="title">
+ <string>Cape</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_2">
+ <item>
+ <widget class="QComboBox" name="capeCombo"/>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QDialogButtonBox" name="buttonBox">
+ <property name="standardButtons">
+ <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/launcher/ui/dialogs/UpdateDialog.cpp b/launcher/ui/dialogs/UpdateDialog.cpp
new file mode 100644
index 00000000..4a6a1fdd
--- /dev/null
+++ b/launcher/ui/dialogs/UpdateDialog.cpp
@@ -0,0 +1,182 @@
+#include "UpdateDialog.h"
+#include "ui_UpdateDialog.h"
+#include <QDebug>
+#include "Application.h"
+#include <settings/SettingsObject.h>
+#include <Json.h>
+
+#include "BuildConfig.h"
+#include "HoeDown.h"
+
+UpdateDialog::UpdateDialog(bool hasUpdate, QWidget *parent) : QDialog(parent), ui(new Ui::UpdateDialog)
+{
+ ui->setupUi(this);
+ auto channel = APPLICATION->settings()->get("UpdateChannel").toString();
+ if(hasUpdate)
+ {
+ ui->label->setText(tr("A new %1 update is available!").arg(channel));
+ }
+ else
+ {
+ ui->label->setText(tr("No %1 updates found. You are running the latest version.").arg(channel));
+ ui->btnUpdateNow->setHidden(true);
+ ui->btnUpdateLater->setText(tr("Close"));
+ }
+ ui->changelogBrowser->setHtml(tr("<center><h1>Loading changelog...</h1></center>"));
+ loadChangelog();
+ restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get("UpdateDialogGeometry").toByteArray()));
+}
+
+UpdateDialog::~UpdateDialog()
+{
+}
+
+void UpdateDialog::loadChangelog()
+{
+ auto channel = APPLICATION->settings()->get("UpdateChannel").toString();
+ dljob.reset(new NetJob("Changelog"));
+ QString url;
+ if(channel == "stable")
+ {
+ url = QString("https://raw.githubusercontent.com/MultiMC/Launcher/%1/changelog.md").arg(channel);
+ m_changelogType = CHANGELOG_MARKDOWN;
+ }
+ else
+ {
+ url = QString("https://api.github.com/repos/MultiMC/Launcher/compare/%1...%2").arg(BuildConfig.GIT_COMMIT, channel);
+ m_changelogType = CHANGELOG_COMMITS;
+ }
+ dljob->addNetAction(Net::Download::makeByteArray(QUrl(url), &changelogData));
+ connect(dljob.get(), &NetJob::succeeded, this, &UpdateDialog::changelogLoaded);
+ connect(dljob.get(), &NetJob::failed, this, &UpdateDialog::changelogFailed);
+ dljob->start(APPLICATION->network());
+}
+
+QString reprocessMarkdown(QByteArray markdown)
+{
+ HoeDown hoedown;
+ QString output = hoedown.process(markdown);
+
+ // HACK: easier than customizing hoedown
+ output.replace(QRegExp("GH-([0-9]+)"), "<a href=\"https://github.com/MultiMC/Launcher/issues/\\1\">GH-\\1</a>");
+ qDebug() << output;
+ return output;
+}
+
+QString reprocessCommits(QByteArray json)
+{
+ auto channel = APPLICATION->settings()->get("UpdateChannel").toString();
+ try
+ {
+ QString result;
+ auto document = Json::requireDocument(json);
+ auto rootobject = Json::requireObject(document);
+ auto status = Json::requireString(rootobject, "status");
+ auto diff_url = Json::requireString(rootobject, "html_url");
+
+ auto print_commits = [&]()
+ {
+ result += "<table cellspacing=0 cellpadding=2 style='border-width: 1px; border-style: solid'>";
+ auto commitarray = Json::requireArray(rootobject, "commits");
+ for(int i = commitarray.size() - 1; i >= 0; i--)
+ {
+ const auto & commitval = commitarray[i];
+ auto commitobj = Json::requireObject(commitval);
+ auto parents_info = Json::ensureArray(commitobj, "parents");
+ // NOTE: this ignores merge commits, because they have more than one parent
+ if(parents_info.size() > 1)
+ {
+ continue;
+ }
+ auto commit_url = Json::requireString(commitobj, "html_url");
+ auto commit_info = Json::requireObject(commitobj, "commit");
+ auto commit_message = Json::requireString(commit_info, "message");
+ auto lines = commit_message.split('\n');
+ QRegularExpression regexp("(?<prefix>(GH-(?<issuenr>[0-9]+))|(NOISSUE)|(SCRATCH))? *(?<rest>.*) *");
+ auto match = regexp.match(lines.takeFirst(), 0, QRegularExpression::NormalMatch);
+ auto issuenr = match.captured("issuenr");
+ auto prefix = match.captured("prefix");
+ auto rest = match.captured("rest");
+ result += "<tr><td>";
+ if(issuenr.length())
+ {
+ result += QString("<a href=\"https://github.com/MultiMC/Launcher/issues/%1\">GH-%2</a>").arg(issuenr, issuenr);
+ }
+ else if(prefix.length())
+ {
+ result += QString("<a href=\"%1\">%2</a>").arg(commit_url, prefix);
+ }
+ else
+ {
+ result += QString("<a href=\"%1\">NOISSUE</a>").arg(commit_url);
+ }
+ result += "</td>";
+ lines.prepend(rest);
+ result += "<td><p>" + lines.join("<br />") + "</p></td></tr>";
+ }
+ result += "</table>";
+ };
+
+ if(status == "identical")
+ {
+ return QObject::tr("<p>There are no code changes between your current version and latest %1.</p>").arg(channel);
+ }
+ else if(status == "ahead")
+ {
+ result += QObject::tr("<p>Following commits were added since last update:</p>");
+ print_commits();
+ }
+ else if(status == "diverged")
+ {
+ auto commit_ahead = Json::requireInteger(rootobject, "ahead_by");
+ auto commit_behind = Json::requireInteger(rootobject, "behind_by");
+ result += QObject::tr("<p>The update removes %1 commits and adds the following %2:</p>").arg(commit_behind).arg(commit_ahead);
+ print_commits();
+ }
+ result += QObject::tr("<p>You can <a href=\"%1\">look at the changes on github</a>.</p>").arg(diff_url);
+ return result;
+ }
+ catch (const JSONValidationError &e)
+ {
+ qWarning() << "Got an unparseable commit log from github:" << e.what();
+ qDebug() << json;
+ }
+ return QString();
+}
+
+void UpdateDialog::changelogLoaded()
+{
+ QString result;
+ switch(m_changelogType)
+ {
+ case CHANGELOG_COMMITS:
+ result = reprocessCommits(changelogData);
+ break;
+ case CHANGELOG_MARKDOWN:
+ result = reprocessMarkdown(changelogData);
+ break;
+ }
+ changelogData.clear();
+ ui->changelogBrowser->setHtml(result);
+}
+
+void UpdateDialog::changelogFailed(QString reason)
+{
+ ui->changelogBrowser->setHtml(tr("<p align=\"center\" <span style=\"font-size:22pt;\">Failed to fetch changelog... Error: %1</span></p>").arg(reason));
+}
+
+void UpdateDialog::on_btnUpdateLater_clicked()
+{
+ reject();
+}
+
+void UpdateDialog::on_btnUpdateNow_clicked()
+{
+ done(UPDATE_NOW);
+}
+
+void UpdateDialog::closeEvent(QCloseEvent* evt)
+{
+ APPLICATION->settings()->set("UpdateDialogGeometry", saveGeometry().toBase64());
+ QDialog::closeEvent(evt);
+}
diff --git a/launcher/ui/dialogs/UpdateDialog.h b/launcher/ui/dialogs/UpdateDialog.h
new file mode 100644
index 00000000..07cbe09f
--- /dev/null
+++ b/launcher/ui/dialogs/UpdateDialog.h
@@ -0,0 +1,67 @@
+/* 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 <QDialog>
+#include "net/NetJob.h"
+
+namespace Ui
+{
+class UpdateDialog;
+}
+
+enum UpdateAction
+{
+ UPDATE_LATER = QDialog::Rejected,
+ UPDATE_NOW = QDialog::Accepted,
+};
+
+enum ChangelogType
+{
+ CHANGELOG_MARKDOWN,
+ CHANGELOG_COMMITS
+};
+
+class UpdateDialog : public QDialog
+{
+ Q_OBJECT
+
+public:
+ explicit UpdateDialog(bool hasUpdate = true, QWidget *parent = 0);
+ ~UpdateDialog();
+
+public slots:
+ void on_btnUpdateNow_clicked();
+ void on_btnUpdateLater_clicked();
+
+ /// Starts loading the changelog
+ void loadChangelog();
+
+ /// Slot for when the chengelog loads successfully.
+ void changelogLoaded();
+
+ /// Slot for when the chengelog fails to load...
+ void changelogFailed(QString reason);
+
+protected:
+ void closeEvent(QCloseEvent * ) override;
+
+private:
+ Ui::UpdateDialog *ui;
+ QByteArray changelogData;
+ NetJob::Ptr dljob;
+ ChangelogType m_changelogType = CHANGELOG_MARKDOWN;
+};
diff --git a/launcher/ui/dialogs/UpdateDialog.ui b/launcher/ui/dialogs/UpdateDialog.ui
new file mode 100644
index 00000000..b0b3dd83
--- /dev/null
+++ b/launcher/ui/dialogs/UpdateDialog.ui
@@ -0,0 +1,91 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>UpdateDialog</class>
+ <widget class="QDialog" name="UpdateDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>657</width>
+ <height>673</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>MultiMC Update</string>
+ </property>
+ <property name="windowIcon">
+ <iconset>
+ <normaloff>:/icons/toolbar/checkupdate</normaloff>:/icons/toolbar/checkupdate</iconset>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_2">
+ <item>
+ <widget class="QLabel" name="label">
+ <property name="font">
+ <font>
+ <pointsize>14</pointsize>
+ </font>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
+ </property>
+ <property name="buddy">
+ <cstring>changelogBrowser</cstring>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QTextBrowser" name="changelogBrowser">
+ <property name="openExternalLinks">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <layout class="QGridLayout" name="gridLayout">
+ <item row="0" column="0">
+ <widget class="QPushButton" name="btnUpdateNow">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Update now</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QPushButton" name="btnUpdateLater">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Don't update yet</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ <tabstops>
+ <tabstop>changelogBrowser</tabstop>
+ <tabstop>btnUpdateNow</tabstop>
+ <tabstop>btnUpdateLater</tabstop>
+ </tabstops>
+ <resources>
+ <include location="../../resources/multimc/multimc.qrc"/>
+ </resources>
+ <connections/>
+</ui>
diff --git a/launcher/ui/dialogs/VersionSelectDialog.cpp b/launcher/ui/dialogs/VersionSelectDialog.cpp
new file mode 100644
index 00000000..70ef72d6
--- /dev/null
+++ b/launcher/ui/dialogs/VersionSelectDialog.cpp
@@ -0,0 +1,141 @@
+/* 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 "VersionSelectDialog.h"
+
+#include <QtWidgets/QButtonGroup>
+#include <QtWidgets/QDialogButtonBox>
+#include <QtWidgets/QHBoxLayout>
+#include <QtWidgets/QPushButton>
+#include <QtWidgets/QVBoxLayout>
+#include <QDebug>
+
+#include "ui/dialogs/ProgressDialog.h"
+#include "ui/widgets/VersionSelectWidget.h"
+#include "ui/dialogs/CustomMessageBox.h"
+
+#include "BaseVersion.h"
+#include "BaseVersionList.h"
+#include "tasks/Task.h"
+#include "Application.h"
+#include "VersionProxyModel.h"
+
+VersionSelectDialog::VersionSelectDialog(BaseVersionList *vlist, QString title, QWidget *parent, bool cancelable)
+ : QDialog(parent)
+{
+ setObjectName(QStringLiteral("VersionSelectDialog"));
+ resize(400, 347);
+ m_verticalLayout = new QVBoxLayout(this);
+ m_verticalLayout->setObjectName(QStringLiteral("verticalLayout"));
+
+ m_versionWidget = new VersionSelectWidget(parent);
+ m_verticalLayout->addWidget(m_versionWidget);
+
+ m_horizontalLayout = new QHBoxLayout();
+ m_horizontalLayout->setObjectName(QStringLiteral("horizontalLayout"));
+
+ m_refreshButton = new QPushButton(this);
+ m_refreshButton->setObjectName(QStringLiteral("refreshButton"));
+ m_horizontalLayout->addWidget(m_refreshButton);
+
+ m_buttonBox = new QDialogButtonBox(this);
+ m_buttonBox->setObjectName(QStringLiteral("buttonBox"));
+ m_buttonBox->setOrientation(Qt::Horizontal);
+ m_buttonBox->setStandardButtons(QDialogButtonBox::Cancel|QDialogButtonBox::Ok);
+ m_horizontalLayout->addWidget(m_buttonBox);
+
+ m_verticalLayout->addLayout(m_horizontalLayout);
+
+ retranslate();
+
+ QObject::connect(m_buttonBox, SIGNAL(accepted()), this, SLOT(accept()));
+ QObject::connect(m_buttonBox, SIGNAL(rejected()), this, SLOT(reject()));
+
+ QMetaObject::connectSlotsByName(this);
+ setWindowModality(Qt::WindowModal);
+ setWindowTitle(title);
+
+ m_vlist = vlist;
+
+ if (!cancelable)
+ {
+ m_buttonBox->button(QDialogButtonBox::Cancel)->setEnabled(false);
+ }
+}
+
+void VersionSelectDialog::retranslate()
+{
+ // FIXME: overrides custom title given in constructor!
+ setWindowTitle(tr("Choose Version"));
+ m_refreshButton->setToolTip(tr("Reloads the version list."));
+ m_refreshButton->setText(tr("&Refresh"));
+}
+
+void VersionSelectDialog::setCurrentVersion(const QString& version)
+{
+ m_currentVersion = version;
+ m_versionWidget->setCurrentVersion(version);
+}
+
+void VersionSelectDialog::setEmptyString(QString emptyString)
+{
+ m_versionWidget->setEmptyString(emptyString);
+}
+
+void VersionSelectDialog::setEmptyErrorString(QString emptyErrorString)
+{
+ m_versionWidget->setEmptyErrorString(emptyErrorString);
+}
+
+void VersionSelectDialog::setResizeOn(int column)
+{
+ resizeOnColumn = column;
+}
+
+int VersionSelectDialog::exec()
+{
+ QDialog::open();
+ m_versionWidget->initialize(m_vlist);
+ if(resizeOnColumn != -1)
+ {
+ m_versionWidget->setResizeOn(resizeOnColumn);
+ }
+ return QDialog::exec();
+}
+
+void VersionSelectDialog::selectRecommended()
+{
+ m_versionWidget->selectRecommended();
+}
+
+BaseVersionPtr VersionSelectDialog::selectedVersion() const
+{
+ return m_versionWidget->selectedVersion();
+}
+
+void VersionSelectDialog::on_refreshButton_clicked()
+{
+ m_versionWidget->loadList();
+}
+
+void VersionSelectDialog::setExactFilter(BaseVersionList::ModelRoles role, QString filter)
+{
+ m_versionWidget->setExactFilter(role, filter);
+}
+
+void VersionSelectDialog::setFuzzyFilter(BaseVersionList::ModelRoles role, QString filter)
+{
+ m_versionWidget->setFuzzyFilter(role, filter);
+}
diff --git a/launcher/ui/dialogs/VersionSelectDialog.h b/launcher/ui/dialogs/VersionSelectDialog.h
new file mode 100644
index 00000000..ed30d3f3
--- /dev/null
+++ b/launcher/ui/dialogs/VersionSelectDialog.h
@@ -0,0 +1,78 @@
+/* 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 <QDialog>
+#include <QSortFilterProxyModel>
+
+
+#include "BaseVersionList.h"
+
+class QVBoxLayout;
+class QHBoxLayout;
+class QDialogButtonBox;
+class VersionSelectWidget;
+class QPushButton;
+
+namespace Ui
+{
+class VersionSelectDialog;
+}
+
+class VersionProxyModel;
+
+class VersionSelectDialog : public QDialog
+{
+ Q_OBJECT
+
+public:
+ explicit VersionSelectDialog(BaseVersionList *vlist, QString title, QWidget *parent = 0, bool cancelable = true);
+ virtual ~VersionSelectDialog() {};
+
+ int exec() override;
+
+ BaseVersionPtr selectedVersion() const;
+
+ void setCurrentVersion(const QString & version);
+ void setFuzzyFilter(BaseVersionList::ModelRoles role, QString filter);
+ void setExactFilter(BaseVersionList::ModelRoles role, QString filter);
+ void setEmptyString(QString emptyString);
+ void setEmptyErrorString(QString emptyErrorString);
+ void setResizeOn(int column);
+
+private slots:
+ void on_refreshButton_clicked();
+
+private:
+ void retranslate();
+ void selectRecommended();
+
+private:
+ QString m_currentVersion;
+ VersionSelectWidget *m_versionWidget = nullptr;
+ QVBoxLayout *m_verticalLayout = nullptr;
+ QHBoxLayout *m_horizontalLayout = nullptr;
+ QPushButton *m_refreshButton = nullptr;
+ QDialogButtonBox *m_buttonBox = nullptr;
+
+ BaseVersionList *m_vlist = nullptr;
+
+ VersionProxyModel *m_proxyModel = nullptr;
+
+ int resizeOnColumn = -1;
+
+ Task * loadTask = nullptr;
+};
diff --git a/launcher/ui/instanceview/AccessibleInstanceView.cpp b/launcher/ui/instanceview/AccessibleInstanceView.cpp
new file mode 100644
index 00000000..7de3ac72
--- /dev/null
+++ b/launcher/ui/instanceview/AccessibleInstanceView.cpp
@@ -0,0 +1,778 @@
+#include "InstanceView.h"
+#include "AccessibleInstanceView.h"
+#include "AccessibleInstanceView_p.h"
+
+#include <qvariant.h>
+#include <qaccessible.h>
+#include <qheaderview.h>
+
+#ifndef QT_NO_ACCESSIBILITY
+
+QAccessibleInterface *groupViewAccessibleFactory(const QString &classname, QObject *object)
+{
+ QAccessibleInterface *iface = 0;
+ if (!object || !object->isWidgetType())
+ return iface;
+
+ QWidget *widget = static_cast<QWidget*>(object);
+
+ if (classname == QLatin1String("InstanceView")) {
+ iface = new AccessibleInstanceView((InstanceView *)widget);
+ }
+ return iface;
+}
+
+
+QAbstractItemView *AccessibleInstanceView::view() const
+{
+ return qobject_cast<QAbstractItemView*>(object());
+}
+
+int AccessibleInstanceView::logicalIndex(const QModelIndex &index) const
+{
+ if (!view()->model() || !index.isValid())
+ return -1;
+ return index.row() * (index.model()->columnCount()) + index.column();
+}
+
+AccessibleInstanceView::AccessibleInstanceView(QWidget *w)
+ : QAccessibleObject(w)
+{
+ Q_ASSERT(view());
+}
+
+bool AccessibleInstanceView::isValid() const
+{
+ return view();
+}
+
+AccessibleInstanceView::~AccessibleInstanceView()
+{
+ for (QAccessible::Id id : childToId) {
+ QAccessible::deleteAccessibleInterface(id);
+ }
+}
+
+QAccessibleInterface *AccessibleInstanceView::cellAt(int row, int column) const
+{
+ if (!view()->model()) {
+ return 0;
+ }
+
+ QModelIndex index = view()->model()->index(row, column, view()->rootIndex());
+ if (Q_UNLIKELY(!index.isValid())) {
+ qWarning() << "AccessibleInstanceView::cellAt: invalid index: " << index << " for " << view();
+ return 0;
+ }
+
+ return child(logicalIndex(index));
+}
+
+QAccessibleInterface *AccessibleInstanceView::caption() const
+{
+ return 0;
+}
+
+QString AccessibleInstanceView::columnDescription(int column) const
+{
+ if (!view()->model())
+ return QString();
+
+ return view()->model()->headerData(column, Qt::Horizontal).toString();
+}
+
+int AccessibleInstanceView::columnCount() const
+{
+ if (!view()->model())
+ return 0;
+ return 1;
+}
+
+int AccessibleInstanceView::rowCount() const
+{
+ if (!view()->model())
+ return 0;
+ return view()->model()->rowCount();
+}
+
+int AccessibleInstanceView::selectedCellCount() const
+{
+ if (!view()->selectionModel())
+ return 0;
+ return view()->selectionModel()->selectedIndexes().count();
+}
+
+int AccessibleInstanceView::selectedColumnCount() const
+{
+ if (!view()->selectionModel())
+ return 0;
+ return view()->selectionModel()->selectedColumns().count();
+}
+
+int AccessibleInstanceView::selectedRowCount() const
+{
+ if (!view()->selectionModel())
+ return 0;
+ return view()->selectionModel()->selectedRows().count();
+}
+
+QString AccessibleInstanceView::rowDescription(int row) const
+{
+ if (!view()->model())
+ return QString();
+ return view()->model()->headerData(row, Qt::Vertical).toString();
+}
+
+QList<QAccessibleInterface *> AccessibleInstanceView::selectedCells() const
+{
+ QList<QAccessibleInterface*> cells;
+ if (!view()->selectionModel())
+ return cells;
+ const QModelIndexList selectedIndexes = view()->selectionModel()->selectedIndexes();
+ cells.reserve(selectedIndexes.size());
+ for (const QModelIndex &index : selectedIndexes)
+ cells.append(child(logicalIndex(index)));
+ return cells;
+}
+
+QList<int> AccessibleInstanceView::selectedColumns() const
+{
+ if (!view()->selectionModel()) {
+ return QList<int>();
+ }
+
+ const QModelIndexList selectedColumns = view()->selectionModel()->selectedColumns();
+
+ QList<int> columns;
+ columns.reserve(selectedColumns.size());
+ for (const QModelIndex &index : selectedColumns) {
+ columns.append(index.column());
+ }
+
+ return columns;
+}
+
+QList<int> AccessibleInstanceView::selectedRows() const
+{
+ if (!view()->selectionModel()) {
+ return QList<int>();
+ }
+
+ QList<int> rows;
+
+ const QModelIndexList selectedRows = view()->selectionModel()->selectedRows();
+
+ rows.reserve(selectedRows.size());
+ for (const QModelIndex &index : selectedRows) {
+ rows.append(index.row());
+ }
+
+ return rows;
+}
+
+QAccessibleInterface *AccessibleInstanceView::summary() const
+{
+ return 0;
+}
+
+bool AccessibleInstanceView::isColumnSelected(int column) const
+{
+ if (!view()->selectionModel()) {
+ return false;
+ }
+
+ return view()->selectionModel()->isColumnSelected(column, QModelIndex());
+}
+
+bool AccessibleInstanceView::isRowSelected(int row) const
+{
+ if (!view()->selectionModel()) {
+ return false;
+ }
+
+ return view()->selectionModel()->isRowSelected(row, QModelIndex());
+}
+
+bool AccessibleInstanceView::selectRow(int row)
+{
+ if (!view()->model() || !view()->selectionModel()) {
+ return false;
+ }
+ QModelIndex index = view()->model()->index(row, 0, view()->rootIndex());
+
+ if (!index.isValid() || view()->selectionBehavior() == QAbstractItemView::SelectColumns) {
+ return false;
+ }
+
+ switch (view()->selectionMode()) {
+ case QAbstractItemView::NoSelection: {
+ return false;
+ }
+ case QAbstractItemView::SingleSelection: {
+ if (view()->selectionBehavior() != QAbstractItemView::SelectRows && columnCount() > 1 )
+ return false;
+ view()->clearSelection();
+ break;
+ }
+ case QAbstractItemView::ContiguousSelection: {
+ if ((!row || !view()->selectionModel()->isRowSelected(row - 1, view()->rootIndex())) && !view()->selectionModel()->isRowSelected(row + 1, view()->rootIndex())) {
+ view()->clearSelection();
+ }
+ break;
+ }
+ default: {
+ break;
+ }
+ }
+
+ view()->selectionModel()->select(index, QItemSelectionModel::Select | QItemSelectionModel::Rows);
+ return true;
+}
+
+bool AccessibleInstanceView::selectColumn(int column)
+{
+ if (!view()->model() || !view()->selectionModel()) {
+ return false;
+ }
+ QModelIndex index = view()->model()->index(0, column, view()->rootIndex());
+
+ if (!index.isValid() || view()->selectionBehavior() == QAbstractItemView::SelectRows) {
+ return false;
+ }
+
+ switch (view()->selectionMode()) {
+ case QAbstractItemView::NoSelection: {
+ return false;
+ }
+ case QAbstractItemView::SingleSelection: {
+ if (view()->selectionBehavior() != QAbstractItemView::SelectColumns && rowCount() > 1) {
+ return false;
+ }
+ // fallthrough intentional
+ }
+ case QAbstractItemView::ContiguousSelection: {
+ if ((!column || !view()->selectionModel()->isColumnSelected(column - 1, view()->rootIndex())) && !view()->selectionModel()->isColumnSelected(column + 1, view()->rootIndex())) {
+ view()->clearSelection();
+ }
+ break;
+ }
+ default: {
+ break;
+ }
+ }
+
+ view()->selectionModel()->select(index, QItemSelectionModel::Select | QItemSelectionModel::Columns);
+ return true;
+}
+
+bool AccessibleInstanceView::unselectRow(int row)
+{
+ if (!view()->model() || !view()->selectionModel()) {
+ return false;
+ }
+
+ QModelIndex index = view()->model()->index(row, 0, view()->rootIndex());
+ if (!index.isValid()) {
+ return false;
+ }
+
+ QItemSelection selection(index, index);
+ auto selectionModel = view()->selectionModel();
+
+ switch (view()->selectionMode()) {
+ case QAbstractItemView::SingleSelection:
+ // no unselect
+ if (selectedRowCount() == 1) {
+ return false;
+ }
+ break;
+ case QAbstractItemView::ContiguousSelection: {
+ // no unselect
+ if (selectedRowCount() == 1) {
+ return false;
+ }
+
+
+ if ((!row || selectionModel->isRowSelected(row - 1, view()->rootIndex())) && selectionModel->isRowSelected(row + 1, view()->rootIndex())) {
+ //If there are rows selected both up the current row and down the current rown,
+ //the ones which are down the current row will be deselected
+ selection = QItemSelection(index, view()->model()->index(rowCount() - 1, 0, view()->rootIndex()));
+ }
+ }
+ default: {
+ break;
+ }
+ }
+
+ selectionModel->select(selection, QItemSelectionModel::Deselect | QItemSelectionModel::Rows);
+ return true;
+}
+
+bool AccessibleInstanceView::unselectColumn(int column)
+{
+ auto model = view()->model();
+ if (!model || !view()->selectionModel()) {
+ return false;
+ }
+
+ QModelIndex index = model->index(0, column, view()->rootIndex());
+ if (!index.isValid()) {
+ return false;
+ }
+
+ QItemSelection selection(index, index);
+
+ switch (view()->selectionMode()) {
+ case QAbstractItemView::SingleSelection: {
+ //In SingleSelection and ContiguousSelection once an item
+ //is selected, there's no way for the user to unselect all items
+ if (selectedColumnCount() == 1) {
+ return false;
+ }
+ break;
+ }
+ case QAbstractItemView::ContiguousSelection:
+ if (selectedColumnCount() == 1) {
+ return false;
+ }
+
+ if ((!column || view()->selectionModel()->isColumnSelected(column - 1, view()->rootIndex()))
+ && view()->selectionModel()->isColumnSelected(column + 1, view()->rootIndex())) {
+ //If there are columns selected both at the left of the current row and at the right
+ //of the current row, the ones which are at the right will be deselected
+ selection = QItemSelection(index, model->index(0, columnCount() - 1, view()->rootIndex()));
+ }
+ default:
+ break;
+ }
+
+ view()->selectionModel()->select(selection, QItemSelectionModel::Deselect | QItemSelectionModel::Columns);
+ return true;
+}
+
+QAccessible::Role AccessibleInstanceView::role() const
+{
+ return QAccessible::List;
+}
+
+QAccessible::State AccessibleInstanceView::state() const
+{
+ return QAccessible::State();
+}
+
+QAccessibleInterface *AccessibleInstanceView::childAt(int x, int y) const
+{
+ QPoint viewportOffset = view()->viewport()->mapTo(view(), QPoint(0,0));
+ QPoint indexPosition = view()->mapFromGlobal(QPoint(x, y) - viewportOffset);
+ // FIXME: if indexPosition < 0 in one coordinate, return header
+
+ QModelIndex index = view()->indexAt(indexPosition);
+ if (index.isValid()) {
+ return child(logicalIndex(index));
+ }
+ return 0;
+}
+
+int AccessibleInstanceView::childCount() const
+{
+ if (!view()->model()) {
+ return 0;
+ }
+ return (view()->model()->rowCount()) * (view()->model()->columnCount());
+}
+
+int AccessibleInstanceView::indexOfChild(const QAccessibleInterface *iface) const
+{
+ if (!view()->model())
+ return -1;
+ QAccessibleInterface *parent = iface->parent();
+ if (parent->object() != view())
+ return -1;
+
+ Q_ASSERT(iface->role() != QAccessible::TreeItem); // should be handled by tree class
+ if (iface->role() == QAccessible::Cell || iface->role() == QAccessible::ListItem) {
+ const AccessibleInstanceViewItem* cell = static_cast<const AccessibleInstanceViewItem*>(iface);
+ return logicalIndex(cell->m_index);
+ } else if (iface->role() == QAccessible::Pane) {
+ return 0; // corner button
+ } else {
+ qWarning() << "AccessibleInstanceView::indexOfChild has a child with unknown role..." << iface->role() << iface->text(QAccessible::Name);
+ }
+ // FIXME: we are in denial of our children. this should stop.
+ return -1;
+}
+
+QString AccessibleInstanceView::text(QAccessible::Text t) const
+{
+ if (t == QAccessible::Description)
+ return view()->accessibleDescription();
+ return view()->accessibleName();
+}
+
+QRect AccessibleInstanceView::rect() const
+{
+ if (!view()->isVisible())
+ return QRect();
+ QPoint pos = view()->mapToGlobal(QPoint(0, 0));
+ return QRect(pos.x(), pos.y(), view()->width(), view()->height());
+}
+
+QAccessibleInterface *AccessibleInstanceView::parent() const
+{
+ if (view() && view()->parent()) {
+ if (qstrcmp("QComboBoxPrivateContainer", view()->parent()->metaObject()->className()) == 0) {
+ return QAccessible::queryAccessibleInterface(view()->parent()->parent());
+ }
+ return QAccessible::queryAccessibleInterface(view()->parent());
+ }
+ return 0;
+}
+
+QAccessibleInterface *AccessibleInstanceView::child(int logicalIndex) const
+{
+ if (!view()->model())
+ return 0;
+
+ auto id = childToId.constFind(logicalIndex);
+ if (id != childToId.constEnd())
+ return QAccessible::accessibleInterface(id.value());
+
+ int columns = view()->model()->columnCount();
+
+ int row = logicalIndex / columns;
+ int column = logicalIndex % columns;
+
+ QAccessibleInterface *iface = 0;
+
+ QModelIndex index = view()->model()->index(row, column, view()->rootIndex());
+ if (Q_UNLIKELY(!index.isValid())) {
+ qWarning("AccessibleInstanceView::child: Invalid index at: %d %d", row, column);
+ return 0;
+ }
+ iface = new AccessibleInstanceViewItem(view(), index);
+
+ QAccessible::registerAccessibleInterface(iface);
+ childToId.insert(logicalIndex, QAccessible::uniqueId(iface));
+ return iface;
+}
+
+void *AccessibleInstanceView::interface_cast(QAccessible::InterfaceType t)
+{
+ if (t == QAccessible::TableInterface)
+ return static_cast<QAccessibleTableInterface*>(this);
+ return 0;
+}
+
+void AccessibleInstanceView::modelChange(QAccessibleTableModelChangeEvent *event)
+{
+ // if there is no cache yet, we don't update anything
+ if (childToId.isEmpty())
+ return;
+
+ switch (event->modelChangeType()) {
+ case QAccessibleTableModelChangeEvent::ModelReset:
+ for (QAccessible::Id id : childToId)
+ QAccessible::deleteAccessibleInterface(id);
+ childToId.clear();
+ break;
+
+ // rows are inserted: move every row after that
+ case QAccessibleTableModelChangeEvent::RowsInserted:
+ case QAccessibleTableModelChangeEvent::ColumnsInserted: {
+
+ ChildCache newCache;
+ ChildCache::ConstIterator iter = childToId.constBegin();
+
+ while (iter != childToId.constEnd()) {
+ QAccessible::Id id = iter.value();
+ QAccessibleInterface *iface = QAccessible::accessibleInterface(id);
+ Q_ASSERT(iface);
+ if (indexOfChild(iface) >= 0) {
+ newCache.insert(indexOfChild(iface), id);
+ } else {
+ // ### This should really not happen,
+ // but it might if the view has a root index set.
+ // This needs to be fixed.
+ QAccessible::deleteAccessibleInterface(id);
+ }
+ ++iter;
+ }
+ childToId = newCache;
+ break;
+ }
+
+ case QAccessibleTableModelChangeEvent::ColumnsRemoved:
+ case QAccessibleTableModelChangeEvent::RowsRemoved: {
+ ChildCache newCache;
+ ChildCache::ConstIterator iter = childToId.constBegin();
+ while (iter != childToId.constEnd()) {
+ QAccessible::Id id = iter.value();
+ QAccessibleInterface *iface = QAccessible::accessibleInterface(id);
+ Q_ASSERT(iface);
+ if (iface->role() == QAccessible::Cell || iface->role() == QAccessible::ListItem) {
+ Q_ASSERT(iface->tableCellInterface());
+ AccessibleInstanceViewItem *cell = static_cast<AccessibleInstanceViewItem*>(iface->tableCellInterface());
+ // Since it is a QPersistentModelIndex, we only need to check if it is valid
+ if (cell->m_index.isValid())
+ newCache.insert(indexOfChild(cell), id);
+ else
+ QAccessible::deleteAccessibleInterface(id);
+ }
+ ++iter;
+ }
+ childToId = newCache;
+ break;
+ }
+
+ case QAccessibleTableModelChangeEvent::DataChanged:
+ // nothing to do in this case
+ break;
+ }
+}
+
+// TABLE CELL
+
+AccessibleInstanceViewItem::AccessibleInstanceViewItem(QAbstractItemView *view_, const QModelIndex &index_)
+ : view(view_), m_index(index_)
+{
+ if (Q_UNLIKELY(!index_.isValid()))
+ qWarning() << "AccessibleInstanceViewItem::AccessibleInstanceViewItem with invalid index: " << index_;
+}
+
+void *AccessibleInstanceViewItem::interface_cast(QAccessible::InterfaceType t)
+{
+ if (t == QAccessible::TableCellInterface)
+ return static_cast<QAccessibleTableCellInterface*>(this);
+ if (t == QAccessible::ActionInterface)
+ return static_cast<QAccessibleActionInterface*>(this);
+ return 0;
+}
+
+int AccessibleInstanceViewItem::columnExtent() const { return 1; }
+int AccessibleInstanceViewItem::rowExtent() const { return 1; }
+
+QList<QAccessibleInterface*> AccessibleInstanceViewItem::rowHeaderCells() const
+{
+ return {};
+}
+
+QList<QAccessibleInterface*> AccessibleInstanceViewItem::columnHeaderCells() const
+{
+ return {};
+}
+
+int AccessibleInstanceViewItem::columnIndex() const
+{
+ if (!isValid()) {
+ return -1;
+ }
+
+ return m_index.column();
+}
+
+int AccessibleInstanceViewItem::rowIndex() const
+{
+ if (!isValid()) {
+ return -1;
+ }
+
+ return m_index.row();
+}
+
+bool AccessibleInstanceViewItem::isSelected() const
+{
+ if (!isValid()) {
+ return false;
+ }
+
+ return view->selectionModel()->isSelected(m_index);
+}
+
+QStringList AccessibleInstanceViewItem::actionNames() const
+{
+ QStringList names;
+ names << toggleAction();
+ return names;
+}
+
+void AccessibleInstanceViewItem::doAction(const QString& actionName)
+{
+ if (actionName == toggleAction()) {
+ if (isSelected()) {
+ unselectCell();
+ }
+ else {
+ selectCell();
+ }
+ }
+}
+
+QStringList AccessibleInstanceViewItem::keyBindingsForAction(const QString &) const
+{
+ return QStringList();
+}
+
+
+void AccessibleInstanceViewItem::selectCell()
+{
+ if (!isValid()) {
+ return;
+ }
+ QAbstractItemView::SelectionMode selectionMode = view->selectionMode();
+ if (selectionMode == QAbstractItemView::NoSelection) {
+ return;
+ }
+
+ Q_ASSERT(table());
+ QAccessibleTableInterface *cellTable = table()->tableInterface();
+
+ switch (view->selectionBehavior()) {
+ case QAbstractItemView::SelectItems:
+ break;
+ case QAbstractItemView::SelectColumns:
+ if (cellTable)
+ cellTable->selectColumn(m_index.column());
+ return;
+ case QAbstractItemView::SelectRows:
+ if (cellTable)
+ cellTable->selectRow(m_index.row());
+ return;
+ }
+
+ if (selectionMode == QAbstractItemView::SingleSelection) {
+ view->clearSelection();
+ }
+
+ view->selectionModel()->select(m_index, QItemSelectionModel::Select);
+}
+
+void AccessibleInstanceViewItem::unselectCell()
+{
+ if (!isValid())
+ return;
+ QAbstractItemView::SelectionMode selectionMode = view->selectionMode();
+ if (selectionMode == QAbstractItemView::NoSelection)
+ return;
+
+ QAccessibleTableInterface *cellTable = table()->tableInterface();
+
+ switch (view->selectionBehavior()) {
+ case QAbstractItemView::SelectItems:
+ break;
+ case QAbstractItemView::SelectColumns:
+ if (cellTable)
+ cellTable->unselectColumn(m_index.column());
+ return;
+ case QAbstractItemView::SelectRows:
+ if (cellTable)
+ cellTable->unselectRow(m_index.row());
+ return;
+ }
+
+ //If the mode is not MultiSelection or ExtendedSelection and only
+ //one cell is selected it cannot be unselected by the user
+ if ((selectionMode != QAbstractItemView::MultiSelection) && (selectionMode != QAbstractItemView::ExtendedSelection) && (view->selectionModel()->selectedIndexes().count() <= 1))
+ return;
+
+ view->selectionModel()->select(m_index, QItemSelectionModel::Deselect);
+}
+
+QAccessibleInterface *AccessibleInstanceViewItem::table() const
+{
+ return QAccessible::queryAccessibleInterface(view);
+}
+
+QAccessible::Role AccessibleInstanceViewItem::role() const
+{
+ return QAccessible::ListItem;
+}
+
+QAccessible::State AccessibleInstanceViewItem::state() const
+{
+ QAccessible::State st;
+ if (!isValid())
+ return st;
+
+ QRect globalRect = view->rect();
+ globalRect.translate(view->mapToGlobal(QPoint(0,0)));
+ if (!globalRect.intersects(rect()))
+ st.invisible = true;
+
+ if (view->selectionModel()->isSelected(m_index))
+ st.selected = true;
+ if (view->selectionModel()->currentIndex() == m_index)
+ st.focused = true;
+ if (m_index.model()->data(m_index, Qt::CheckStateRole).toInt() == Qt::Checked)
+ st.checked = true;
+
+ Qt::ItemFlags flags = m_index.flags();
+ if (flags & Qt::ItemIsSelectable) {
+ st.selectable = true;
+ st.focusable = true;
+ if (view->selectionMode() == QAbstractItemView::MultiSelection)
+ st.multiSelectable = true;
+ if (view->selectionMode() == QAbstractItemView::ExtendedSelection)
+ st.extSelectable = true;
+ }
+ return st;
+}
+
+
+QRect AccessibleInstanceViewItem::rect() const
+{
+ QRect r;
+ if (!isValid())
+ return r;
+ r = view->visualRect(m_index);
+
+ if (!r.isNull()) {
+ r.translate(view->viewport()->mapTo(view, QPoint(0,0)));
+ r.translate(view->mapToGlobal(QPoint(0, 0)));
+ }
+ return r;
+}
+
+QString AccessibleInstanceViewItem::text(QAccessible::Text t) const
+{
+ QString value;
+ if (!isValid())
+ return value;
+ QAbstractItemModel *model = view->model();
+ switch (t) {
+ case QAccessible::Name:
+ value = model->data(m_index, Qt::AccessibleTextRole).toString();
+ if (value.isEmpty())
+ value = model->data(m_index, Qt::DisplayRole).toString();
+ break;
+ case QAccessible::Description:
+ value = model->data(m_index, Qt::AccessibleDescriptionRole).toString();
+ break;
+ default:
+ break;
+ }
+ return value;
+}
+
+void AccessibleInstanceViewItem::setText(QAccessible::Text /*t*/, const QString &text)
+{
+ if (!isValid() || !(m_index.flags() & Qt::ItemIsEditable))
+ return;
+ view->model()->setData(m_index, text);
+}
+
+bool AccessibleInstanceViewItem::isValid() const
+{
+ return view && view->model() && m_index.isValid();
+}
+
+QAccessibleInterface *AccessibleInstanceViewItem::parent() const
+{
+ return QAccessible::queryAccessibleInterface(view);
+}
+
+QAccessibleInterface *AccessibleInstanceViewItem::child(int) const
+{
+ return 0;
+}
+
+#endif /* !QT_NO_ACCESSIBILITY */
diff --git a/launcher/ui/instanceview/AccessibleInstanceView.h b/launcher/ui/instanceview/AccessibleInstanceView.h
new file mode 100644
index 00000000..9bfd1745
--- /dev/null
+++ b/launcher/ui/instanceview/AccessibleInstanceView.h
@@ -0,0 +1,6 @@
+#pragma once
+
+#include <QString>
+class QAccessibleInterface;
+
+QAccessibleInterface *groupViewAccessibleFactory(const QString &classname, QObject *object);
diff --git a/launcher/ui/instanceview/AccessibleInstanceView_p.h b/launcher/ui/instanceview/AccessibleInstanceView_p.h
new file mode 100644
index 00000000..26462f51
--- /dev/null
+++ b/launcher/ui/instanceview/AccessibleInstanceView_p.h
@@ -0,0 +1,118 @@
+#pragma once
+
+#include "QtCore/qpointer.h"
+#include <QtGui/qaccessible.h>
+#include <QAccessibleWidget>
+#include <QAbstractItemView>
+#ifndef QT_NO_ACCESSIBILITY
+#include "InstanceView.h"
+// #include <QHeaderView>
+
+class QAccessibleTableCell;
+class QAccessibleTableHeaderCell;
+
+class AccessibleInstanceView :public QAccessibleTableInterface, public QAccessibleObject
+{
+public:
+ explicit AccessibleInstanceView(QWidget *w);
+ bool isValid() const override;
+
+ QAccessible::Role role() const override;
+ QAccessible::State state() const override;
+ QString text(QAccessible::Text t) const override;
+ QRect rect() const override;
+
+ QAccessibleInterface *childAt(int x, int y) const override;
+ int childCount() const override;
+ int indexOfChild(const QAccessibleInterface *) const override;
+
+ QAccessibleInterface *parent() const override;
+ QAccessibleInterface *child(int index) const override;
+
+ void *interface_cast(QAccessible::InterfaceType t) override;
+
+ // table interface
+ QAccessibleInterface *cellAt(int row, int column) const override;
+ QAccessibleInterface *caption() const override;
+ QAccessibleInterface *summary() const override;
+ QString columnDescription(int column) const override;
+ QString rowDescription(int row) const override;
+ int columnCount() const override;
+ int rowCount() const override;
+
+ // selection
+ int selectedCellCount() const override;
+ int selectedColumnCount() const override;
+ int selectedRowCount() const override;
+ QList<QAccessibleInterface*> selectedCells() const override;
+ QList<int> selectedColumns() const override;
+ QList<int> selectedRows() const override;
+ bool isColumnSelected(int column) const override;
+ bool isRowSelected(int row) const override;
+ bool selectRow(int row) override;
+ bool selectColumn(int column) override;
+ bool unselectRow(int row) override;
+ bool unselectColumn(int column) override;
+
+ QAbstractItemView *view() const;
+
+ void modelChange(QAccessibleTableModelChangeEvent *event) override;
+
+protected:
+ // maybe vector
+ typedef QHash<int, QAccessible::Id> ChildCache;
+ mutable ChildCache childToId;
+
+ virtual ~AccessibleInstanceView();
+
+private:
+ inline int logicalIndex(const QModelIndex &index) const;
+};
+
+class AccessibleInstanceViewItem: public QAccessibleInterface, public QAccessibleTableCellInterface, public QAccessibleActionInterface
+{
+public:
+ AccessibleInstanceViewItem(QAbstractItemView *view, const QModelIndex &m_index);
+
+ void *interface_cast(QAccessible::InterfaceType t) override;
+ QObject *object() const override { return nullptr; }
+ QAccessible::Role role() const override;
+ QAccessible::State state() const override;
+ QRect rect() const override;
+ bool isValid() const override;
+
+ QAccessibleInterface *childAt(int, int) const override { return nullptr; }
+ int childCount() const override { return 0; }
+ int indexOfChild(const QAccessibleInterface *) const override { return -1; }
+
+ QString text(QAccessible::Text t) const override;
+ void setText(QAccessible::Text t, const QString &text) override;
+
+ QAccessibleInterface *parent() const override;
+ QAccessibleInterface *child(int) const override;
+
+ // cell interface
+ int columnExtent() const override;
+ QList<QAccessibleInterface*> columnHeaderCells() const override;
+ int columnIndex() const override;
+ int rowExtent() const override;
+ QList<QAccessibleInterface*> rowHeaderCells() const override;
+ int rowIndex() const override;
+ bool isSelected() const override;
+ QAccessibleInterface* table() const override;
+
+ //action interface
+ QStringList actionNames() const override;
+ void doAction(const QString &actionName) override;
+ QStringList keyBindingsForAction(const QString &actionName) const override;
+
+private:
+ QPointer<QAbstractItemView > view;
+ QPersistentModelIndex m_index;
+
+ void selectCell();
+ void unselectCell();
+
+ friend class AccessibleInstanceView;
+};
+#endif /* !QT_NO_ACCESSIBILITY */
diff --git a/launcher/ui/instanceview/InstanceDelegate.cpp b/launcher/ui/instanceview/InstanceDelegate.cpp
new file mode 100644
index 00000000..3c4ca63f
--- /dev/null
+++ b/launcher/ui/instanceview/InstanceDelegate.cpp
@@ -0,0 +1,428 @@
+/* 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 "InstanceDelegate.h"
+#include <QPainter>
+#include <QTextOption>
+#include <QTextLayout>
+#include <QApplication>
+#include <QtMath>
+#include <QDebug>
+
+#include "InstanceView.h"
+#include "BaseInstance.h"
+#include "InstanceList.h"
+#include <xdgicon.h>
+#include <QTextEdit>
+
+// Origin: Qt
+static void viewItemTextLayout(QTextLayout &textLayout, int lineWidth, qreal &height,
+ qreal &widthUsed)
+{
+ height = 0;
+ widthUsed = 0;
+ textLayout.beginLayout();
+ QString str = textLayout.text();
+ while (true)
+ {
+ QTextLine line = textLayout.createLine();
+ if (!line.isValid())
+ break;
+ if (line.textLength() == 0)
+ break;
+ line.setLineWidth(lineWidth);
+ line.setPosition(QPointF(0, height));
+ height += line.height();
+ widthUsed = qMax(widthUsed, line.naturalTextWidth());
+ }
+ textLayout.endLayout();
+}
+
+ListViewDelegate::ListViewDelegate(QObject *parent) : QStyledItemDelegate(parent)
+{
+}
+
+void drawSelectionRect(QPainter *painter, const QStyleOptionViewItem &option,
+ const QRect &rect)
+{
+ if ((option.state & QStyle::State_Selected))
+ painter->fillRect(rect, option.palette.brush(QPalette::Highlight));
+ else
+ {
+ QColor backgroundColor = option.palette.color(QPalette::Background);
+ backgroundColor.setAlpha(160);
+ painter->fillRect(rect, QBrush(backgroundColor));
+ }
+}
+
+void drawFocusRect(QPainter *painter, const QStyleOptionViewItem &option, const QRect &rect)
+{
+ if (!(option.state & QStyle::State_HasFocus))
+ return;
+ QStyleOptionFocusRect opt;
+ opt.direction = option.direction;
+ opt.fontMetrics = option.fontMetrics;
+ opt.palette = option.palette;
+ opt.rect = rect;
+ // opt.state = option.state | QStyle::State_KeyboardFocusChange |
+ // QStyle::State_Item;
+ auto col = option.state & QStyle::State_Selected ? QPalette::Highlight : QPalette::Base;
+ opt.backgroundColor = option.palette.color(col);
+ // Apparently some widget styles expect this hint to not be set
+ painter->setRenderHint(QPainter::Antialiasing, false);
+
+ QStyle *style = option.widget ? option.widget->style() : QApplication::style();
+
+ style->drawPrimitive(QStyle::PE_FrameFocusRect, &opt, painter, option.widget);
+
+ painter->setRenderHint(QPainter::Antialiasing);
+}
+
+// TODO this can be made a lot prettier
+void drawProgressOverlay(QPainter *painter, const QStyleOptionViewItem &option,
+ const int value, const int maximum)
+{
+ if (maximum == 0 || value == maximum)
+ {
+ return;
+ }
+
+ painter->save();
+
+ qreal percent = (qreal)value / (qreal)maximum;
+ QColor color = option.palette.color(QPalette::Dark);
+ color.setAlphaF(0.70f);
+ painter->setBrush(color);
+ painter->setPen(QPen(QBrush(), 0));
+ painter->drawPie(option.rect, 90 * 16, -percent * 360 * 16);
+
+ painter->restore();
+}
+
+void drawBadges(QPainter *painter, const QStyleOptionViewItem &option, BaseInstance *instance, QIcon::Mode mode, QIcon::State state)
+{
+ QList<QString> pixmaps;
+ if (instance->isRunning())
+ {
+ pixmaps.append("status-running");
+ }
+ else if (instance->hasCrashed() || instance->hasVersionBroken())
+ {
+ pixmaps.append("status-bad");
+ }
+ if (instance->hasUpdateAvailable())
+ {
+ pixmaps.append("checkupdate");
+ }
+
+ static const int itemSide = 24;
+ static const int spacing = 1;
+ const int itemsPerRow = qMax(1, qFloor(double(option.rect.width() + spacing) / double(itemSide + spacing)));
+ const int rows = qCeil((double)pixmaps.size() / (double)itemsPerRow);
+ QListIterator<QString> it(pixmaps);
+ painter->translate(option.rect.topLeft());
+ for (int y = 0; y < rows; ++y)
+ {
+ for (int x = 0; x < itemsPerRow; ++x)
+ {
+ if (!it.hasNext())
+ {
+ return;
+ }
+ // FIXME: inject this.
+ auto icon = XdgIcon::fromTheme(it.next());
+ // opt.icon.paint(painter, iconbox, Qt::AlignCenter, mode, state);
+ const QPixmap pixmap;
+ // itemSide
+ QRect badgeRect(
+ option.rect.width() - x * itemSide + qMax(x - 1, 0) * spacing - itemSide,
+ y * itemSide + qMax(y - 1, 0) * spacing,
+ itemSide,
+ itemSide
+ );
+ icon.paint(painter, badgeRect, Qt::AlignCenter, mode, state);
+ }
+ }
+ painter->translate(-option.rect.topLeft());
+}
+
+static QSize viewItemTextSize(const QStyleOptionViewItem *option)
+{
+ QStyle *style = option->widget ? option->widget->style() : QApplication::style();
+ QTextOption textOption;
+ textOption.setWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere);
+ QTextLayout textLayout;
+ textLayout.setTextOption(textOption);
+ textLayout.setFont(option->font);
+ textLayout.setText(option->text);
+ const int textMargin = style->pixelMetric(QStyle::PM_FocusFrameHMargin, option, option->widget) + 1;
+ QRect bounds(0, 0, 100 - 2 * textMargin, 600);
+ qreal height = 0, widthUsed = 0;
+ viewItemTextLayout(textLayout, bounds.width(), height, widthUsed);
+ const QSize size(qCeil(widthUsed), qCeil(height));
+ return QSize(size.width() + 2 * textMargin, size.height());
+}
+
+void ListViewDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option,
+ const QModelIndex &index) const
+{
+ QStyleOptionViewItem opt = option;
+ initStyleOption(&opt, index);
+ painter->save();
+ painter->setClipRect(opt.rect);
+
+ opt.features |= QStyleOptionViewItem::WrapText;
+ opt.text = index.data().toString();
+ opt.textElideMode = Qt::ElideRight;
+ opt.displayAlignment = Qt::AlignTop | Qt::AlignHCenter;
+
+ QStyle *style = opt.widget ? opt.widget->style() : QApplication::style();
+
+ // const int iconSize = style->pixelMetric(QStyle::PM_IconViewIconSize);
+ const int iconSize = 48;
+ QRect iconbox = opt.rect;
+ const int textMargin = style->pixelMetric(QStyle::PM_FocusFrameHMargin, 0, opt.widget) + 1;
+ QRect textRect = opt.rect;
+ QRect textHighlightRect = textRect;
+ // clip the decoration on top, remove width padding
+ textRect.adjust(textMargin, iconSize + textMargin + 5, -textMargin, 0);
+
+ textHighlightRect.adjust(0, iconSize + 5, 0, 0);
+
+ // draw background
+ {
+ // FIXME: unused
+ // QSize textSize = viewItemTextSize ( &opt );
+ drawSelectionRect(painter, opt, textHighlightRect);
+ /*
+ QPalette::ColorGroup cg;
+ QStyleOptionViewItem opt2(opt);
+
+ if ((opt.widget && opt.widget->isEnabled()) || (opt.state & QStyle::State_Enabled))
+ {
+ if (!(opt.state & QStyle::State_Active))
+ cg = QPalette::Inactive;
+ else
+ cg = QPalette::Normal;
+ }
+ else
+ {
+ cg = QPalette::Disabled;
+ }
+ */
+ /*
+ opt2.palette.setCurrentColorGroup(cg);
+
+ // fill in background, if any
+
+
+ if (opt.backgroundBrush.style() != Qt::NoBrush)
+ {
+ QPointF oldBO = painter->brushOrigin();
+ painter->setBrushOrigin(opt.rect.topLeft());
+ painter->fillRect(opt.rect, opt.backgroundBrush);
+ painter->setBrushOrigin(oldBO);
+ }
+
+ drawSelectionRect(painter, opt2, textHighlightRect);
+ */
+
+ /*
+ if (opt.showDecorationSelected)
+ {
+ drawSelectionRect(painter, opt2, opt.rect);
+ drawFocusRect(painter, opt2, opt.rect);
+ // painter->fillRect ( opt.rect, opt.palette.brush ( cg, QPalette::Highlight ) );
+ }
+ else
+ {
+
+ // if ( opt.state & QStyle::State_Selected )
+ {
+ // QRect textRect = subElementRect ( QStyle::SE_ItemViewItemText, opt,
+ // opt.widget );
+ // painter->fillRect ( textHighlightRect, opt.palette.brush ( cg,
+ // QPalette::Highlight ) );
+ drawSelectionRect(painter, opt2, textHighlightRect);
+ drawFocusRect(painter, opt2, textHighlightRect);
+ }
+ }
+ */
+ }
+
+ // icon mode and state, also used for badges
+ QIcon::Mode mode = QIcon::Normal;
+ if (!(opt.state & QStyle::State_Enabled))
+ mode = QIcon::Disabled;
+ else if (opt.state & QStyle::State_Selected)
+ mode = QIcon::Selected;
+ QIcon::State state = opt.state & QStyle::State_Open ? QIcon::On : QIcon::Off;
+
+ // draw the icon
+ {
+ iconbox.setHeight(iconSize);
+ opt.icon.paint(painter, iconbox, Qt::AlignCenter, mode, state);
+ }
+ // set the text colors
+ QPalette::ColorGroup cg =
+ opt.state & QStyle::State_Enabled ? QPalette::Normal : QPalette::Disabled;
+ if (cg == QPalette::Normal && !(opt.state & QStyle::State_Active))
+ cg = QPalette::Inactive;
+ if (opt.state & QStyle::State_Selected)
+ {
+ painter->setPen(opt.palette.color(cg, QPalette::HighlightedText));
+ }
+ else
+ {
+ painter->setPen(opt.palette.color(cg, QPalette::Text));
+ }
+
+ // draw the text
+ QTextOption textOption;
+ textOption.setWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere);
+ textOption.setTextDirection(opt.direction);
+ textOption.setAlignment(QStyle::visualAlignment(opt.direction, opt.displayAlignment));
+ QTextLayout textLayout;
+ textLayout.setTextOption(textOption);
+ textLayout.setFont(opt.font);
+ textLayout.setText(opt.text);
+
+ qreal width, height;
+ viewItemTextLayout(textLayout, textRect.width(), height, width);
+
+ const int lineCount = textLayout.lineCount();
+
+ const QRect layoutRect = QStyle::alignedRect(
+ opt.direction, opt.displayAlignment, QSize(textRect.width(), int(height)), textRect);
+ const QPointF position = layoutRect.topLeft();
+ for (int i = 0; i < lineCount; ++i)
+ {
+ const QTextLine line = textLayout.lineAt(i);
+ line.draw(painter, position);
+ }
+
+ // FIXME: this really has no business of being here. Make generic.
+ auto instance = (BaseInstance*)index.data(InstanceList::InstancePointerRole)
+ .value<void *>();
+ if (instance)
+ {
+ drawBadges(painter, opt, instance, mode, state);
+ }
+
+ drawProgressOverlay(painter, opt, index.data(InstanceViewRoles::ProgressValueRole).toInt(),
+ index.data(InstanceViewRoles::ProgressMaximumRole).toInt());
+
+ painter->restore();
+}
+
+QSize ListViewDelegate::sizeHint(const QStyleOptionViewItem &option,
+ const QModelIndex &index) const
+{
+ QStyleOptionViewItem opt = option;
+ initStyleOption(&opt, index);
+ opt.features |= QStyleOptionViewItem::WrapText;
+ opt.text = index.data().toString();
+ opt.textElideMode = Qt::ElideRight;
+ opt.displayAlignment = Qt::AlignTop | Qt::AlignHCenter;
+
+ QStyle *style = opt.widget ? opt.widget->style() : QApplication::style();
+ const int textMargin = style->pixelMetric(QStyle::PM_FocusFrameHMargin, &option, opt.widget) + 1;
+ int height = 48 + textMargin * 2 + 5; // TODO: turn constants into variables
+ QSize szz = viewItemTextSize(&opt);
+ height += szz.height();
+ // FIXME: maybe the icon items could scale and keep proportions?
+ QSize sz(100, height);
+ return sz;
+}
+
+class NoReturnTextEdit: public QTextEdit
+{
+ Q_OBJECT
+public:
+ explicit NoReturnTextEdit(QWidget *parent) : QTextEdit(parent)
+ {
+ setTextInteractionFlags(Qt::TextEditorInteraction);
+ setHorizontalScrollBarPolicy(Qt::ScrollBarPolicy::ScrollBarAlwaysOff);
+ setVerticalScrollBarPolicy(Qt::ScrollBarPolicy::ScrollBarAlwaysOff);
+ }
+ bool event(QEvent * event) override
+ {
+ auto eventType = event->type();
+ if(eventType == QEvent::KeyPress || eventType == QEvent::KeyRelease)
+ {
+ QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
+ auto key = keyEvent->key();
+ if (key == Qt::Key_Return || key == Qt::Key_Enter)
+ {
+ emit editingDone();
+ return true;
+ }
+ if(key == Qt::Key_Tab)
+ {
+ return true;
+ }
+ }
+ return QTextEdit::event(event);
+ }
+signals:
+ void editingDone();
+};
+
+void ListViewDelegate::updateEditorGeometry(QWidget* editor, const QStyleOptionViewItem& option, const QModelIndex& index) const
+{
+ const int iconSize = 48;
+ QRect textRect = option.rect;
+ // QStyle *style = option.widget ? option.widget->style() : QApplication::style();
+ textRect.adjust(0, iconSize + 5, 0, 0);
+ editor->setGeometry(textRect);
+}
+
+void ListViewDelegate::setEditorData(QWidget* editor, const QModelIndex& index) const
+{
+ auto text = index.data(Qt::EditRole).toString();
+ QTextEdit * realeditor = qobject_cast<NoReturnTextEdit *>(editor);
+ realeditor->setAlignment(Qt::AlignHCenter | Qt::AlignTop);
+ realeditor->append(text);
+ realeditor->selectAll();
+ realeditor->document()->clearUndoRedoStacks();
+}
+
+void ListViewDelegate::setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const
+{
+ QTextEdit * realeditor = qobject_cast<NoReturnTextEdit *>(editor);
+ QString text = realeditor->toPlainText();
+ text.replace(QChar('\n'), QChar(' '));
+ text = text.trimmed();
+ if(text.size() != 0)
+ {
+ model->setData(index, text);
+ }
+}
+
+QWidget * ListViewDelegate::createEditor(QWidget* parent, const QStyleOptionViewItem& option, const QModelIndex& index) const
+{
+ auto editor = new NoReturnTextEdit(parent);
+ connect(editor, &NoReturnTextEdit::editingDone, this, &ListViewDelegate::editingDone);
+ return editor;
+}
+
+void ListViewDelegate::editingDone()
+{
+ NoReturnTextEdit *editor = qobject_cast<NoReturnTextEdit *>(sender());
+ emit commitData(editor);
+ emit closeEditor(editor);
+}
+
+#include "InstanceDelegate.moc"
diff --git a/launcher/ui/instanceview/InstanceDelegate.h b/launcher/ui/instanceview/InstanceDelegate.h
new file mode 100644
index 00000000..d95279f3
--- /dev/null
+++ b/launcher/ui/instanceview/InstanceDelegate.h
@@ -0,0 +1,39 @@
+/* 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 <QStyledItemDelegate>
+#include <QCache>
+
+class ListViewDelegate : public QStyledItemDelegate
+{
+ Q_OBJECT
+
+public:
+ explicit ListViewDelegate(QObject *parent = 0);
+ virtual ~ListViewDelegate() {}
+
+ void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
+ QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override;
+ void updateEditorGeometry(QWidget * editor, const QStyleOptionViewItem & option, const QModelIndex & index) const override;
+ QWidget * createEditor(QWidget * parent, const QStyleOptionViewItem & option, const QModelIndex & index) const override;
+
+ void setEditorData(QWidget * editor, const QModelIndex & index) const override;
+ void setModelData(QWidget * editor, QAbstractItemModel * model, const QModelIndex & index) const override;
+
+private slots:
+ void editingDone();
+};
diff --git a/launcher/ui/instanceview/InstanceProxyModel.cpp b/launcher/ui/instanceview/InstanceProxyModel.cpp
new file mode 100644
index 00000000..d8de93ed
--- /dev/null
+++ b/launcher/ui/instanceview/InstanceProxyModel.cpp
@@ -0,0 +1,71 @@
+/* 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 "InstanceProxyModel.h"
+
+#include "InstanceView.h"
+#include "Application.h"
+#include <BaseInstance.h>
+#include <icons/IconList.h>
+
+#include <QDebug>
+
+InstanceProxyModel::InstanceProxyModel(QObject *parent) : QSortFilterProxyModel(parent) {
+ m_naturalSort.setNumericMode(true);
+ m_naturalSort.setCaseSensitivity(Qt::CaseSensitivity::CaseInsensitive);
+ // FIXME: use loaded translation as source of locale instead, hook this up to translation changes
+ m_naturalSort.setLocale(QLocale::system());
+}
+
+QVariant InstanceProxyModel::data(const QModelIndex & index, int role) const
+{
+ QVariant data = QSortFilterProxyModel::data(index, role);
+ if(role == Qt::DecorationRole)
+ {
+ return QVariant(APPLICATION->icons()->getIcon(data.toString()));
+ }
+ return data;
+}
+
+bool InstanceProxyModel::lessThan(const QModelIndex &left, const QModelIndex &right) const {
+ const QString leftCategory = left.data(InstanceViewRoles::GroupRole).toString();
+ const QString rightCategory = right.data(InstanceViewRoles::GroupRole).toString();
+ if (leftCategory == rightCategory) {
+ return subSortLessThan(left, right);
+ }
+ else {
+ // FIXME: real group sorting happens in InstanceView::updateGeometries(), see LocaleString
+ auto result = leftCategory.localeAwareCompare(rightCategory);
+ if(result == 0) {
+ return subSortLessThan(left, right);
+ }
+ return result < 0;
+ }
+}
+
+bool InstanceProxyModel::subSortLessThan(const QModelIndex &left, const QModelIndex &right) const
+{
+ BaseInstance *pdataLeft = static_cast<BaseInstance *>(left.internalPointer());
+ BaseInstance *pdataRight = static_cast<BaseInstance *>(right.internalPointer());
+ QString sortMode = APPLICATION->settings()->get("InstSortMode").toString();
+ if (sortMode == "LastLaunch")
+ {
+ return pdataLeft->lastLaunch() > pdataRight->lastLaunch();
+ }
+ else
+ {
+ return m_naturalSort.compare(pdataLeft->name(), pdataRight->name()) < 0;
+ }
+}
diff --git a/launcher/ui/instanceview/InstanceProxyModel.h b/launcher/ui/instanceview/InstanceProxyModel.h
new file mode 100644
index 00000000..bba8d2b5
--- /dev/null
+++ b/launcher/ui/instanceview/InstanceProxyModel.h
@@ -0,0 +1,35 @@
+/* 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 <QSortFilterProxyModel>
+#include <QCollator>
+
+class InstanceProxyModel : public QSortFilterProxyModel
+{
+ Q_OBJECT
+
+public:
+ InstanceProxyModel(QObject *parent = 0);
+
+protected:
+ QVariant data(const QModelIndex & index, int role) const override;
+ bool lessThan(const QModelIndex &left, const QModelIndex &right) const override;
+ bool subSortLessThan(const QModelIndex &left, const QModelIndex &right) const;
+
+private:
+ QCollator m_naturalSort;
+};
diff --git a/launcher/ui/instanceview/InstanceView.cpp b/launcher/ui/instanceview/InstanceView.cpp
new file mode 100644
index 00000000..1f044866
--- /dev/null
+++ b/launcher/ui/instanceview/InstanceView.cpp
@@ -0,0 +1,1010 @@
+/* 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 "InstanceView.h"
+
+#include <QPainter>
+#include <QApplication>
+#include <QtMath>
+#include <QMouseEvent>
+#include <QListView>
+#include <QPersistentModelIndex>
+#include <QDrag>
+#include <QMimeData>
+#include <QCache>
+#include <QScrollBar>
+#include <QAccessible>
+
+#include "VisualGroup.h"
+#include <QDebug>
+
+#include <Application.h>
+#include <InstanceList.h>
+
+
+template <typename T> bool listsIntersect(const QList<T> &l1, const QList<T> t2)
+{
+ for (auto &item : l1)
+ {
+ if (t2.contains(item))
+ {
+ return true;
+ }
+ }
+ return false;
+}
+
+InstanceView::InstanceView(QWidget *parent)
+ : QAbstractItemView(parent)
+{
+ setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+ setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
+ setAcceptDrops(true);
+ setAutoScroll(true);
+}
+
+InstanceView::~InstanceView()
+{
+ qDeleteAll(m_groups);
+ m_groups.clear();
+}
+
+void InstanceView::setModel(QAbstractItemModel *model)
+{
+ QAbstractItemView::setModel(model);
+ connect(model, &QAbstractItemModel::modelReset, this, &InstanceView::modelReset);
+ connect(model, &QAbstractItemModel::rowsRemoved, this, &InstanceView::rowsRemoved);
+}
+
+void InstanceView::dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector<int> &roles)
+{
+ scheduleDelayedItemsLayout();
+}
+void InstanceView::rowsInserted(const QModelIndex &parent, int start, int end)
+{
+ scheduleDelayedItemsLayout();
+}
+
+void InstanceView::rowsAboutToBeRemoved(const QModelIndex &parent, int start, int end)
+{
+ scheduleDelayedItemsLayout();
+}
+
+void InstanceView::modelReset()
+{
+ scheduleDelayedItemsLayout();
+}
+
+void InstanceView::rowsRemoved()
+{
+ scheduleDelayedItemsLayout();
+}
+
+void InstanceView::currentChanged(const QModelIndex& current, const QModelIndex& previous)
+{
+ QAbstractItemView::currentChanged(current, previous);
+ // TODO: for accessibility support, implement+register a factory, steal QAccessibleTable from Qt and return an instance of it for InstanceView.
+#ifndef QT_NO_ACCESSIBILITY
+ if (QAccessible::isActive() && current.isValid()) {
+ QAccessibleEvent event(this, QAccessible::Focus);
+ event.setChild(current.row());
+ QAccessible::updateAccessibility(&event);
+ }
+#endif /* !QT_NO_ACCESSIBILITY */
+}
+
+
+class LocaleString : public QString
+{
+public:
+ LocaleString(const char *s) : QString(s)
+ {
+ }
+ LocaleString(const QString &s) : QString(s)
+ {
+ }
+};
+
+inline bool operator<(const LocaleString &lhs, const LocaleString &rhs)
+{
+ return (QString::localeAwareCompare(lhs, rhs) < 0);
+}
+
+void InstanceView::updateScrollbar()
+{
+ int previousScroll = verticalScrollBar()->value();
+ if (m_groups.isEmpty())
+ {
+ verticalScrollBar()->setRange(0, 0);
+ }
+ else
+ {
+ int totalHeight = 0;
+ // top margin
+ totalHeight += m_categoryMargin;
+ int itemScroll = 0;
+ for (auto category : m_groups)
+ {
+ category->m_verticalPosition = totalHeight;
+ totalHeight += category->totalHeight() + m_categoryMargin;
+ if(!itemScroll && category->totalHeight() != 0)
+ {
+ itemScroll = category->contentHeight() / category->numRows();
+ }
+ }
+ // do not divide by zero
+ if(itemScroll == 0)
+ itemScroll = 64;
+
+ totalHeight += m_bottomMargin;
+ verticalScrollBar()->setSingleStep ( itemScroll );
+ const int rowsPerPage = qMax ( viewport()->height() / itemScroll, 1 );
+ verticalScrollBar()->setPageStep ( rowsPerPage * itemScroll );
+
+ verticalScrollBar()->setRange(0, totalHeight - height());
+ }
+
+ verticalScrollBar()->setValue(qMin(previousScroll, verticalScrollBar()->maximum()));
+}
+
+void InstanceView::updateGeometries()
+{
+ geometryCache.clear();
+
+ QMap<LocaleString, VisualGroup *> cats;
+
+ for (int i = 0; i < model()->rowCount(); ++i)
+ {
+ const QString groupName = model()->index(i, 0).data(InstanceViewRoles::GroupRole).toString();
+ if (!cats.contains(groupName))
+ {
+ VisualGroup *old = this->category(groupName);
+ if (old)
+ {
+ auto cat = new VisualGroup(old);
+ cats.insert(groupName, cat);
+ cat->update();
+ }
+ else
+ {
+ auto cat = new VisualGroup(groupName, this);
+ if(fVisibility) {
+ cat->collapsed = fVisibility(groupName);
+ }
+ cats.insert(groupName, cat);
+ cat->update();
+ }
+ }
+ }
+
+ qDeleteAll(m_groups);
+ m_groups = cats.values();
+ updateScrollbar();
+ viewport()->update();
+}
+
+bool InstanceView::isIndexHidden(const QModelIndex &index) const
+{
+ VisualGroup *cat = category(index);
+ if (cat)
+ {
+ return cat->collapsed;
+ }
+ else
+ {
+ return false;
+ }
+}
+
+VisualGroup *InstanceView::category(const QModelIndex &index) const
+{
+ return category(index.data(InstanceViewRoles::GroupRole).toString());
+}
+
+VisualGroup *InstanceView::category(const QString &cat) const
+{
+ for (auto group : m_groups)
+ {
+ if (group->text == cat)
+ {
+ return group;
+ }
+ }
+ return nullptr;
+}
+
+VisualGroup *InstanceView::categoryAt(const QPoint &pos, VisualGroup::HitResults & result) const
+{
+ for (auto group : m_groups)
+ {
+ result = group->hitScan(pos);
+ if(result != VisualGroup::NoHit)
+ {
+ return group;
+ }
+ }
+ result = VisualGroup::NoHit;
+ return nullptr;
+}
+
+QString InstanceView::groupNameAt(const QPoint &point)
+{
+ executeDelayedItemsLayout();
+
+ VisualGroup::HitResults hitresult;
+ auto group = categoryAt(point + offset(), hitresult);
+ if(group && (hitresult & (VisualGroup::HeaderHit | VisualGroup::BodyHit)))
+ {
+ return group->text;
+ }
+ return QString();
+}
+
+int InstanceView::calculateItemsPerRow() const
+{
+ return qFloor((qreal)(contentWidth()) / (qreal)(itemWidth() + m_spacing));
+}
+
+int InstanceView::contentWidth() const
+{
+ return width() - m_leftMargin - m_rightMargin;
+}
+
+int InstanceView::itemWidth() const
+{
+ return m_itemWidth;
+}
+
+void InstanceView::mousePressEvent(QMouseEvent *event)
+{
+ executeDelayedItemsLayout();
+
+ QPoint visualPos = event->pos();
+ QPoint geometryPos = event->pos() + offset();
+
+ QPersistentModelIndex index = indexAt(visualPos);
+
+ m_pressedIndex = index;
+ m_pressedAlreadySelected = selectionModel()->isSelected(m_pressedIndex);
+ m_pressedPosition = geometryPos;
+
+ VisualGroup::HitResults hitresult;
+ m_pressedCategory = categoryAt(geometryPos, hitresult);
+ if (m_pressedCategory && hitresult & VisualGroup::CheckboxHit)
+ {
+ setState(m_pressedCategory->collapsed ? ExpandingState : CollapsingState);
+ event->accept();
+ return;
+ }
+
+ if (index.isValid() && (index.flags() & Qt::ItemIsEnabled))
+ {
+ if(index != currentIndex())
+ {
+ // FIXME: better!
+ m_currentCursorColumn = -1;
+ }
+ // we disable scrollTo for mouse press so the item doesn't change position
+ // when the user is interacting with it (ie. clicking on it)
+ bool autoScroll = hasAutoScroll();
+ setAutoScroll(false);
+ selectionModel()->setCurrentIndex(index, QItemSelectionModel::NoUpdate);
+
+ setAutoScroll(autoScroll);
+ QRect rect(visualPos, visualPos);
+ setSelection(rect, QItemSelectionModel::ClearAndSelect);
+
+ // signal handlers may change the model
+ emit pressed(index);
+ }
+ else
+ {
+ // Forces a finalize() even if mouse is pressed, but not on a item
+ selectionModel()->select(QModelIndex(), QItemSelectionModel::Select);
+ }
+}
+
+void InstanceView::mouseMoveEvent(QMouseEvent *event)
+{
+ executeDelayedItemsLayout();
+
+ QPoint topLeft;
+ QPoint visualPos = event->pos();
+ QPoint geometryPos = event->pos() + offset();
+
+ if (state() == ExpandingState || state() == CollapsingState)
+ {
+ return;
+ }
+
+ if (state() == DraggingState)
+ {
+ topLeft = m_pressedPosition - offset();
+ if ((topLeft - event->pos()).manhattanLength() > QApplication::startDragDistance())
+ {
+ m_pressedIndex = QModelIndex();
+ startDrag(model()->supportedDragActions());
+ setState(NoState);
+ stopAutoScroll();
+ }
+ return;
+ }
+
+ if (selectionMode() != SingleSelection)
+ {
+ topLeft = m_pressedPosition - offset();
+ }
+ else
+ {
+ topLeft = geometryPos;
+ }
+
+ if (m_pressedIndex.isValid() && (state() != DragSelectingState) &&
+ (event->buttons() != Qt::NoButton) && !selectedIndexes().isEmpty())
+ {
+ setState(DraggingState);
+ return;
+ }
+
+ if ((event->buttons() & Qt::LeftButton) && selectionModel())
+ {
+ setState(DragSelectingState);
+
+ setSelection(QRect(visualPos, visualPos), QItemSelectionModel::ClearAndSelect);
+ QModelIndex index = indexAt(visualPos);
+
+ // set at the end because it might scroll the view
+ if (index.isValid() && (index != selectionModel()->currentIndex()) &&
+ (index.flags() & Qt::ItemIsEnabled))
+ {
+ selectionModel()->setCurrentIndex(index, QItemSelectionModel::NoUpdate);
+ }
+ }
+}
+
+void InstanceView::mouseReleaseEvent(QMouseEvent *event)
+{
+ executeDelayedItemsLayout();
+
+ QPoint visualPos = event->pos();
+ QPoint geometryPos = event->pos() + offset();
+ QPersistentModelIndex index = indexAt(visualPos);
+
+ VisualGroup::HitResults hitresult;
+
+ bool click = (index == m_pressedIndex && index.isValid()) ||
+ (m_pressedCategory && m_pressedCategory == categoryAt(geometryPos, hitresult));
+
+ if (click && m_pressedCategory)
+ {
+ if (state() == ExpandingState)
+ {
+ m_pressedCategory->collapsed = false;
+ emit groupStateChanged(m_pressedCategory->text, false);
+
+ updateGeometries();
+ viewport()->update();
+ event->accept();
+ m_pressedCategory = nullptr;
+ setState(NoState);
+ return;
+ }
+ else if (state() == CollapsingState)
+ {
+ m_pressedCategory->collapsed = true;
+ emit groupStateChanged(m_pressedCategory->text, true);
+
+ updateGeometries();
+ viewport()->update();
+ event->accept();
+ m_pressedCategory = nullptr;
+ setState(NoState);
+ return;
+ }
+ }
+
+ m_ctrlDragSelectionFlag = QItemSelectionModel::NoUpdate;
+
+ setState(NoState);
+
+ if (click)
+ {
+ if (event->button() == Qt::LeftButton)
+ {
+ emit clicked(index);
+ }
+ QStyleOptionViewItem option = viewOptions();
+ if (m_pressedAlreadySelected)
+ {
+ option.state |= QStyle::State_Selected;
+ }
+ if ((model()->flags(index) & Qt::ItemIsEnabled) &&
+ style()->styleHint(QStyle::SH_ItemView_ActivateItemOnSingleClick, &option, this))
+ {
+ emit activated(index);
+ }
+ }
+}
+
+void InstanceView::mouseDoubleClickEvent(QMouseEvent *event)
+{
+ executeDelayedItemsLayout();
+
+ QModelIndex index = indexAt(event->pos());
+ if (!index.isValid() || !(index.flags() & Qt::ItemIsEnabled) || (m_pressedIndex != index))
+ {
+ QMouseEvent me(
+ QEvent::MouseButtonPress,
+ event->localPos(),
+ event->windowPos(),
+ event->screenPos(),
+ event->button(),
+ event->buttons(),
+ event->modifiers()
+ );
+ mousePressEvent(&me);
+ return;
+ }
+ // signal handlers may change the model
+ QPersistentModelIndex persistent = index;
+ emit doubleClicked(persistent);
+
+ QStyleOptionViewItem option = viewOptions();
+ if ((model()->flags(index) & Qt::ItemIsEnabled) && !style()->styleHint(QStyle::SH_ItemView_ActivateItemOnSingleClick, &option, this))
+ {
+ emit activated(index);
+ }
+}
+
+void InstanceView::paintEvent(QPaintEvent *event)
+{
+ executeDelayedItemsLayout();
+
+ QPainter painter(this->viewport());
+
+ QStyleOptionViewItem option(viewOptions());
+ option.widget = this;
+
+ int wpWidth = viewport()->width();
+ option.rect.setWidth(wpWidth);
+ for (int i = 0; i < m_groups.size(); ++i)
+ {
+ VisualGroup *category = m_groups.at(i);
+ int y = category->verticalPosition();
+ y -= verticalOffset();
+ QRect backup = option.rect;
+ int height = category->totalHeight();
+ option.rect.setTop(y);
+ option.rect.setHeight(height);
+ option.rect.setLeft(m_leftMargin);
+ option.rect.setRight(wpWidth - m_rightMargin);
+ category->drawHeader(&painter, option);
+ y += category->totalHeight() + m_categoryMargin;
+ option.rect = backup;
+ }
+
+ for (int i = 0; i < model()->rowCount(); ++i)
+ {
+ const QModelIndex index = model()->index(i, 0);
+ if (isIndexHidden(index))
+ {
+ continue;
+ }
+ Qt::ItemFlags flags = index.flags();
+ option.rect = visualRect(index);
+ option.features |= QStyleOptionViewItem::WrapText;
+ if (flags & Qt::ItemIsSelectable && selectionModel()->isSelected(index))
+ {
+ option.state |= selectionModel()->isSelected(index) ? QStyle::State_Selected
+ : QStyle::State_None;
+ }
+ else
+ {
+ option.state &= ~QStyle::State_Selected;
+ }
+ option.state |= (index == currentIndex()) ? QStyle::State_HasFocus : QStyle::State_None;
+ if (!(flags & Qt::ItemIsEnabled))
+ {
+ option.state &= ~QStyle::State_Enabled;
+ }
+ itemDelegate()->paint(&painter, option, index);
+ }
+
+ /*
+ * Drop indicators for manual reordering...
+ */
+#if 0
+ if (!m_lastDragPosition.isNull())
+ {
+ QPair<Group *, int> pair = rowDropPos(m_lastDragPosition);
+ Group *category = pair.first;
+ int row = pair.second;
+ if (category)
+ {
+ int internalRow = row - category->firstItemIndex;
+ QLine line;
+ if (internalRow >= category->numItems())
+ {
+ QRect toTheRightOfRect = visualRect(category->lastItem());
+ line = QLine(toTheRightOfRect.topRight(), toTheRightOfRect.bottomRight());
+ }
+ else
+ {
+ QRect toTheLeftOfRect = visualRect(model()->index(row, 0));
+ line = QLine(toTheLeftOfRect.topLeft(), toTheLeftOfRect.bottomLeft());
+ }
+ painter.save();
+ painter.setPen(QPen(Qt::black, 3));
+ painter.drawLine(line);
+ painter.restore();
+ }
+ }
+#endif
+}
+
+void InstanceView::resizeEvent(QResizeEvent *event)
+{
+ int newItemsPerRow = calculateItemsPerRow();
+ if(newItemsPerRow != m_currentItemsPerRow)
+ {
+ m_currentCursorColumn = -1;
+ m_currentItemsPerRow = newItemsPerRow;
+ updateGeometries();
+ }
+ else
+ {
+ updateScrollbar();
+ }
+}
+
+void InstanceView::dragEnterEvent(QDragEnterEvent *event)
+{
+ executeDelayedItemsLayout();
+
+ if (!isDragEventAccepted(event))
+ {
+ return;
+ }
+ m_lastDragPosition = event->pos() + offset();
+ viewport()->update();
+ event->accept();
+}
+
+void InstanceView::dragMoveEvent(QDragMoveEvent *event)
+{
+ executeDelayedItemsLayout();
+
+ if (!isDragEventAccepted(event))
+ {
+ return;
+ }
+ m_lastDragPosition = event->pos() + offset();
+ viewport()->update();
+ event->accept();
+}
+
+void InstanceView::dragLeaveEvent(QDragLeaveEvent *event)
+{
+ executeDelayedItemsLayout();
+
+ m_lastDragPosition = QPoint();
+ viewport()->update();
+}
+
+void InstanceView::dropEvent(QDropEvent *event)
+{
+ executeDelayedItemsLayout();
+
+ m_lastDragPosition = QPoint();
+
+ stopAutoScroll();
+ setState(NoState);
+
+ auto mimedata = event->mimeData();
+
+ if (event->source() == this)
+ {
+ if(event->possibleActions() & Qt::MoveAction)
+ {
+ QPair<VisualGroup *, VisualGroup::HitResults> dropPos = rowDropPos(event->pos());
+ const VisualGroup *group = dropPos.first;
+ auto hitresult = dropPos.second;
+
+ if (hitresult == VisualGroup::HitResult::NoHit)
+ {
+ viewport()->update();
+ return;
+ }
+ auto instanceId = QString::fromUtf8(mimedata->data("application/x-instanceid"));
+ auto instanceList = APPLICATION->instances().get();
+ instanceList->setInstanceGroup(instanceId, group->text);
+ event->setDropAction(Qt::MoveAction);
+ event->accept();
+
+ updateGeometries();
+ viewport()->update();
+ }
+ return;
+ }
+
+ // check if the action is supported
+ if (!mimedata)
+ {
+ return;
+ }
+
+ // files dropped from outside?
+ if (mimedata->hasUrls())
+ {
+ auto urls = mimedata->urls();
+ event->accept();
+ emit droppedURLs(urls);
+ }
+}
+
+void InstanceView::startDrag(Qt::DropActions supportedActions)
+{
+ executeDelayedItemsLayout();
+
+ QModelIndexList indexes = selectionModel()->selectedIndexes();
+ if(indexes.count() == 0)
+ return;
+
+ QMimeData *data = model()->mimeData(indexes);
+ if (!data)
+ {
+ return;
+ }
+ QRect rect;
+ QPixmap pixmap = renderToPixmap(indexes, &rect);
+ QDrag *drag = new QDrag(this);
+ drag->setPixmap(pixmap);
+ drag->setMimeData(data);
+ drag->setHotSpot(m_pressedPosition - rect.topLeft());
+ Qt::DropAction defaultDropAction = Qt::IgnoreAction;
+ if (this->defaultDropAction() != Qt::IgnoreAction && (supportedActions & this->defaultDropAction()))
+ {
+ defaultDropAction = this->defaultDropAction();
+ }
+ /*auto action = */
+ drag->exec(supportedActions, defaultDropAction);
+}
+
+QRect InstanceView::visualRect(const QModelIndex &index) const
+{
+ const_cast<InstanceView*>(this)->executeDelayedItemsLayout();
+
+ return geometryRect(index).translated(-offset());
+}
+
+QRect InstanceView::geometryRect(const QModelIndex &index) const
+{
+ const_cast<InstanceView*>(this)->executeDelayedItemsLayout();
+
+ if (!index.isValid() || isIndexHidden(index) || index.column() > 0)
+ {
+ return QRect();
+ }
+
+ int row = index.row();
+ if(geometryCache.contains(row))
+ {
+ return *geometryCache[row];
+ }
+
+ const VisualGroup *cat = category(index);
+ QPair<int, int> pos = cat->positionOf(index);
+ int x = pos.first;
+ // int y = pos.second;
+
+ QRect out;
+ out.setTop(cat->verticalPosition() + cat->headerHeight() + 5 + cat->rowTopOf(index));
+ out.setLeft(m_spacing + x * (itemWidth() + m_spacing));
+ out.setSize(itemDelegate()->sizeHint(viewOptions(), index));
+ geometryCache.insert(row, new QRect(out));
+ return out;
+}
+
+QModelIndex InstanceView::indexAt(const QPoint &point) const
+{
+ const_cast<InstanceView*>(this)->executeDelayedItemsLayout();
+
+ for (int i = 0; i < model()->rowCount(); ++i)
+ {
+ QModelIndex index = model()->index(i, 0);
+ if (visualRect(index).contains(point))
+ {
+ return index;
+ }
+ }
+ return QModelIndex();
+}
+
+void InstanceView::setSelection(const QRect &rect, const QItemSelectionModel::SelectionFlags commands)
+{
+ executeDelayedItemsLayout();
+
+ for (int i = 0; i < model()->rowCount(); ++i)
+ {
+ QModelIndex index = model()->index(i, 0);
+ QRect itemRect = visualRect(index);
+ if (itemRect.intersects(rect))
+ {
+ selectionModel()->select(index, commands);
+ update(itemRect.translated(-offset()));
+ }
+ }
+}
+
+QPixmap InstanceView::renderToPixmap(const QModelIndexList &indices, QRect *r) const
+{
+ Q_ASSERT(r);
+ auto paintPairs = draggablePaintPairs(indices, r);
+ if (paintPairs.isEmpty())
+ {
+ return QPixmap();
+ }
+ QPixmap pixmap(r->size());
+ pixmap.fill(Qt::transparent);
+ QPainter painter(&pixmap);
+ QStyleOptionViewItem option = viewOptions();
+ option.state |= QStyle::State_Selected;
+ for (int j = 0; j < paintPairs.count(); ++j)
+ {
+ option.rect = paintPairs.at(j).first.translated(-r->topLeft());
+ const QModelIndex &current = paintPairs.at(j).second;
+ itemDelegate()->paint(&painter, option, current);
+ }
+ return pixmap;
+}
+
+QList<QPair<QRect, QModelIndex>> InstanceView::draggablePaintPairs(const QModelIndexList &indices, QRect *r) const
+{
+ Q_ASSERT(r);
+ QRect &rect = *r;
+ QList<QPair<QRect, QModelIndex>> ret;
+ for (int i = 0; i < indices.count(); ++i)
+ {
+ const QModelIndex &index = indices.at(i);
+ const QRect current = geometryRect(index);
+ ret += qMakePair(current, index);
+ rect |= current;
+ }
+ return ret;
+}
+
+bool InstanceView::isDragEventAccepted(QDropEvent *event)
+{
+ return true;
+}
+
+QPair<VisualGroup *, VisualGroup::HitResults> InstanceView::rowDropPos(const QPoint &pos)
+{
+ VisualGroup::HitResults hitresult;
+ auto group = categoryAt(pos + offset(), hitresult);
+ return qMakePair<VisualGroup*, int>(group, hitresult);
+}
+
+QPoint InstanceView::offset() const
+{
+ return QPoint(horizontalOffset(), verticalOffset());
+}
+
+QRegion InstanceView::visualRegionForSelection(const QItemSelection &selection) const
+{
+ QRegion region;
+ for (auto &range : selection)
+ {
+ int start_row = range.top();
+ int end_row = range.bottom();
+ for (int row = start_row; row <= end_row; ++row)
+ {
+ int start_column = range.left();
+ int end_column = range.right();
+ for (int column = start_column; column <= end_column; ++column)
+ {
+ QModelIndex index = model()->index(row, column, rootIndex());
+ region += visualRect(index); // OK
+ }
+ }
+ }
+ return region;
+}
+
+QModelIndex InstanceView::moveCursor(QAbstractItemView::CursorAction cursorAction, Qt::KeyboardModifiers modifiers)
+{
+ auto current = currentIndex();
+ if(!current.isValid())
+ {
+ return current;
+ }
+ auto cat = category(current);
+ int group_index = m_groups.indexOf(cat);
+ if(group_index < 0)
+ return current;
+
+ auto real_group = m_groups[group_index];
+ int beginning_row = 0;
+ for(auto group: m_groups)
+ {
+ if(group == real_group)
+ break;
+ beginning_row += group->numRows();
+ }
+
+ QPair<int, int> pos = cat->positionOf(current);
+ int column = pos.first;
+ int row = pos.second;
+ if(m_currentCursorColumn < 0)
+ {
+ m_currentCursorColumn = column;
+ }
+ switch(cursorAction)
+ {
+ case MoveUp:
+ {
+ if(row == 0)
+ {
+ int prevgroupindex = group_index-1;
+ while(prevgroupindex >= 0)
+ {
+ auto prevgroup = m_groups[prevgroupindex];
+ if(prevgroup->collapsed)
+ {
+ prevgroupindex--;
+ continue;
+ }
+ int newRow = prevgroup->numRows() - 1;
+ int newRowSize = prevgroup->rows[newRow].size();
+ int newColumn = m_currentCursorColumn;
+ if (m_currentCursorColumn >= newRowSize)
+ {
+ newColumn = newRowSize - 1;
+ }
+ return prevgroup->rows[newRow][newColumn];
+ }
+ }
+ else
+ {
+ int newRow = row - 1;
+ int newRowSize = cat->rows[newRow].size();
+ int newColumn = m_currentCursorColumn;
+ if (m_currentCursorColumn >= newRowSize)
+ {
+ newColumn = newRowSize - 1;
+ }
+ return cat->rows[newRow][newColumn];
+ }
+ return current;
+ }
+ case MoveDown:
+ {
+ if(row == cat->rows.size() - 1)
+ {
+ int nextgroupindex = group_index+1;
+ while (nextgroupindex < m_groups.size())
+ {
+ auto nextgroup = m_groups[nextgroupindex];
+ if(nextgroup->collapsed)
+ {
+ nextgroupindex++;
+ continue;
+ }
+ int newRowSize = nextgroup->rows[0].size();
+ int newColumn = m_currentCursorColumn;
+ if (m_currentCursorColumn >= newRowSize)
+ {
+ newColumn = newRowSize - 1;
+ }
+ return nextgroup->rows[0][newColumn];
+ }
+ }
+ else
+ {
+ int newRow = row + 1;
+ int newRowSize = cat->rows[newRow].size();
+ int newColumn = m_currentCursorColumn;
+ if (m_currentCursorColumn >= newRowSize)
+ {
+ newColumn = newRowSize - 1;
+ }
+ return cat->rows[newRow][newColumn];
+ }
+ return current;
+ }
+ case MoveLeft:
+ {
+ if(column > 0)
+ {
+ m_currentCursorColumn = column - 1;
+ return cat->rows[row][column - 1];
+ }
+ // TODO: moving to previous line
+ return current;
+ }
+ case MoveRight:
+ {
+ if(column < cat->rows[row].size() - 1)
+ {
+ m_currentCursorColumn = column + 1;
+ return cat->rows[row][column + 1];
+ }
+ // TODO: moving to next line
+ return current;
+ }
+ case MoveHome:
+ {
+ m_currentCursorColumn = 0;
+ return cat->rows[row][0];
+ }
+ case MoveEnd:
+ {
+ auto last = cat->rows[row].size() - 1;
+ m_currentCursorColumn = last;
+ return cat->rows[row][last];
+ }
+ default:
+ break;
+ }
+ return current;
+}
+
+int InstanceView::horizontalOffset() const
+{
+ return horizontalScrollBar()->value();
+}
+
+int InstanceView::verticalOffset() const
+{
+ return verticalScrollBar()->value();
+}
+
+void InstanceView::scrollContentsBy(int dx, int dy)
+{
+ scrollDirtyRegion(dx, dy);
+ viewport()->scroll(dx, dy);
+}
+
+void InstanceView::scrollTo(const QModelIndex &index, ScrollHint hint)
+{
+ if (!index.isValid())
+ return;
+
+ const QRect rect = visualRect(index);
+ if (hint == EnsureVisible && viewport()->rect().contains(rect))
+ {
+ viewport()->update(rect);
+ return;
+ }
+
+ verticalScrollBar()->setValue(verticalScrollToValue(index, rect, hint));
+}
+
+int InstanceView::verticalScrollToValue(const QModelIndex &index, const QRect &rect, QListView::ScrollHint hint) const
+{
+ const QRect area = viewport()->rect();
+ const bool above = (hint == QListView::EnsureVisible && rect.top() < area.top());
+ const bool below = (hint == QListView::EnsureVisible && rect.bottom() > area.bottom());
+
+ int verticalValue = verticalScrollBar()->value();
+ QRect adjusted = rect.adjusted(-spacing(), -spacing(), spacing(), spacing());
+ if (hint == QListView::PositionAtTop || above)
+ verticalValue += adjusted.top();
+ else if (hint == QListView::PositionAtBottom || below)
+ verticalValue += qMin(adjusted.top(), adjusted.bottom() - area.height() + 1);
+ else if (hint == QListView::PositionAtCenter)
+ verticalValue += adjusted.top() - ((area.height() - adjusted.height()) / 2);
+ return verticalValue;
+}
diff --git a/launcher/ui/instanceview/InstanceView.h b/launcher/ui/instanceview/InstanceView.h
new file mode 100644
index 00000000..406362e6
--- /dev/null
+++ b/launcher/ui/instanceview/InstanceView.h
@@ -0,0 +1,153 @@
+/* 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 <QListView>
+#include <QLineEdit>
+#include <QScrollBar>
+#include <QCache>
+#include "VisualGroup.h"
+#include <functional>
+
+struct InstanceViewRoles
+{
+ enum
+ {
+ GroupRole = Qt::UserRole,
+ ProgressValueRole,
+ ProgressMaximumRole
+ };
+};
+
+class InstanceView : public QAbstractItemView
+{
+ Q_OBJECT
+
+public:
+ InstanceView(QWidget *parent = 0);
+ ~InstanceView();
+
+ void setModel(QAbstractItemModel *model) override;
+
+ using visibilityFunction = std::function<bool(const QString &)>;
+ void setSourceOfGroupCollapseStatus(visibilityFunction f) {
+ fVisibility = f;
+ }
+
+ /// return geometry rectangle occupied by the specified model item
+ QRect geometryRect(const QModelIndex &index) const;
+ /// return visual rectangle occupied by the specified model item
+ virtual QRect visualRect(const QModelIndex &index) const override;
+ /// get the model index at the specified visual point
+ virtual QModelIndex indexAt(const QPoint &point) const override;
+ QString groupNameAt(const QPoint &point);
+ void setSelection(const QRect &rect, const QItemSelectionModel::SelectionFlags commands) override;
+
+ virtual int horizontalOffset() const override;
+ virtual int verticalOffset() const override;
+ virtual void scrollContentsBy(int dx, int dy) override;
+ virtual void scrollTo(const QModelIndex &index, ScrollHint hint = EnsureVisible) override;
+
+ virtual QModelIndex moveCursor(CursorAction cursorAction, Qt::KeyboardModifiers modifiers) override;
+
+ virtual QRegion visualRegionForSelection(const QItemSelection &selection) const override;
+
+ int spacing() const
+ {
+ return m_spacing;
+ };
+
+public slots:
+ virtual void updateGeometries() override;
+
+protected slots:
+ virtual void dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector<int> &roles) override;
+ virtual void rowsInserted(const QModelIndex &parent, int start, int end) override;
+ virtual void rowsAboutToBeRemoved(const QModelIndex &parent, int start, int end) override;
+ void modelReset();
+ void rowsRemoved();
+ void currentChanged(const QModelIndex &current, const QModelIndex &previous) override;
+
+signals:
+ void droppedURLs(QList<QUrl> urls);
+ void groupStateChanged(QString group, bool collapsed);
+
+protected:
+ bool isIndexHidden(const QModelIndex &index) const override;
+ void mousePressEvent(QMouseEvent *event) override;
+ void mouseMoveEvent(QMouseEvent *event) override;
+ void mouseReleaseEvent(QMouseEvent *event) override;
+ void mouseDoubleClickEvent(QMouseEvent *event) override;
+ void paintEvent(QPaintEvent *event) override;
+ void resizeEvent(QResizeEvent *event) override;
+
+ void dragEnterEvent(QDragEnterEvent *event) override;
+ void dragMoveEvent(QDragMoveEvent *event) override;
+ void dragLeaveEvent(QDragLeaveEvent *event) override;
+ void dropEvent(QDropEvent *event) override;
+
+ void startDrag(Qt::DropActions supportedActions) override;
+
+ void updateScrollbar();
+
+private:
+ friend struct VisualGroup;
+ QList<VisualGroup *> m_groups;
+
+ visibilityFunction fVisibility;
+
+ // geometry
+ int m_leftMargin = 5;
+ int m_rightMargin = 5;
+ int m_bottomMargin = 5;
+ int m_categoryMargin = 5;
+ int m_spacing = 5;
+ int m_itemWidth = 100;
+ int m_currentItemsPerRow = -1;
+ int m_currentCursorColumn= -1;
+ mutable QCache<int, QRect> geometryCache;
+
+ // point where the currently active mouse action started in geometry coordinates
+ QPoint m_pressedPosition;
+ QPersistentModelIndex m_pressedIndex;
+ bool m_pressedAlreadySelected;
+ VisualGroup *m_pressedCategory;
+ QItemSelectionModel::SelectionFlag m_ctrlDragSelectionFlag;
+ QPoint m_lastDragPosition;
+
+ VisualGroup *category(const QModelIndex &index) const;
+ VisualGroup *category(const QString &cat) const;
+ VisualGroup *categoryAt(const QPoint &pos, VisualGroup::HitResults & result) const;
+
+ int itemsPerRow() const
+ {
+ return m_currentItemsPerRow;
+ };
+ int contentWidth() const;
+
+private: /* methods */
+ int itemWidth() const;
+ int calculateItemsPerRow() const;
+ int verticalScrollToValue(const QModelIndex &index, const QRect &rect, QListView::ScrollHint hint) const;
+ QPixmap renderToPixmap(const QModelIndexList &indices, QRect *r) const;
+ QList<QPair<QRect, QModelIndex>> draggablePaintPairs(const QModelIndexList &indices, QRect *r) const;
+
+ bool isDragEventAccepted(QDropEvent *event);
+
+ QPair<VisualGroup *, VisualGroup::HitResults> rowDropPos(const QPoint &pos);
+
+ QPoint offset() const;
+};
diff --git a/launcher/ui/instanceview/VisualGroup.cpp b/launcher/ui/instanceview/VisualGroup.cpp
new file mode 100644
index 00000000..8991fb2d
--- /dev/null
+++ b/launcher/ui/instanceview/VisualGroup.cpp
@@ -0,0 +1,317 @@
+/* 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 "VisualGroup.h"
+
+#include <QModelIndex>
+#include <QPainter>
+#include <QtMath>
+#include <QApplication>
+#include <QDebug>
+
+#include "InstanceView.h"
+
+VisualGroup::VisualGroup(const QString &text, InstanceView *view) : view(view), text(text), collapsed(false)
+{
+}
+
+VisualGroup::VisualGroup(const VisualGroup *other)
+ : view(other->view), text(other->text), collapsed(other->collapsed)
+{
+}
+
+void VisualGroup::update()
+{
+ auto temp_items = items();
+ auto itemsPerRow = view->itemsPerRow();
+
+ int numRows = qMax(1, qCeil((qreal)temp_items.size() / (qreal)itemsPerRow));
+ rows = QVector<VisualRow>(numRows);
+
+ int maxRowHeight = 0;
+ int positionInRow = 0;
+ int currentRow = 0;
+ int offsetFromTop = 0;
+ for (auto item: temp_items)
+ {
+ if(positionInRow == itemsPerRow)
+ {
+ rows[currentRow].height = maxRowHeight;
+ rows[currentRow].top = offsetFromTop;
+ currentRow ++;
+ offsetFromTop += maxRowHeight + 5;
+ positionInRow = 0;
+ maxRowHeight = 0;
+ }
+ auto itemHeight = view->itemDelegate()->sizeHint(view->viewOptions(), item).height();
+ if(itemHeight > maxRowHeight)
+ {
+ maxRowHeight = itemHeight;
+ }
+ rows[currentRow].items.append(item);
+ positionInRow++;
+ }
+ rows[currentRow].height = maxRowHeight;
+ rows[currentRow].top = offsetFromTop;
+}
+
+QPair<int, int> VisualGroup::positionOf(const QModelIndex &index) const
+{
+ int y = 0;
+ for (auto & row: rows)
+ {
+ for(auto x = 0; x < row.items.size(); x++)
+ {
+ if(row.items[x] == index)
+ {
+ return qMakePair(x,y);
+ }
+ }
+ y++;
+ }
+ qWarning() << "Item" << index.row() << index.data(Qt::DisplayRole).toString() << "not found in visual group" << text;
+ return qMakePair(0, 0);
+}
+
+int VisualGroup::rowTopOf(const QModelIndex &index) const
+{
+ auto position = positionOf(index);
+ return rows[position.second].top;
+}
+
+int VisualGroup::rowHeightOf(const QModelIndex &index) const
+{
+ auto position = positionOf(index);
+ return rows[position.second].height;
+}
+
+VisualGroup::HitResults VisualGroup::hitScan(const QPoint &pos) const
+{
+ VisualGroup::HitResults results = VisualGroup::NoHit;
+ int y_start = verticalPosition();
+ int body_start = y_start + headerHeight();
+ int body_end = body_start + contentHeight() + 5; // FIXME: wtf is this 5?
+ int y = pos.y();
+ // int x = pos.x();
+ if (y < y_start)
+ {
+ results = VisualGroup::NoHit;
+ }
+ else if (y < body_start)
+ {
+ results = VisualGroup::HeaderHit;
+ int collapseSize = headerHeight() - 4;
+
+ // the icon
+ QRect iconRect = QRect(view->m_leftMargin + 2, 2 + y_start, collapseSize, collapseSize);
+ if (iconRect.contains(pos))
+ {
+ results |= VisualGroup::CheckboxHit;
+ }
+ }
+ else if (y < body_end)
+ {
+ results |= VisualGroup::BodyHit;
+ }
+ return results;
+}
+
+void VisualGroup::drawHeader(QPainter *painter, const QStyleOptionViewItem &option)
+{
+ painter->setRenderHint(QPainter::Antialiasing);
+
+ const QRect optRect = option.rect;
+ QFont font(QApplication::font());
+ font.setBold(true);
+ const QFontMetrics fontMetrics = QFontMetrics(font);
+
+ QColor outlineColor = option.palette.text().color();
+ outlineColor.setAlphaF(0.35);
+
+ //BEGIN: top left corner
+ {
+ painter->save();
+ painter->setPen(outlineColor);
+ const QPointF topLeft(optRect.topLeft());
+ QRectF arc(topLeft, QSizeF(4, 4));
+ arc.translate(0.5, 0.5);
+ painter->drawArc(arc, 1440, 1440);
+ painter->restore();
+ }
+ //END: top left corner
+
+ //BEGIN: left vertical line
+ {
+ QPoint start(optRect.topLeft());
+ start.ry() += 3;
+ QPoint verticalGradBottom(optRect.topLeft());
+ verticalGradBottom.ry() += fontMetrics.height() + 5;
+ QLinearGradient gradient(start, verticalGradBottom);
+ gradient.setColorAt(0, outlineColor);
+ gradient.setColorAt(1, Qt::transparent);
+ painter->fillRect(QRect(start, QSize(1, fontMetrics.height() + 5)), gradient);
+ }
+ //END: left vertical line
+
+ //BEGIN: horizontal line
+ {
+ QPoint start(optRect.topLeft());
+ start.rx() += 3;
+ QPoint horizontalGradTop(optRect.topLeft());
+ horizontalGradTop.rx() += optRect.width() - 6;
+ painter->fillRect(QRect(start, QSize(optRect.width() - 6, 1)), outlineColor);
+ }
+ //END: horizontal line
+
+ //BEGIN: top right corner
+ {
+ painter->save();
+ painter->setPen(outlineColor);
+ QPointF topRight(optRect.topRight());
+ topRight.rx() -= 4;
+ QRectF arc(topRight, QSizeF(4, 4));
+ arc.translate(0.5, 0.5);
+ painter->drawArc(arc, 0, 1440);
+ painter->restore();
+ }
+ //END: top right corner
+
+ //BEGIN: right vertical line
+ {
+ QPoint start(optRect.topRight());
+ start.ry() += 3;
+ QPoint verticalGradBottom(optRect.topRight());
+ verticalGradBottom.ry() += fontMetrics.height() + 5;
+ QLinearGradient gradient(start, verticalGradBottom);
+ gradient.setColorAt(0, outlineColor);
+ gradient.setColorAt(1, Qt::transparent);
+ painter->fillRect(QRect(start, QSize(1, fontMetrics.height() + 5)), gradient);
+ }
+ //END: right vertical line
+
+ //BEGIN: checkboxy thing
+ {
+ painter->save();
+ painter->setRenderHint(QPainter::Antialiasing, false);
+ painter->setFont(font);
+ QColor penColor(option.palette.text().color());
+ penColor.setAlphaF(0.6);
+ painter->setPen(penColor);
+ QRect iconSubRect(option.rect);
+ iconSubRect.setTop(iconSubRect.top() + 7);
+ iconSubRect.setLeft(iconSubRect.left() + 7);
+
+ int sizing = fontMetrics.height();
+ int even = ( (sizing - 1) % 2 );
+
+ iconSubRect.setHeight(sizing - even);
+ iconSubRect.setWidth(sizing - even);
+ painter->drawRect(iconSubRect);
+
+
+ /*
+ if(collapsed)
+ painter->drawText(iconSubRect, Qt::AlignHCenter | Qt::AlignVCenter, "+");
+ else
+ painter->drawText(iconSubRect, Qt::AlignHCenter | Qt::AlignVCenter, "-");
+ */
+ painter->setBrush(option.palette.text());
+ painter->fillRect(iconSubRect.x(), iconSubRect.y() + iconSubRect.height() / 2,
+ iconSubRect.width(), 2, penColor);
+ if (collapsed)
+ {
+ painter->fillRect(iconSubRect.x() + iconSubRect.width() / 2, iconSubRect.y(), 2,
+ iconSubRect.height(), penColor);
+ }
+
+ painter->restore();
+ }
+ //END: checkboxy thing
+
+ //BEGIN: text
+ {
+ QRect textRect(option.rect);
+ textRect.setTop(textRect.top() + 7);
+ textRect.setLeft(textRect.left() + 7 + fontMetrics.height() + 7);
+ textRect.setHeight(fontMetrics.height());
+ textRect.setRight(textRect.right() - 7);
+
+ painter->save();
+ painter->setFont(font);
+ QColor penColor(option.palette.text().color());
+ penColor.setAlphaF(0.6);
+ painter->setPen(penColor);
+ painter->drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter, text);
+ painter->restore();
+ }
+ //END: text
+}
+
+int VisualGroup::totalHeight() const
+{
+ return headerHeight() + 5 + contentHeight(); // FIXME: wtf is that '5'?
+}
+
+int VisualGroup::headerHeight() const
+{
+ QFont font(QApplication::font());
+ font.setBold(true);
+ QFontMetrics fontMetrics(font);
+
+ const int height = fontMetrics.height() + 1 /* 1 pixel-width gradient */
+ + 11 /* top and bottom separation */;
+ return height;
+ /*
+ int raw = view->viewport()->fontMetrics().height() + 4;
+ // add english. maybe. depends on font height.
+ if (raw % 2 == 0)
+ raw++;
+ return std::min(raw, 25);
+ */
+}
+
+int VisualGroup::contentHeight() const
+{
+ if (collapsed)
+ {
+ return 0;
+ }
+ auto last = rows[numRows() - 1];
+ return last.top + last.height;
+}
+
+int VisualGroup::numRows() const
+{
+ return rows.size();
+}
+
+int VisualGroup::verticalPosition() const
+{
+ return m_verticalPosition;
+}
+
+QList<QModelIndex> VisualGroup::items() const
+{
+ QList<QModelIndex> indices;
+ for (int i = 0; i < view->model()->rowCount(); ++i)
+ {
+ const QModelIndex index = view->model()->index(i, 0);
+ if (index.data(InstanceViewRoles::GroupRole).toString() == text)
+ {
+ indices.append(index);
+ }
+ }
+ return indices;
+}
diff --git a/launcher/ui/instanceview/VisualGroup.h b/launcher/ui/instanceview/VisualGroup.h
new file mode 100644
index 00000000..5a743aa1
--- /dev/null
+++ b/launcher/ui/instanceview/VisualGroup.h
@@ -0,0 +1,106 @@
+/* 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 <QString>
+#include <QRect>
+#include <QVector>
+#include <QStyleOption>
+
+class InstanceView;
+class QPainter;
+class QModelIndex;
+
+struct VisualRow
+{
+ QList<QModelIndex> items;
+ int height = 0;
+ int top = 0;
+ inline int size() const
+ {
+ return items.size();
+ }
+ inline QModelIndex &operator[](int i)
+ {
+ return items[i];
+ }
+};
+
+struct VisualGroup
+{
+/* constructors */
+ VisualGroup(const QString &text, InstanceView *view);
+ VisualGroup(const VisualGroup *other);
+
+/* data */
+ InstanceView *view = nullptr;
+ QString text;
+ bool collapsed = false;
+ QVector<VisualRow> rows;
+ int firstItemIndex = 0;
+ int m_verticalPosition = 0;
+
+/* logic */
+ /// update the internal list of items and flow them into the rows.
+ void update();
+
+ /// draw the header at y-position.
+ void drawHeader(QPainter *painter, const QStyleOptionViewItem &option);
+
+ /// height of the group, in total. includes a small bit of padding.
+ int totalHeight() const;
+
+ /// height of the group header, in pixels
+ int headerHeight() const;
+
+ /// height of the group content, in pixels
+ int contentHeight() const;
+
+ /// the number of visual rows this group has
+ int numRows() const;
+
+ /// actually calculate the above value
+ int calculateNumRows() const;
+
+ /// the height at which this group starts, in pixels
+ int verticalPosition() const;
+
+ /// relative geometry - top of the row of the given item
+ int rowTopOf(const QModelIndex &index) const;
+
+ /// height of the row of the given item
+ int rowHeightOf(const QModelIndex &index) const;
+
+ /// x/y position of the given item inside the group (in items!)
+ QPair<int, int> positionOf(const QModelIndex &index) const;
+
+ enum HitResult
+ {
+ NoHit = 0x0,
+ TextHit = 0x1,
+ CheckboxHit = 0x2,
+ HeaderHit = 0x4,
+ BodyHit = 0x8
+ };
+ Q_DECLARE_FLAGS(HitResults, HitResult)
+
+ /// shoot! BANG! what did we hit?
+ HitResults hitScan (const QPoint &pos) const;
+
+ QList<QModelIndex> items() const;
+};
+
+Q_DECLARE_OPERATORS_FOR_FLAGS(VisualGroup::HitResults)
diff --git a/launcher/ui/pagedialog/PageDialog.cpp b/launcher/ui/pagedialog/PageDialog.cpp
new file mode 100644
index 00000000..18d61dc2
--- /dev/null
+++ b/launcher/ui/pagedialog/PageDialog.cpp
@@ -0,0 +1,62 @@
+/* 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 "PageDialog.h"
+
+#include <QDialogButtonBox>
+#include <QPushButton>
+#include <QVBoxLayout>
+#include <QKeyEvent>
+
+#include "Application.h"
+#include "settings/SettingsObject.h"
+
+#include "ui/widgets/IconLabel.h"
+#include "ui/widgets/PageContainer.h"
+
+PageDialog::PageDialog(BasePageProvider *pageProvider, QString defaultId, QWidget *parent)
+ : QDialog(parent)
+{
+ setWindowTitle(pageProvider->dialogTitle());
+ m_container = new PageContainer(pageProvider, defaultId, this);
+
+ QVBoxLayout *mainLayout = new QVBoxLayout;
+ mainLayout->addWidget(m_container);
+ mainLayout->setSpacing(0);
+ mainLayout->setContentsMargins(0, 0, 0, 0);
+ setLayout(mainLayout);
+
+ QDialogButtonBox *buttons = new QDialogButtonBox(QDialogButtonBox::Help | QDialogButtonBox::Close);
+ buttons->button(QDialogButtonBox::Close)->setDefault(true);
+ buttons->setContentsMargins(6, 0, 6, 0);
+ m_container->addButtons(buttons);
+
+ connect(buttons->button(QDialogButtonBox::Close), SIGNAL(clicked()), this, SLOT(close()));
+ connect(buttons->button(QDialogButtonBox::Help), SIGNAL(clicked()), m_container, SLOT(help()));
+
+ restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get("PagedGeometry").toByteArray()));
+}
+
+void PageDialog::closeEvent(QCloseEvent *event)
+{
+ qDebug() << "Paged dialog close requested";
+ if (m_container->prepareToClose())
+ {
+ qDebug() << "Paged dialog close approved";
+ APPLICATION->settings()->set("PagedGeometry", saveGeometry().toBase64());
+ qDebug() << "Paged dialog geometry saved";
+ QDialog::closeEvent(event);
+ }
+}
diff --git a/launcher/ui/pagedialog/PageDialog.h b/launcher/ui/pagedialog/PageDialog.h
new file mode 100644
index 00000000..00d8b725
--- /dev/null
+++ b/launcher/ui/pagedialog/PageDialog.h
@@ -0,0 +1,35 @@
+/* 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 <QDialog>
+#include "ui/pages/BasePageProvider.h"
+
+class PageContainer;
+class PageDialog : public QDialog
+{
+ Q_OBJECT
+public:
+ explicit PageDialog(BasePageProvider *pageProvider, QString defaultId = QString(), QWidget *parent = 0);
+ virtual ~PageDialog() {}
+
+private
+slots:
+ virtual void closeEvent(QCloseEvent *event);
+
+private:
+ PageContainer * m_container;
+};
diff --git a/launcher/ui/pages/BasePage.h b/launcher/ui/pages/BasePage.h
new file mode 100644
index 00000000..408965d0
--- /dev/null
+++ b/launcher/ui/pages/BasePage.h
@@ -0,0 +1,58 @@
+/* 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 <QString>
+#include <QIcon>
+#include <memory>
+
+#include "BasePageContainer.h"
+
+class BasePage
+{
+public:
+ virtual ~BasePage() {}
+ virtual QString id() const = 0;
+ virtual QString displayName() const = 0;
+ virtual QIcon icon() const = 0;
+ virtual bool apply() { return true; }
+ virtual bool shouldDisplay() const { return true; }
+ virtual QString helpPage() const { return QString(); }
+ void opened()
+ {
+ isOpened = true;
+ openedImpl();
+ }
+ void closed()
+ {
+ isOpened = false;
+ closedImpl();
+ }
+ virtual void openedImpl() {}
+ virtual void closedImpl() {}
+ virtual void setParentContainer(BasePageContainer * container)
+ {
+ m_container = container;
+ };
+public:
+ int stackIndex = -1;
+ int listIndex = -1;
+protected:
+ BasePageContainer * m_container = nullptr;
+ bool isOpened = false;
+};
+
+typedef std::shared_ptr<BasePage> BasePagePtr;
diff --git a/launcher/ui/pages/BasePageContainer.h b/launcher/ui/pages/BasePageContainer.h
new file mode 100644
index 00000000..f8c7adeb
--- /dev/null
+++ b/launcher/ui/pages/BasePageContainer.h
@@ -0,0 +1,10 @@
+#pragma once
+
+class BasePageContainer
+{
+public:
+ virtual ~BasePageContainer(){};
+ virtual bool selectPage(QString pageId) = 0;
+ virtual void refreshContainer() = 0;
+ virtual bool requestClose() = 0;
+};
diff --git a/launcher/ui/pages/BasePageProvider.h b/launcher/ui/pages/BasePageProvider.h
new file mode 100644
index 00000000..873e8dce
--- /dev/null
+++ b/launcher/ui/pages/BasePageProvider.h
@@ -0,0 +1,68 @@
+/* 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 "ui/pages/BasePage.h"
+#include <memory>
+#include <functional>
+
+class BasePageProvider
+{
+public:
+ virtual QList<BasePage *> getPages() = 0;
+ virtual QString dialogTitle() = 0;
+};
+
+class GenericPageProvider : public BasePageProvider
+{
+ typedef std::function<BasePage *()> PageCreator;
+public:
+ explicit GenericPageProvider(const QString &dialogTitle)
+ : m_dialogTitle(dialogTitle)
+ {
+ }
+ virtual ~GenericPageProvider() {}
+
+ QList<BasePage *> getPages() override
+ {
+ QList<BasePage *> pages;
+ for (PageCreator creator : m_creators)
+ {
+ pages.append(creator());
+ }
+ return pages;
+ }
+ QString dialogTitle() override { return m_dialogTitle; }
+
+ void setDialogTitle(const QString &title)
+ {
+ m_dialogTitle = title;
+ }
+ void addPageCreator(PageCreator page)
+ {
+ m_creators.append(page);
+ }
+
+ template<typename PageClass>
+ void addPage()
+ {
+ addPageCreator([](){return new PageClass();});
+ }
+
+private:
+ QList<PageCreator> m_creators;
+ QString m_dialogTitle;
+};
diff --git a/launcher/ui/pages/global/AccountListPage.cpp b/launcher/ui/pages/global/AccountListPage.cpp
new file mode 100644
index 00000000..816dce47
--- /dev/null
+++ b/launcher/ui/pages/global/AccountListPage.cpp
@@ -0,0 +1,260 @@
+/* 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 "AccountListPage.h"
+#include "ui_AccountListPage.h"
+
+#include <QItemSelectionModel>
+#include <QMenu>
+
+#include <QDebug>
+
+#include "net/NetJob.h"
+
+#include "ui/dialogs/ProgressDialog.h"
+#include "ui/dialogs/LoginDialog.h"
+#include "ui/dialogs/MSALoginDialog.h"
+#include "ui/dialogs/CustomMessageBox.h"
+#include "ui/dialogs/SkinUploadDialog.h"
+
+#include "tasks/Task.h"
+#include "minecraft/auth/AccountTask.h"
+#include "minecraft/services/SkinDelete.h"
+
+#include "Application.h"
+
+#include "BuildConfig.h"
+
+#include "Secrets.h"
+
+AccountListPage::AccountListPage(QWidget *parent)
+ : QMainWindow(parent), ui(new Ui::AccountListPage)
+{
+ ui->setupUi(this);
+ ui->listView->setEmptyString(tr(
+ "Welcome!\n"
+ "If you're new here, you can click the \"Add\" button to add your Mojang or Minecraft account."
+ ));
+ ui->listView->setEmptyMode(VersionListView::String);
+ ui->listView->setContextMenuPolicy(Qt::CustomContextMenu);
+
+ m_accounts = APPLICATION->accounts();
+
+ ui->listView->setModel(m_accounts.get());
+ ui->listView->header()->setSectionResizeMode(0, QHeaderView::Stretch);
+ ui->listView->header()->setSectionResizeMode(1, QHeaderView::Stretch);
+ ui->listView->header()->setSectionResizeMode(2, QHeaderView::ResizeToContents);
+ ui->listView->setSelectionMode(QAbstractItemView::SingleSelection);
+
+ // Expand the account column
+
+ QItemSelectionModel *selectionModel = ui->listView->selectionModel();
+
+ connect(selectionModel, &QItemSelectionModel::selectionChanged, [this](const QItemSelection &sel, const QItemSelection &dsel) {
+ updateButtonStates();
+ });
+ connect(ui->listView, &VersionListView::customContextMenuRequested, this, &AccountListPage::ShowContextMenu);
+
+ connect(m_accounts.get(), &AccountList::listChanged, this, &AccountListPage::listChanged);
+ connect(m_accounts.get(), &AccountList::listActivityChanged, this, &AccountListPage::listChanged);
+ connect(m_accounts.get(), &AccountList::defaultAccountChanged, this, &AccountListPage::listChanged);
+
+ updateButtonStates();
+
+ // Xbox authentication won't work without a client identifier, so disable the button if it is missing
+ ui->actionAddMicrosoft->setVisible(Secrets::hasMSAClientID());
+}
+
+AccountListPage::~AccountListPage()
+{
+ delete ui;
+}
+
+void AccountListPage::ShowContextMenu(const QPoint& pos)
+{
+ auto menu = ui->toolBar->createContextMenu(this, tr("Context menu"));
+ menu->exec(ui->listView->mapToGlobal(pos));
+ delete menu;
+}
+
+void AccountListPage::changeEvent(QEvent* event)
+{
+ if (event->type() == QEvent::LanguageChange)
+ {
+ ui->retranslateUi(this);
+ }
+ QMainWindow::changeEvent(event);
+}
+
+QMenu * AccountListPage::createPopupMenu()
+{
+ QMenu* filteredMenu = QMainWindow::createPopupMenu();
+ filteredMenu->removeAction(ui->toolBar->toggleViewAction() );
+ return filteredMenu;
+}
+
+
+void AccountListPage::listChanged()
+{
+ updateButtonStates();
+}
+
+void AccountListPage::on_actionAddMojang_triggered()
+{
+ MinecraftAccountPtr account = LoginDialog::newAccount(
+ this,
+ tr("Please enter your Mojang account email and password to add your account.")
+ );
+
+ if (account)
+ {
+ m_accounts->addAccount(account);
+ if (m_accounts->count() == 1) {
+ m_accounts->setDefaultAccount(account);
+ }
+ }
+}
+
+void AccountListPage::on_actionAddMicrosoft_triggered()
+{
+ if(BuildConfig.BUILD_PLATFORM == "osx64") {
+ CustomMessageBox::selectable(
+ this,
+ tr("Microsoft Accounts not available"),
+ tr(
+ "Microsoft accounts are only usable on macOS 10.13 or newer, with fully updated MultiMC.\n\n"
+ "Please update both your operating system and MultiMC."
+ ),
+ QMessageBox::Warning
+ )->exec();
+ return;
+ }
+ MinecraftAccountPtr account = MSALoginDialog::newAccount(
+ this,
+ tr("Please enter your Mojang account email and password to add your account.")
+ );
+
+ if (account)
+ {
+ m_accounts->addAccount(account);
+ if (m_accounts->count() == 1) {
+ m_accounts->setDefaultAccount(account);
+ }
+ }
+}
+
+void AccountListPage::on_actionRemove_triggered()
+{
+ QModelIndexList selection = ui->listView->selectionModel()->selectedIndexes();
+ if (selection.size() > 0)
+ {
+ QModelIndex selected = selection.first();
+ m_accounts->removeAccount(selected);
+ }
+}
+
+void AccountListPage::on_actionRefresh_triggered() {
+ QModelIndexList selection = ui->listView->selectionModel()->selectedIndexes();
+ if (selection.size() > 0) {
+ QModelIndex selected = selection.first();
+ MinecraftAccountPtr account = selected.data(AccountList::PointerRole).value<MinecraftAccountPtr>();
+ AuthSessionPtr session = std::make_shared<AuthSession>();
+ auto task = account->refresh(session);
+ if (task) {
+ ProgressDialog progDialog(this);
+ progDialog.execWithTask(task.get());
+ // TODO: respond to results of the task
+ }
+ }
+}
+
+
+void AccountListPage::on_actionSetDefault_triggered()
+{
+ QModelIndexList selection = ui->listView->selectionModel()->selectedIndexes();
+ if (selection.size() > 0)
+ {
+ QModelIndex selected = selection.first();
+ MinecraftAccountPtr account = selected.data(AccountList::PointerRole).value<MinecraftAccountPtr>();
+ m_accounts->setDefaultAccount(account);
+ }
+}
+
+void AccountListPage::on_actionNoDefault_triggered()
+{
+ m_accounts->setDefaultAccount(nullptr);
+}
+
+void AccountListPage::updateButtonStates()
+{
+ // If there is no selection, disable buttons that require something selected.
+ QModelIndexList selection = ui->listView->selectionModel()->selectedIndexes();
+ bool hasSelection = selection.size() > 0;
+ bool accountIsReady = false;
+ if (hasSelection)
+ {
+ QModelIndex selected = selection.first();
+ MinecraftAccountPtr account = selected.data(AccountList::PointerRole).value<MinecraftAccountPtr>();
+ accountIsReady = !account->isActive();
+ }
+ ui->actionRemove->setEnabled(accountIsReady);
+ ui->actionSetDefault->setEnabled(accountIsReady);
+ ui->actionUploadSkin->setEnabled(accountIsReady);
+ ui->actionDeleteSkin->setEnabled(accountIsReady);
+ ui->actionRefresh->setEnabled(accountIsReady);
+
+ if(m_accounts->defaultAccount().get() == nullptr) {
+ ui->actionNoDefault->setEnabled(false);
+ ui->actionNoDefault->setChecked(true);
+ }
+ else {
+ ui->actionNoDefault->setEnabled(true);
+ ui->actionNoDefault->setChecked(false);
+ }
+}
+
+void AccountListPage::on_actionUploadSkin_triggered()
+{
+ QModelIndexList selection = ui->listView->selectionModel()->selectedIndexes();
+ if (selection.size() > 0)
+ {
+ QModelIndex selected = selection.first();
+ MinecraftAccountPtr account = selected.data(AccountList::PointerRole).value<MinecraftAccountPtr>();
+ SkinUploadDialog dialog(account, this);
+ dialog.exec();
+ }
+}
+
+void AccountListPage::on_actionDeleteSkin_triggered()
+{
+ QModelIndexList selection = ui->listView->selectionModel()->selectedIndexes();
+ if (selection.size() <= 0)
+ return;
+
+ QModelIndex selected = selection.first();
+ AuthSessionPtr session = std::make_shared<AuthSession>();
+ MinecraftAccountPtr account = selected.data(AccountList::PointerRole).value<MinecraftAccountPtr>();
+ auto login = account->refresh(session);
+ ProgressDialog prog(this);
+ if (prog.execWithTask((Task*)login.get()) != QDialog::Accepted) {
+ CustomMessageBox::selectable(this, tr("Skin Delete"), tr("Failed to login!"), QMessageBox::Warning)->exec();
+ return;
+ }
+ auto deleteSkinTask = std::make_shared<SkinDelete>(this, session);
+ if (prog.execWithTask((Task*)deleteSkinTask.get()) != QDialog::Accepted) {
+ CustomMessageBox::selectable(this, tr("Skin Delete"), tr("Failed to delete current skin!"), QMessageBox::Warning)->exec();
+ return;
+ }
+}
diff --git a/launcher/ui/pages/global/AccountListPage.h b/launcher/ui/pages/global/AccountListPage.h
new file mode 100644
index 00000000..1c65e708
--- /dev/null
+++ b/launcher/ui/pages/global/AccountListPage.h
@@ -0,0 +1,85 @@
+/* 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 <QMainWindow>
+#include <memory>
+
+#include "ui/pages/BasePage.h"
+
+#include "minecraft/auth/AccountList.h"
+#include "Application.h"
+
+namespace Ui
+{
+class AccountListPage;
+}
+
+class AuthenticateTask;
+
+class AccountListPage : public QMainWindow, public BasePage
+{
+ Q_OBJECT
+public:
+ explicit AccountListPage(QWidget *parent = 0);
+ ~AccountListPage();
+
+ QString displayName() const override
+ {
+ return tr("Accounts");
+ }
+ QIcon icon() const override
+ {
+ auto icon = APPLICATION->getThemedIcon("accounts");
+ if(icon.isNull())
+ {
+ icon = APPLICATION->getThemedIcon("noaccount");
+ }
+ return icon;
+ }
+ QString id() const override
+ {
+ return "accounts";
+ }
+ QString helpPage() const override
+ {
+ return "Getting-Started#adding-an-account";
+ }
+
+public slots:
+ void on_actionAddMojang_triggered();
+ void on_actionAddMicrosoft_triggered();
+ void on_actionRemove_triggered();
+ void on_actionRefresh_triggered();
+ void on_actionSetDefault_triggered();
+ void on_actionNoDefault_triggered();
+ void on_actionUploadSkin_triggered();
+ void on_actionDeleteSkin_triggered();
+
+ void listChanged();
+
+ //! Updates the states of the dialog's buttons.
+ void updateButtonStates();
+
+protected slots:
+ void ShowContextMenu(const QPoint &pos);
+
+private:
+ void changeEvent(QEvent * event) override;
+ QMenu * createPopupMenu() override;
+ shared_qobject_ptr<AccountList> m_accounts;
+ Ui::AccountListPage *ui;
+};
diff --git a/launcher/ui/pages/global/AccountListPage.ui b/launcher/ui/pages/global/AccountListPage.ui
new file mode 100644
index 00000000..29738c02
--- /dev/null
+++ b/launcher/ui/pages/global/AccountListPage.ui
@@ -0,0 +1,129 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>AccountListPage</class>
+ <widget class="QMainWindow" name="AccountListPage">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>800</width>
+ <height>600</height>
+ </rect>
+ </property>
+ <widget class="QWidget" name="centralwidget">
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="VersionListView" name="listView">
+ <property name="alternatingRowColors">
+ <bool>true</bool>
+ </property>
+ <property name="rootIsDecorated">
+ <bool>false</bool>
+ </property>
+ <property name="itemsExpandable">
+ <bool>false</bool>
+ </property>
+ <property name="allColumnsShowFocus">
+ <bool>true</bool>
+ </property>
+ <attribute name="headerStretchLastSection">
+ <bool>false</bool>
+ </attribute>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <widget class="WideBar" name="toolBar">
+ <attribute name="toolBarArea">
+ <enum>RightToolBarArea</enum>
+ </attribute>
+ <attribute name="toolBarBreak">
+ <bool>false</bool>
+ </attribute>
+ <addaction name="actionAddMicrosoft"/>
+ <addaction name="actionAddMojang"/>
+ <addaction name="actionRefresh"/>
+ <addaction name="actionRemove"/>
+ <addaction name="actionSetDefault"/>
+ <addaction name="actionNoDefault"/>
+ <addaction name="separator"/>
+ <addaction name="actionUploadSkin"/>
+ <addaction name="actionDeleteSkin"/>
+ </widget>
+ <action name="actionAddMojang">
+ <property name="text">
+ <string>Add Mojang</string>
+ </property>
+ </action>
+ <action name="actionRemove">
+ <property name="text">
+ <string>Remove</string>
+ </property>
+ </action>
+ <action name="actionSetDefault">
+ <property name="text">
+ <string>Set Default</string>
+ </property>
+ </action>
+ <action name="actionNoDefault">
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ <property name="text">
+ <string>No Default</string>
+ </property>
+ </action>
+ <action name="actionUploadSkin">
+ <property name="text">
+ <string>Upload Skin</string>
+ </property>
+ </action>
+ <action name="actionDeleteSkin">
+ <property name="text">
+ <string>Delete Skin</string>
+ </property>
+ <property name="toolTip">
+ <string>Delete the currently active skin and go back to the default one</string>
+ </property>
+ </action>
+ <action name="actionAddMicrosoft">
+ <property name="text">
+ <string>Add Microsoft</string>
+ </property>
+ </action>
+ <action name="actionRefresh">
+ <property name="text">
+ <string>Refresh</string>
+ </property>
+ <property name="toolTip">
+ <string>Refresh the account tokens</string>
+ </property>
+ </action>
+ </widget>
+ <customwidgets>
+ <customwidget>
+ <class>VersionListView</class>
+ <extends>QTreeView</extends>
+ <header>ui/widgets/VersionListView.h</header>
+ </customwidget>
+ <customwidget>
+ <class>WideBar</class>
+ <extends>QToolBar</extends>
+ <header>ui/widgets/WideBar.h</header>
+ </customwidget>
+ </customwidgets>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/launcher/ui/pages/global/CustomCommandsPage.cpp b/launcher/ui/pages/global/CustomCommandsPage.cpp
new file mode 100644
index 00000000..8541e3c1
--- /dev/null
+++ b/launcher/ui/pages/global/CustomCommandsPage.cpp
@@ -0,0 +1,51 @@
+#include "CustomCommandsPage.h"
+#include <QVBoxLayout>
+#include <QTabWidget>
+#include <QTabBar>
+
+CustomCommandsPage::CustomCommandsPage(QWidget* parent): QWidget(parent)
+{
+
+ auto verticalLayout = new QVBoxLayout(this);
+ verticalLayout->setObjectName(QStringLiteral("verticalLayout"));
+ verticalLayout->setContentsMargins(0, 0, 0, 0);
+
+ auto tabWidget = new QTabWidget(this);
+ tabWidget->setObjectName(QStringLiteral("tabWidget"));
+ commands = new CustomCommands(this);
+ commands->setContentsMargins(6, 6, 6, 6);
+ tabWidget->addTab(commands, "Foo");
+ tabWidget->tabBar()->hide();
+ verticalLayout->addWidget(tabWidget);
+ loadSettings();
+}
+
+CustomCommandsPage::~CustomCommandsPage()
+{
+}
+
+bool CustomCommandsPage::apply()
+{
+ applySettings();
+ return true;
+}
+
+void CustomCommandsPage::applySettings()
+{
+ auto s = APPLICATION->settings();
+ s->set("PreLaunchCommand", commands->prelaunchCommand());
+ s->set("WrapperCommand", commands->wrapperCommand());
+ s->set("PostExitCommand", commands->postexitCommand());
+}
+
+void CustomCommandsPage::loadSettings()
+{
+ auto s = APPLICATION->settings();
+ commands->initialize(
+ false,
+ true,
+ s->get("PreLaunchCommand").toString(),
+ s->get("WrapperCommand").toString(),
+ s->get("PostExitCommand").toString()
+ );
+}
diff --git a/launcher/ui/pages/global/CustomCommandsPage.h b/launcher/ui/pages/global/CustomCommandsPage.h
new file mode 100644
index 00000000..a1155e0e
--- /dev/null
+++ b/launcher/ui/pages/global/CustomCommandsPage.h
@@ -0,0 +1,55 @@
+/* Copyright 2018-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 <memory>
+#include <QDialog>
+
+#include "ui/pages/BasePage.h"
+#include <Application.h>
+#include "ui/widgets/CustomCommands.h"
+
+class CustomCommandsPage : public QWidget, public BasePage
+{
+ Q_OBJECT
+
+public:
+ explicit CustomCommandsPage(QWidget *parent = 0);
+ ~CustomCommandsPage();
+
+ QString displayName() const override
+ {
+ return tr("Custom Commands");
+ }
+ QIcon icon() const override
+ {
+ return APPLICATION->getThemedIcon("custom-commands");
+ }
+ QString id() const override
+ {
+ return "custom-commands";
+ }
+ QString helpPage() const override
+ {
+ return "Custom-commands";
+ }
+ bool apply() override;
+
+private:
+ void applySettings();
+ void loadSettings();
+ CustomCommands * commands;
+};
diff --git a/launcher/ui/pages/global/ExternalToolsPage.cpp b/launcher/ui/pages/global/ExternalToolsPage.cpp
new file mode 100644
index 00000000..41d900aa
--- /dev/null
+++ b/launcher/ui/pages/global/ExternalToolsPage.cpp
@@ -0,0 +1,233 @@
+/* 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 "ExternalToolsPage.h"
+#include "ui_ExternalToolsPage.h"
+
+#include <QMessageBox>
+#include <QFileDialog>
+#include <QStandardPaths>
+#include <QTabBar>
+
+#include "settings/SettingsObject.h"
+#include "tools/BaseProfiler.h"
+#include <FileSystem.h>
+#include "Application.h"
+#include <tools/MCEditTool.h>
+
+ExternalToolsPage::ExternalToolsPage(QWidget *parent) :
+ QWidget(parent),
+ ui(new Ui::ExternalToolsPage)
+{
+ ui->setupUi(this);
+ ui->tabWidget->tabBar()->hide();
+
+ #if QT_VERSION >= QT_VERSION_CHECK(5, 2, 0)
+ ui->jsonEditorTextBox->setClearButtonEnabled(true);
+ #endif
+
+ ui->mceditLink->setOpenExternalLinks(true);
+ ui->jvisualvmLink->setOpenExternalLinks(true);
+ ui->jprofilerLink->setOpenExternalLinks(true);
+ loadSettings();
+}
+
+ExternalToolsPage::~ExternalToolsPage()
+{
+ delete ui;
+}
+
+void ExternalToolsPage::loadSettings()
+{
+ auto s = APPLICATION->settings();
+ ui->jprofilerPathEdit->setText(s->get("JProfilerPath").toString());
+ ui->jvisualvmPathEdit->setText(s->get("JVisualVMPath").toString());
+ ui->mceditPathEdit->setText(s->get("MCEditPath").toString());
+
+ // Editors
+ ui->jsonEditorTextBox->setText(s->get("JsonEditor").toString());
+}
+void ExternalToolsPage::applySettings()
+{
+ auto s = APPLICATION->settings();
+
+ s->set("JProfilerPath", ui->jprofilerPathEdit->text());
+ s->set("JVisualVMPath", ui->jvisualvmPathEdit->text());
+ s->set("MCEditPath", ui->mceditPathEdit->text());
+
+ // Editors
+ QString jsonEditor = ui->jsonEditorTextBox->text();
+ if (!jsonEditor.isEmpty() &&
+ (!QFileInfo(jsonEditor).exists() || !QFileInfo(jsonEditor).isExecutable()))
+ {
+ QString found = QStandardPaths::findExecutable(jsonEditor);
+ if (!found.isEmpty())
+ {
+ jsonEditor = found;
+ }
+ }
+ s->set("JsonEditor", jsonEditor);
+}
+
+void ExternalToolsPage::on_jprofilerPathBtn_clicked()
+{
+ QString raw_dir = ui->jprofilerPathEdit->text();
+ QString error;
+ do
+ {
+ raw_dir = QFileDialog::getExistingDirectory(this, tr("JProfiler Folder"), raw_dir);
+ if (raw_dir.isEmpty())
+ {
+ break;
+ }
+ QString cooked_dir = FS::NormalizePath(raw_dir);
+ if (!APPLICATION->profilers()["jprofiler"]->check(cooked_dir, &error))
+ {
+ QMessageBox::critical(this, tr("Error"), tr("Error while checking JProfiler install:\n%1").arg(error));
+ continue;
+ }
+ else
+ {
+ ui->jprofilerPathEdit->setText(cooked_dir);
+ break;
+ }
+ } while (1);
+}
+void ExternalToolsPage::on_jprofilerCheckBtn_clicked()
+{
+ QString error;
+ if (!APPLICATION->profilers()["jprofiler"]->check(ui->jprofilerPathEdit->text(), &error))
+ {
+ QMessageBox::critical(this, tr("Error"), tr("Error while checking JProfiler install:\n%1").arg(error));
+ }
+ else
+ {
+ QMessageBox::information(this, tr("OK"), tr("JProfiler setup seems to be OK"));
+ }
+}
+
+void ExternalToolsPage::on_jvisualvmPathBtn_clicked()
+{
+ QString raw_dir = ui->jvisualvmPathEdit->text();
+ QString error;
+ do
+ {
+ raw_dir = QFileDialog::getOpenFileName(this, tr("JVisualVM Executable"), raw_dir);
+ if (raw_dir.isEmpty())
+ {
+ break;
+ }
+ QString cooked_dir = FS::NormalizePath(raw_dir);
+ if (!APPLICATION->profilers()["jvisualvm"]->check(cooked_dir, &error))
+ {
+ QMessageBox::critical(this, tr("Error"), tr("Error while checking JVisualVM install:\n%1").arg(error));
+ continue;
+ }
+ else
+ {
+ ui->jvisualvmPathEdit->setText(cooked_dir);
+ break;
+ }
+ } while (1);
+}
+void ExternalToolsPage::on_jvisualvmCheckBtn_clicked()
+{
+ QString error;
+ if (!APPLICATION->profilers()["jvisualvm"]->check(ui->jvisualvmPathEdit->text(), &error))
+ {
+ QMessageBox::critical(this, tr("Error"), tr("Error while checking JVisualVM install:\n%1").arg(error));
+ }
+ else
+ {
+ QMessageBox::information(this, tr("OK"), tr("JVisualVM setup seems to be OK"));
+ }
+}
+
+void ExternalToolsPage::on_mceditPathBtn_clicked()
+{
+ QString raw_dir = ui->mceditPathEdit->text();
+ QString error;
+ do
+ {
+#ifdef Q_OS_OSX
+ raw_dir = QFileDialog::getOpenFileName(this, tr("MCEdit Application"), raw_dir);
+#else
+ raw_dir = QFileDialog::getExistingDirectory(this, tr("MCEdit Folder"), raw_dir);
+#endif
+ if (raw_dir.isEmpty())
+ {
+ break;
+ }
+ QString cooked_dir = FS::NormalizePath(raw_dir);
+ if (!APPLICATION->mcedit()->check(cooked_dir, error))
+ {
+ QMessageBox::critical(this, tr("Error"), tr("Error while checking MCEdit install:\n%1").arg(error));
+ continue;
+ }
+ else
+ {
+ ui->mceditPathEdit->setText(cooked_dir);
+ break;
+ }
+ } while (1);
+}
+void ExternalToolsPage::on_mceditCheckBtn_clicked()
+{
+ QString error;
+ if (!APPLICATION->mcedit()->check(ui->mceditPathEdit->text(), error))
+ {
+ QMessageBox::critical(this, tr("Error"), tr("Error while checking MCEdit install:\n%1").arg(error));
+ }
+ else
+ {
+ QMessageBox::information(this, tr("OK"), tr("MCEdit setup seems to be OK"));
+ }
+}
+
+void ExternalToolsPage::on_jsonEditorBrowseBtn_clicked()
+{
+ QString raw_file = QFileDialog::getOpenFileName(
+ this, tr("JSON Editor"),
+ ui->jsonEditorTextBox->text().isEmpty()
+#if defined(Q_OS_LINUX)
+ ? QString("/usr/bin")
+#else
+ ? QStandardPaths::standardLocations(QStandardPaths::ApplicationsLocation).first()
+#endif
+ : ui->jsonEditorTextBox->text());
+
+ if (raw_file.isEmpty())
+ {
+ return;
+ }
+ QString cooked_file = FS::NormalizePath(raw_file);
+
+ // it has to exist and be an executable
+ if (QFileInfo(cooked_file).exists() && QFileInfo(cooked_file).isExecutable())
+ {
+ ui->jsonEditorTextBox->setText(cooked_file);
+ }
+ else
+ {
+ QMessageBox::warning(this, tr("Invalid"),
+ tr("The file chosen does not seem to be an executable"));
+ }
+}
+
+bool ExternalToolsPage::apply()
+{
+ applySettings();
+ return true;
+}
diff --git a/launcher/ui/pages/global/ExternalToolsPage.h b/launcher/ui/pages/global/ExternalToolsPage.h
new file mode 100644
index 00000000..5ae6148d
--- /dev/null
+++ b/launcher/ui/pages/global/ExternalToolsPage.h
@@ -0,0 +1,74 @@
+/* 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 <QWidget>
+
+#include "ui/pages/BasePage.h"
+#include <Application.h>
+
+namespace Ui {
+class ExternalToolsPage;
+}
+
+class ExternalToolsPage : public QWidget, public BasePage
+{
+ Q_OBJECT
+
+public:
+ explicit ExternalToolsPage(QWidget *parent = 0);
+ ~ExternalToolsPage();
+
+ QString displayName() const override
+ {
+ return tr("External Tools");
+ }
+ QIcon icon() const override
+ {
+ auto icon = APPLICATION->getThemedIcon("externaltools");
+ if(icon.isNull())
+ {
+ icon = APPLICATION->getThemedIcon("loadermods");
+ }
+ return icon;
+ }
+ QString id() const override
+ {
+ return "external-tools";
+ }
+ QString helpPage() const override
+ {
+ return "Tools";
+ }
+ virtual bool apply() override;
+
+private:
+ void loadSettings();
+ void applySettings();
+
+private:
+ Ui::ExternalToolsPage *ui;
+
+private
+slots:
+ void on_jprofilerPathBtn_clicked();
+ void on_jprofilerCheckBtn_clicked();
+ void on_jvisualvmPathBtn_clicked();
+ void on_jvisualvmCheckBtn_clicked();
+ void on_mceditPathBtn_clicked();
+ void on_mceditCheckBtn_clicked();
+ void on_jsonEditorBrowseBtn_clicked();
+};
diff --git a/launcher/ui/pages/global/ExternalToolsPage.ui b/launcher/ui/pages/global/ExternalToolsPage.ui
new file mode 100644
index 00000000..e79e9388
--- /dev/null
+++ b/launcher/ui/pages/global/ExternalToolsPage.ui
@@ -0,0 +1,194 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>ExternalToolsPage</class>
+ <widget class="QWidget" name="ExternalToolsPage">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>673</width>
+ <height>751</height>
+ </rect>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QTabWidget" name="tabWidget">
+ <property name="currentIndex">
+ <number>0</number>
+ </property>
+ <widget class="QWidget" name="tab">
+ <attribute name="title">
+ <string notr="true">Tab 1</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_2">
+ <item>
+ <widget class="QGroupBox" name="groupBox_2">
+ <property name="title">
+ <string notr="true">JProfiler</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_10">
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_4">
+ <item>
+ <widget class="QLineEdit" name="jprofilerPathEdit"/>
+ </item>
+ <item>
+ <widget class="QPushButton" name="jprofilerPathBtn">
+ <property name="text">
+ <string notr="true">...</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QPushButton" name="jprofilerCheckBtn">
+ <property name="text">
+ <string>Check</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="jprofilerLink">
+ <property name="text">
+ <string notr="true">&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;a href=&quot;https://www.ej-technologies.com/products/jprofiler/overview.html&quot;&gt;https://www.ej-technologies.com/products/jprofiler/overview.html&lt;/a&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="groupBox_3">
+ <property name="title">
+ <string notr="true">JVisualVM</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_11">
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_5">
+ <item>
+ <widget class="QLineEdit" name="jvisualvmPathEdit"/>
+ </item>
+ <item>
+ <widget class="QPushButton" name="jvisualvmPathBtn">
+ <property name="text">
+ <string notr="true">...</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QPushButton" name="jvisualvmCheckBtn">
+ <property name="text">
+ <string>Check</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="jvisualvmLink">
+ <property name="text">
+ <string notr="true">&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;a href=&quot;https://visualvm.github.io/&quot;&gt;https://visualvm.github.io/&lt;/a&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="groupBox_4">
+ <property name="title">
+ <string notr="true">MCEdit</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_12">
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_6">
+ <item>
+ <widget class="QLineEdit" name="mceditPathEdit"/>
+ </item>
+ <item>
+ <widget class="QPushButton" name="mceditPathBtn">
+ <property name="text">
+ <string notr="true">...</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QPushButton" name="mceditCheckBtn">
+ <property name="text">
+ <string>Check</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="mceditLink">
+ <property name="text">
+ <string notr="true">&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;a href=&quot;https://www.mcedit.net/&quot;&gt;https://www.mcedit.net/&lt;/a&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="editorsBox">
+ <property name="title">
+ <string>External Editors (leave empty for system default)</string>
+ </property>
+ <layout class="QGridLayout" name="foldersBoxLayout_2">
+ <item row="0" column="1">
+ <widget class="QLineEdit" name="jsonEditorTextBox"/>
+ </item>
+ <item row="0" column="0">
+ <widget class="QLabel" name="labelJsonEditor">
+ <property name="text">
+ <string>Text Editor:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="2">
+ <widget class="QToolButton" name="jsonEditorBrowseBtn">
+ <property name="text">
+ <string notr="true">...</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <spacer name="verticalSpacer">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>216</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/launcher/ui/pages/global/JavaPage.cpp b/launcher/ui/pages/global/JavaPage.cpp
new file mode 100644
index 00000000..bd79f11a
--- /dev/null
+++ b/launcher/ui/pages/global/JavaPage.cpp
@@ -0,0 +1,153 @@
+/* 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 "JavaPage.h"
+#include "JavaCommon.h"
+#include "ui_JavaPage.h"
+
+#include <QFileDialog>
+#include <QMessageBox>
+#include <QDir>
+#include <QTabBar>
+
+#include "ui/dialogs/VersionSelectDialog.h"
+
+#include "java/JavaUtils.h"
+#include "java/JavaInstallList.h"
+
+#include "settings/SettingsObject.h"
+#include <FileSystem.h>
+#include "Application.h"
+#include <sys.h>
+
+JavaPage::JavaPage(QWidget *parent) : QWidget(parent), ui(new Ui::JavaPage)
+{
+ ui->setupUi(this);
+ ui->tabWidget->tabBar()->hide();
+
+ auto sysMiB = Sys::getSystemRam() / Sys::mebibyte;
+ ui->maxMemSpinBox->setMaximum(sysMiB);
+ loadSettings();
+}
+
+JavaPage::~JavaPage()
+{
+ delete ui;
+}
+
+bool JavaPage::apply()
+{
+ applySettings();
+ return true;
+}
+
+void JavaPage::applySettings()
+{
+ auto s = APPLICATION->settings();
+
+ // Memory
+ int min = ui->minMemSpinBox->value();
+ int max = ui->maxMemSpinBox->value();
+ if(min < max)
+ {
+ s->set("MinMemAlloc", min);
+ s->set("MaxMemAlloc", max);
+ }
+ else
+ {
+ s->set("MinMemAlloc", max);
+ s->set("MaxMemAlloc", min);
+ }
+ s->set("PermGen", ui->permGenSpinBox->value());
+
+ // Java Settings
+ s->set("JavaPath", ui->javaPathTextBox->text());
+ s->set("JvmArgs", ui->jvmArgsTextBox->text());
+ JavaCommon::checkJVMArgs(s->get("JvmArgs").toString(), this->parentWidget());
+}
+void JavaPage::loadSettings()
+{
+ auto s = APPLICATION->settings();
+ // Memory
+ int min = s->get("MinMemAlloc").toInt();
+ int max = s->get("MaxMemAlloc").toInt();
+ if(min < max)
+ {
+ ui->minMemSpinBox->setValue(min);
+ ui->maxMemSpinBox->setValue(max);
+ }
+ else
+ {
+ ui->minMemSpinBox->setValue(max);
+ ui->maxMemSpinBox->setValue(min);
+ }
+ ui->permGenSpinBox->setValue(s->get("PermGen").toInt());
+
+ // Java Settings
+ ui->javaPathTextBox->setText(s->get("JavaPath").toString());
+ ui->jvmArgsTextBox->setText(s->get("JvmArgs").toString());
+}
+
+void JavaPage::on_javaDetectBtn_clicked()
+{
+ JavaInstallPtr java;
+
+ VersionSelectDialog vselect(APPLICATION->javalist().get(), tr("Select a Java version"), this, true);
+ vselect.setResizeOn(2);
+ vselect.exec();
+
+ if (vselect.result() == QDialog::Accepted && vselect.selectedVersion())
+ {
+ java = std::dynamic_pointer_cast<JavaInstall>(vselect.selectedVersion());
+ ui->javaPathTextBox->setText(java->path);
+ }
+}
+
+void JavaPage::on_javaBrowseBtn_clicked()
+{
+ QString raw_path = QFileDialog::getOpenFileName(this, tr("Find Java executable"));
+
+ // do not allow current dir - it's dirty. Do not allow dirs that don't exist
+ if(raw_path.isEmpty())
+ {
+ return;
+ }
+
+ QString cooked_path = FS::NormalizePath(raw_path);
+ QFileInfo javaInfo(cooked_path);;
+ if(!javaInfo.exists() || !javaInfo.isExecutable())
+ {
+ return;
+ }
+ ui->javaPathTextBox->setText(cooked_path);
+}
+
+void JavaPage::on_javaTestBtn_clicked()
+{
+ if(checker)
+ {
+ return;
+ }
+ checker.reset(new JavaCommon::TestCheck(
+ this, ui->javaPathTextBox->text(), ui->jvmArgsTextBox->text(),
+ ui->minMemSpinBox->value(), ui->maxMemSpinBox->value(), ui->permGenSpinBox->value()));
+ connect(checker.get(), SIGNAL(finished()), SLOT(checkerFinished()));
+ checker->run();
+}
+
+void JavaPage::checkerFinished()
+{
+ checker.reset();
+}
diff --git a/launcher/ui/pages/global/JavaPage.h b/launcher/ui/pages/global/JavaPage.h
new file mode 100644
index 00000000..8f9b3323
--- /dev/null
+++ b/launcher/ui/pages/global/JavaPage.h
@@ -0,0 +1,72 @@
+/* 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 <memory>
+#include <QDialog>
+#include "ui/pages/BasePage.h"
+#include "JavaCommon.h"
+#include <Application.h>
+#include <QObjectPtr.h>
+
+class SettingsObject;
+
+namespace Ui
+{
+class JavaPage;
+}
+
+class JavaPage : public QWidget, public BasePage
+{
+ Q_OBJECT
+
+public:
+ explicit JavaPage(QWidget *parent = 0);
+ ~JavaPage();
+
+ QString displayName() const override
+ {
+ return tr("Java");
+ }
+ QIcon icon() const override
+ {
+ return APPLICATION->getThemedIcon("java");
+ }
+ QString id() const override
+ {
+ return "java-settings";
+ }
+ QString helpPage() const override
+ {
+ return "Java-settings";
+ }
+ bool apply() override;
+
+private:
+ void applySettings();
+ void loadSettings();
+
+private
+slots:
+ void on_javaDetectBtn_clicked();
+ void on_javaTestBtn_clicked();
+ void on_javaBrowseBtn_clicked();
+ void checkerFinished();
+
+private:
+ Ui::JavaPage *ui;
+ unique_qobject_ptr<JavaCommon::TestCheck> checker;
+};
diff --git a/launcher/ui/pages/global/JavaPage.ui b/launcher/ui/pages/global/JavaPage.ui
new file mode 100644
index 00000000..b67e9994
--- /dev/null
+++ b/launcher/ui/pages/global/JavaPage.ui
@@ -0,0 +1,260 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>JavaPage</class>
+ <widget class="QWidget" name="JavaPage">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>545</width>
+ <height>580</height>
+ </rect>
+ </property>
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QTabWidget" name="tabWidget">
+ <property name="currentIndex">
+ <number>0</number>
+ </property>
+ <widget class="QWidget" name="tab">
+ <attribute name="title">
+ <string notr="true">Tab 1</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_2">
+ <item>
+ <widget class="QGroupBox" name="memoryGroupBox">
+ <property name="title">
+ <string>Memory</string>
+ </property>
+ <layout class="QGridLayout" name="gridLayout_2">
+ <item row="1" column="1">
+ <widget class="QSpinBox" name="maxMemSpinBox">
+ <property name="toolTip">
+ <string>The maximum amount of memory Minecraft is allowed to use.</string>
+ </property>
+ <property name="suffix">
+ <string notr="true"> MiB</string>
+ </property>
+ <property name="minimum">
+ <number>128</number>
+ </property>
+ <property name="maximum">
+ <number>65536</number>
+ </property>
+ <property name="singleStep">
+ <number>128</number>
+ </property>
+ <property name="value">
+ <number>1024</number>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="0">
+ <widget class="QLabel" name="labelMinMem">
+ <property name="text">
+ <string>Minimum memory allocation:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0">
+ <widget class="QLabel" name="labelMaxMem">
+ <property name="text">
+ <string>Maximum memory allocation:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QSpinBox" name="minMemSpinBox">
+ <property name="toolTip">
+ <string>The amount of memory Minecraft is started with.</string>
+ </property>
+ <property name="suffix">
+ <string notr="true"> MiB</string>
+ </property>
+ <property name="minimum">
+ <number>128</number>
+ </property>
+ <property name="maximum">
+ <number>65536</number>
+ </property>
+ <property name="singleStep">
+ <number>128</number>
+ </property>
+ <property name="value">
+ <number>256</number>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="0">
+ <widget class="QLabel" name="labelPermGen">
+ <property name="text">
+ <string notr="true">PermGen:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="1">
+ <widget class="QSpinBox" name="permGenSpinBox">
+ <property name="toolTip">
+ <string>The amount of memory available to store loaded Java classes.</string>
+ </property>
+ <property name="suffix">
+ <string notr="true"> MiB</string>
+ </property>
+ <property name="minimum">
+ <number>64</number>
+ </property>
+ <property name="maximum">
+ <number>999999999</number>
+ </property>
+ <property name="singleStep">
+ <number>8</number>
+ </property>
+ <property name="value">
+ <number>64</number>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="javaSettingsGroupBox">
+ <property name="title">
+ <string>Java Runtime</string>
+ </property>
+ <layout class="QGridLayout" name="gridLayout_3">
+ <item row="0" column="0">
+ <widget class="QLabel" name="labelJavaPath">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Java path:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1" colspan="2">
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <item>
+ <widget class="QLineEdit" name="javaPathTextBox"/>
+ </item>
+ <item>
+ <widget class="QPushButton" name="javaBrowseBtn">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>28</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="text">
+ <string notr="true">...</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item row="2" column="1" colspan="2">
+ <widget class="QLineEdit" name="jvmArgsTextBox"/>
+ </item>
+ <item row="2" column="0">
+ <widget class="QLabel" name="labelJVMArgs">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>JVM arguments:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="1">
+ <widget class="QPushButton" name="javaDetectBtn">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Auto-detect...</string>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="2">
+ <widget class="QPushButton" name="javaTestBtn">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Test</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <spacer name="verticalSpacer">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>40</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <tabstops>
+ <tabstop>minMemSpinBox</tabstop>
+ <tabstop>maxMemSpinBox</tabstop>
+ <tabstop>permGenSpinBox</tabstop>
+ <tabstop>javaBrowseBtn</tabstop>
+ <tabstop>javaPathTextBox</tabstop>
+ <tabstop>jvmArgsTextBox</tabstop>
+ <tabstop>javaDetectBtn</tabstop>
+ <tabstop>javaTestBtn</tabstop>
+ <tabstop>tabWidget</tabstop>
+ </tabstops>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/launcher/ui/pages/global/LanguagePage.cpp b/launcher/ui/pages/global/LanguagePage.cpp
new file mode 100644
index 00000000..359fdeeb
--- /dev/null
+++ b/launcher/ui/pages/global/LanguagePage.cpp
@@ -0,0 +1,51 @@
+#include "LanguagePage.h"
+
+#include "ui/widgets/LanguageSelectionWidget.h"
+#include <QVBoxLayout>
+
+LanguagePage::LanguagePage(QWidget* parent) :
+ QWidget(parent)
+{
+ setObjectName(QStringLiteral("languagePage"));
+ auto layout = new QVBoxLayout(this);
+ mainWidget = new LanguageSelectionWidget(this);
+ layout->setContentsMargins(0,0,0,0);
+ layout->addWidget(mainWidget);
+ retranslate();
+}
+
+LanguagePage::~LanguagePage()
+{
+}
+
+bool LanguagePage::apply()
+{
+ applySettings();
+ return true;
+}
+
+void LanguagePage::applySettings()
+{
+ auto settings = APPLICATION->settings();
+ QString key = mainWidget->getSelectedLanguageKey();
+ settings->set("Language", key);
+}
+
+void LanguagePage::loadSettings()
+{
+ // NIL
+}
+
+void LanguagePage::retranslate()
+{
+ mainWidget->retranslate();
+}
+
+void LanguagePage::changeEvent(QEvent* event)
+{
+ if (event->type() == QEvent::LanguageChange)
+ {
+ retranslate();
+ }
+ QWidget::changeEvent(event);
+}
diff --git a/launcher/ui/pages/global/LanguagePage.h b/launcher/ui/pages/global/LanguagePage.h
new file mode 100644
index 00000000..b1dd05ad
--- /dev/null
+++ b/launcher/ui/pages/global/LanguagePage.h
@@ -0,0 +1,60 @@
+/* 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 <memory>
+#include "ui/pages/BasePage.h"
+#include <Application.h>
+#include <QWidget>
+
+class LanguageSelectionWidget;
+
+class LanguagePage : public QWidget, public BasePage
+{
+ Q_OBJECT
+
+public:
+ explicit LanguagePage(QWidget *parent = 0);
+ virtual ~LanguagePage();
+
+ QString displayName() const override
+ {
+ return tr("Language");
+ }
+ QIcon icon() const override
+ {
+ return APPLICATION->getThemedIcon("language");
+ }
+ QString id() const override
+ {
+ return "language-settings";
+ }
+ QString helpPage() const override
+ {
+ return "Language-settings";
+ }
+ bool apply() override;
+
+ void changeEvent(QEvent * ) override;
+
+private:
+ void applySettings();
+ void loadSettings();
+ void retranslate();
+
+private:
+ LanguageSelectionWidget *mainWidget;
+};
diff --git a/launcher/ui/pages/global/LauncherPage.cpp b/launcher/ui/pages/global/LauncherPage.cpp
new file mode 100644
index 00000000..2eb73e44
--- /dev/null
+++ b/launcher/ui/pages/global/LauncherPage.cpp
@@ -0,0 +1,466 @@
+/* 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 "LauncherPage.h"
+#include "ui_LauncherPage.h"
+
+#include <QFileDialog>
+#include <QMessageBox>
+#include <QDir>
+#include <QTextCharFormat>
+
+#include "updater/UpdateChecker.h"
+
+#include "settings/SettingsObject.h"
+#include <FileSystem.h>
+#include "Application.h"
+#include "BuildConfig.h"
+#include "ui/themes/ITheme.h"
+
+#include <QApplication>
+#include <QProcess>
+
+// FIXME: possibly move elsewhere
+enum InstSortMode
+{
+ // Sort alphabetically by name.
+ Sort_Name,
+ // Sort by which instance was launched most recently.
+ Sort_LastLaunch
+};
+
+LauncherPage::LauncherPage(QWidget *parent) : QWidget(parent), ui(new Ui::LauncherPage)
+{
+ ui->setupUi(this);
+ auto origForeground = ui->fontPreview->palette().color(ui->fontPreview->foregroundRole());
+ auto origBackground = ui->fontPreview->palette().color(ui->fontPreview->backgroundRole());
+ m_colors.reset(new LogColorCache(origForeground, origBackground));
+
+ ui->sortingModeGroup->setId(ui->sortByNameBtn, Sort_Name);
+ ui->sortingModeGroup->setId(ui->sortLastLaunchedBtn, Sort_LastLaunch);
+
+ defaultFormat = new QTextCharFormat(ui->fontPreview->currentCharFormat());
+
+ m_languageModel = APPLICATION->translations();
+ loadSettings();
+
+ if(BuildConfig.UPDATER_ENABLED)
+ {
+ QObject::connect(APPLICATION->updateChecker().get(), &UpdateChecker::channelListLoaded, this, &LauncherPage::refreshUpdateChannelList);
+
+ if (APPLICATION->updateChecker()->hasChannels())
+ {
+ refreshUpdateChannelList();
+ }
+ else
+ {
+ APPLICATION->updateChecker()->updateChanList(false);
+ }
+ }
+ else
+ {
+ ui->updateSettingsBox->setHidden(true);
+ }
+ // Analytics
+ if(BuildConfig.ANALYTICS_ID.isEmpty())
+ {
+ ui->tabWidget->removeTab(ui->tabWidget->indexOf(ui->analyticsTab));
+ }
+ connect(ui->fontSizeBox, SIGNAL(valueChanged(int)), SLOT(refreshFontPreview()));
+ connect(ui->consoleFont, SIGNAL(currentFontChanged(QFont)), SLOT(refreshFontPreview()));
+
+ //move mac data button
+ QFile file(QDir::current().absolutePath() + "/dontmovemacdata");
+ if (!file.exists())
+ {
+ ui->migrateDataFolderMacBtn->setVisible(false);
+ }
+}
+
+LauncherPage::~LauncherPage()
+{
+ delete ui;
+ delete defaultFormat;
+}
+
+bool LauncherPage::apply()
+{
+ applySettings();
+ return true;
+}
+
+void LauncherPage::on_instDirBrowseBtn_clicked()
+{
+ QString raw_dir = QFileDialog::getExistingDirectory(this, tr("Instance Folder"), ui->instDirTextBox->text());
+
+ // do not allow current dir - it's dirty. Do not allow dirs that don't exist
+ if (!raw_dir.isEmpty() && QDir(raw_dir).exists())
+ {
+ QString cooked_dir = FS::NormalizePath(raw_dir);
+ if (FS::checkProblemticPathJava(QDir(cooked_dir)))
+ {
+ QMessageBox warning;
+ warning.setText(tr("You're trying to specify an instance folder which\'s path "
+ "contains at least one \'!\'. "
+ "Java is known to cause problems if that is the case, your "
+ "instances (probably) won't start!"));
+ warning.setInformativeText(
+ tr("Do you really want to use this path? "
+ "Selecting \"No\" will close this and not alter your instance path."));
+ warning.setStandardButtons(QMessageBox::Yes | QMessageBox::No);
+ int result = warning.exec();
+ if (result == QMessageBox::Yes)
+ {
+ ui->instDirTextBox->setText(cooked_dir);
+ }
+ }
+ else
+ {
+ ui->instDirTextBox->setText(cooked_dir);
+ }
+ }
+}
+
+void LauncherPage::on_iconsDirBrowseBtn_clicked()
+{
+ QString raw_dir = QFileDialog::getExistingDirectory(this, tr("Icons Folder"), ui->iconsDirTextBox->text());
+
+ // do not allow current dir - it's dirty. Do not allow dirs that don't exist
+ if (!raw_dir.isEmpty() && QDir(raw_dir).exists())
+ {
+ QString cooked_dir = FS::NormalizePath(raw_dir);
+ ui->iconsDirTextBox->setText(cooked_dir);
+ }
+}
+void LauncherPage::on_modsDirBrowseBtn_clicked()
+{
+ QString raw_dir = QFileDialog::getExistingDirectory(this, tr("Mods Folder"), ui->modsDirTextBox->text());
+
+ // do not allow current dir - it's dirty. Do not allow dirs that don't exist
+ if (!raw_dir.isEmpty() && QDir(raw_dir).exists())
+ {
+ QString cooked_dir = FS::NormalizePath(raw_dir);
+ ui->modsDirTextBox->setText(cooked_dir);
+ }
+}
+void LauncherPage::on_migrateDataFolderMacBtn_clicked()
+{
+ QFile file(QDir::current().absolutePath() + "/dontmovemacdata");
+ file.remove();
+ QProcess::startDetached(qApp->arguments()[0]);
+ qApp->quit();
+}
+
+void LauncherPage::refreshUpdateChannelList()
+{
+ // Stop listening for selection changes. It's going to change a lot while we update it and
+ // we don't need to update the
+ // description label constantly.
+ QObject::disconnect(ui->updateChannelComboBox, SIGNAL(currentIndexChanged(int)), this,
+ SLOT(updateChannelSelectionChanged(int)));
+
+ QList<UpdateChecker::ChannelListEntry> channelList = APPLICATION->updateChecker()->getChannelList();
+ ui->updateChannelComboBox->clear();
+ int selection = -1;
+ for (int i = 0; i < channelList.count(); i++)
+ {
+ UpdateChecker::ChannelListEntry entry = channelList.at(i);
+
+ // When it comes to selection, we'll rely on the indexes of a channel entry being the
+ // same in the
+ // combo box as it is in the update checker's channel list.
+ // This probably isn't very safe, but the channel list doesn't change often enough (or
+ // at all) for
+ // this to be a big deal. Hope it doesn't break...
+ ui->updateChannelComboBox->addItem(entry.name);
+
+ // If the update channel we just added was the selected one, set the current index in
+ // the combo box to it.
+ if (entry.id == m_currentUpdateChannel)
+ {
+ qDebug() << "Selected index" << i << "channel id" << m_currentUpdateChannel;
+ selection = i;
+ }
+ }
+
+ ui->updateChannelComboBox->setCurrentIndex(selection);
+
+ // Start listening for selection changes again and update the description label.
+ QObject::connect(ui->updateChannelComboBox, SIGNAL(currentIndexChanged(int)), this,
+ SLOT(updateChannelSelectionChanged(int)));
+ refreshUpdateChannelDesc();
+
+ // Now that we've updated the channel list, we can enable the combo box.
+ // It starts off disabled so that if the channel list hasn't been loaded, it will be
+ // disabled.
+ ui->updateChannelComboBox->setEnabled(true);
+}
+
+void LauncherPage::updateChannelSelectionChanged(int index)
+{
+ refreshUpdateChannelDesc();
+}
+
+void LauncherPage::refreshUpdateChannelDesc()
+{
+ // Get the channel list.
+ QList<UpdateChecker::ChannelListEntry> channelList = APPLICATION->updateChecker()->getChannelList();
+ int selectedIndex = ui->updateChannelComboBox->currentIndex();
+ if (selectedIndex < 0)
+ {
+ return;
+ }
+ if (selectedIndex < channelList.count())
+ {
+ // Find the channel list entry with the given index.
+ UpdateChecker::ChannelListEntry selected = channelList.at(selectedIndex);
+
+ // Set the description text.
+ ui->updateChannelDescLabel->setText(selected.description);
+
+ // Set the currently selected channel ID.
+ m_currentUpdateChannel = selected.id;
+ }
+}
+
+void LauncherPage::applySettings()
+{
+ auto s = APPLICATION->settings();
+
+ if (ui->resetNotificationsBtn->isChecked())
+ {
+ s->set("ShownNotifications", QString());
+ }
+
+ // Updates
+ s->set("AutoUpdate", ui->autoUpdateCheckBox->isChecked());
+ s->set("UpdateChannel", m_currentUpdateChannel);
+ auto original = s->get("IconTheme").toString();
+ //FIXME: make generic
+ switch (ui->themeComboBox->currentIndex())
+ {
+ case 1:
+ s->set("IconTheme", "pe_dark");
+ break;
+ case 2:
+ s->set("IconTheme", "pe_light");
+ break;
+ case 3:
+ s->set("IconTheme", "pe_blue");
+ break;
+ case 4:
+ s->set("IconTheme", "pe_colored");
+ break;
+ case 5:
+ s->set("IconTheme", "OSX");
+ break;
+ case 6:
+ s->set("IconTheme", "iOS");
+ break;
+ case 7:
+ s->set("IconTheme", "flat");
+ break;
+ case 8:
+ s->set("IconTheme", "custom");
+ break;
+ case 0:
+ default:
+ s->set("IconTheme", "multimc");
+ break;
+ }
+
+ if(original != s->get("IconTheme"))
+ {
+ APPLICATION->setIconTheme(s->get("IconTheme").toString());
+ }
+
+ auto originalAppTheme = s->get("ApplicationTheme").toString();
+ auto newAppTheme = ui->themeComboBoxColors->currentData().toString();
+ if(originalAppTheme != newAppTheme)
+ {
+ s->set("ApplicationTheme", newAppTheme);
+ APPLICATION->setApplicationTheme(newAppTheme, false);
+ }
+
+ // Console settings
+ s->set("ShowConsole", ui->showConsoleCheck->isChecked());
+ s->set("AutoCloseConsole", ui->autoCloseConsoleCheck->isChecked());
+ s->set("ShowConsoleOnError", ui->showConsoleErrorCheck->isChecked());
+ QString consoleFontFamily = ui->consoleFont->currentFont().family();
+ s->set("ConsoleFont", consoleFontFamily);
+ s->set("ConsoleFontSize", ui->fontSizeBox->value());
+ s->set("ConsoleMaxLines", ui->lineLimitSpinBox->value());
+ s->set("ConsoleOverflowStop", ui->checkStopLogging->checkState() != Qt::Unchecked);
+
+ // Folders
+ // TODO: Offer to move instances to new instance folder.
+ s->set("InstanceDir", ui->instDirTextBox->text());
+ s->set("CentralModsDir", ui->modsDirTextBox->text());
+ s->set("IconsDir", ui->iconsDirTextBox->text());
+
+ auto sortMode = (InstSortMode)ui->sortingModeGroup->checkedId();
+ switch (sortMode)
+ {
+ case Sort_LastLaunch:
+ s->set("InstSortMode", "LastLaunch");
+ break;
+ case Sort_Name:
+ default:
+ s->set("InstSortMode", "Name");
+ break;
+ }
+
+ // Analytics
+ if(!BuildConfig.ANALYTICS_ID.isEmpty())
+ {
+ s->set("Analytics", ui->analyticsCheck->isChecked());
+ }
+}
+void LauncherPage::loadSettings()
+{
+ auto s = APPLICATION->settings();
+ // Updates
+ ui->autoUpdateCheckBox->setChecked(s->get("AutoUpdate").toBool());
+ m_currentUpdateChannel = s->get("UpdateChannel").toString();
+ //FIXME: make generic
+ auto theme = s->get("IconTheme").toString();
+ if (theme == "pe_dark")
+ {
+ ui->themeComboBox->setCurrentIndex(1);
+ }
+ else if (theme == "pe_light")
+ {
+ ui->themeComboBox->setCurrentIndex(2);
+ }
+ else if (theme == "pe_blue")
+ {
+ ui->themeComboBox->setCurrentIndex(3);
+ }
+ else if (theme == "pe_colored")
+ {
+ ui->themeComboBox->setCurrentIndex(4);
+ }
+ else if (theme == "OSX")
+ {
+ ui->themeComboBox->setCurrentIndex(5);
+ }
+ else if (theme == "iOS")
+ {
+ ui->themeComboBox->setCurrentIndex(6);
+ }
+ else if (theme == "flat")
+ {
+ ui->themeComboBox->setCurrentIndex(7);
+ }
+ else if (theme == "custom")
+ {
+ ui->themeComboBox->setCurrentIndex(8);
+ }
+ else
+ {
+ ui->themeComboBox->setCurrentIndex(0);
+ }
+
+ {
+ auto currentTheme = s->get("ApplicationTheme").toString();
+ auto themes = APPLICATION->getValidApplicationThemes();
+ int idx = 0;
+ for(auto &theme: themes)
+ {
+ ui->themeComboBoxColors->addItem(theme->name(), theme->id());
+ if(currentTheme == theme->id())
+ {
+ ui->themeComboBoxColors->setCurrentIndex(idx);
+ }
+ idx++;
+ }
+ }
+
+ // Console settings
+ ui->showConsoleCheck->setChecked(s->get("ShowConsole").toBool());
+ ui->autoCloseConsoleCheck->setChecked(s->get("AutoCloseConsole").toBool());
+ ui->showConsoleErrorCheck->setChecked(s->get("ShowConsoleOnError").toBool());
+ QString fontFamily = APPLICATION->settings()->get("ConsoleFont").toString();
+ QFont consoleFont(fontFamily);
+ ui->consoleFont->setCurrentFont(consoleFont);
+
+ bool conversionOk = true;
+ int fontSize = APPLICATION->settings()->get("ConsoleFontSize").toInt(&conversionOk);
+ if(!conversionOk)
+ {
+ fontSize = 11;
+ }
+ ui->fontSizeBox->setValue(fontSize);
+ refreshFontPreview();
+ ui->lineLimitSpinBox->setValue(s->get("ConsoleMaxLines").toInt());
+ ui->checkStopLogging->setChecked(s->get("ConsoleOverflowStop").toBool());
+
+ // Folders
+ ui->instDirTextBox->setText(s->get("InstanceDir").toString());
+ ui->modsDirTextBox->setText(s->get("CentralModsDir").toString());
+ ui->iconsDirTextBox->setText(s->get("IconsDir").toString());
+
+ QString sortMode = s->get("InstSortMode").toString();
+
+ if (sortMode == "LastLaunch")
+ {
+ ui->sortLastLaunchedBtn->setChecked(true);
+ }
+ else
+ {
+ ui->sortByNameBtn->setChecked(true);
+ }
+
+ // Analytics
+ if(!BuildConfig.ANALYTICS_ID.isEmpty())
+ {
+ ui->analyticsCheck->setChecked(s->get("Analytics").toBool());
+ }
+}
+
+void LauncherPage::refreshFontPreview()
+{
+ int fontSize = ui->fontSizeBox->value();
+ QString fontFamily = ui->consoleFont->currentFont().family();
+ ui->fontPreview->clear();
+ defaultFormat->setFont(QFont(fontFamily, fontSize));
+ {
+ QTextCharFormat format(*defaultFormat);
+ format.setForeground(m_colors->getFront(MessageLevel::Error));
+ // append a paragraph/line
+ auto workCursor = ui->fontPreview->textCursor();
+ workCursor.movePosition(QTextCursor::End);
+ workCursor.insertText(tr("[Something/ERROR] A spooky error!"), format);
+ workCursor.insertBlock();
+ }
+ {
+ QTextCharFormat format(*defaultFormat);
+ format.setForeground(m_colors->getFront(MessageLevel::Message));
+ // append a paragraph/line
+ auto workCursor = ui->fontPreview->textCursor();
+ workCursor.movePosition(QTextCursor::End);
+ workCursor.insertText(tr("[Test/INFO] A harmless message..."), format);
+ workCursor.insertBlock();
+ }
+ {
+ QTextCharFormat format(*defaultFormat);
+ format.setForeground(m_colors->getFront(MessageLevel::Warning));
+ // append a paragraph/line
+ auto workCursor = ui->fontPreview->textCursor();
+ workCursor.movePosition(QTextCursor::End);
+ workCursor.insertText(tr("[Something/WARN] A not so spooky warning."), format);
+ workCursor.insertBlock();
+ }
+}
diff --git a/launcher/ui/pages/global/LauncherPage.h b/launcher/ui/pages/global/LauncherPage.h
new file mode 100644
index 00000000..4d0cf3c9
--- /dev/null
+++ b/launcher/ui/pages/global/LauncherPage.h
@@ -0,0 +1,103 @@
+/* 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 <memory>
+#include <QDialog>
+
+#include "java/JavaChecker.h"
+#include "ui/pages/BasePage.h"
+#include <Application.h>
+#include "ui/ColorCache.h"
+#include <translations/TranslationsModel.h>
+
+class QTextCharFormat;
+class SettingsObject;
+
+namespace Ui
+{
+class LauncherPage;
+}
+
+class LauncherPage : public QWidget, public BasePage
+{
+ Q_OBJECT
+
+public:
+ explicit LauncherPage(QWidget *parent = 0);
+ ~LauncherPage();
+
+ QString displayName() const override
+ {
+ return "Launcher";
+ }
+ QIcon icon() const override
+ {
+ return APPLICATION->getThemedIcon("launcher");
+ }
+ QString id() const override
+ {
+ return "launcher-settings";
+ }
+ QString helpPage() const override
+ {
+ return "Launcher-settings";
+ }
+ bool apply() override;
+
+private:
+ void applySettings();
+ void loadSettings();
+
+private
+slots:
+ void on_instDirBrowseBtn_clicked();
+ void on_modsDirBrowseBtn_clicked();
+ void on_iconsDirBrowseBtn_clicked();
+ void on_migrateDataFolderMacBtn_clicked();
+
+ /*!
+ * Updates the list of update channels in the combo box.
+ */
+ void refreshUpdateChannelList();
+
+ /*!
+ * Updates the channel description label.
+ */
+ void refreshUpdateChannelDesc();
+
+ /*!
+ * Updates the font preview
+ */
+ void refreshFontPreview();
+
+ void updateChannelSelectionChanged(int index);
+
+private:
+ Ui::LauncherPage *ui;
+
+ /*!
+ * Stores the currently selected update channel.
+ */
+ QString m_currentUpdateChannel;
+
+ // default format for the font preview...
+ QTextCharFormat *defaultFormat;
+
+ std::unique_ptr<LogColorCache> m_colors;
+
+ std::shared_ptr<TranslationsModel> m_languageModel;
+};
diff --git a/launcher/ui/pages/global/LauncherPage.ui b/launcher/ui/pages/global/LauncherPage.ui
new file mode 100644
index 00000000..62a66d73
--- /dev/null
+++ b/launcher/ui/pages/global/LauncherPage.ui
@@ -0,0 +1,584 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>LauncherPage</class>
+ <widget class="QWidget" name="LauncherPage">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>514</width>
+ <height>629</height>
+ </rect>
+ </property>
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <layout class="QVBoxLayout" name="mainLayout">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QTabWidget" name="tabWidget">
+ <property name="toolTip">
+ <string notr="true"/>
+ </property>
+ <property name="tabShape">
+ <enum>QTabWidget::Rounded</enum>
+ </property>
+ <property name="currentIndex">
+ <number>0</number>
+ </property>
+ <widget class="QWidget" name="featuresTab">
+ <attribute name="title">
+ <string>Features</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_9">
+ <item>
+ <widget class="QGroupBox" name="updateSettingsBox">
+ <property name="title">
+ <string>Update Settings</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_7">
+ <item>
+ <widget class="QCheckBox" name="autoUpdateCheckBox">
+ <property name="text">
+ <string>Check for updates on start?</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="updateChannelLabel">
+ <property name="text">
+ <string>Up&amp;date Channel:</string>
+ </property>
+ <property name="buddy">
+ <cstring>updateChannelComboBox</cstring>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QComboBox" name="updateChannelComboBox">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="updateChannelDescLabel">
+ <property name="text">
+ <string>No channel selected.</string>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="foldersBox">
+ <property name="title">
+ <string>Folders</string>
+ </property>
+ <layout class="QGridLayout" name="foldersBoxLayout">
+ <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="0" column="1">
+ <widget class="QLineEdit" name="instDirTextBox"/>
+ </item>
+ <item row="0" column="2">
+ <widget class="QToolButton" name="instDirBrowseBtn">
+ <property name="text">
+ <string notr="true">...</string>
+ </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="1" column="1">
+ <widget class="QLineEdit" name="modsDirTextBox"/>
+ </item>
+ <item row="1" column="2">
+ <widget class="QToolButton" name="modsDirBrowseBtn">
+ <property name="text">
+ <string notr="true">...</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="1">
+ <widget class="QLineEdit" name="iconsDirTextBox"/>
+ </item>
+ <item row="2" column="0">
+ <widget class="QLabel" name="labelIconsDir">
+ <property name="text">
+ <string>&amp;Icons:</string>
+ </property>
+ <property name="buddy">
+ <cstring>iconsDirTextBox</cstring>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="2">
+ <widget class="QToolButton" name="iconsDirBrowseBtn">
+ <property name="text">
+ <string notr="true">...</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="migrateDataFolderMacBtn">
+ <property name="text">
+ <string>Move the data to new location (will restart the launcher)</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="verticalSpacer_2">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>0</width>
+ <height>0</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QWidget" name="generalTab">
+ <attribute name="title">
+ <string>User Interface</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_6">
+ <item>
+ <widget class="QGroupBox" name="groupBox_3">
+ <property name="title">
+ <string>Launcher notifications</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_5">
+ <item>
+ <widget class="QPushButton" name="resetNotificationsBtn">
+ <property name="text">
+ <string>Reset hidden notifications</string>
+ </property>
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="sortingModeBox">
+ <property name="enabled">
+ <bool>true</bool>
+ </property>
+ <property name="title">
+ <string>Instance view sorting mode</string>
+ </property>
+ <layout class="QHBoxLayout" name="sortingModeBoxLayout">
+ <item>
+ <widget class="QRadioButton" name="sortLastLaunchedBtn">
+ <property name="text">
+ <string>By &amp;last launched</string>
+ </property>
+ <attribute name="buttonGroup">
+ <string notr="true">sortingModeGroup</string>
+ </attribute>
+ </widget>
+ </item>
+ <item>
+ <widget class="QRadioButton" name="sortByNameBtn">
+ <property name="text">
+ <string>By &amp;name</string>
+ </property>
+ <attribute name="buttonGroup">
+ <string notr="true">sortingModeGroup</string>
+ </attribute>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="themeBox">
+ <property name="title">
+ <string>Theme</string>
+ </property>
+ <layout class="QFormLayout" name="formLayout">
+ <item row="0" column="0">
+ <widget class="QLabel" name="label_3">
+ <property name="text">
+ <string>&amp;Icons</string>
+ </property>
+ <property name="buddy">
+ <cstring>themeComboBox</cstring>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QComboBox" name="themeComboBox">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="focusPolicy">
+ <enum>Qt::StrongFocus</enum>
+ </property>
+ <item>
+ <property name="text">
+ <string>Default</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Simple (Dark Icons)</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Simple (Light Icons)</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Simple (Blue Icons)</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Simple (Colored Icons)</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string notr="true">OSX</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string notr="true">iOS</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string notr="true">Flat</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Custom</string>
+ </property>
+ </item>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QComboBox" name="themeComboBoxColors">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="focusPolicy">
+ <enum>Qt::StrongFocus</enum>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0">
+ <widget class="QLabel" name="label_4">
+ <property name="text">
+ <string>Colors</string>
+ </property>
+ <property name="buddy">
+ <cstring>themeComboBoxColors</cstring>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <spacer name="generalTabSpacer">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>0</width>
+ <height>0</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QWidget" name="consoleTab">
+ <attribute name="title">
+ <string>Console</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_2">
+ <item>
+ <widget class="QGroupBox" name="consoleSettingsBox">
+ <property name="title">
+ <string>Console Settings</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_3">
+ <item>
+ <widget class="QCheckBox" name="showConsoleCheck">
+ <property name="text">
+ <string>Show console while the game is running?</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="autoCloseConsoleCheck">
+ <property name="text">
+ <string>Automatically close console when the game quits?</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="showConsoleErrorCheck">
+ <property name="text">
+ <string>Show console when the game crashes?</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="groupBox_4">
+ <property name="title">
+ <string>History limit</string>
+ </property>
+ <layout class="QGridLayout" name="gridLayout_3">
+ <item row="1" column="0">
+ <widget class="QCheckBox" name="checkStopLogging">
+ <property name="text">
+ <string>Stop logging when log overflows</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="0">
+ <widget class="QSpinBox" name="lineLimitSpinBox">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="suffix">
+ <string> lines</string>
+ </property>
+ <property name="minimum">
+ <number>10000</number>
+ </property>
+ <property name="maximum">
+ <number>1000000</number>
+ </property>
+ <property name="singleStep">
+ <number>10000</number>
+ </property>
+ <property name="value">
+ <number>100000</number>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="themeBox_2">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="MinimumExpanding">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="title">
+ <string>Console font</string>
+ </property>
+ <layout class="QGridLayout" name="gridLayout_2">
+ <item row="1" column="0" colspan="2">
+ <widget class="QTextEdit" name="fontPreview">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="MinimumExpanding">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="horizontalScrollBarPolicy">
+ <enum>Qt::ScrollBarAlwaysOff</enum>
+ </property>
+ <property name="undoRedoEnabled">
+ <bool>false</bool>
+ </property>
+ <property name="textInteractionFlags">
+ <set>Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="0">
+ <widget class="QFontComboBox" name="consoleFont">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QSpinBox" name="fontSizeBox">
+ <property name="minimum">
+ <number>5</number>
+ </property>
+ <property name="maximum">
+ <number>16</number>
+ </property>
+ <property name="value">
+ <number>11</number>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QWidget" name="analyticsTab">
+ <attribute name="title">
+ <string>Analytics</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_8">
+ <item>
+ <widget class="QGroupBox" name="consoleSettingsBox_2">
+ <property name="title">
+ <string>Analytics Settings</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_4">
+ <item>
+ <widget class="QCheckBox" name="analyticsCheck">
+ <property name="text">
+ <string>Send anonymous usage statistics?</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="Line" name="line">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="label_5">
+ <property name="text">
+ <string>&lt;html&gt;&lt;head/&gt;
+&lt;body&gt;
+&lt;p&gt;The launcher sends anonymous usage statistics on every start of the application.&lt;/p&gt;&lt;p&gt;The following data is collected:&lt;/p&gt;
+&lt;ul&gt;
+&lt;li&gt;Launcher version.&lt;/li&gt;
+&lt;li&gt;Operating system name, version and architecture.&lt;/li&gt;
+&lt;li&gt;CPU architecture (kernel architecture on linux).&lt;/li&gt;
+&lt;li&gt;Size of system memory.&lt;/li&gt;
+&lt;li&gt;Java version, architecture and memory settings.&lt;/li&gt;
+&lt;/ul&gt;
+&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <spacer name="verticalSpacer">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>40</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <tabstops>
+ <tabstop>tabWidget</tabstop>
+ <tabstop>autoUpdateCheckBox</tabstop>
+ <tabstop>updateChannelComboBox</tabstop>
+ <tabstop>instDirTextBox</tabstop>
+ <tabstop>instDirBrowseBtn</tabstop>
+ <tabstop>modsDirTextBox</tabstop>
+ <tabstop>modsDirBrowseBtn</tabstop>
+ <tabstop>iconsDirTextBox</tabstop>
+ <tabstop>iconsDirBrowseBtn</tabstop>
+ <tabstop>resetNotificationsBtn</tabstop>
+ <tabstop>sortLastLaunchedBtn</tabstop>
+ <tabstop>sortByNameBtn</tabstop>
+ <tabstop>themeComboBox</tabstop>
+ <tabstop>themeComboBoxColors</tabstop>
+ <tabstop>showConsoleCheck</tabstop>
+ <tabstop>autoCloseConsoleCheck</tabstop>
+ <tabstop>showConsoleErrorCheck</tabstop>
+ <tabstop>lineLimitSpinBox</tabstop>
+ <tabstop>checkStopLogging</tabstop>
+ <tabstop>consoleFont</tabstop>
+ <tabstop>fontSizeBox</tabstop>
+ <tabstop>fontPreview</tabstop>
+ </tabstops>
+ <resources/>
+ <connections/>
+ <buttongroups>
+ <buttongroup name="sortingModeGroup"/>
+ </buttongroups>
+</ui>
diff --git a/launcher/ui/pages/global/MinecraftPage.cpp b/launcher/ui/pages/global/MinecraftPage.cpp
new file mode 100644
index 00000000..c763f8ac
--- /dev/null
+++ b/launcher/ui/pages/global/MinecraftPage.cpp
@@ -0,0 +1,91 @@
+/* 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 "MinecraftPage.h"
+#include "ui_MinecraftPage.h"
+
+#include <QMessageBox>
+#include <QDir>
+#include <QTabBar>
+
+#include "settings/SettingsObject.h"
+#include "Application.h"
+
+MinecraftPage::MinecraftPage(QWidget *parent) : QWidget(parent), ui(new Ui::MinecraftPage)
+{
+ ui->setupUi(this);
+ ui->tabWidget->tabBar()->hide();
+ loadSettings();
+ updateCheckboxStuff();
+}
+
+MinecraftPage::~MinecraftPage()
+{
+ delete ui;
+}
+
+bool MinecraftPage::apply()
+{
+ applySettings();
+ return true;
+}
+
+void MinecraftPage::updateCheckboxStuff()
+{
+ ui->windowWidthSpinBox->setEnabled(!ui->maximizedCheckBox->isChecked());
+ ui->windowHeightSpinBox->setEnabled(!ui->maximizedCheckBox->isChecked());
+}
+
+void MinecraftPage::on_maximizedCheckBox_clicked(bool checked)
+{
+ Q_UNUSED(checked);
+ updateCheckboxStuff();
+}
+
+void MinecraftPage::applySettings()
+{
+ auto s = APPLICATION->settings();
+
+ // Window Size
+ s->set("LaunchMaximized", ui->maximizedCheckBox->isChecked());
+ s->set("MinecraftWinWidth", ui->windowWidthSpinBox->value());
+ s->set("MinecraftWinHeight", ui->windowHeightSpinBox->value());
+
+ // Native library workarounds
+ s->set("UseNativeOpenAL", ui->useNativeOpenALCheck->isChecked());
+ s->set("UseNativeGLFW", ui->useNativeGLFWCheck->isChecked());
+
+ // Game time
+ s->set("ShowGameTime", ui->showGameTime->isChecked());
+ s->set("ShowGlobalGameTime", ui->showGlobalGameTime->isChecked());
+ s->set("RecordGameTime", ui->recordGameTime->isChecked());
+}
+
+void MinecraftPage::loadSettings()
+{
+ auto s = APPLICATION->settings();
+
+ // Window Size
+ ui->maximizedCheckBox->setChecked(s->get("LaunchMaximized").toBool());
+ ui->windowWidthSpinBox->setValue(s->get("MinecraftWinWidth").toInt());
+ ui->windowHeightSpinBox->setValue(s->get("MinecraftWinHeight").toInt());
+
+ ui->useNativeOpenALCheck->setChecked(s->get("UseNativeOpenAL").toBool());
+ ui->useNativeGLFWCheck->setChecked(s->get("UseNativeGLFW").toBool());
+
+ ui->showGameTime->setChecked(s->get("ShowGameTime").toBool());
+ ui->showGlobalGameTime->setChecked(s->get("ShowGlobalGameTime").toBool());
+ ui->recordGameTime->setChecked(s->get("RecordGameTime").toBool());
+}
diff --git a/launcher/ui/pages/global/MinecraftPage.h b/launcher/ui/pages/global/MinecraftPage.h
new file mode 100644
index 00000000..42626d94
--- /dev/null
+++ b/launcher/ui/pages/global/MinecraftPage.h
@@ -0,0 +1,70 @@
+/* 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 <memory>
+#include <QDialog>
+
+#include "java/JavaChecker.h"
+#include "ui/pages/BasePage.h"
+#include <Application.h>
+
+class SettingsObject;
+
+namespace Ui
+{
+class MinecraftPage;
+}
+
+class MinecraftPage : public QWidget, public BasePage
+{
+ Q_OBJECT
+
+public:
+ explicit MinecraftPage(QWidget *parent = 0);
+ ~MinecraftPage();
+
+ QString displayName() const override
+ {
+ return tr("Minecraft");
+ }
+ QIcon icon() const override
+ {
+ return APPLICATION->getThemedIcon("minecraft");
+ }
+ QString id() const override
+ {
+ return "minecraft-settings";
+ }
+ QString helpPage() const override
+ {
+ return "Minecraft-settings";
+ }
+ bool apply() override;
+
+private:
+ void updateCheckboxStuff();
+ void applySettings();
+ void loadSettings();
+
+private
+slots:
+ void on_maximizedCheckBox_clicked(bool checked);
+
+private:
+ Ui::MinecraftPage *ui;
+
+};
diff --git a/launcher/ui/pages/global/MinecraftPage.ui b/launcher/ui/pages/global/MinecraftPage.ui
new file mode 100644
index 00000000..857b8cfb
--- /dev/null
+++ b/launcher/ui/pages/global/MinecraftPage.ui
@@ -0,0 +1,196 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>MinecraftPage</class>
+ <widget class="QWidget" name="MinecraftPage">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>936</width>
+ <height>1134</height>
+ </rect>
+ </property>
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <layout class="QVBoxLayout" name="mainLayout">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QTabWidget" name="tabWidget">
+ <property name="tabShape">
+ <enum>QTabWidget::Rounded</enum>
+ </property>
+ <property name="currentIndex">
+ <number>0</number>
+ </property>
+ <widget class="QWidget" name="minecraftTab">
+ <attribute name="title">
+ <string notr="true">Minecraft</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_3">
+ <item>
+ <widget class="QGroupBox" name="windowSizeGroupBox">
+ <property name="title">
+ <string>Window Size</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_4">
+ <item>
+ <widget class="QCheckBox" name="maximizedCheckBox">
+ <property name="text">
+ <string>Start Minecraft maximized?</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <layout class="QGridLayout" name="gridLayoutWindowSize">
+ <item row="1" column="0">
+ <widget class="QLabel" name="labelWindowHeight">
+ <property name="text">
+ <string>Window hei&amp;ght:</string>
+ </property>
+ <property name="buddy">
+ <cstring>windowHeightSpinBox</cstring>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="0">
+ <widget class="QLabel" name="labelWindowWidth">
+ <property name="text">
+ <string>W&amp;indow width:</string>
+ </property>
+ <property name="buddy">
+ <cstring>windowWidthSpinBox</cstring>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QSpinBox" name="windowWidthSpinBox">
+ <property name="minimum">
+ <number>1</number>
+ </property>
+ <property name="maximum">
+ <number>65536</number>
+ </property>
+ <property name="singleStep">
+ <number>1</number>
+ </property>
+ <property name="value">
+ <number>854</number>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QSpinBox" name="windowHeightSpinBox">
+ <property name="minimum">
+ <number>1</number>
+ </property>
+ <property name="maximum">
+ <number>65536</number>
+ </property>
+ <property name="value">
+ <number>480</number>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="nativeLibWorkaroundGroupBox">
+ <property name="title">
+ <string>Native library workarounds</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_5">
+ <item>
+ <widget class="QCheckBox" name="useNativeGLFWCheck">
+ <property name="text">
+ <string>Use system installation of GLFW</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="useNativeOpenALCheck">
+ <property name="text">
+ <string>Use system installation of OpenAL</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="gameTimeGroupBox">
+ <property name="title">
+ <string>Game time</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_6">
+ <item>
+ <widget class="QCheckBox" name="showGameTime">
+ <property name="text">
+ <string>Show time spent playing instances</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="showGlobalGameTime">
+ <property name="text">
+ <string>Show time spent playing across all instances</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="recordGameTime">
+ <property name="text">
+ <string>Record time spent playing instances</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <spacer name="verticalSpacerMinecraft">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>0</width>
+ <height>0</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <tabstops>
+ <tabstop>tabWidget</tabstop>
+ <tabstop>maximizedCheckBox</tabstop>
+ <tabstop>windowWidthSpinBox</tabstop>
+ <tabstop>windowHeightSpinBox</tabstop>
+ <tabstop>useNativeGLFWCheck</tabstop>
+ <tabstop>useNativeOpenALCheck</tabstop>
+ </tabstops>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/launcher/ui/pages/global/PasteEEPage.cpp b/launcher/ui/pages/global/PasteEEPage.cpp
new file mode 100644
index 00000000..4b375d9a
--- /dev/null
+++ b/launcher/ui/pages/global/PasteEEPage.cpp
@@ -0,0 +1,81 @@
+/* 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 "PasteEEPage.h"
+#include "ui_PasteEEPage.h"
+
+#include <QMessageBox>
+#include <QFileDialog>
+#include <QStandardPaths>
+#include <QTabBar>
+
+#include "settings/SettingsObject.h"
+#include "tools/BaseProfiler.h"
+#include "Application.h"
+
+PasteEEPage::PasteEEPage(QWidget *parent) :
+ QWidget(parent),
+ ui(new Ui::PasteEEPage)
+{
+ ui->setupUi(this);
+ ui->tabWidget->tabBar()->hide();\
+ connect(ui->customAPIkeyEdit, &QLineEdit::textEdited, this, &PasteEEPage::textEdited);
+ loadSettings();
+}
+
+PasteEEPage::~PasteEEPage()
+{
+ delete ui;
+}
+
+void PasteEEPage::loadSettings()
+{
+ auto s = APPLICATION->settings();
+ QString keyToUse = s->get("PasteEEAPIKey").toString();
+ if(keyToUse == "multimc")
+ {
+ ui->multimcButton->setChecked(true);
+ }
+ else
+ {
+ ui->customButton->setChecked(true);
+ ui->customAPIkeyEdit->setText(keyToUse);
+ }
+}
+
+void PasteEEPage::applySettings()
+{
+ auto s = APPLICATION->settings();
+
+ QString pasteKeyToUse;
+ if (ui->customButton->isChecked())
+ pasteKeyToUse = ui->customAPIkeyEdit->text();
+ else
+ {
+ pasteKeyToUse = "multimc";
+ }
+ s->set("PasteEEAPIKey", pasteKeyToUse);
+}
+
+bool PasteEEPage::apply()
+{
+ applySettings();
+ return true;
+}
+
+void PasteEEPage::textEdited(const QString& text)
+{
+ ui->customButton->setChecked(true);
+}
diff --git a/launcher/ui/pages/global/PasteEEPage.h b/launcher/ui/pages/global/PasteEEPage.h
new file mode 100644
index 00000000..a1c7d434
--- /dev/null
+++ b/launcher/ui/pages/global/PasteEEPage.h
@@ -0,0 +1,62 @@
+/* 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 <QWidget>
+
+#include "ui/pages/BasePage.h"
+#include <Application.h>
+
+namespace Ui {
+class PasteEEPage;
+}
+
+class PasteEEPage : public QWidget, public BasePage
+{
+ Q_OBJECT
+
+public:
+ explicit PasteEEPage(QWidget *parent = 0);
+ ~PasteEEPage();
+
+ QString displayName() const override
+ {
+ return tr("Log Upload");
+ }
+ QIcon icon() const override
+ {
+ return APPLICATION->getThemedIcon("log");
+ }
+ QString id() const override
+ {
+ return "log-upload";
+ }
+ QString helpPage() const override
+ {
+ return "Log-Upload";
+ }
+ virtual bool apply() override;
+
+private:
+ void loadSettings();
+ void applySettings();
+
+private slots:
+ void textEdited(const QString &text);
+
+private:
+ Ui::PasteEEPage *ui;
+};
diff --git a/launcher/ui/pages/global/PasteEEPage.ui b/launcher/ui/pages/global/PasteEEPage.ui
new file mode 100644
index 00000000..10883781
--- /dev/null
+++ b/launcher/ui/pages/global/PasteEEPage.ui
@@ -0,0 +1,128 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>PasteEEPage</class>
+ <widget class="QWidget" name="PasteEEPage">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>491</width>
+ <height>474</height>
+ </rect>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QTabWidget" name="tabWidget">
+ <property name="currentIndex">
+ <number>0</number>
+ </property>
+ <widget class="QWidget" name="tab">
+ <attribute name="title">
+ <string notr="true">Tab 1</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_2">
+ <item>
+ <widget class="QGroupBox" name="groupBox_2">
+ <property name="title">
+ <string>paste.ee API key</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_10">
+ <item>
+ <widget class="QRadioButton" name="multimcButton">
+ <property name="text">
+ <string>MultiMC key - 12MB &amp;upload limit</string>
+ </property>
+ <attribute name="buttonGroup">
+ <string notr="true">pasteButtonGroup</string>
+ </attribute>
+ </widget>
+ </item>
+ <item>
+ <widget class="QRadioButton" name="customButton">
+ <property name="text">
+ <string>&amp;Your own key - 12MB upload limit:</string>
+ </property>
+ <attribute name="buttonGroup">
+ <string notr="true">pasteButtonGroup</string>
+ </attribute>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLineEdit" name="customAPIkeyEdit">
+ <property name="echoMode">
+ <enum>QLineEdit::Password</enum>
+ </property>
+ <property name="placeholderText">
+ <string>Paste your API key here!</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="Line" name="line">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="label">
+ <property name="text">
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;a href=&quot;https://paste.ee&quot;&gt;paste.ee&lt;/a&gt; is used by MultiMC for log uploads. If you have a &lt;a href=&quot;https://paste.ee&quot;&gt;paste.ee&lt;/a&gt; account, you can add your API key here and have your uploaded logs paired with your account.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ <property name="textFormat">
+ <enum>Qt::RichText</enum>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ <property name="openExternalLinks">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <spacer name="verticalSpacer">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>216</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <tabstops>
+ <tabstop>tabWidget</tabstop>
+ <tabstop>multimcButton</tabstop>
+ <tabstop>customButton</tabstop>
+ <tabstop>customAPIkeyEdit</tabstop>
+ </tabstops>
+ <resources/>
+ <connections/>
+ <buttongroups>
+ <buttongroup name="pasteButtonGroup"/>
+ </buttongroups>
+</ui>
diff --git a/launcher/ui/pages/global/ProxyPage.cpp b/launcher/ui/pages/global/ProxyPage.cpp
new file mode 100644
index 00000000..5bc8199e
--- /dev/null
+++ b/launcher/ui/pages/global/ProxyPage.cpp
@@ -0,0 +1,106 @@
+/* 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 "ProxyPage.h"
+#include "ui_ProxyPage.h"
+
+#include <QTabBar>
+
+#include "settings/SettingsObject.h"
+#include "Application.h"
+#include "Application.h"
+
+ProxyPage::ProxyPage(QWidget *parent) : QWidget(parent), ui(new Ui::ProxyPage)
+{
+ ui->setupUi(this);
+ ui->tabWidget->tabBar()->hide();
+ loadSettings();
+ updateCheckboxStuff();
+
+ connect(ui->proxyGroup, SIGNAL(buttonClicked(int)), SLOT(proxyChanged(int)));
+}
+
+ProxyPage::~ProxyPage()
+{
+ delete ui;
+}
+
+bool ProxyPage::apply()
+{
+ applySettings();
+ return true;
+}
+
+void ProxyPage::updateCheckboxStuff()
+{
+ ui->proxyAddrBox->setEnabled(!ui->proxyNoneBtn->isChecked() &&
+ !ui->proxyDefaultBtn->isChecked());
+ ui->proxyAuthBox->setEnabled(!ui->proxyNoneBtn->isChecked() &&
+ !ui->proxyDefaultBtn->isChecked());
+}
+
+void ProxyPage::proxyChanged(int)
+{
+ updateCheckboxStuff();
+}
+
+void ProxyPage::applySettings()
+{
+ auto s = APPLICATION->settings();
+
+ // Proxy
+ QString proxyType = "None";
+ if (ui->proxyDefaultBtn->isChecked())
+ proxyType = "Default";
+ else if (ui->proxyNoneBtn->isChecked())
+ proxyType = "None";
+ else if (ui->proxySOCKS5Btn->isChecked())
+ proxyType = "SOCKS5";
+ else if (ui->proxyHTTPBtn->isChecked())
+ proxyType = "HTTP";
+
+ s->set("ProxyType", proxyType);
+ s->set("ProxyAddr", ui->proxyAddrEdit->text());
+ s->set("ProxyPort", ui->proxyPortEdit->value());
+ s->set("ProxyUser", ui->proxyUserEdit->text());
+ s->set("ProxyPass", ui->proxyPassEdit->text());
+
+ APPLICATION->updateProxySettings(
+ proxyType,
+ ui->proxyAddrEdit->text(),
+ ui->proxyPortEdit->value(),
+ ui->proxyUserEdit->text(),
+ ui->proxyPassEdit->text()
+ );
+}
+void ProxyPage::loadSettings()
+{
+ auto s = APPLICATION->settings();
+ // Proxy
+ QString proxyType = s->get("ProxyType").toString();
+ if (proxyType == "Default")
+ ui->proxyDefaultBtn->setChecked(true);
+ else if (proxyType == "None")
+ ui->proxyNoneBtn->setChecked(true);
+ else if (proxyType == "SOCKS5")
+ ui->proxySOCKS5Btn->setChecked(true);
+ else if (proxyType == "HTTP")
+ ui->proxyHTTPBtn->setChecked(true);
+
+ ui->proxyAddrEdit->setText(s->get("ProxyAddr").toString());
+ ui->proxyPortEdit->setValue(s->get("ProxyPort").value<uint16_t>());
+ ui->proxyUserEdit->setText(s->get("ProxyUser").toString());
+ ui->proxyPassEdit->setText(s->get("ProxyPass").toString());
+}
diff --git a/launcher/ui/pages/global/ProxyPage.h b/launcher/ui/pages/global/ProxyPage.h
new file mode 100644
index 00000000..6698c349
--- /dev/null
+++ b/launcher/ui/pages/global/ProxyPage.h
@@ -0,0 +1,66 @@
+/* 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 <memory>
+#include <QDialog>
+
+#include "ui/pages/BasePage.h"
+#include <Application.h>
+
+namespace Ui
+{
+class ProxyPage;
+}
+
+class ProxyPage : public QWidget, public BasePage
+{
+ Q_OBJECT
+
+public:
+ explicit ProxyPage(QWidget *parent = 0);
+ ~ProxyPage();
+
+ QString displayName() const override
+ {
+ return tr("Proxy");
+ }
+ QIcon icon() const override
+ {
+ return APPLICATION->getThemedIcon("proxy");
+ }
+ QString id() const override
+ {
+ return "proxy-settings";
+ }
+ QString helpPage() const override
+ {
+ return "Proxy-settings";
+ }
+ bool apply() override;
+
+private:
+ void updateCheckboxStuff();
+ void applySettings();
+ void loadSettings();
+
+private
+slots:
+ void proxyChanged(int);
+
+private:
+ Ui::ProxyPage *ui;
+};
diff --git a/launcher/ui/pages/global/ProxyPage.ui b/launcher/ui/pages/global/ProxyPage.ui
new file mode 100644
index 00000000..347fa86c
--- /dev/null
+++ b/launcher/ui/pages/global/ProxyPage.ui
@@ -0,0 +1,203 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>ProxyPage</class>
+ <widget class="QWidget" name="ProxyPage">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>598</width>
+ <height>617</height>
+ </rect>
+ </property>
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_2">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QTabWidget" name="tabWidget">
+ <widget class="QWidget" name="tabWidgetPage1">
+ <attribute name="title">
+ <string notr="true"/>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <widget class="QLabel" name="proxyPlainTextWarningLabel_2">
+ <property name="text">
+ <string>This only applies to the launcher. Minecraft does not accept proxy settings.</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignCenter</set>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="proxyTypeBox">
+ <property name="title">
+ <string>Type</string>
+ </property>
+ <layout class="QHBoxLayout" name="horizontalLayout_3">
+ <item>
+ <widget class="QRadioButton" name="proxyDefaultBtn">
+ <property name="toolTip">
+ <string>Uses your system's default proxy settings.</string>
+ </property>
+ <property name="text">
+ <string>&amp;Default</string>
+ </property>
+ <attribute name="buttonGroup">
+ <string notr="true">proxyGroup</string>
+ </attribute>
+ </widget>
+ </item>
+ <item>
+ <widget class="QRadioButton" name="proxyNoneBtn">
+ <property name="text">
+ <string>&amp;None</string>
+ </property>
+ <attribute name="buttonGroup">
+ <string notr="true">proxyGroup</string>
+ </attribute>
+ </widget>
+ </item>
+ <item>
+ <widget class="QRadioButton" name="proxySOCKS5Btn">
+ <property name="text">
+ <string>SOC&amp;KS5</string>
+ </property>
+ <attribute name="buttonGroup">
+ <string notr="true">proxyGroup</string>
+ </attribute>
+ </widget>
+ </item>
+ <item>
+ <widget class="QRadioButton" name="proxyHTTPBtn">
+ <property name="text">
+ <string>H&amp;TTP</string>
+ </property>
+ <attribute name="buttonGroup">
+ <string notr="true">proxyGroup</string>
+ </attribute>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="proxyAddrBox">
+ <property name="title">
+ <string>Address and Port</string>
+ </property>
+ <layout class="QHBoxLayout" name="horizontalLayout_2">
+ <item>
+ <widget class="QLineEdit" name="proxyAddrEdit">
+ <property name="placeholderText">
+ <string notr="true">127.0.0.1</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QSpinBox" name="proxyPortEdit">
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ <property name="buttonSymbols">
+ <enum>QAbstractSpinBox::PlusMinus</enum>
+ </property>
+ <property name="maximum">
+ <number>65535</number>
+ </property>
+ <property name="value">
+ <number>8080</number>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="proxyAuthBox">
+ <property name="title">
+ <string>Authentication</string>
+ </property>
+ <layout class="QGridLayout" name="gridLayout_5">
+ <item row="0" column="1">
+ <widget class="QLineEdit" name="proxyUserEdit"/>
+ </item>
+ <item row="0" column="0">
+ <widget class="QLabel" name="proxyUsernameLabel">
+ <property name="text">
+ <string>Username:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0">
+ <widget class="QLabel" name="proxyPasswordLabel">
+ <property name="text">
+ <string>Password:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QLineEdit" name="proxyPassEdit">
+ <property name="echoMode">
+ <enum>QLineEdit::Password</enum>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="0" colspan="2">
+ <widget class="QLabel" name="proxyPlainTextWarningLabel">
+ <property name="text">
+ <string>Note: Proxy username and password are stored in plain text inside the launcher's configuration file!</string>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <spacer name="verticalSpacer">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>40</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+ <buttongroups>
+ <buttongroup name="proxyGroup"/>
+ </buttongroups>
+</ui>
diff --git a/launcher/ui/pages/instance/GameOptionsPage.cpp b/launcher/ui/pages/instance/GameOptionsPage.cpp
new file mode 100644
index 00000000..782f2ab3
--- /dev/null
+++ b/launcher/ui/pages/instance/GameOptionsPage.cpp
@@ -0,0 +1,37 @@
+#include "GameOptionsPage.h"
+#include "ui_GameOptionsPage.h"
+#include "minecraft/MinecraftInstance.h"
+#include "minecraft/gameoptions/GameOptions.h"
+
+GameOptionsPage::GameOptionsPage(MinecraftInstance * inst, QWidget* parent)
+ : QWidget(parent), ui(new Ui::GameOptionsPage)
+{
+ ui->setupUi(this);
+ ui->tabWidget->tabBar()->hide();
+ m_model = inst->gameOptionsModel();
+ ui->optionsView->setModel(m_model.get());
+ auto head = ui->optionsView->header();
+ if(head->count())
+ {
+ head->setSectionResizeMode(0, QHeaderView::ResizeToContents);
+ for(int i = 1; i < head->count(); i++)
+ {
+ head->setSectionResizeMode(i, QHeaderView::Stretch);
+ }
+ }
+}
+
+GameOptionsPage::~GameOptionsPage()
+{
+ // m_model->save();
+}
+
+void GameOptionsPage::openedImpl()
+{
+ // m_model->observe();
+}
+
+void GameOptionsPage::closedImpl()
+{
+ // m_model->unobserve();
+}
diff --git a/launcher/ui/pages/instance/GameOptionsPage.h b/launcher/ui/pages/instance/GameOptionsPage.h
new file mode 100644
index 00000000..878903eb
--- /dev/null
+++ b/launcher/ui/pages/instance/GameOptionsPage.h
@@ -0,0 +1,63 @@
+/* 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 <QWidget>
+#include <QString>
+
+#include "ui/pages/BasePage.h"
+#include <Application.h>
+
+namespace Ui
+{
+class GameOptionsPage;
+}
+
+class GameOptions;
+class MinecraftInstance;
+
+class GameOptionsPage : public QWidget, public BasePage
+{
+ Q_OBJECT
+
+public:
+ explicit GameOptionsPage(MinecraftInstance *inst, QWidget *parent = 0);
+ virtual ~GameOptionsPage();
+
+ void openedImpl() override;
+ void closedImpl() override;
+
+ virtual QString displayName() const override
+ {
+ return tr("Game Options");
+ }
+ virtual QIcon icon() const override
+ {
+ return APPLICATION->getThemedIcon("settings");
+ }
+ virtual QString id() const override
+ {
+ return "gameoptions";
+ }
+ virtual QString helpPage() const override
+ {
+ return "Game-Options-management";
+ }
+
+private: // data
+ Ui::GameOptionsPage *ui = nullptr;
+ std::shared_ptr<GameOptions> m_model;
+};
diff --git a/launcher/ui/pages/instance/GameOptionsPage.ui b/launcher/ui/pages/instance/GameOptionsPage.ui
new file mode 100644
index 00000000..f0a5ce0e
--- /dev/null
+++ b/launcher/ui/pages/instance/GameOptionsPage.ui
@@ -0,0 +1,88 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>GameOptionsPage</class>
+ <widget class="QWidget" name="GameOptionsPage">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>706</width>
+ <height>575</height>
+ </rect>
+ </property>
+ <layout class="QGridLayout" name="gridLayout">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item row="0" column="0">
+ <widget class="QTabWidget" name="tabWidget">
+ <property name="currentIndex">
+ <number>0</number>
+ </property>
+ <widget class="QWidget" name="tab">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <attribute name="title">
+ <string notr="true">Tab 1</string>
+ </attribute>
+ <layout class="QGridLayout" name="gridLayout_2">
+ <item row="0" column="0" colspan="2">
+ <widget class="QTreeView" name="optionsView">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="acceptDrops">
+ <bool>true</bool>
+ </property>
+ <property name="alternatingRowColors">
+ <bool>true</bool>
+ </property>
+ <property name="selectionMode">
+ <enum>QAbstractItemView::SingleSelection</enum>
+ </property>
+ <property name="selectionBehavior">
+ <enum>QAbstractItemView::SelectRows</enum>
+ </property>
+ <property name="iconSize">
+ <size>
+ <width>64</width>
+ <height>64</height>
+ </size>
+ </property>
+ <property name="rootIsDecorated">
+ <bool>false</bool>
+ </property>
+ <attribute name="headerStretchLastSection">
+ <bool>false</bool>
+ </attribute>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <tabstops>
+ <tabstop>tabWidget</tabstop>
+ <tabstop>optionsView</tabstop>
+ </tabstops>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.cpp b/launcher/ui/pages/instance/InstanceSettingsPage.cpp
new file mode 100644
index 00000000..b0e18af4
--- /dev/null
+++ b/launcher/ui/pages/instance/InstanceSettingsPage.cpp
@@ -0,0 +1,341 @@
+#include "InstanceSettingsPage.h"
+#include "ui_InstanceSettingsPage.h"
+
+#include <QFileDialog>
+#include <QDialog>
+#include <QMessageBox>
+
+#include <sys.h>
+
+#include "ui/dialogs/VersionSelectDialog.h"
+#include "ui/widgets/CustomCommands.h"
+
+#include "JavaCommon.h"
+#include "Application.h"
+
+#include "java/JavaInstallList.h"
+#include "FileSystem.h"
+
+
+InstanceSettingsPage::InstanceSettingsPage(BaseInstance *inst, QWidget *parent)
+ : QWidget(parent), ui(new Ui::InstanceSettingsPage), m_instance(inst)
+{
+ m_settings = inst->settings();
+ ui->setupUi(this);
+ auto sysMB = Sys::getSystemRam() / Sys::mebibyte;
+ ui->maxMemSpinBox->setMaximum(sysMB);
+ connect(ui->openGlobalJavaSettingsButton, &QCommandLinkButton::clicked, this, &InstanceSettingsPage::globalSettingsButtonClicked);
+ connect(APPLICATION, &Application::globalSettingsAboutToOpen, this, &InstanceSettingsPage::applySettings);
+ connect(APPLICATION, &Application::globalSettingsClosed, this, &InstanceSettingsPage::loadSettings);
+ loadSettings();
+}
+
+bool InstanceSettingsPage::shouldDisplay() const
+{
+ return !m_instance->isRunning();
+}
+
+InstanceSettingsPage::~InstanceSettingsPage()
+{
+ delete ui;
+}
+
+void InstanceSettingsPage::globalSettingsButtonClicked(bool)
+{
+ switch(ui->settingsTabs->currentIndex()) {
+ case 0:
+ APPLICATION->ShowGlobalSettings(this, "java-settings");
+ return;
+ case 1:
+ APPLICATION->ShowGlobalSettings(this, "minecraft-settings");
+ return;
+ case 2:
+ APPLICATION->ShowGlobalSettings(this, "custom-commands");
+ return;
+ }
+}
+
+bool InstanceSettingsPage::apply()
+{
+ applySettings();
+ return true;
+}
+
+void InstanceSettingsPage::applySettings()
+{
+ SettingsObject::Lock lock(m_settings);
+
+ // Console
+ bool console = ui->consoleSettingsBox->isChecked();
+ m_settings->set("OverrideConsole", console);
+ if (console)
+ {
+ m_settings->set("ShowConsole", ui->showConsoleCheck->isChecked());
+ m_settings->set("AutoCloseConsole", ui->autoCloseConsoleCheck->isChecked());
+ m_settings->set("ShowConsoleOnError", ui->showConsoleErrorCheck->isChecked());
+ }
+ else
+ {
+ m_settings->reset("ShowConsole");
+ m_settings->reset("AutoCloseConsole");
+ m_settings->reset("ShowConsoleOnError");
+ }
+
+ // Window Size
+ bool window = ui->windowSizeGroupBox->isChecked();
+ m_settings->set("OverrideWindow", window);
+ if (window)
+ {
+ m_settings->set("LaunchMaximized", ui->maximizedCheckBox->isChecked());
+ m_settings->set("MinecraftWinWidth", ui->windowWidthSpinBox->value());
+ m_settings->set("MinecraftWinHeight", ui->windowHeightSpinBox->value());
+ }
+ else
+ {
+ m_settings->reset("LaunchMaximized");
+ m_settings->reset("MinecraftWinWidth");
+ m_settings->reset("MinecraftWinHeight");
+ }
+
+ // Memory
+ bool memory = ui->memoryGroupBox->isChecked();
+ m_settings->set("OverrideMemory", memory);
+ if (memory)
+ {
+ int min = ui->minMemSpinBox->value();
+ int max = ui->maxMemSpinBox->value();
+ if(min < max)
+ {
+ m_settings->set("MinMemAlloc", min);
+ m_settings->set("MaxMemAlloc", max);
+ }
+ else
+ {
+ m_settings->set("MinMemAlloc", max);
+ m_settings->set("MaxMemAlloc", min);
+ }
+ m_settings->set("PermGen", ui->permGenSpinBox->value());
+ }
+ else
+ {
+ m_settings->reset("MinMemAlloc");
+ m_settings->reset("MaxMemAlloc");
+ m_settings->reset("PermGen");
+ }
+
+ // Java Install Settings
+ bool javaInstall = ui->javaSettingsGroupBox->isChecked();
+ m_settings->set("OverrideJavaLocation", javaInstall);
+ if (javaInstall)
+ {
+ m_settings->set("JavaPath", ui->javaPathTextBox->text());
+ }
+ else
+ {
+ m_settings->reset("JavaPath");
+ }
+
+ // Java arguments
+ bool javaArgs = ui->javaArgumentsGroupBox->isChecked();
+ m_settings->set("OverrideJavaArgs", javaArgs);
+ if(javaArgs)
+ {
+ m_settings->set("JvmArgs", ui->jvmArgsTextBox->toPlainText().replace("\n", " "));
+ JavaCommon::checkJVMArgs(m_settings->get("JvmArgs").toString(), this->parentWidget());
+ }
+ else
+ {
+ m_settings->reset("JvmArgs");
+ }
+
+ // old generic 'override both' is removed.
+ m_settings->reset("OverrideJava");
+
+ // Custom Commands
+ bool custcmd = ui->customCommands->checked();
+ m_settings->set("OverrideCommands", custcmd);
+ if (custcmd)
+ {
+ m_settings->set("PreLaunchCommand", ui->customCommands->prelaunchCommand());
+ m_settings->set("WrapperCommand", ui->customCommands->wrapperCommand());
+ m_settings->set("PostExitCommand", ui->customCommands->postexitCommand());
+ }
+ else
+ {
+ m_settings->reset("PreLaunchCommand");
+ m_settings->reset("WrapperCommand");
+ m_settings->reset("PostExitCommand");
+ }
+
+ // Workarounds
+ bool workarounds = ui->nativeWorkaroundsGroupBox->isChecked();
+ m_settings->set("OverrideNativeWorkarounds", workarounds);
+ if(workarounds)
+ {
+ m_settings->set("UseNativeOpenAL", ui->useNativeOpenALCheck->isChecked());
+ m_settings->set("UseNativeGLFW", ui->useNativeGLFWCheck->isChecked());
+ }
+ else
+ {
+ m_settings->reset("UseNativeOpenAL");
+ m_settings->reset("UseNativeGLFW");
+ }
+
+ // Game time
+ bool gameTime = ui->gameTimeGroupBox->isChecked();
+ m_settings->set("OverrideGameTime", gameTime);
+ if (gameTime)
+ {
+ m_settings->set("ShowGameTime", ui->showGameTime->isChecked());
+ m_settings->set("RecordGameTime", ui->recordGameTime->isChecked());
+ }
+ else
+ {
+ m_settings->reset("ShowGameTime");
+ m_settings->reset("RecordGameTime");
+ }
+
+ // Join server on launch
+ bool joinServerOnLaunch = ui->serverJoinGroupBox->isChecked();
+ m_settings->set("JoinServerOnLaunch", joinServerOnLaunch);
+ if (joinServerOnLaunch)
+ {
+ m_settings->set("JoinServerOnLaunchAddress", ui->serverJoinAddress->text());
+ }
+ else
+ {
+ m_settings->reset("JoinServerOnLaunchAddress");
+ }
+}
+
+void InstanceSettingsPage::loadSettings()
+{
+ // Console
+ ui->consoleSettingsBox->setChecked(m_settings->get("OverrideConsole").toBool());
+ ui->showConsoleCheck->setChecked(m_settings->get("ShowConsole").toBool());
+ ui->autoCloseConsoleCheck->setChecked(m_settings->get("AutoCloseConsole").toBool());
+ ui->showConsoleErrorCheck->setChecked(m_settings->get("ShowConsoleOnError").toBool());
+
+ // Window Size
+ ui->windowSizeGroupBox->setChecked(m_settings->get("OverrideWindow").toBool());
+ ui->maximizedCheckBox->setChecked(m_settings->get("LaunchMaximized").toBool());
+ ui->windowWidthSpinBox->setValue(m_settings->get("MinecraftWinWidth").toInt());
+ ui->windowHeightSpinBox->setValue(m_settings->get("MinecraftWinHeight").toInt());
+
+ // Memory
+ ui->memoryGroupBox->setChecked(m_settings->get("OverrideMemory").toBool());
+ int min = m_settings->get("MinMemAlloc").toInt();
+ int max = m_settings->get("MaxMemAlloc").toInt();
+ if(min < max)
+ {
+ ui->minMemSpinBox->setValue(min);
+ ui->maxMemSpinBox->setValue(max);
+ }
+ else
+ {
+ ui->minMemSpinBox->setValue(max);
+ ui->maxMemSpinBox->setValue(min);
+ }
+ ui->permGenSpinBox->setValue(m_settings->get("PermGen").toInt());
+ bool permGenVisible = m_settings->get("PermGenVisible").toBool();
+ ui->permGenSpinBox->setVisible(permGenVisible);
+ ui->labelPermGen->setVisible(permGenVisible);
+ ui->labelPermgenNote->setVisible(permGenVisible);
+
+
+ // Java Settings
+ bool overrideJava = m_settings->get("OverrideJava").toBool();
+ bool overrideLocation = m_settings->get("OverrideJavaLocation").toBool() || overrideJava;
+ bool overrideArgs = m_settings->get("OverrideJavaArgs").toBool() || overrideJava;
+
+ ui->javaSettingsGroupBox->setChecked(overrideLocation);
+ ui->javaPathTextBox->setText(m_settings->get("JavaPath").toString());
+
+ ui->javaArgumentsGroupBox->setChecked(overrideArgs);
+ ui->jvmArgsTextBox->setPlainText(m_settings->get("JvmArgs").toString());
+
+ // Custom commands
+ ui->customCommands->initialize(
+ true,
+ m_settings->get("OverrideCommands").toBool(),
+ m_settings->get("PreLaunchCommand").toString(),
+ m_settings->get("WrapperCommand").toString(),
+ m_settings->get("PostExitCommand").toString()
+ );
+
+ // Workarounds
+ ui->nativeWorkaroundsGroupBox->setChecked(m_settings->get("OverrideNativeWorkarounds").toBool());
+ ui->useNativeGLFWCheck->setChecked(m_settings->get("UseNativeGLFW").toBool());
+ ui->useNativeOpenALCheck->setChecked(m_settings->get("UseNativeOpenAL").toBool());
+
+ // Miscellanous
+ ui->gameTimeGroupBox->setChecked(m_settings->get("OverrideGameTime").toBool());
+ ui->showGameTime->setChecked(m_settings->get("ShowGameTime").toBool());
+ ui->recordGameTime->setChecked(m_settings->get("RecordGameTime").toBool());
+
+ ui->serverJoinGroupBox->setChecked(m_settings->get("JoinServerOnLaunch").toBool());
+ ui->serverJoinAddress->setText(m_settings->get("JoinServerOnLaunchAddress").toString());
+}
+
+void InstanceSettingsPage::on_javaDetectBtn_clicked()
+{
+ JavaInstallPtr java;
+
+ VersionSelectDialog vselect(APPLICATION->javalist().get(), tr("Select a Java version"), this, true);
+ vselect.setResizeOn(2);
+ vselect.exec();
+
+ if (vselect.result() == QDialog::Accepted && vselect.selectedVersion())
+ {
+ java = std::dynamic_pointer_cast<JavaInstall>(vselect.selectedVersion());
+ ui->javaPathTextBox->setText(java->path);
+ bool visible = java->id.requiresPermGen() && m_settings->get("OverrideMemory").toBool();
+ ui->permGenSpinBox->setVisible(visible);
+ ui->labelPermGen->setVisible(visible);
+ ui->labelPermgenNote->setVisible(visible);
+ m_settings->set("PermGenVisible", visible);
+ }
+}
+
+void InstanceSettingsPage::on_javaBrowseBtn_clicked()
+{
+ QString raw_path = QFileDialog::getOpenFileName(this, tr("Find Java executable"));
+
+ // do not allow current dir - it's dirty. Do not allow dirs that don't exist
+ if(raw_path.isEmpty())
+ {
+ return;
+ }
+ QString cooked_path = FS::NormalizePath(raw_path);
+
+ QFileInfo javaInfo(cooked_path);
+ if(!javaInfo.exists() || !javaInfo.isExecutable())
+ {
+ return;
+ }
+ ui->javaPathTextBox->setText(cooked_path);
+
+ // custom Java could be anything... enable perm gen option
+ ui->permGenSpinBox->setVisible(true);
+ ui->labelPermGen->setVisible(true);
+ ui->labelPermgenNote->setVisible(true);
+ m_settings->set("PermGenVisible", true);
+}
+
+void InstanceSettingsPage::on_javaTestBtn_clicked()
+{
+ if(checker)
+ {
+ return;
+ }
+ checker.reset(new JavaCommon::TestCheck(
+ this, ui->javaPathTextBox->text(), ui->jvmArgsTextBox->toPlainText().replace("\n", " "),
+ ui->minMemSpinBox->value(), ui->maxMemSpinBox->value(), ui->permGenSpinBox->value()));
+ connect(checker.get(), SIGNAL(finished()), SLOT(checkerFinished()));
+ checker->run();
+}
+
+void InstanceSettingsPage::checkerFinished()
+{
+ checker.reset();
+}
diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.h b/launcher/ui/pages/instance/InstanceSettingsPage.h
new file mode 100644
index 00000000..5c8c8e66
--- /dev/null
+++ b/launcher/ui/pages/instance/InstanceSettingsPage.h
@@ -0,0 +1,76 @@
+/* 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 <QWidget>
+
+#include "java/JavaChecker.h"
+#include "BaseInstance.h"
+#include <QObjectPtr.h>
+#include "ui/pages/BasePage.h"
+#include "JavaCommon.h"
+#include "Application.h"
+
+class JavaChecker;
+namespace Ui
+{
+class InstanceSettingsPage;
+}
+
+class InstanceSettingsPage : public QWidget, public BasePage
+{
+ Q_OBJECT
+
+public:
+ explicit InstanceSettingsPage(BaseInstance *inst, QWidget *parent = 0);
+ virtual ~InstanceSettingsPage();
+ virtual QString displayName() const override
+ {
+ return tr("Settings");
+ }
+ virtual QIcon icon() const override
+ {
+ return APPLICATION->getThemedIcon("instance-settings");
+ }
+ virtual QString id() const override
+ {
+ return "settings";
+ }
+ virtual bool apply() override;
+ virtual QString helpPage() const override
+ {
+ return "Instance-settings";
+ }
+ virtual bool shouldDisplay() const override;
+
+private slots:
+ void on_javaDetectBtn_clicked();
+ void on_javaTestBtn_clicked();
+ void on_javaBrowseBtn_clicked();
+
+ void applySettings();
+ void loadSettings();
+
+ void checkerFinished();
+
+ void globalSettingsButtonClicked(bool checked);
+
+private:
+ Ui::InstanceSettingsPage *ui;
+ BaseInstance *m_instance;
+ SettingsObjectPtr m_settings;
+ unique_qobject_ptr<JavaCommon::TestCheck> checker;
+};
diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.ui b/launcher/ui/pages/instance/InstanceSettingsPage.ui
new file mode 100644
index 00000000..729f8e2a
--- /dev/null
+++ b/launcher/ui/pages/instance/InstanceSettingsPage.ui
@@ -0,0 +1,548 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>InstanceSettingsPage</class>
+ <widget class="QWidget" name="InstanceSettingsPage">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>691</width>
+ <height>581</height>
+ </rect>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QCommandLinkButton" name="openGlobalJavaSettingsButton">
+ <property name="text">
+ <string>Open Global Settings</string>
+ </property>
+ <property name="description">
+ <string>The settings here are overrides for global settings.</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QTabWidget" name="settingsTabs">
+ <property name="tabShape">
+ <enum>QTabWidget::Rounded</enum>
+ </property>
+ <property name="currentIndex">
+ <number>0</number>
+ </property>
+ <widget class="QWidget" name="minecraftTab">
+ <attribute name="title">
+ <string notr="true">Java</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_5">
+ <item>
+ <widget class="QGroupBox" name="javaSettingsGroupBox">
+ <property name="enabled">
+ <bool>true</bool>
+ </property>
+ <property name="title">
+ <string>Java insta&amp;llation</string>
+ </property>
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ <property name="checked">
+ <bool>false</bool>
+ </property>
+ <layout class="QGridLayout" name="gridLayout">
+ <item row="0" column="0" colspan="3">
+ <widget class="QLineEdit" name="javaPathTextBox"/>
+ </item>
+ <item row="1" column="0">
+ <widget class="QPushButton" name="javaDetectBtn">
+ <property name="text">
+ <string>Auto-detect...</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QPushButton" name="javaBrowseBtn">
+ <property name="text">
+ <string>Browse...</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="2">
+ <widget class="QPushButton" name="javaTestBtn">
+ <property name="text">
+ <string>Test</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="memoryGroupBox">
+ <property name="enabled">
+ <bool>true</bool>
+ </property>
+ <property name="title">
+ <string>Memor&amp;y</string>
+ </property>
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ <property name="checked">
+ <bool>false</bool>
+ </property>
+ <layout class="QGridLayout" name="gridLayout_2">
+ <item row="0" column="0">
+ <widget class="QLabel" name="labelMinMem">
+ <property name="text">
+ <string>Minimum memory allocation:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QSpinBox" name="maxMemSpinBox">
+ <property name="toolTip">
+ <string>The maximum amount of memory Minecraft is allowed to use.</string>
+ </property>
+ <property name="suffix">
+ <string notr="true"> MiB</string>
+ </property>
+ <property name="minimum">
+ <number>128</number>
+ </property>
+ <property name="maximum">
+ <number>65536</number>
+ </property>
+ <property name="singleStep">
+ <number>128</number>
+ </property>
+ <property name="value">
+ <number>1024</number>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QSpinBox" name="minMemSpinBox">
+ <property name="toolTip">
+ <string>The amount of memory Minecraft is started with.</string>
+ </property>
+ <property name="suffix">
+ <string notr="true"> MiB</string>
+ </property>
+ <property name="minimum">
+ <number>128</number>
+ </property>
+ <property name="maximum">
+ <number>65536</number>
+ </property>
+ <property name="singleStep">
+ <number>128</number>
+ </property>
+ <property name="value">
+ <number>256</number>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="1">
+ <widget class="QSpinBox" name="permGenSpinBox">
+ <property name="toolTip">
+ <string>The amount of memory available to store loaded Java classes.</string>
+ </property>
+ <property name="suffix">
+ <string notr="true"> MiB</string>
+ </property>
+ <property name="minimum">
+ <number>64</number>
+ </property>
+ <property name="maximum">
+ <number>999999999</number>
+ </property>
+ <property name="singleStep">
+ <number>8</number>
+ </property>
+ <property name="value">
+ <number>64</number>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="0">
+ <widget class="QLabel" name="labelPermGen">
+ <property name="text">
+ <string notr="true">PermGen:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0">
+ <widget class="QLabel" name="labelMaxMem">
+ <property name="text">
+ <string>Maximum memory allocation:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="0" colspan="2">
+ <widget class="QLabel" name="labelPermgenNote">
+ <property name="text">
+ <string>Note: Permgen is set automatically by Java 8 and later</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="javaArgumentsGroupBox">
+ <property name="enabled">
+ <bool>true</bool>
+ </property>
+ <property name="title">
+ <string>Java argumen&amp;ts</string>
+ </property>
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ <property name="checked">
+ <bool>false</bool>
+ </property>
+ <layout class="QGridLayout" name="gridLayout_5">
+ <item row="1" column="1">
+ <widget class="QPlainTextEdit" name="jvmArgsTextBox"/>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QWidget" name="javaTab">
+ <attribute name="title">
+ <string>Game windows</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_3">
+ <item>
+ <widget class="QGroupBox" name="windowSizeGroupBox">
+ <property name="enabled">
+ <bool>true</bool>
+ </property>
+ <property name="title">
+ <string>Game Window</string>
+ </property>
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ <property name="checked">
+ <bool>false</bool>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_4">
+ <item>
+ <widget class="QCheckBox" name="maximizedCheckBox">
+ <property name="text">
+ <string>Start Minecraft maximized?</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <layout class="QGridLayout" name="gridLayoutWindowSize">
+ <item row="1" column="0">
+ <widget class="QLabel" name="labelWindowHeight">
+ <property name="text">
+ <string>Window height:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="0">
+ <widget class="QLabel" name="labelWindowWidth">
+ <property name="text">
+ <string>Window width:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QSpinBox" name="windowWidthSpinBox">
+ <property name="minimum">
+ <number>1</number>
+ </property>
+ <property name="maximum">
+ <number>65536</number>
+ </property>
+ <property name="singleStep">
+ <number>1</number>
+ </property>
+ <property name="value">
+ <number>854</number>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QSpinBox" name="windowHeightSpinBox">
+ <property name="minimum">
+ <number>1</number>
+ </property>
+ <property name="maximum">
+ <number>65536</number>
+ </property>
+ <property name="value">
+ <number>480</number>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="consoleSettingsBox">
+ <property name="enabled">
+ <bool>true</bool>
+ </property>
+ <property name="title">
+ <string>Conso&amp;le Settings</string>
+ </property>
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ <property name="checked">
+ <bool>false</bool>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_2">
+ <item>
+ <widget class="QCheckBox" name="showConsoleCheck">
+ <property name="text">
+ <string>Show console while the game is running?</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="autoCloseConsoleCheck">
+ <property name="text">
+ <string>Automatically close console when the game quits?</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="showConsoleErrorCheck">
+ <property name="text">
+ <string>Show console when the game crashes?</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <spacer name="verticalSpacerMinecraft_2">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>88</width>
+ <height>125</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QWidget" name="tab">
+ <attribute name="title">
+ <string>Custom commands</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_6">
+ <item>
+ <widget class="CustomCommands" name="customCommands" native="true"/>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QWidget" name="workaroundsPage">
+ <attribute name="title">
+ <string>Workarounds</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_8">
+ <item>
+ <widget class="QGroupBox" name="nativeWorkaroundsGroupBox">
+ <property name="enabled">
+ <bool>true</bool>
+ </property>
+ <property name="title">
+ <string>Native libraries</string>
+ </property>
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ <property name="checked">
+ <bool>false</bool>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_7">
+ <item>
+ <widget class="QCheckBox" name="useNativeGLFWCheck">
+ <property name="text">
+ <string>Use system installation of GLFW</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="useNativeOpenALCheck">
+ <property name="text">
+ <string>Use system installation of OpenAL</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <spacer name="verticalSpacer">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>40</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QWidget" name="miscellaneousPage">
+ <attribute name="title">
+ <string>Miscellaneous</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_9">
+ <item>
+ <widget class="QGroupBox" name="gameTimeGroupBox">
+ <property name="enabled">
+ <bool>true</bool>
+ </property>
+ <property name="title">
+ <string>Override global game time settings</string>
+ </property>
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ <property name="checked">
+ <bool>false</bool>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_10">
+ <item>
+ <widget class="QCheckBox" name="showGameTime">
+ <property name="text">
+ <string>Show time spent playing this instance</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="recordGameTime">
+ <property name="text">
+ <string>Record time spent playing this instance</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="serverJoinGroupBox">
+ <property name="title">
+ <string>Set a server to join on launch</string>
+ </property>
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ <property name="checked">
+ <bool>false</bool>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_11">
+ <item>
+ <layout class="QGridLayout" name="serverJoinLayout">
+ <item row="0" column="0">
+ <widget class="QLabel" name="serverJoinAddressLabel">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Server address:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QLineEdit" name="serverJoinAddress"/>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <spacer name="verticalSpacerMiscellaneous">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>40</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <customwidgets>
+ <customwidget>
+ <class>CustomCommands</class>
+ <extends>QWidget</extends>
+ <header>ui/widgets/CustomCommands.h</header>
+ <container>1</container>
+ </customwidget>
+ </customwidgets>
+ <tabstops>
+ <tabstop>openGlobalJavaSettingsButton</tabstop>
+ <tabstop>settingsTabs</tabstop>
+ <tabstop>javaSettingsGroupBox</tabstop>
+ <tabstop>javaPathTextBox</tabstop>
+ <tabstop>javaDetectBtn</tabstop>
+ <tabstop>javaBrowseBtn</tabstop>
+ <tabstop>javaTestBtn</tabstop>
+ <tabstop>memoryGroupBox</tabstop>
+ <tabstop>minMemSpinBox</tabstop>
+ <tabstop>maxMemSpinBox</tabstop>
+ <tabstop>permGenSpinBox</tabstop>
+ <tabstop>javaArgumentsGroupBox</tabstop>
+ <tabstop>jvmArgsTextBox</tabstop>
+ <tabstop>windowSizeGroupBox</tabstop>
+ <tabstop>maximizedCheckBox</tabstop>
+ <tabstop>windowWidthSpinBox</tabstop>
+ <tabstop>windowHeightSpinBox</tabstop>
+ <tabstop>consoleSettingsBox</tabstop>
+ <tabstop>showConsoleCheck</tabstop>
+ <tabstop>autoCloseConsoleCheck</tabstop>
+ <tabstop>showConsoleErrorCheck</tabstop>
+ <tabstop>nativeWorkaroundsGroupBox</tabstop>
+ <tabstop>useNativeGLFWCheck</tabstop>
+ <tabstop>useNativeOpenALCheck</tabstop>
+ <tabstop>showGameTime</tabstop>
+ <tabstop>recordGameTime</tabstop>
+ </tabstops>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/launcher/ui/pages/instance/LegacyUpgradePage.cpp b/launcher/ui/pages/instance/LegacyUpgradePage.cpp
new file mode 100644
index 00000000..cb78af02
--- /dev/null
+++ b/launcher/ui/pages/instance/LegacyUpgradePage.cpp
@@ -0,0 +1,51 @@
+#include "LegacyUpgradePage.h"
+#include "ui_LegacyUpgradePage.h"
+
+#include "InstanceList.h"
+#include "minecraft/legacy/LegacyInstance.h"
+#include "minecraft/legacy/LegacyUpgradeTask.h"
+#include "Application.h"
+
+#include "ui/dialogs/CustomMessageBox.h"
+#include "ui/dialogs/ProgressDialog.h"
+
+LegacyUpgradePage::LegacyUpgradePage(InstancePtr inst, QWidget *parent)
+ : QWidget(parent), ui(new Ui::LegacyUpgradePage), m_inst(inst)
+{
+ ui->setupUi(this);
+}
+
+LegacyUpgradePage::~LegacyUpgradePage()
+{
+ delete ui;
+}
+
+void LegacyUpgradePage::runModalTask(Task *task)
+{
+ connect(task, &Task::failed, [this](QString reason)
+ {
+ CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Warning)->show();
+ });
+ ProgressDialog loadDialog(this);
+ loadDialog.setSkipButton(true, tr("Abort"));
+ if(loadDialog.execWithTask(task) == QDialog::Accepted)
+ {
+ m_container->requestClose();
+ }
+}
+
+void LegacyUpgradePage::on_upgradeButton_clicked()
+{
+ QString newName = tr("%1 (Migrated)").arg(m_inst->name());
+ auto upgradeTask = new LegacyUpgradeTask(m_inst);
+ upgradeTask->setName(newName);
+ upgradeTask->setGroup(APPLICATION->instances()->getInstanceGroup(m_inst->id()));
+ upgradeTask->setIcon(m_inst->iconKey());
+ unique_qobject_ptr<Task> task(APPLICATION->instances()->wrapInstanceTask(upgradeTask));
+ runModalTask(task.get());
+}
+
+bool LegacyUpgradePage::shouldDisplay() const
+{
+ return !m_inst->isRunning();
+}
diff --git a/launcher/ui/pages/instance/LegacyUpgradePage.h b/launcher/ui/pages/instance/LegacyUpgradePage.h
new file mode 100644
index 00000000..7c51956b
--- /dev/null
+++ b/launcher/ui/pages/instance/LegacyUpgradePage.h
@@ -0,0 +1,64 @@
+/* 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 <QWidget>
+
+#include "minecraft/legacy/LegacyInstance.h"
+#include "ui/pages/BasePage.h"
+#include <Application.h>
+#include "tasks/Task.h"
+
+namespace Ui
+{
+class LegacyUpgradePage;
+}
+
+class LegacyUpgradePage : public QWidget, public BasePage
+{
+ Q_OBJECT
+
+public:
+ explicit LegacyUpgradePage(InstancePtr inst, QWidget *parent = 0);
+ virtual ~LegacyUpgradePage();
+ virtual QString displayName() const override
+ {
+ return tr("Upgrade");
+ }
+ virtual QIcon icon() const override
+ {
+ return APPLICATION->getThemedIcon("checkupdate");
+ }
+ virtual QString id() const override
+ {
+ return "upgrade";
+ }
+ virtual QString helpPage() const override
+ {
+ return "Legacy-upgrade";
+ }
+ virtual bool shouldDisplay() const override;
+
+private slots:
+ void on_upgradeButton_clicked();
+
+private:
+ void runModalTask(Task *task);
+
+private:
+ Ui::LegacyUpgradePage *ui;
+ InstancePtr m_inst;
+};
diff --git a/launcher/ui/pages/instance/LegacyUpgradePage.ui b/launcher/ui/pages/instance/LegacyUpgradePage.ui
new file mode 100644
index 00000000..085919e3
--- /dev/null
+++ b/launcher/ui/pages/instance/LegacyUpgradePage.ui
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>LegacyUpgradePage</class>
+ <widget class="QWidget" name="LegacyUpgradePage">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>546</width>
+ <height>405</height>
+ </rect>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_5">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QTextBrowser" name="textBrowser">
+ <property name="html">
+ <string>&lt;html&gt;&lt;body&gt;&lt;h1&gt;Upgrade is required&lt;/h1&gt;&lt;p&gt;MultiMC now supports old Minecraft versions and all the required features in the new (OneSix) instance format. As a consequence, the old (Legacy) format has been entirely disabled and old instances need to be upgraded.&lt;/p&gt;&lt;p&gt;The upgrade will create a new instance with the same contents as the current one, in the new format. The original instance will remain untouched, in case anything goes wrong in the process.&lt;/p&gt;&lt;p&gt;Please report any issues on our &lt;a href=&quot;https://github.com/MultiMC/Launcher/issues&quot;&gt;github issues page&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;There is also a &lt;a href=&quot;https://discord.gg/GtPmv93&quot;&gt;discord channel for testing here&lt;/a&gt;.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ <property name="openExternalLinks">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCommandLinkButton" name="upgradeButton">
+ <property name="text">
+ <string>Upgrade the instance</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/launcher/ui/pages/instance/LogPage.cpp b/launcher/ui/pages/instance/LogPage.cpp
new file mode 100644
index 00000000..b66c6cc7
--- /dev/null
+++ b/launcher/ui/pages/instance/LogPage.cpp
@@ -0,0 +1,330 @@
+#include "LogPage.h"
+#include "ui_LogPage.h"
+
+#include "Application.h"
+
+#include <QIcon>
+#include <QScrollBar>
+#include <QShortcut>
+
+#include "launch/LaunchTask.h"
+#include "settings/Setting.h"
+
+#include "ui/GuiUtil.h"
+#include "ui/ColorCache.h"
+
+#include <BuildConfig.h>
+
+class LogFormatProxyModel : public QIdentityProxyModel
+{
+public:
+ LogFormatProxyModel(QObject* parent = nullptr) : QIdentityProxyModel(parent)
+ {
+ }
+ QVariant data(const QModelIndex &index, int role) const override
+ {
+ switch(role)
+ {
+ case Qt::FontRole:
+ return m_font;
+ case Qt::TextColorRole:
+ {
+ MessageLevel::Enum level = (MessageLevel::Enum) QIdentityProxyModel::data(index, LogModel::LevelRole).toInt();
+ return m_colors->getFront(level);
+ }
+ case Qt::BackgroundRole:
+ {
+ MessageLevel::Enum level = (MessageLevel::Enum) QIdentityProxyModel::data(index, LogModel::LevelRole).toInt();
+ return m_colors->getBack(level);
+ }
+ default:
+ return QIdentityProxyModel::data(index, role);
+ }
+ }
+
+ void setFont(QFont font)
+ {
+ m_font = font;
+ }
+
+ void setColors(LogColorCache* colors)
+ {
+ m_colors.reset(colors);
+ }
+
+ QModelIndex find(const QModelIndex &start, const QString &value, bool reverse) const
+ {
+ QModelIndex parentIndex = parent(start);
+ auto compare = [&](int r) -> QModelIndex
+ {
+ QModelIndex idx = index(r, start.column(), parentIndex);
+ if (!idx.isValid() || idx == start)
+ {
+ return QModelIndex();
+ }
+ QVariant v = data(idx, Qt::DisplayRole);
+ QString t = v.toString();
+ if (t.contains(value, Qt::CaseInsensitive))
+ return idx;
+ return QModelIndex();
+ };
+ if(reverse)
+ {
+ int from = start.row();
+ int to = 0;
+
+ for (int i = 0; i < 2; ++i)
+ {
+ for (int r = from; (r >= to); --r)
+ {
+ auto idx = compare(r);
+ if(idx.isValid())
+ return idx;
+ }
+ // prepare for the next iteration
+ from = rowCount() - 1;
+ to = start.row();
+ }
+ }
+ else
+ {
+ int from = start.row();
+ int to = rowCount(parentIndex);
+
+ for (int i = 0; i < 2; ++i)
+ {
+ for (int r = from; (r < to); ++r)
+ {
+ auto idx = compare(r);
+ if(idx.isValid())
+ return idx;
+ }
+ // prepare for the next iteration
+ from = 0;
+ to = start.row();
+ }
+ }
+ return QModelIndex();
+ }
+private:
+ QFont m_font;
+ std::unique_ptr<LogColorCache> m_colors;
+};
+
+LogPage::LogPage(InstancePtr instance, QWidget *parent)
+ : QWidget(parent), ui(new Ui::LogPage), m_instance(instance)
+{
+ ui->setupUi(this);
+ ui->tabWidget->tabBar()->hide();
+
+ m_proxy = new LogFormatProxyModel(this);
+ // set up text colors in the log proxy and adapt them to the current theme foreground and background
+ {
+ auto origForeground = ui->text->palette().color(ui->text->foregroundRole());
+ auto origBackground = ui->text->palette().color(ui->text->backgroundRole());
+ m_proxy->setColors(new LogColorCache(origForeground, origBackground));
+ }
+
+ // set up fonts in the log proxy
+ {
+ QString fontFamily = APPLICATION->settings()->get("ConsoleFont").toString();
+ bool conversionOk = false;
+ int fontSize = APPLICATION->settings()->get("ConsoleFontSize").toInt(&conversionOk);
+ if(!conversionOk)
+ {
+ fontSize = 11;
+ }
+ m_proxy->setFont(QFont(fontFamily, fontSize));
+ }
+
+ ui->text->setModel(m_proxy);
+
+ // set up instance and launch process recognition
+ {
+ auto launchTask = m_instance->getLaunchTask();
+ if(launchTask)
+ {
+ setInstanceLaunchTaskChanged(launchTask, true);
+ }
+ connect(m_instance.get(), &BaseInstance::launchTaskChanged, this, &LogPage::onInstanceLaunchTaskChanged);
+ }
+
+ auto findShortcut = new QShortcut(QKeySequence(QKeySequence::Find), this);
+ connect(findShortcut, SIGNAL(activated()), SLOT(findActivated()));
+ auto findNextShortcut = new QShortcut(QKeySequence(QKeySequence::FindNext), this);
+ connect(findNextShortcut, SIGNAL(activated()), SLOT(findNextActivated()));
+ connect(ui->searchBar, SIGNAL(returnPressed()), SLOT(on_findButton_clicked()));
+ auto findPreviousShortcut = new QShortcut(QKeySequence(QKeySequence::FindPrevious), this);
+ connect(findPreviousShortcut, SIGNAL(activated()), SLOT(findPreviousActivated()));
+}
+
+LogPage::~LogPage()
+{
+ delete ui;
+}
+
+void LogPage::modelStateToUI()
+{
+ if(m_model->wrapLines())
+ {
+ ui->text->setWordWrap(true);
+ ui->wrapCheckbox->setCheckState(Qt::Checked);
+ }
+ else
+ {
+ ui->text->setWordWrap(false);
+ ui->wrapCheckbox->setCheckState(Qt::Unchecked);
+ }
+ if(m_model->suspended())
+ {
+ ui->trackLogCheckbox->setCheckState(Qt::Unchecked);
+ }
+ else
+ {
+ ui->trackLogCheckbox->setCheckState(Qt::Checked);
+ }
+}
+
+void LogPage::UIToModelState()
+{
+ if(!m_model)
+ {
+ return;
+ }
+ m_model->setLineWrap(ui->wrapCheckbox->checkState() == Qt::Checked);
+ m_model->suspend(ui->trackLogCheckbox->checkState() != Qt::Checked);
+}
+
+void LogPage::setInstanceLaunchTaskChanged(shared_qobject_ptr<LaunchTask> proc, bool initial)
+{
+ m_process = proc;
+ if(m_process)
+ {
+ m_model = proc->getLogModel();
+ m_proxy->setSourceModel(m_model.get());
+ if(initial)
+ {
+ modelStateToUI();
+ }
+ else
+ {
+ UIToModelState();
+ }
+ }
+ else
+ {
+ m_proxy->setSourceModel(nullptr);
+ m_model.reset();
+ }
+}
+
+void LogPage::onInstanceLaunchTaskChanged(shared_qobject_ptr<LaunchTask> proc)
+{
+ setInstanceLaunchTaskChanged(proc, false);
+}
+
+bool LogPage::apply()
+{
+ return true;
+}
+
+bool LogPage::shouldDisplay() const
+{
+ return m_instance->isRunning() || m_proxy->rowCount() > 0;
+}
+
+void LogPage::on_btnPaste_clicked()
+{
+ if(!m_model)
+ return;
+
+ //FIXME: turn this into a proper task and move the upload logic out of GuiUtil!
+ m_model->append(
+ MessageLevel::Launcher,
+ QString("%2: Log upload triggered at: %1").arg(
+ QDateTime::currentDateTime().toString(Qt::RFC2822Date),
+ BuildConfig.LAUNCHER_NAME
+ )
+ );
+ auto url = GuiUtil::uploadPaste(m_model->toPlainText(), this);
+ if(!url.isEmpty())
+ {
+ m_model->append(
+ MessageLevel::Launcher,
+ QString("%2: Log uploaded to: %1").arg(
+ url,
+ BuildConfig.LAUNCHER_NAME
+ )
+ );
+ }
+ else
+ {
+ m_model->append(
+ MessageLevel::Error,
+ QString("%1: Log upload failed!").arg(BuildConfig.LAUNCHER_NAME)
+ );
+ }
+}
+
+void LogPage::on_btnCopy_clicked()
+{
+ if(!m_model)
+ return;
+ m_model->append(MessageLevel::Launcher, QString("Clipboard copy at: %1").arg(QDateTime::currentDateTime().toString(Qt::RFC2822Date)));
+ GuiUtil::setClipboardText(m_model->toPlainText());
+}
+
+void LogPage::on_btnClear_clicked()
+{
+ if(!m_model)
+ return;
+ m_model->clear();
+ m_container->refreshContainer();
+}
+
+void LogPage::on_btnBottom_clicked()
+{
+ ui->text->scrollToBottom();
+}
+
+void LogPage::on_trackLogCheckbox_clicked(bool checked)
+{
+ if(!m_model)
+ return;
+ m_model->suspend(!checked);
+}
+
+void LogPage::on_wrapCheckbox_clicked(bool checked)
+{
+ ui->text->setWordWrap(checked);
+ if(!m_model)
+ return;
+ m_model->setLineWrap(checked);
+}
+
+void LogPage::on_findButton_clicked()
+{
+ auto modifiers = QApplication::keyboardModifiers();
+ bool reverse = modifiers & Qt::ShiftModifier;
+ ui->text->findNext(ui->searchBar->text(), reverse);
+}
+
+void LogPage::findNextActivated()
+{
+ ui->text->findNext(ui->searchBar->text(), false);
+}
+
+void LogPage::findPreviousActivated()
+{
+ ui->text->findNext(ui->searchBar->text(), true);
+}
+
+void LogPage::findActivated()
+{
+ // focus the search bar if it doesn't have focus
+ if (!ui->searchBar->hasFocus())
+ {
+ ui->searchBar->setFocus();
+ ui->searchBar->selectAll();
+ }
+}
diff --git a/launcher/ui/pages/instance/LogPage.h b/launcher/ui/pages/instance/LogPage.h
new file mode 100644
index 00000000..cab25563
--- /dev/null
+++ b/launcher/ui/pages/instance/LogPage.h
@@ -0,0 +1,86 @@
+/* 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 <QWidget>
+
+#include "BaseInstance.h"
+#include "launch/LaunchTask.h"
+#include "ui/pages/BasePage.h"
+#include <Application.h>
+
+namespace Ui
+{
+class LogPage;
+}
+class QTextCharFormat;
+class LogFormatProxyModel;
+
+class LogPage : public QWidget, public BasePage
+{
+ Q_OBJECT
+
+public:
+ explicit LogPage(InstancePtr instance, QWidget *parent = 0);
+ virtual ~LogPage();
+ virtual QString displayName() const override
+ {
+ return tr("Minecraft Log");
+ }
+ virtual QIcon icon() const override
+ {
+ return APPLICATION->getThemedIcon("log");
+ }
+ virtual QString id() const override
+ {
+ return "console";
+ }
+ virtual bool apply() override;
+ virtual QString helpPage() const override
+ {
+ return "Minecraft-Logs";
+ }
+ virtual bool shouldDisplay() const override;
+
+private slots:
+ void on_btnPaste_clicked();
+ void on_btnCopy_clicked();
+ void on_btnClear_clicked();
+ void on_btnBottom_clicked();
+
+ void on_trackLogCheckbox_clicked(bool checked);
+ void on_wrapCheckbox_clicked(bool checked);
+
+ void on_findButton_clicked();
+ void findActivated();
+ void findNextActivated();
+ void findPreviousActivated();
+
+ void onInstanceLaunchTaskChanged(shared_qobject_ptr<LaunchTask> proc);
+
+private:
+ void modelStateToUI();
+ void UIToModelState();
+ void setInstanceLaunchTaskChanged(shared_qobject_ptr<LaunchTask> proc, bool initial);
+
+private:
+ Ui::LogPage *ui;
+ InstancePtr m_instance;
+ shared_qobject_ptr<LaunchTask> m_process;
+
+ LogFormatProxyModel * m_proxy;
+ shared_qobject_ptr <LogModel> m_model;
+};
diff --git a/launcher/ui/pages/instance/LogPage.ui b/launcher/ui/pages/instance/LogPage.ui
new file mode 100644
index 00000000..ccfc1551
--- /dev/null
+++ b/launcher/ui/pages/instance/LogPage.ui
@@ -0,0 +1,182 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>LogPage</class>
+ <widget class="QWidget" name="LogPage">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>825</width>
+ <height>782</height>
+ </rect>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QTabWidget" name="tabWidget">
+ <property name="currentIndex">
+ <number>0</number>
+ </property>
+ <widget class="QWidget" name="tab">
+ <attribute name="title">
+ <string notr="true">Tab 1</string>
+ </attribute>
+ <layout class="QGridLayout" name="gridLayout">
+ <item row="1" column="0" colspan="5">
+ <widget class="LogView" name="text">
+ <property name="undoRedoEnabled">
+ <bool>false</bool>
+ </property>
+ <property name="readOnly">
+ <bool>true</bool>
+ </property>
+ <property name="plainText">
+ <string notr="true"/>
+ </property>
+ <property name="textInteractionFlags">
+ <set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
+ </property>
+ <property name="centerOnScroll">
+ <bool>false</bool>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="0" colspan="5">
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <item>
+ <widget class="QCheckBox" name="trackLogCheckbox">
+ <property name="text">
+ <string>Keep updating</string>
+ </property>
+ <property name="checked">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="wrapCheckbox">
+ <property name="text">
+ <string>Wrap lines</string>
+ </property>
+ <property name="checked">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QPushButton" name="btnCopy">
+ <property name="toolTip">
+ <string>Copy the whole log into the clipboard</string>
+ </property>
+ <property name="text">
+ <string>&amp;Copy</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="btnPaste">
+ <property name="toolTip">
+ <string>Upload the log to paste.ee - it will stay online for a month</string>
+ </property>
+ <property name="text">
+ <string>Upload</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="btnClear">
+ <property name="toolTip">
+ <string>Clear the log</string>
+ </property>
+ <property name="text">
+ <string>Clear</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item row="2" column="0">
+ <widget class="QLabel" name="label">
+ <property name="text">
+ <string>Search:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="2">
+ <widget class="QPushButton" name="findButton">
+ <property name="text">
+ <string>Find</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="1">
+ <widget class="QLineEdit" name="searchBar"/>
+ </item>
+ <item row="2" column="4">
+ <widget class="QPushButton" name="btnBottom">
+ <property name="toolTip">
+ <string>Scroll all the way to bottom</string>
+ </property>
+ <property name="text">
+ <string>Bottom</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="3">
+ <widget class="Line" name="line">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <customwidgets>
+ <customwidget>
+ <class>LogView</class>
+ <extends>QPlainTextEdit</extends>
+ <header>ui/widgets/LogView.h</header>
+ </customwidget>
+ </customwidgets>
+ <tabstops>
+ <tabstop>tabWidget</tabstop>
+ <tabstop>trackLogCheckbox</tabstop>
+ <tabstop>wrapCheckbox</tabstop>
+ <tabstop>btnCopy</tabstop>
+ <tabstop>btnPaste</tabstop>
+ <tabstop>btnClear</tabstop>
+ <tabstop>text</tabstop>
+ <tabstop>searchBar</tabstop>
+ <tabstop>findButton</tabstop>
+ </tabstops>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/launcher/ui/pages/instance/ModFolderPage.cpp b/launcher/ui/pages/instance/ModFolderPage.cpp
new file mode 100644
index 00000000..e63b1434
--- /dev/null
+++ b/launcher/ui/pages/instance/ModFolderPage.cpp
@@ -0,0 +1,366 @@
+/* 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 "ModFolderPage.h"
+#include "ui_ModFolderPage.h"
+
+#include <QMessageBox>
+#include <QEvent>
+#include <QKeyEvent>
+#include <QAbstractItemModel>
+#include <QMenu>
+#include <QSortFilterProxyModel>
+
+#include "Application.h"
+
+#include "ui/dialogs/CustomMessageBox.h"
+#include "ui/GuiUtil.h"
+
+#include "DesktopServices.h"
+
+#include "minecraft/mod/ModFolderModel.h"
+#include "minecraft/mod/Mod.h"
+#include "minecraft/VersionFilterData.h"
+#include "minecraft/PackProfile.h"
+
+#include "Version.h"
+
+namespace {
+ // FIXME: wasteful
+ void RemoveThePrefix(QString & string) {
+ QRegularExpression regex(QStringLiteral("^(([Tt][Hh][eE])|([Tt][eE][Hh])) +"));
+ string.remove(regex);
+ string = string.trimmed();
+ }
+}
+
+class ModSortProxy : public QSortFilterProxyModel
+{
+public:
+ explicit ModSortProxy(QObject *parent = 0) : QSortFilterProxyModel(parent)
+ {
+ }
+
+protected:
+ bool filterAcceptsRow(int source_row, const QModelIndex & source_parent) const override {
+ ModFolderModel *model = qobject_cast<ModFolderModel *>(sourceModel());
+ if(!model) {
+ return false;
+ }
+ const auto &mod = model->at(source_row);
+ if(mod.name().contains(filterRegExp())) {
+ return true;
+ }
+ if(mod.description().contains(filterRegExp())) {
+ return true;
+ }
+ for(auto & author: mod.authors()) {
+ if (author.contains(filterRegExp())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ bool lessThan(const QModelIndex & source_left, const QModelIndex & source_right) const override
+ {
+ ModFolderModel *model = qobject_cast<ModFolderModel *>(sourceModel());
+ if(
+ !model ||
+ !source_left.isValid() ||
+ !source_right.isValid() ||
+ source_left.column() != source_right.column()
+ ) {
+ return QSortFilterProxyModel::lessThan(source_left, source_right);
+ }
+
+ // we are now guaranteed to have two valid indexes in the same column... we love the provided invariants unconditionally and proceed.
+
+ auto column = (ModFolderModel::Columns) source_left.column();
+ bool invert = false;
+ switch(column) {
+ // GH-2550 - sort by enabled/disabled
+ case ModFolderModel::ActiveColumn: {
+ auto dataL = source_left.data(Qt::CheckStateRole).toBool();
+ auto dataR = source_right.data(Qt::CheckStateRole).toBool();
+ if(dataL != dataR) {
+ return dataL > dataR;
+ }
+ // fallthrough
+ invert = sortOrder() == Qt::DescendingOrder;
+ }
+ // GH-2722 - sort mod names in a way that discards "The" prefixes
+ case ModFolderModel::NameColumn: {
+ auto dataL = model->data(model->index(source_left.row(), ModFolderModel::NameColumn)).toString();
+ RemoveThePrefix(dataL);
+ auto dataR = model->data(model->index(source_right.row(), ModFolderModel::NameColumn)).toString();
+ RemoveThePrefix(dataR);
+
+ auto less = dataL.compare(dataR, sortCaseSensitivity());
+ if(less != 0) {
+ return invert ? (less > 0) : (less < 0);
+ }
+ // fallthrough
+ invert = sortOrder() == Qt::DescendingOrder;
+ }
+ // GH-2762 - sort versions by parsing them as versions
+ case ModFolderModel::VersionColumn: {
+ auto dataL = Version(model->data(model->index(source_left.row(), ModFolderModel::VersionColumn)).toString());
+ auto dataR = Version(model->data(model->index(source_right.row(), ModFolderModel::VersionColumn)).toString());
+ return invert ? (dataL > dataR) : (dataL < dataR);
+ }
+ default: {
+ return QSortFilterProxyModel::lessThan(source_left, source_right);
+ }
+ }
+ }
+};
+
+ModFolderPage::ModFolderPage(
+ BaseInstance *inst,
+ std::shared_ptr<ModFolderModel> mods,
+ QString id,
+ QString iconName,
+ QString displayName,
+ QString helpPage,
+ QWidget *parent
+) :
+ QMainWindow(parent),
+ ui(new Ui::ModFolderPage)
+{
+ ui->setupUi(this);
+ ui->actionsToolbar->insertSpacer(ui->actionView_configs);
+
+ m_inst = inst;
+ on_RunningState_changed(m_inst && m_inst->isRunning());
+ m_mods = mods;
+ m_id = id;
+ m_displayName = displayName;
+ m_iconName = iconName;
+ m_helpName = helpPage;
+ m_fileSelectionFilter = "%1 (*.zip *.jar)";
+ m_filterModel = new ModSortProxy(this);
+ m_filterModel->setDynamicSortFilter(true);
+ m_filterModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
+ m_filterModel->setSortCaseSensitivity(Qt::CaseInsensitive);
+ m_filterModel->setSourceModel(m_mods.get());
+ m_filterModel->setFilterKeyColumn(-1);
+ ui->modTreeView->setModel(m_filterModel);
+ ui->modTreeView->installEventFilter(this);
+ ui->modTreeView->sortByColumn(1, Qt::AscendingOrder);
+ ui->modTreeView->setContextMenuPolicy(Qt::CustomContextMenu);
+ connect(ui->modTreeView, &ModListView::customContextMenuRequested, this, &ModFolderPage::ShowContextMenu);
+ connect(ui->modTreeView, &ModListView::activated, this, &ModFolderPage::modItemActivated);
+
+ auto smodel = ui->modTreeView->selectionModel();
+ connect(smodel, &QItemSelectionModel::currentChanged, this, &ModFolderPage::modCurrent);
+ connect(ui->filterEdit, &QLineEdit::textChanged, this, &ModFolderPage::on_filterTextChanged);
+ connect(m_inst, &BaseInstance::runningStatusChanged, this, &ModFolderPage::on_RunningState_changed);
+}
+
+void ModFolderPage::modItemActivated(const QModelIndex&)
+{
+ if(!m_controlsEnabled) {
+ return;
+ }
+ auto selection = m_filterModel->mapSelectionToSource(ui->modTreeView->selectionModel()->selection());
+ m_mods->setModStatus(selection.indexes(), ModFolderModel::Toggle);
+}
+
+QMenu * ModFolderPage::createPopupMenu()
+{
+ QMenu* filteredMenu = QMainWindow::createPopupMenu();
+ filteredMenu->removeAction(ui->actionsToolbar->toggleViewAction() );
+ return filteredMenu;
+}
+
+void ModFolderPage::ShowContextMenu(const QPoint& pos)
+{
+ auto menu = ui->actionsToolbar->createContextMenu(this, tr("Context menu"));
+ menu->exec(ui->modTreeView->mapToGlobal(pos));
+ delete menu;
+}
+
+void ModFolderPage::openedImpl()
+{
+ m_mods->startWatching();
+}
+
+void ModFolderPage::closedImpl()
+{
+ m_mods->stopWatching();
+}
+
+void ModFolderPage::on_filterTextChanged(const QString& newContents)
+{
+ m_viewFilter = newContents;
+ m_filterModel->setFilterFixedString(m_viewFilter);
+}
+
+
+CoreModFolderPage::CoreModFolderPage(BaseInstance *inst, std::shared_ptr<ModFolderModel> mods,
+ QString id, QString iconName, QString displayName,
+ QString helpPage, QWidget *parent)
+ : ModFolderPage(inst, mods, id, iconName, displayName, helpPage, parent)
+{
+}
+
+ModFolderPage::~ModFolderPage()
+{
+ m_mods->stopWatching();
+ delete ui;
+}
+
+void ModFolderPage::on_RunningState_changed(bool running)
+{
+ if(m_controlsEnabled == !running) {
+ return;
+ }
+ m_controlsEnabled = !running;
+ ui->actionAdd->setEnabled(m_controlsEnabled);
+ ui->actionDisable->setEnabled(m_controlsEnabled);
+ ui->actionEnable->setEnabled(m_controlsEnabled);
+ ui->actionRemove->setEnabled(m_controlsEnabled);
+}
+
+bool ModFolderPage::shouldDisplay() const
+{
+ return true;
+}
+
+bool CoreModFolderPage::shouldDisplay() const
+{
+ if (ModFolderPage::shouldDisplay())
+ {
+ auto inst = dynamic_cast<MinecraftInstance *>(m_inst);
+ if (!inst)
+ return true;
+ auto version = inst->getPackProfile();
+ if (!version)
+ return true;
+ if(!version->getComponent("net.minecraftforge"))
+ {
+ return false;
+ }
+ if(!version->getComponent("net.minecraft"))
+ {
+ return false;
+ }
+ if(version->getComponent("net.minecraft")->getReleaseDateTime() < g_VersionFilterData.legacyCutoffDate)
+ {
+ return true;
+ }
+ }
+ return false;
+}
+
+bool ModFolderPage::modListFilter(QKeyEvent *keyEvent)
+{
+ switch (keyEvent->key())
+ {
+ case Qt::Key_Delete:
+ on_actionRemove_triggered();
+ return true;
+ case Qt::Key_Plus:
+ on_actionAdd_triggered();
+ return true;
+ default:
+ break;
+ }
+ return QWidget::eventFilter(ui->modTreeView, keyEvent);
+}
+
+bool ModFolderPage::eventFilter(QObject *obj, QEvent *ev)
+{
+ if (ev->type() != QEvent::KeyPress)
+ {
+ return QWidget::eventFilter(obj, ev);
+ }
+ QKeyEvent *keyEvent = static_cast<QKeyEvent *>(ev);
+ if (obj == ui->modTreeView)
+ return modListFilter(keyEvent);
+ return QWidget::eventFilter(obj, ev);
+}
+
+void ModFolderPage::on_actionAdd_triggered()
+{
+ if(!m_controlsEnabled) {
+ return;
+ }
+ auto list = GuiUtil::BrowseForFiles(
+ m_helpName,
+ tr("Select %1",
+ "Select whatever type of files the page contains. Example: 'Loader Mods'")
+ .arg(m_displayName),
+ m_fileSelectionFilter.arg(m_displayName), APPLICATION->settings()->get("CentralModsDir").toString(),
+ this->parentWidget());
+ if (!list.empty())
+ {
+ for (auto filename : list)
+ {
+ m_mods->installMod(filename);
+ }
+ }
+}
+
+void ModFolderPage::on_actionEnable_triggered()
+{
+ if(!m_controlsEnabled) {
+ return;
+ }
+ auto selection = m_filterModel->mapSelectionToSource(ui->modTreeView->selectionModel()->selection());
+ m_mods->setModStatus(selection.indexes(), ModFolderModel::Enable);
+}
+
+void ModFolderPage::on_actionDisable_triggered()
+{
+ if(!m_controlsEnabled) {
+ return;
+ }
+ auto selection = m_filterModel->mapSelectionToSource(ui->modTreeView->selectionModel()->selection());
+ m_mods->setModStatus(selection.indexes(), ModFolderModel::Disable);
+}
+
+void ModFolderPage::on_actionRemove_triggered()
+{
+ if(!m_controlsEnabled) {
+ return;
+ }
+ auto selection = m_filterModel->mapSelectionToSource(ui->modTreeView->selectionModel()->selection());
+ m_mods->deleteMods(selection.indexes());
+}
+
+void ModFolderPage::on_actionView_configs_triggered()
+{
+ DesktopServices::openDirectory(m_inst->instanceConfigFolder(), true);
+}
+
+void ModFolderPage::on_actionView_Folder_triggered()
+{
+ DesktopServices::openDirectory(m_mods->dir().absolutePath(), true);
+}
+
+void ModFolderPage::modCurrent(const QModelIndex &current, const QModelIndex &previous)
+{
+ if (!current.isValid())
+ {
+ ui->frame->clear();
+ return;
+ }
+ auto sourceCurrent = m_filterModel->mapToSource(current);
+ int row = sourceCurrent.row();
+ Mod &m = m_mods->operator[](row);
+ ui->frame->updateWithMod(m);
+}
diff --git a/launcher/ui/pages/instance/ModFolderPage.h b/launcher/ui/pages/instance/ModFolderPage.h
new file mode 100644
index 00000000..8ef7559b
--- /dev/null
+++ b/launcher/ui/pages/instance/ModFolderPage.h
@@ -0,0 +1,120 @@
+/* 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 <QMainWindow>
+
+#include "minecraft/MinecraftInstance.h"
+#include "ui/pages/BasePage.h"
+
+#include <Application.h>
+
+class ModFolderModel;
+namespace Ui
+{
+class ModFolderPage;
+}
+
+class ModFolderPage : public QMainWindow, public BasePage
+{
+ Q_OBJECT
+
+public:
+ explicit ModFolderPage(
+ BaseInstance *inst,
+ std::shared_ptr<ModFolderModel> mods,
+ QString id,
+ QString iconName,
+ QString displayName,
+ QString helpPage = "",
+ QWidget *parent = 0
+ );
+ virtual ~ModFolderPage();
+
+ void setFilter(const QString & filter)
+ {
+ m_fileSelectionFilter = filter;
+ }
+
+ virtual QString displayName() const override
+ {
+ return m_displayName;
+ }
+ virtual QIcon icon() const override
+ {
+ return APPLICATION->getThemedIcon(m_iconName);
+ }
+ virtual QString id() const override
+ {
+ return m_id;
+ }
+ virtual QString helpPage() const override
+ {
+ return m_helpName;
+ }
+ virtual bool shouldDisplay() const override;
+
+ virtual void openedImpl() override;
+ virtual void closedImpl() override;
+protected:
+ bool eventFilter(QObject *obj, QEvent *ev) override;
+ bool modListFilter(QKeyEvent *ev);
+ QMenu * createPopupMenu() override;
+
+protected:
+ BaseInstance *m_inst = nullptr;
+
+protected:
+ Ui::ModFolderPage *ui = nullptr;
+ std::shared_ptr<ModFolderModel> m_mods;
+ QSortFilterProxyModel *m_filterModel = nullptr;
+ QString m_iconName;
+ QString m_id;
+ QString m_displayName;
+ QString m_helpName;
+ QString m_fileSelectionFilter;
+ QString m_viewFilter;
+ bool m_controlsEnabled = true;
+
+public
+slots:
+ void modCurrent(const QModelIndex &current, const QModelIndex &previous);
+
+private
+slots:
+ void modItemActivated(const QModelIndex &index);
+ void on_filterTextChanged(const QString & newContents);
+ void on_RunningState_changed(bool running);
+ void on_actionAdd_triggered();
+ void on_actionRemove_triggered();
+ void on_actionEnable_triggered();
+ void on_actionDisable_triggered();
+ void on_actionView_Folder_triggered();
+ void on_actionView_configs_triggered();
+ void ShowContextMenu(const QPoint &pos);
+};
+
+class CoreModFolderPage : public ModFolderPage
+{
+public:
+ explicit CoreModFolderPage(BaseInstance *inst, std::shared_ptr<ModFolderModel> mods, QString id,
+ QString iconName, QString displayName, QString helpPage = "",
+ QWidget *parent = 0);
+ virtual ~CoreModFolderPage()
+ {
+ }
+ virtual bool shouldDisplay() const;
+};
diff --git a/launcher/ui/pages/instance/ModFolderPage.ui b/launcher/ui/pages/instance/ModFolderPage.ui
new file mode 100644
index 00000000..0fb51e84
--- /dev/null
+++ b/launcher/ui/pages/instance/ModFolderPage.ui
@@ -0,0 +1,164 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>ModFolderPage</class>
+ <widget class="QMainWindow" name="ModFolderPage">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>1042</width>
+ <height>501</height>
+ </rect>
+ </property>
+ <widget class="QWidget" name="centralwidget">
+ <layout class="QGridLayout" name="gridLayout">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item row="4" column="1" colspan="3">
+ <layout class="QGridLayout" name="gridLayout_2">
+ <item row="0" column="1">
+ <widget class="QLineEdit" name="filterEdit">
+ <property name="clearButtonEnabled">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="0">
+ <widget class="QLabel" name="filterLabel">
+ <property name="text">
+ <string>Filter:</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item row="2" column="1" colspan="3">
+ <widget class="MCModInfoFrame" name="frame">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1" colspan="3">
+ <widget class="ModListView" name="modTreeView">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="acceptDrops">
+ <bool>true</bool>
+ </property>
+ <property name="dragDropMode">
+ <enum>QAbstractItemView::DropOnly</enum>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <widget class="WideBar" name="actionsToolbar">
+ <property name="windowTitle">
+ <string>Actions</string>
+ </property>
+ <property name="toolButtonStyle">
+ <enum>Qt::ToolButtonTextOnly</enum>
+ </property>
+ <attribute name="toolBarArea">
+ <enum>RightToolBarArea</enum>
+ </attribute>
+ <attribute name="toolBarBreak">
+ <bool>false</bool>
+ </attribute>
+ <addaction name="actionAdd"/>
+ <addaction name="separator"/>
+ <addaction name="actionRemove"/>
+ <addaction name="actionEnable"/>
+ <addaction name="actionDisable"/>
+ <addaction name="actionView_configs"/>
+ <addaction name="actionView_Folder"/>
+ </widget>
+ <action name="actionAdd">
+ <property name="text">
+ <string>&amp;Add</string>
+ </property>
+ <property name="toolTip">
+ <string>Add mods</string>
+ </property>
+ </action>
+ <action name="actionRemove">
+ <property name="text">
+ <string>&amp;Remove</string>
+ </property>
+ <property name="toolTip">
+ <string>Remove selected mods</string>
+ </property>
+ </action>
+ <action name="actionEnable">
+ <property name="text">
+ <string>&amp;Enable</string>
+ </property>
+ <property name="toolTip">
+ <string>Enable selected mods</string>
+ </property>
+ </action>
+ <action name="actionDisable">
+ <property name="text">
+ <string>&amp;Disable</string>
+ </property>
+ <property name="toolTip">
+ <string>Disable selected mods</string>
+ </property>
+ </action>
+ <action name="actionView_configs">
+ <property name="text">
+ <string>View &amp;Configs</string>
+ </property>
+ <property name="toolTip">
+ <string>Open the 'config' folder in the system file manager.</string>
+ </property>
+ </action>
+ <action name="actionView_Folder">
+ <property name="text">
+ <string>View &amp;Folder</string>
+ </property>
+ </action>
+ </widget>
+ <customwidgets>
+ <customwidget>
+ <class>ModListView</class>
+ <extends>QTreeView</extends>
+ <header>ui/widgets/ModListView.h</header>
+ </customwidget>
+ <customwidget>
+ <class>MCModInfoFrame</class>
+ <extends>QFrame</extends>
+ <header>ui/widgets/MCModInfoFrame.h</header>
+ <container>1</container>
+ </customwidget>
+ <customwidget>
+ <class>WideBar</class>
+ <extends>QToolBar</extends>
+ <header>ui/widgets/WideBar.h</header>
+ </customwidget>
+ </customwidgets>
+ <tabstops>
+ <tabstop>modTreeView</tabstop>
+ <tabstop>filterEdit</tabstop>
+ </tabstops>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/launcher/ui/pages/instance/NotesPage.cpp b/launcher/ui/pages/instance/NotesPage.cpp
new file mode 100644
index 00000000..fa966c91
--- /dev/null
+++ b/launcher/ui/pages/instance/NotesPage.cpp
@@ -0,0 +1,21 @@
+#include "NotesPage.h"
+#include "ui_NotesPage.h"
+#include <QTabBar>
+
+NotesPage::NotesPage(BaseInstance *inst, QWidget *parent)
+ : QWidget(parent), ui(new Ui::NotesPage), m_inst(inst)
+{
+ ui->setupUi(this);
+ ui->noteEditor->setText(m_inst->notes());
+}
+
+NotesPage::~NotesPage()
+{
+ delete ui;
+}
+
+bool NotesPage::apply()
+{
+ m_inst->setNotes(ui->noteEditor->toPlainText());
+ return true;
+}
diff --git a/launcher/ui/pages/instance/NotesPage.h b/launcher/ui/pages/instance/NotesPage.h
new file mode 100644
index 00000000..539401ee
--- /dev/null
+++ b/launcher/ui/pages/instance/NotesPage.h
@@ -0,0 +1,60 @@
+/* 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 <QWidget>
+
+#include "BaseInstance.h"
+#include "ui/pages/BasePage.h"
+#include <Application.h>
+
+namespace Ui
+{
+class NotesPage;
+}
+
+class NotesPage : public QWidget, public BasePage
+{
+ Q_OBJECT
+
+public:
+ explicit NotesPage(BaseInstance *inst, QWidget *parent = 0);
+ virtual ~NotesPage();
+ virtual QString displayName() const override
+ {
+ return tr("Notes");
+ }
+ virtual QIcon icon() const override
+ {
+ auto icon = APPLICATION->getThemedIcon("notes");
+ if(icon.isNull())
+ icon = APPLICATION->getThemedIcon("news");
+ return icon;
+ }
+ virtual QString id() const override
+ {
+ return "notes";
+ }
+ virtual bool apply() override;
+ virtual QString helpPage() const override
+ {
+ return "Notes";
+ }
+
+private:
+ Ui::NotesPage *ui;
+ BaseInstance *m_inst;
+};
diff --git a/launcher/ui/pages/instance/NotesPage.ui b/launcher/ui/pages/instance/NotesPage.ui
new file mode 100644
index 00000000..67cb261c
--- /dev/null
+++ b/launcher/ui/pages/instance/NotesPage.ui
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>NotesPage</class>
+ <widget class="QWidget" name="NotesPage">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>731</width>
+ <height>538</height>
+ </rect>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QTextEdit" name="noteEditor">
+ <property name="verticalScrollBarPolicy">
+ <enum>Qt::ScrollBarAlwaysOn</enum>
+ </property>
+ <property name="tabChangesFocus">
+ <bool>true</bool>
+ </property>
+ <property name="acceptRichText">
+ <bool>false</bool>
+ </property>
+ <property name="textInteractionFlags">
+ <set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextEditable|Qt::TextEditorInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <tabstops>
+ <tabstop>noteEditor</tabstop>
+ </tabstops>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/launcher/ui/pages/instance/OtherLogsPage.cpp b/launcher/ui/pages/instance/OtherLogsPage.cpp
new file mode 100644
index 00000000..0131c5c1
--- /dev/null
+++ b/launcher/ui/pages/instance/OtherLogsPage.cpp
@@ -0,0 +1,314 @@
+/* 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 "OtherLogsPage.h"
+#include "ui_OtherLogsPage.h"
+
+#include <QMessageBox>
+
+#include "ui/GuiUtil.h"
+
+#include "RecursiveFileSystemWatcher.h"
+#include <GZip.h>
+#include <FileSystem.h>
+#include <QShortcut>
+
+OtherLogsPage::OtherLogsPage(QString path, IPathMatcher::Ptr fileFilter, QWidget *parent)
+ : QWidget(parent), ui(new Ui::OtherLogsPage), m_path(path), m_fileFilter(fileFilter),
+ m_watcher(new RecursiveFileSystemWatcher(this))
+{
+ ui->setupUi(this);
+ ui->tabWidget->tabBar()->hide();
+
+ m_watcher->setMatcher(fileFilter);
+ m_watcher->setRootDir(QDir::current().absoluteFilePath(m_path));
+
+ connect(m_watcher, &RecursiveFileSystemWatcher::filesChanged, this, &OtherLogsPage::populateSelectLogBox);
+ populateSelectLogBox();
+
+ auto findShortcut = new QShortcut(QKeySequence(QKeySequence::Find), this);
+ connect(findShortcut, &QShortcut::activated, this, &OtherLogsPage::findActivated);
+
+ auto findNextShortcut = new QShortcut(QKeySequence(QKeySequence::FindNext), this);
+ connect(findNextShortcut, &QShortcut::activated, this, &OtherLogsPage::findNextActivated);
+
+ auto findPreviousShortcut = new QShortcut(QKeySequence(QKeySequence::FindPrevious), this);
+ connect(findPreviousShortcut, &QShortcut::activated, this, &OtherLogsPage::findPreviousActivated);
+
+ connect(ui->searchBar, &QLineEdit::returnPressed, this, &OtherLogsPage::on_findButton_clicked);
+}
+
+OtherLogsPage::~OtherLogsPage()
+{
+ delete ui;
+}
+
+void OtherLogsPage::openedImpl()
+{
+ m_watcher->enable();
+}
+void OtherLogsPage::closedImpl()
+{
+ m_watcher->disable();
+}
+
+void OtherLogsPage::populateSelectLogBox()
+{
+ ui->selectLogBox->clear();
+ ui->selectLogBox->addItems(m_watcher->files());
+ if (m_currentFile.isEmpty())
+ {
+ setControlsEnabled(false);
+ ui->selectLogBox->setCurrentIndex(-1);
+ }
+ else
+ {
+ const int index = ui->selectLogBox->findText(m_currentFile);
+ if (index != -1)
+ {
+ ui->selectLogBox->setCurrentIndex(index);
+ setControlsEnabled(true);
+ }
+ else
+ {
+ setControlsEnabled(false);
+ }
+ }
+}
+
+void OtherLogsPage::on_selectLogBox_currentIndexChanged(const int index)
+{
+ QString file;
+ if (index != -1)
+ {
+ file = ui->selectLogBox->itemText(index);
+ }
+
+ if (file.isEmpty() || !QFile::exists(FS::PathCombine(m_path, file)))
+ {
+ m_currentFile = QString();
+ ui->text->clear();
+ setControlsEnabled(false);
+ }
+ else
+ {
+ m_currentFile = file;
+ on_btnReload_clicked();
+ setControlsEnabled(true);
+ }
+}
+
+void OtherLogsPage::on_btnReload_clicked()
+{
+ if(m_currentFile.isEmpty())
+ {
+ setControlsEnabled(false);
+ return;
+ }
+ QFile file(FS::PathCombine(m_path, m_currentFile));
+ if (!file.open(QFile::ReadOnly))
+ {
+ setControlsEnabled(false);
+ ui->btnReload->setEnabled(true); // allow reload
+ m_currentFile = QString();
+ QMessageBox::critical(this, tr("Error"), tr("Unable to open %1 for reading: %2")
+ .arg(m_currentFile, file.errorString()));
+ }
+ else
+ {
+ auto setPlainText = [&](const QString & text)
+ {
+ QString fontFamily = APPLICATION->settings()->get("ConsoleFont").toString();
+ bool conversionOk = false;
+ int fontSize = APPLICATION->settings()->get("ConsoleFontSize").toInt(&conversionOk);
+ if(!conversionOk)
+ {
+ fontSize = 11;
+ }
+ QTextDocument *doc = ui->text->document();
+ doc->setDefaultFont(QFont(fontFamily, fontSize));
+ ui->text->setPlainText(text);
+ };
+ auto showTooBig = [&]()
+ {
+ setPlainText(
+ tr("The file (%1) is too big. You may want to open it in a viewer optimized "
+ "for large files.").arg(file.fileName()));
+ };
+ if(file.size() > (1024ll * 1024ll * 12ll))
+ {
+ showTooBig();
+ return;
+ }
+ QString content;
+ if(file.fileName().endsWith(".gz"))
+ {
+ QByteArray temp;
+ if(!GZip::unzip(file.readAll(), temp))
+ {
+ setPlainText(
+ tr("The file (%1) is not readable.").arg(file.fileName()));
+ return;
+ }
+ content = QString::fromUtf8(temp);
+ }
+ else
+ {
+ content = QString::fromUtf8(file.readAll());
+ }
+ if (content.size() >= 50000000ll)
+ {
+ showTooBig();
+ return;
+ }
+ setPlainText(content);
+ }
+}
+
+void OtherLogsPage::on_btnPaste_clicked()
+{
+ GuiUtil::uploadPaste(ui->text->toPlainText(), this);
+}
+
+void OtherLogsPage::on_btnCopy_clicked()
+{
+ GuiUtil::setClipboardText(ui->text->toPlainText());
+}
+
+void OtherLogsPage::on_btnDelete_clicked()
+{
+ if(m_currentFile.isEmpty())
+ {
+ setControlsEnabled(false);
+ return;
+ }
+ if (QMessageBox::question(this, tr("Delete"),
+ tr("Do you really want to delete %1?").arg(m_currentFile),
+ QMessageBox::Yes, QMessageBox::No) == QMessageBox::No)
+ {
+ return;
+ }
+ QFile file(FS::PathCombine(m_path, m_currentFile));
+ if (!file.remove())
+ {
+ QMessageBox::critical(this, tr("Error"), tr("Unable to delete %1: %2")
+ .arg(m_currentFile, file.errorString()));
+ }
+}
+
+
+
+void OtherLogsPage::on_btnClean_clicked()
+{
+ auto toDelete = m_watcher->files();
+ if(toDelete.isEmpty())
+ {
+ return;
+ }
+ QMessageBox *messageBox = new QMessageBox(this);
+ messageBox->setWindowTitle(tr("Clean up"));
+ if(toDelete.size() > 5)
+ {
+ messageBox->setText(tr("Do you really want to delete all log files?"));
+ messageBox->setDetailedText(toDelete.join('\n'));
+ }
+ else
+ {
+ messageBox->setText(tr("Do you really want to delete these files?\n%1").arg(toDelete.join('\n')));
+ }
+ messageBox->setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel);
+ messageBox->setDefaultButton(QMessageBox::Ok);
+ messageBox->setTextInteractionFlags(Qt::TextSelectableByMouse);
+ messageBox->setIcon(QMessageBox::Question);
+ messageBox->setTextInteractionFlags(Qt::TextBrowserInteraction);
+
+ if (messageBox->exec() != QMessageBox::Ok)
+ {
+ return;
+ }
+ QStringList failed;
+ for(auto item: toDelete)
+ {
+ QFile file(FS::PathCombine(m_path, item));
+ if (!file.remove())
+ {
+ failed.push_back(item);
+ }
+ }
+ if(!failed.empty())
+ {
+ QMessageBox *messageBox = new QMessageBox(this);
+ messageBox->setWindowTitle(tr("Error"));
+ if(failed.size() > 5)
+ {
+ messageBox->setText(tr("Couldn't delete some files!"));
+ messageBox->setDetailedText(failed.join('\n'));
+ }
+ else
+ {
+ messageBox->setText(tr("Couldn't delete some files:\n%1").arg(failed.join('\n')));
+ }
+ messageBox->setStandardButtons(QMessageBox::Ok);
+ messageBox->setDefaultButton(QMessageBox::Ok);
+ messageBox->setTextInteractionFlags(Qt::TextSelectableByMouse);
+ messageBox->setIcon(QMessageBox::Critical);
+ messageBox->setTextInteractionFlags(Qt::TextBrowserInteraction);
+ messageBox->exec();
+ }
+}
+
+
+void OtherLogsPage::setControlsEnabled(const bool enabled)
+{
+ ui->btnReload->setEnabled(enabled);
+ ui->btnDelete->setEnabled(enabled);
+ ui->btnCopy->setEnabled(enabled);
+ ui->btnPaste->setEnabled(enabled);
+ ui->text->setEnabled(enabled);
+ ui->btnClean->setEnabled(enabled);
+}
+
+// FIXME: HACK, use LogView instead?
+static void findNext(QPlainTextEdit * _this, const QString& what, bool reverse)
+{
+ _this->find(what, reverse ? QTextDocument::FindFlag::FindBackward : QTextDocument::FindFlag(0));
+}
+
+void OtherLogsPage::on_findButton_clicked()
+{
+ auto modifiers = QApplication::keyboardModifiers();
+ bool reverse = modifiers & Qt::ShiftModifier;
+ findNext(ui->text, ui->searchBar->text(), reverse);
+}
+
+void OtherLogsPage::findNextActivated()
+{
+ findNext(ui->text, ui->searchBar->text(), false);
+}
+
+void OtherLogsPage::findPreviousActivated()
+{
+ findNext(ui->text, ui->searchBar->text(), true);
+}
+
+void OtherLogsPage::findActivated()
+{
+ // focus the search bar if it doesn't have focus
+ if (!ui->searchBar->hasFocus())
+ {
+ ui->searchBar->setFocus();
+ ui->searchBar->selectAll();
+ }
+}
diff --git a/launcher/ui/pages/instance/OtherLogsPage.h b/launcher/ui/pages/instance/OtherLogsPage.h
new file mode 100644
index 00000000..b2b2a91b
--- /dev/null
+++ b/launcher/ui/pages/instance/OtherLogsPage.h
@@ -0,0 +1,81 @@
+/* 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 <QWidget>
+
+#include "ui/pages/BasePage.h"
+#include <Application.h>
+#include <pathmatcher/IPathMatcher.h>
+
+namespace Ui
+{
+class OtherLogsPage;
+}
+
+class RecursiveFileSystemWatcher;
+
+class OtherLogsPage : public QWidget, public BasePage
+{
+ Q_OBJECT
+
+public:
+ explicit OtherLogsPage(QString path, IPathMatcher::Ptr fileFilter, QWidget *parent = 0);
+ ~OtherLogsPage();
+
+ QString id() const override
+ {
+ return "logs";
+ }
+ QString displayName() const override
+ {
+ return tr("Other logs");
+ }
+ QIcon icon() const override
+ {
+ return APPLICATION->getThemedIcon("log");
+ }
+ QString helpPage() const override
+ {
+ return "Minecraft-Logs";
+ }
+ void openedImpl() override;
+ void closedImpl() override;
+
+private slots:
+ void populateSelectLogBox();
+ void on_selectLogBox_currentIndexChanged(const int index);
+ void on_btnReload_clicked();
+ void on_btnPaste_clicked();
+ void on_btnCopy_clicked();
+ void on_btnDelete_clicked();
+ void on_btnClean_clicked();
+
+ void on_findButton_clicked();
+ void findActivated();
+ void findNextActivated();
+ void findPreviousActivated();
+
+private:
+ void setControlsEnabled(const bool enabled);
+
+private:
+ Ui::OtherLogsPage *ui;
+ QString m_path;
+ QString m_currentFile;
+ IPathMatcher::Ptr m_fileFilter;
+ RecursiveFileSystemWatcher *m_watcher;
+};
diff --git a/launcher/ui/pages/instance/OtherLogsPage.ui b/launcher/ui/pages/instance/OtherLogsPage.ui
new file mode 100644
index 00000000..56ff3b62
--- /dev/null
+++ b/launcher/ui/pages/instance/OtherLogsPage.ui
@@ -0,0 +1,150 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>OtherLogsPage</class>
+ <widget class="QWidget" name="OtherLogsPage">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>657</width>
+ <height>538</height>
+ </rect>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_2">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QTabWidget" name="tabWidget">
+ <property name="currentIndex">
+ <number>0</number>
+ </property>
+ <widget class="QWidget" name="tab">
+ <attribute name="title">
+ <string notr="true">Tab 1</string>
+ </attribute>
+ <layout class="QGridLayout" name="gridLayout_2">
+ <item row="2" column="1">
+ <widget class="QLineEdit" name="searchBar"/>
+ </item>
+ <item row="2" column="2">
+ <widget class="QPushButton" name="findButton">
+ <property name="text">
+ <string>Find</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0" colspan="4">
+ <widget class="QPlainTextEdit" name="text">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ <property name="verticalScrollBarPolicy">
+ <enum>Qt::ScrollBarAlwaysOn</enum>
+ </property>
+ <property name="readOnly">
+ <bool>true</bool>
+ </property>
+ <property name="textInteractionFlags">
+ <set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="0" colspan="4">
+ <layout class="QGridLayout" name="gridLayout">
+ <item row="3" column="1">
+ <widget class="QPushButton" name="btnCopy">
+ <property name="toolTip">
+ <string>Copy the whole log into the clipboard</string>
+ </property>
+ <property name="text">
+ <string>&amp;Copy</string>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="3">
+ <widget class="QPushButton" name="btnDelete">
+ <property name="toolTip">
+ <string>Clear the log</string>
+ </property>
+ <property name="text">
+ <string>Delete</string>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="2">
+ <widget class="QPushButton" name="btnPaste">
+ <property name="toolTip">
+ <string>Upload the log to paste.ee - it will stay online for a month</string>
+ </property>
+ <property name="text">
+ <string>Upload</string>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="4">
+ <widget class="QPushButton" name="btnClean">
+ <property name="toolTip">
+ <string>Clear the log</string>
+ </property>
+ <property name="text">
+ <string>Clean</string>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="0">
+ <widget class="QPushButton" name="btnReload">
+ <property name="text">
+ <string>Reload</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="0" colspan="5">
+ <widget class="QComboBox" name="selectLogBox">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item row="2" column="0">
+ <widget class="QLabel" name="label">
+ <property name="text">
+ <string>Search:</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <tabstops>
+ <tabstop>tabWidget</tabstop>
+ <tabstop>selectLogBox</tabstop>
+ <tabstop>btnReload</tabstop>
+ <tabstop>btnCopy</tabstop>
+ <tabstop>btnPaste</tabstop>
+ <tabstop>btnDelete</tabstop>
+ <tabstop>btnClean</tabstop>
+ <tabstop>text</tabstop>
+ <tabstop>searchBar</tabstop>
+ <tabstop>findButton</tabstop>
+ </tabstops>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/launcher/ui/pages/instance/ResourcePackPage.h b/launcher/ui/pages/instance/ResourcePackPage.h
new file mode 100644
index 00000000..1486bf52
--- /dev/null
+++ b/launcher/ui/pages/instance/ResourcePackPage.h
@@ -0,0 +1,23 @@
+#pragma once
+
+#include "ModFolderPage.h"
+#include "ui_ModFolderPage.h"
+
+class ResourcePackPage : public ModFolderPage
+{
+ Q_OBJECT
+public:
+ explicit ResourcePackPage(MinecraftInstance *instance, QWidget *parent = 0)
+ : ModFolderPage(instance, instance->resourcePackList(), "resourcepacks",
+ "resourcepacks", tr("Resource packs"), "Resource-packs", parent)
+ {
+ ui->actionView_configs->setVisible(false);
+ }
+ virtual ~ResourcePackPage() {}
+
+ virtual bool shouldDisplay() const override
+ {
+ return !m_inst->traits().contains("no-texturepacks") &&
+ !m_inst->traits().contains("texturepacks");
+ }
+};
diff --git a/launcher/ui/pages/instance/ScreenshotsPage.cpp b/launcher/ui/pages/instance/ScreenshotsPage.cpp
new file mode 100644
index 00000000..06c4379f
--- /dev/null
+++ b/launcher/ui/pages/instance/ScreenshotsPage.cpp
@@ -0,0 +1,423 @@
+#include "ScreenshotsPage.h"
+#include "ui_ScreenshotsPage.h"
+
+#include <QModelIndex>
+#include <QMutableListIterator>
+#include <QMap>
+#include <QSet>
+#include <QFileIconProvider>
+#include <QFileSystemModel>
+#include <QStyledItemDelegate>
+#include <QLineEdit>
+#include <QEvent>
+#include <QPainter>
+#include <QClipboard>
+#include <QKeyEvent>
+#include <QMenu>
+
+#include <Application.h>
+
+#include "ui/dialogs/ProgressDialog.h"
+#include "ui/dialogs/CustomMessageBox.h"
+
+#include "net/NetJob.h"
+#include "screenshots/ImgurUpload.h"
+#include "screenshots/ImgurAlbumCreation.h"
+#include "tasks/SequentialTask.h"
+
+#include "RWStorage.h"
+#include <FileSystem.h>
+#include <DesktopServices.h>
+
+typedef RWStorage<QString, QIcon> SharedIconCache;
+typedef std::shared_ptr<SharedIconCache> SharedIconCachePtr;
+
+class ThumbnailingResult : public QObject
+{
+ Q_OBJECT
+public slots:
+ inline void emitResultsReady(const QString &path) { emit resultsReady(path); }
+ inline void emitResultsFailed(const QString &path) { emit resultsFailed(path); }
+signals:
+ void resultsReady(const QString &path);
+ void resultsFailed(const QString &path);
+};
+
+class ThumbnailRunnable : public QRunnable
+{
+public:
+ ThumbnailRunnable(QString path, SharedIconCachePtr cache)
+ {
+ m_path = path;
+ m_cache = cache;
+ }
+ void run()
+ {
+ QFileInfo info(m_path);
+ if (info.isDir())
+ return;
+ if ((info.suffix().compare("png", Qt::CaseInsensitive) != 0))
+ return;
+ int tries = 5;
+ while (tries)
+ {
+ if (!m_cache->stale(m_path))
+ return;
+ QImage image(m_path);
+ if (image.isNull())
+ {
+ QThread::msleep(500);
+ tries--;
+ continue;
+ }
+ QImage small;
+ if (image.width() > image.height())
+ small = image.scaledToWidth(512).scaledToWidth(256, Qt::SmoothTransformation);
+ else
+ small = image.scaledToHeight(512).scaledToHeight(256, Qt::SmoothTransformation);
+ QPoint offset((256 - small.width()) / 2, (256 - small.height()) / 2);
+ QImage square(QSize(256, 256), QImage::Format_ARGB32);
+ square.fill(Qt::transparent);
+
+ QPainter painter(&square);
+ painter.drawImage(offset, small);
+ painter.end();
+
+ QIcon icon(QPixmap::fromImage(square));
+ m_cache->add(m_path, icon);
+ m_resultEmitter.emitResultsReady(m_path);
+ return;
+ }
+ m_resultEmitter.emitResultsFailed(m_path);
+ }
+ QString m_path;
+ SharedIconCachePtr m_cache;
+ ThumbnailingResult m_resultEmitter;
+};
+
+// this is about as elegant and well written as a bag of bricks with scribbles done by insane
+// asylum patients.
+class FilterModel : public QIdentityProxyModel
+{
+ Q_OBJECT
+public:
+ explicit FilterModel(QObject *parent = 0) : QIdentityProxyModel(parent)
+ {
+ m_thumbnailingPool.setMaxThreadCount(4);
+ m_thumbnailCache = std::make_shared<SharedIconCache>();
+ m_thumbnailCache->add("placeholder", APPLICATION->getThemedIcon("screenshot-placeholder"));
+ connect(&watcher, SIGNAL(fileChanged(QString)), SLOT(fileChanged(QString)));
+ // FIXME: the watched file set is not updated when files are removed
+ }
+ virtual ~FilterModel() { m_thumbnailingPool.waitForDone(500); }
+ virtual QVariant data(const QModelIndex &proxyIndex, int role = Qt::DisplayRole) const
+ {
+ auto model = sourceModel();
+ if (!model)
+ return QVariant();
+ if (role == Qt::DisplayRole || role == Qt::EditRole)
+ {
+ QVariant result = sourceModel()->data(mapToSource(proxyIndex), role);
+ return result.toString().remove(QRegExp("\\.png$"));
+ }
+ if (role == Qt::DecorationRole)
+ {
+ QVariant result =
+ sourceModel()->data(mapToSource(proxyIndex), QFileSystemModel::FilePathRole);
+ QString filePath = result.toString();
+ QIcon temp;
+ if (!watched.contains(filePath))
+ {
+ ((QFileSystemWatcher &)watcher).addPath(filePath);
+ ((QSet<QString> &)watched).insert(filePath);
+ }
+ if (m_thumbnailCache->get(filePath, temp))
+ {
+ return temp;
+ }
+ if (!m_failed.contains(filePath))
+ {
+ ((FilterModel *)this)->thumbnailImage(filePath);
+ }
+ return (m_thumbnailCache->get("placeholder"));
+ }
+ return sourceModel()->data(mapToSource(proxyIndex), role);
+ }
+ virtual bool setData(const QModelIndex &index, const QVariant &value,
+ int role = Qt::EditRole)
+ {
+ auto model = sourceModel();
+ if (!model)
+ return false;
+ if (role != Qt::EditRole)
+ return false;
+ // FIXME: this is a workaround for a bug in QFileSystemModel, where it doesn't
+ // sort after renames
+ {
+ ((QFileSystemModel *)model)->setNameFilterDisables(true);
+ ((QFileSystemModel *)model)->setNameFilterDisables(false);
+ }
+ return model->setData(mapToSource(index), value.toString() + ".png", role);
+ }
+
+private:
+ void thumbnailImage(QString path)
+ {
+ auto runnable = new ThumbnailRunnable(path, m_thumbnailCache);
+ connect(&(runnable->m_resultEmitter), SIGNAL(resultsReady(QString)),
+ SLOT(thumbnailReady(QString)));
+ connect(&(runnable->m_resultEmitter), SIGNAL(resultsFailed(QString)),
+ SLOT(thumbnailFailed(QString)));
+ ((QThreadPool &)m_thumbnailingPool).start(runnable);
+ }
+private slots:
+ void thumbnailReady(QString path) { emit layoutChanged(); }
+ void thumbnailFailed(QString path) { m_failed.insert(path); }
+ void fileChanged(QString filepath)
+ {
+ m_thumbnailCache->setStale(filepath);
+ thumbnailImage(filepath);
+ // reinsert the path...
+ watcher.removePath(filepath);
+ watcher.addPath(filepath);
+ }
+
+private:
+ SharedIconCachePtr m_thumbnailCache;
+ QThreadPool m_thumbnailingPool;
+ QSet<QString> m_failed;
+ QSet<QString> watched;
+ QFileSystemWatcher watcher;
+};
+
+class CenteredEditingDelegate : public QStyledItemDelegate
+{
+public:
+ explicit CenteredEditingDelegate(QObject *parent = 0) : QStyledItemDelegate(parent) {}
+ virtual ~CenteredEditingDelegate() {}
+ virtual QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option,
+ const QModelIndex &index) const
+ {
+ auto widget = QStyledItemDelegate::createEditor(parent, option, index);
+ auto foo = dynamic_cast<QLineEdit *>(widget);
+ if (foo)
+ {
+ foo->setAlignment(Qt::AlignHCenter);
+ foo->setFrame(true);
+ foo->setMaximumWidth(192);
+ }
+ return widget;
+ }
+};
+
+ScreenshotsPage::ScreenshotsPage(QString path, QWidget *parent)
+ : QMainWindow(parent), ui(new Ui::ScreenshotsPage)
+{
+ m_model.reset(new QFileSystemModel());
+ m_filterModel.reset(new FilterModel());
+ m_filterModel->setSourceModel(m_model.get());
+ m_model->setFilter(QDir::Files | QDir::Writable | QDir::Readable);
+ m_model->setReadOnly(false);
+ m_model->setNameFilters({"*.png"});
+ m_model->setNameFilterDisables(false);
+ m_folder = path;
+ m_valid = FS::ensureFolderPathExists(m_folder);
+
+ ui->setupUi(this);
+ ui->toolBar->insertSpacer(ui->actionView_Folder);
+
+ ui->listView->setIconSize(QSize(128, 128));
+ ui->listView->setGridSize(QSize(192, 160));
+ ui->listView->setSpacing(9);
+ // ui->listView->setUniformItemSizes(true);
+ ui->listView->setLayoutMode(QListView::Batched);
+ ui->listView->setViewMode(QListView::IconMode);
+ ui->listView->setResizeMode(QListView::Adjust);
+ ui->listView->installEventFilter(this);
+ ui->listView->setEditTriggers(0);
+ ui->listView->setItemDelegate(new CenteredEditingDelegate(this));
+ ui->listView->setContextMenuPolicy(Qt::CustomContextMenu);
+ connect(ui->listView, &QListView::customContextMenuRequested, this, &ScreenshotsPage::ShowContextMenu);
+ connect(ui->listView, SIGNAL(activated(QModelIndex)), SLOT(onItemActivated(QModelIndex)));
+}
+
+bool ScreenshotsPage::eventFilter(QObject *obj, QEvent *evt)
+{
+ if (obj != ui->listView)
+ return QWidget::eventFilter(obj, evt);
+ if (evt->type() != QEvent::KeyPress)
+ {
+ return QWidget::eventFilter(obj, evt);
+ }
+ QKeyEvent *keyEvent = static_cast<QKeyEvent *>(evt);
+ switch (keyEvent->key())
+ {
+ case Qt::Key_Delete:
+ on_actionDelete_triggered();
+ return true;
+ case Qt::Key_F2:
+ on_actionRename_triggered();
+ return true;
+ default:
+ break;
+ }
+ return QWidget::eventFilter(obj, evt);
+}
+
+ScreenshotsPage::~ScreenshotsPage()
+{
+ delete ui;
+}
+
+void ScreenshotsPage::ShowContextMenu(const QPoint& pos)
+{
+ auto menu = ui->toolBar->createContextMenu(this, tr("Context menu"));
+ menu->exec(ui->listView->mapToGlobal(pos));
+ delete menu;
+}
+
+QMenu * ScreenshotsPage::createPopupMenu()
+{
+ QMenu* filteredMenu = QMainWindow::createPopupMenu();
+ filteredMenu->removeAction( ui->toolBar->toggleViewAction() );
+ return filteredMenu;
+}
+
+void ScreenshotsPage::onItemActivated(QModelIndex index)
+{
+ if (!index.isValid())
+ return;
+ auto info = m_model->fileInfo(index);
+ QString fileName = info.absoluteFilePath();
+ DesktopServices::openFile(info.absoluteFilePath());
+}
+
+void ScreenshotsPage::on_actionView_Folder_triggered()
+{
+ DesktopServices::openDirectory(m_folder, true);
+}
+
+void ScreenshotsPage::on_actionUpload_triggered()
+{
+ auto selection = ui->listView->selectionModel()->selectedRows();
+ if (selection.isEmpty())
+ return;
+
+ QList<ScreenShot::Ptr> uploaded;
+ auto job = NetJob::Ptr(new NetJob("Screenshot Upload"));
+ if(selection.size() < 2)
+ {
+ auto item = selection.at(0);
+ auto info = m_model->fileInfo(item);
+ auto screenshot = std::make_shared<ScreenShot>(info);
+ job->addNetAction(ImgurUpload::make(screenshot));
+
+ m_uploadActive = true;
+ ProgressDialog dialog(this);
+ if(dialog.execWithTask(job.get()) != QDialog::Accepted)
+ {
+ CustomMessageBox::selectable(this, tr("Failed to upload screenshots!"),
+ tr("Unknown error"), QMessageBox::Warning)->exec();
+ }
+ else
+ {
+ auto link = screenshot->m_url;
+ QClipboard *clipboard = QApplication::clipboard();
+ clipboard->setText(link);
+ CustomMessageBox::selectable(
+ this,
+ tr("Upload finished"),
+ tr("The <a href=\"%1\">link to the uploaded screenshot</a> has been placed in your clipboard.")
+ .arg(link),
+ QMessageBox::Information
+ )->exec();
+ }
+
+ m_uploadActive = false;
+ return;
+ }
+
+ for (auto item : selection)
+ {
+ auto info = m_model->fileInfo(item);
+ auto screenshot = std::make_shared<ScreenShot>(info);
+ uploaded.push_back(screenshot);
+ job->addNetAction(ImgurUpload::make(screenshot));
+ }
+ SequentialTask task;
+ auto albumTask = NetJob::Ptr(new NetJob("Imgur Album Creation"));
+ auto imgurAlbum = ImgurAlbumCreation::make(uploaded);
+ albumTask->addNetAction(imgurAlbum);
+ task.addTask(job);
+ task.addTask(albumTask);
+ m_uploadActive = true;
+ ProgressDialog prog(this);
+ if (prog.execWithTask(&task) != QDialog::Accepted)
+ {
+ CustomMessageBox::selectable(this, tr("Failed to upload screenshots!"),
+ tr("Unknown error"), QMessageBox::Warning)->exec();
+ }
+ else
+ {
+ auto link = QString("https://imgur.com/a/%1").arg(imgurAlbum->id());
+ QClipboard *clipboard = QApplication::clipboard();
+ clipboard->setText(link);
+ CustomMessageBox::selectable(
+ this,
+ tr("Upload finished"),
+ tr("The <a href=\"%1\">link to the uploaded album</a> has been placed in your clipboard.") .arg(link),
+ QMessageBox::Information
+ )->exec();
+ }
+ m_uploadActive = false;
+}
+
+void ScreenshotsPage::on_actionDelete_triggered()
+{
+ auto mbox = CustomMessageBox::selectable(
+ this, tr("Are you sure?"), tr("This will delete all selected screenshots."),
+ QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No);
+ std::unique_ptr<QMessageBox> box(mbox);
+
+ if (box->exec() != QMessageBox::Yes)
+ return;
+
+ auto selected = ui->listView->selectionModel()->selectedIndexes();
+ for (auto item : selected)
+ {
+ m_model->remove(item);
+ }
+}
+
+void ScreenshotsPage::on_actionRename_triggered()
+{
+ auto selection = ui->listView->selectionModel()->selectedIndexes();
+ if (selection.isEmpty())
+ return;
+ ui->listView->edit(selection[0]);
+ // TODO: mass renaming
+}
+
+void ScreenshotsPage::openedImpl()
+{
+ if(!m_valid)
+ {
+ m_valid = FS::ensureFolderPathExists(m_folder);
+ }
+ if (m_valid)
+ {
+ QString path = QDir(m_folder).absolutePath();
+ auto idx = m_model->setRootPath(path);
+ if(idx.isValid())
+ {
+ ui->listView->setModel(m_filterModel.get());
+ ui->listView->setRootIndex(m_filterModel->mapFromSource(idx));
+ }
+ else
+ {
+ ui->listView->setModel(nullptr);
+ }
+ }
+}
+
+#include "ScreenshotsPage.moc"
diff --git a/launcher/ui/pages/instance/ScreenshotsPage.h b/launcher/ui/pages/instance/ScreenshotsPage.h
new file mode 100644
index 00000000..d2f44837
--- /dev/null
+++ b/launcher/ui/pages/instance/ScreenshotsPage.h
@@ -0,0 +1,89 @@
+/* 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 <QMainWindow>
+
+#include "ui/pages/BasePage.h"
+#include <Application.h>
+
+class QFileSystemModel;
+class QIdentityProxyModel;
+namespace Ui
+{
+class ScreenshotsPage;
+}
+
+struct ScreenShot;
+class ScreenshotList;
+class ImgurAlbumCreation;
+
+class ScreenshotsPage : public QMainWindow, public BasePage
+{
+ Q_OBJECT
+
+public:
+ explicit ScreenshotsPage(QString path, QWidget *parent = 0);
+ virtual ~ScreenshotsPage();
+
+ virtual void openedImpl() override;
+
+ enum
+ {
+ NothingDone = 0x42
+ };
+
+ virtual bool eventFilter(QObject *, QEvent *) override;
+ virtual QString displayName() const override
+ {
+ return tr("Screenshots");
+ }
+ virtual QIcon icon() const override
+ {
+ return APPLICATION->getThemedIcon("screenshots");
+ }
+ virtual QString id() const override
+ {
+ return "screenshots";
+ }
+ virtual QString helpPage() const override
+ {
+ return "Screenshots-management";
+ }
+ virtual bool apply() override
+ {
+ return !m_uploadActive;
+ }
+
+protected:
+ QMenu * createPopupMenu() override;
+
+private slots:
+ void on_actionUpload_triggered();
+ void on_actionDelete_triggered();
+ void on_actionRename_triggered();
+ void on_actionView_Folder_triggered();
+ void onItemActivated(QModelIndex);
+ void ShowContextMenu(const QPoint &pos);
+
+private:
+ Ui::ScreenshotsPage *ui;
+ std::shared_ptr<QFileSystemModel> m_model;
+ std::shared_ptr<QIdentityProxyModel> m_filterModel;
+ QString m_folder;
+ bool m_valid = false;
+ bool m_uploadActive = false;
+};
diff --git a/launcher/ui/pages/instance/ScreenshotsPage.ui b/launcher/ui/pages/instance/ScreenshotsPage.ui
new file mode 100644
index 00000000..ec461087
--- /dev/null
+++ b/launcher/ui/pages/instance/ScreenshotsPage.ui
@@ -0,0 +1,87 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>ScreenshotsPage</class>
+ <widget class="QMainWindow" name="ScreenshotsPage">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>800</width>
+ <height>600</height>
+ </rect>
+ </property>
+ <widget class="QWidget" name="centralwidget">
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QListView" name="listView">
+ <property name="selectionMode">
+ <enum>QAbstractItemView::ExtendedSelection</enum>
+ </property>
+ <property name="selectionBehavior">
+ <enum>QAbstractItemView::SelectRows</enum>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <widget class="WideBar" name="toolBar">
+ <property name="windowTitle">
+ <string>Actions</string>
+ </property>
+ <property name="toolButtonStyle">
+ <enum>Qt::ToolButtonTextOnly</enum>
+ </property>
+ <attribute name="toolBarArea">
+ <enum>RightToolBarArea</enum>
+ </attribute>
+ <attribute name="toolBarBreak">
+ <bool>false</bool>
+ </attribute>
+ <addaction name="actionUpload"/>
+ <addaction name="actionDelete"/>
+ <addaction name="actionRename"/>
+ <addaction name="actionView_Folder"/>
+ </widget>
+ <action name="actionUpload">
+ <property name="text">
+ <string>Upload</string>
+ </property>
+ </action>
+ <action name="actionDelete">
+ <property name="text">
+ <string>Delete</string>
+ </property>
+ </action>
+ <action name="actionRename">
+ <property name="text">
+ <string>Rename</string>
+ </property>
+ </action>
+ <action name="actionView_Folder">
+ <property name="text">
+ <string>View Folder</string>
+ </property>
+ </action>
+ </widget>
+ <customwidgets>
+ <customwidget>
+ <class>WideBar</class>
+ <extends>QToolBar</extends>
+ <header>ui/widgets/WideBar.h</header>
+ </customwidget>
+ </customwidgets>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/launcher/ui/pages/instance/ServersPage.cpp b/launcher/ui/pages/instance/ServersPage.cpp
new file mode 100644
index 00000000..8116d2bf
--- /dev/null
+++ b/launcher/ui/pages/instance/ServersPage.cpp
@@ -0,0 +1,768 @@
+#include "ServersPage.h"
+#include "ui_ServersPage.h"
+
+#include <FileSystem.h>
+#include <sstream>
+#include <io/stream_reader.h>
+#include <tag_string.h>
+#include <tag_primitive.h>
+#include <tag_list.h>
+#include <tag_compound.h>
+#include <minecraft/MinecraftInstance.h>
+
+#include <QFileSystemWatcher>
+#include <QMenu>
+
+static const int COLUMN_COUNT = 2; // 3 , TBD: latency and other nice things.
+
+struct Server
+{
+ // Types
+ enum class AcceptsTextures : int
+ {
+ ASK = 0,
+ ALWAYS = 1,
+ NEVER = 2
+ };
+
+ // Methods
+ Server()
+ {
+ m_name = QObject::tr("Minecraft Server");
+ }
+ Server(const QString & name, const QString & address)
+ {
+ m_name = name;
+ m_address = address;
+ }
+ Server(nbt::tag_compound& server)
+ {
+ std::string addressStr(server["ip"]);
+ m_address = QString::fromUtf8(addressStr.c_str());
+
+ std::string nameStr(server["name"]);
+ m_name = QString::fromUtf8(nameStr.c_str());
+
+ if(server["icon"])
+ {
+ std::string base64str(server["icon"]);
+ m_icon = QByteArray::fromBase64(base64str.c_str());
+ }
+
+ if(server.has_key("acceptTextures", nbt::tag_type::Byte))
+ {
+ bool value = server["acceptTextures"].as<nbt::tag_byte>().get();
+ if(value)
+ {
+ m_acceptsTextures = AcceptsTextures::ALWAYS;
+ }
+ else
+ {
+ m_acceptsTextures = AcceptsTextures::NEVER;
+ }
+ }
+ }
+
+ void serialize(nbt::tag_compound& server)
+ {
+ server.insert("name", m_name.trimmed().toUtf8().toStdString());
+ server.insert("ip", m_address.trimmed().toUtf8().toStdString());
+ if(m_icon.size())
+ {
+ server.insert("icon", m_icon.toBase64().toStdString());
+ }
+ if(m_acceptsTextures != AcceptsTextures::ASK)
+ {
+ server.insert("acceptTextures", nbt::tag_byte(m_acceptsTextures == AcceptsTextures::ALWAYS));
+ }
+ }
+
+ // Data - persistent and user changeable
+ QString m_name;
+ QString m_address;
+ AcceptsTextures m_acceptsTextures = AcceptsTextures::ASK;
+
+ // Data - persistent and automatically updated
+ QByteArray m_icon;
+
+ // Data - temporary
+ bool m_checked = false;
+ bool m_up = false;
+ QString m_motd; // https://mctools.org/motd-creator
+ int m_ping = 0;
+ int m_currentPlayers = 0;
+ int m_maxPlayers = 0;
+};
+
+static std::unique_ptr <nbt::tag_compound> parseServersDat(const QString& filename)
+{
+ try
+ {
+ QByteArray input = FS::read(filename);
+ std::istringstream foo(std::string(input.constData(), input.size()));
+ auto pair = nbt::io::read_compound(foo);
+
+ if(pair.first != "")
+ return nullptr;
+
+ if(pair.second == nullptr)
+ return nullptr;
+
+ return std::move(pair.second);
+ }
+ catch (...)
+ {
+ return nullptr;
+ }
+}
+
+static bool serializeServerDat(const QString& filename, nbt::tag_compound * levelInfo)
+{
+ try
+ {
+ if(!FS::ensureFilePathExists(filename))
+ {
+ return false;
+ }
+ std::ostringstream s;
+ nbt::io::write_tag("", *levelInfo, s);
+ QByteArray val(s.str().data(), (int) s.str().size() );
+ FS::write(filename, val);
+ return true;
+ }
+ catch (...)
+ {
+ return false;
+ }
+}
+
+class ServersModel: public QAbstractListModel
+{
+ Q_OBJECT
+public:
+ enum Roles
+ {
+ ServerPtrRole = Qt::UserRole,
+ };
+ explicit ServersModel(const QString &path, QObject *parent = 0)
+ : QAbstractListModel(parent)
+ {
+ m_path = path;
+ m_watcher = new QFileSystemWatcher(this);
+ connect(m_watcher, &QFileSystemWatcher::fileChanged, this, &ServersModel::fileChanged);
+ connect(m_watcher, &QFileSystemWatcher::directoryChanged, this, &ServersModel::dirChanged);
+ m_saveTimer.setSingleShot(true);
+ m_saveTimer.setInterval(5000);
+ connect(&m_saveTimer, &QTimer::timeout, this, &ServersModel::save_internal);
+ }
+ virtual ~ServersModel() {};
+
+ void observe()
+ {
+ if(m_observed)
+ {
+ return;
+ }
+ m_observed = true;
+
+ if(!m_loaded)
+ {
+ load();
+ }
+
+ updateFSObserver();
+ }
+
+ void unobserve()
+ {
+ if(!m_observed)
+ {
+ return;
+ }
+ m_observed = false;
+
+ updateFSObserver();
+ }
+
+ void lock()
+ {
+ if(m_locked)
+ {
+ return;
+ }
+ saveNow();
+
+ m_locked = true;
+ updateFSObserver();
+ }
+
+ void unlock()
+ {
+ if(!m_locked)
+ {
+ return;
+ }
+ m_locked = false;
+
+ updateFSObserver();
+ }
+
+ int addEmptyRow(int position)
+ {
+ if(m_locked)
+ {
+ return -1;
+ }
+ if(position < 0 || position >= rowCount())
+ {
+ position = rowCount();
+ }
+ beginInsertRows(QModelIndex(), position, position);
+ m_servers.insert(position, Server());
+ endInsertRows();
+ scheduleSave();
+ return position;
+ }
+
+ bool removeRow(int row)
+ {
+ if(m_locked)
+ {
+ return false;
+ }
+ if(row < 0 || row >= rowCount())
+ {
+ return false;
+ }
+ beginRemoveRows(QModelIndex(), row, row);
+ m_servers.removeAt(row);
+ endRemoveRows(); // does absolutely nothing, the selected server stays as the next line...
+ scheduleSave();
+ return true;
+ }
+
+ bool moveUp(int row)
+ {
+ if(m_locked)
+ {
+ return false;
+ }
+ if(row <= 0)
+ {
+ return false;
+ }
+ beginMoveRows(QModelIndex(), row, row, QModelIndex(), row - 1);
+ m_servers.swap(row-1, row);
+ endMoveRows();
+ scheduleSave();
+ return true;
+ }
+
+ bool moveDown(int row)
+ {
+ if(m_locked)
+ {
+ return false;
+ }
+ int count = rowCount();
+ if(row + 1 >= count)
+ {
+ return false;
+ }
+ beginMoveRows(QModelIndex(), row, row, QModelIndex(), row + 2);
+ m_servers.swap(row+1, row);
+ endMoveRows();
+ scheduleSave();
+ return true;
+ }
+
+ QVariant headerData(int section, Qt::Orientation orientation, int role) const override
+ {
+ if (section < 0 || section >= COLUMN_COUNT)
+ return QVariant();
+
+ if(role == Qt::DisplayRole)
+ {
+ switch(section)
+ {
+ case 0:
+ return tr("Name");
+ case 1:
+ return tr("Address");
+ case 2:
+ return tr("Latency");
+ }
+ }
+
+ return QAbstractListModel::headerData(section, orientation, role);
+ }
+
+ virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override
+ {
+ if (!index.isValid())
+ return QVariant();
+
+ int row = index.row();
+ int column = index.column();
+ if(column < 0 || column >= COLUMN_COUNT)
+ return QVariant();
+
+ if (row < 0 || row >= m_servers.size())
+ return QVariant();
+
+ switch(column)
+ {
+ case 0:
+ switch (role)
+ {
+ case Qt::DecorationRole:
+ {
+ auto & bytes = m_servers[row].m_icon;
+ if(bytes.size())
+ {
+ QPixmap px;
+ if(px.loadFromData(bytes))
+ return QIcon(px);
+ }
+ return APPLICATION->getThemedIcon("unknown_server");
+ }
+ case Qt::DisplayRole:
+ return m_servers[row].m_name;
+ case ServerPtrRole:
+ return QVariant::fromValue<void *>((void *)&m_servers[row]);
+ default:
+ return QVariant();
+ }
+ case 1:
+ switch (role)
+ {
+ case Qt::DisplayRole:
+ return m_servers[row].m_address;
+ default:
+ return QVariant();
+ }
+ case 2:
+ switch (role)
+ {
+ case Qt::DisplayRole:
+ return m_servers[row].m_ping;
+ default:
+ return QVariant();
+ }
+ default:
+ return QVariant();
+ }
+ }
+
+ virtual int rowCount(const QModelIndex &parent = QModelIndex()) const override
+ {
+ return m_servers.size();
+ }
+ int columnCount(const QModelIndex & parent) const override
+ {
+ return COLUMN_COUNT;
+ }
+
+ Server * at(int index)
+ {
+ if(index < 0 || index >= rowCount())
+ {
+ return nullptr;
+ }
+ return &m_servers[index];
+ }
+
+ void setName(int row, const QString & name)
+ {
+ if(m_locked)
+ {
+ return;
+ }
+ auto server = at(row);
+ if(!server || server->m_name == name)
+ {
+ return;
+ }
+ server->m_name = name;
+ emit dataChanged(index(row, 0), index(row, COLUMN_COUNT - 1));
+ scheduleSave();
+ }
+
+ void setAddress(int row, const QString & address)
+ {
+ if(m_locked)
+ {
+ return;
+ }
+ auto server = at(row);
+ if(!server || server->m_address == address)
+ {
+ return;
+ }
+ server->m_address = address;
+ emit dataChanged(index(row, 0), index(row, COLUMN_COUNT - 1));
+ scheduleSave();
+ }
+
+ void setAcceptsTextures(int row, Server::AcceptsTextures textures)
+ {
+ if(m_locked)
+ {
+ return;
+ }
+ auto server = at(row);
+ if(!server || server->m_acceptsTextures == textures)
+ {
+ return;
+ }
+ server->m_acceptsTextures = textures;
+ emit dataChanged(index(row, 0), index(row, COLUMN_COUNT - 1));
+ scheduleSave();
+ }
+
+ void load()
+ {
+ cancelSave();
+ beginResetModel();
+ QList<Server> servers;
+ auto serversDat = parseServersDat(serversPath());
+ if(serversDat)
+ {
+ auto &serversList = serversDat->at("servers").as<nbt::tag_list>();
+ for(auto iter = serversList.begin(); iter != serversList.end(); iter++)
+ {
+ auto & serverTag = (*iter).as<nbt::tag_compound>();
+ Server s(serverTag);
+ servers.append(s);
+ }
+ }
+ m_servers.swap(servers);
+ m_loaded = true;
+ endResetModel();
+ }
+
+ void saveNow()
+ {
+ if(saveIsScheduled())
+ {
+ save_internal();
+ }
+ }
+
+
+public slots:
+ void dirChanged(const QString& path)
+ {
+ qDebug() << "Changed:" << path;
+ load();
+ }
+ void fileChanged(const QString& path)
+ {
+ qDebug() << "Changed:" << path;
+ }
+
+private slots:
+ void save_internal()
+ {
+ cancelSave();
+ QString path = serversPath();
+ qDebug() << "Server list about to be saved to" << path;
+
+ nbt::tag_compound out;
+ nbt::tag_list list;
+ for(auto & server: m_servers)
+ {
+ nbt::tag_compound serverNbt;
+ server.serialize(serverNbt);
+ list.push_back(std::move(serverNbt));
+ }
+ out.insert("servers", nbt::value(std::move(list)));
+
+ if(!serializeServerDat(path, &out))
+ {
+ qDebug() << "Failed to save server list:" << path << "Will try again.";
+ scheduleSave();
+ }
+ }
+
+private:
+ void scheduleSave()
+ {
+ if(!m_loaded)
+ {
+ qDebug() << "Server list should never save if it didn't successfully load, path:" << m_path;
+ return;
+ }
+ if(!m_dirty)
+ {
+ m_dirty = true;
+ qDebug() << "Server list save is scheduled for" << m_path;
+ }
+ m_saveTimer.start();
+ }
+
+ void cancelSave()
+ {
+ m_dirty = false;
+ m_saveTimer.stop();
+ }
+
+ bool saveIsScheduled() const
+ {
+ return m_dirty;
+ }
+
+ void updateFSObserver()
+ {
+ bool observingFS = m_watcher->directories().contains(m_path);
+ if(m_observed && m_locked)
+ {
+ if(!observingFS)
+ {
+ qWarning() << "Will watch" << m_path;
+ if(!m_watcher->addPath(m_path))
+ {
+ qWarning() << "Failed to start watching" << m_path;
+ }
+ }
+ }
+ else
+ {
+ if(observingFS)
+ {
+ qWarning() << "Will stop watching" << m_path;
+ if(!m_watcher->removePath(m_path))
+ {
+ qWarning() << "Failed to stop watching" << m_path;
+ }
+ }
+ }
+ }
+
+ QString serversPath()
+ {
+ QFileInfo foo(FS::PathCombine(m_path, "servers.dat"));
+ return foo.filePath();
+ }
+
+private:
+ bool m_loaded = false;
+ bool m_locked = false;
+ bool m_observed = false;
+ bool m_dirty = false;
+ QString m_path;
+ QList<Server> m_servers;
+ QFileSystemWatcher *m_watcher = nullptr;
+ QTimer m_saveTimer;
+};
+
+ServersPage::ServersPage(InstancePtr inst, QWidget* parent)
+ : QMainWindow(parent), ui(new Ui::ServersPage)
+{
+ ui->setupUi(this);
+ m_inst = inst;
+ m_model = new ServersModel(inst->gameRoot(), this);
+ ui->serversView->setIconSize(QSize(64,64));
+ ui->serversView->setModel(m_model);
+ ui->serversView->setContextMenuPolicy(Qt::CustomContextMenu);
+ connect(ui->serversView, &QTreeView::customContextMenuRequested, this, &ServersPage::ShowContextMenu);
+
+ auto head = ui->serversView->header();
+ if(head->count())
+ {
+ head->setSectionResizeMode(0, QHeaderView::Stretch);
+ for(int i = 1; i < head->count(); i++)
+ {
+ head->setSectionResizeMode(i, QHeaderView::ResizeToContents);
+ }
+ }
+
+ auto selectionModel = ui->serversView->selectionModel();
+ connect(selectionModel, &QItemSelectionModel::currentChanged, this, &ServersPage::currentChanged);
+ connect(m_inst.get(), &MinecraftInstance::runningStatusChanged, this, &ServersPage::on_RunningState_changed);
+ connect(ui->nameLine, &QLineEdit::textEdited, this, &ServersPage::nameEdited);
+ connect(ui->addressLine, &QLineEdit::textEdited, this, &ServersPage::addressEdited);
+ connect(ui->resourceComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(resourceIndexChanged(int)));
+ connect(m_model, &QAbstractItemModel::rowsRemoved, this, &ServersPage::rowsRemoved);
+
+ m_locked = m_inst->isRunning();
+ if(m_locked)
+ {
+ m_model->lock();
+ }
+
+ updateState();
+}
+
+ServersPage::~ServersPage()
+{
+ m_model->saveNow();
+ delete ui;
+}
+
+void ServersPage::ShowContextMenu(const QPoint& pos)
+{
+ auto menu = ui->toolBar->createContextMenu(this, tr("Context menu"));
+ menu->exec(ui->serversView->mapToGlobal(pos));
+ delete menu;
+}
+
+QMenu * ServersPage::createPopupMenu()
+{
+ QMenu* filteredMenu = QMainWindow::createPopupMenu();
+ filteredMenu->removeAction( ui->toolBar->toggleViewAction() );
+ return filteredMenu;
+}
+
+void ServersPage::on_RunningState_changed(bool running)
+{
+ if(m_locked == running)
+ {
+ return;
+ }
+ m_locked = running;
+ if(m_locked)
+ {
+ m_model->lock();
+ }
+ else
+ {
+ m_model->unlock();
+ }
+ updateState();
+}
+
+void ServersPage::currentChanged(const QModelIndex &current, const QModelIndex &previous)
+{
+ int nextServer = -1;
+ if (!current.isValid())
+ {
+ nextServer = -1;
+ }
+ else
+ {
+ nextServer = current.row();
+ }
+ currentServer = nextServer;
+ updateState();
+}
+
+// WARNING: this is here because currentChanged is not accurate when removing rows. the current item needs to be fixed up after removal.
+void ServersPage::rowsRemoved(const QModelIndex& parent, int first, int last)
+{
+ if(currentServer < first)
+ {
+ // current was before the removal
+ return;
+ }
+ else if(currentServer >= first && currentServer <= last)
+ {
+ // current got removed...
+ return;
+ }
+ else
+ {
+ // current was past the removal
+ int count = last - first + 1;
+ currentServer -= count;
+ }
+}
+
+void ServersPage::nameEdited(const QString& name)
+{
+ m_model->setName(currentServer, name);
+}
+
+void ServersPage::addressEdited(const QString& address)
+{
+ m_model->setAddress(currentServer, address);
+}
+
+void ServersPage::resourceIndexChanged(int index)
+{
+ auto acceptsTextures = Server::AcceptsTextures(index);
+ m_model->setAcceptsTextures(currentServer, acceptsTextures);
+}
+
+void ServersPage::updateState()
+{
+ auto server = m_model->at(currentServer);
+
+ bool serverEditEnabled = server && !m_locked;
+ ui->addressLine->setEnabled(serverEditEnabled);
+ ui->nameLine->setEnabled(serverEditEnabled);
+ ui->resourceComboBox->setEnabled(serverEditEnabled);
+ ui->actionMove_Down->setEnabled(serverEditEnabled);
+ ui->actionMove_Up->setEnabled(serverEditEnabled);
+ ui->actionRemove->setEnabled(serverEditEnabled);
+ ui->actionJoin->setEnabled(serverEditEnabled);
+
+ if(server)
+ {
+ ui->addressLine->setText(server->m_address);
+ ui->nameLine->setText(server->m_name);
+ ui->resourceComboBox->setCurrentIndex(int(server->m_acceptsTextures));
+ }
+ else
+ {
+ ui->addressLine->setText(QString());
+ ui->nameLine->setText(QString());
+ ui->resourceComboBox->setCurrentIndex(0);
+ }
+
+ ui->actionAdd->setDisabled(m_locked);
+}
+
+void ServersPage::openedImpl()
+{
+ m_model->observe();
+}
+
+void ServersPage::closedImpl()
+{
+ m_model->unobserve();
+}
+
+void ServersPage::on_actionAdd_triggered()
+{
+ int position = m_model->addEmptyRow(currentServer + 1);
+ if(position < 0)
+ {
+ return;
+ }
+ // select the new row
+ ui->serversView->selectionModel()->setCurrentIndex(
+ m_model->index(position),
+ QItemSelectionModel::SelectCurrent | QItemSelectionModel::Clear | QItemSelectionModel::Rows
+ );
+ currentServer = position;
+}
+
+void ServersPage::on_actionRemove_triggered()
+{
+ m_model->removeRow(currentServer);
+}
+
+void ServersPage::on_actionMove_Up_triggered()
+{
+ if(m_model->moveUp(currentServer))
+ {
+ currentServer --;
+ }
+}
+
+void ServersPage::on_actionMove_Down_triggered()
+{
+ if(m_model->moveDown(currentServer))
+ {
+ currentServer ++;
+ }
+}
+
+void ServersPage::on_actionJoin_triggered()
+{
+ const auto &address = m_model->at(currentServer)->m_address;
+ APPLICATION->launch(m_inst, true, nullptr, std::make_shared<MinecraftServerTarget>(MinecraftServerTarget::parse(address)));
+}
+
+#include "ServersPage.moc"
diff --git a/launcher/ui/pages/instance/ServersPage.h b/launcher/ui/pages/instance/ServersPage.h
new file mode 100644
index 00000000..d91da2ae
--- /dev/null
+++ b/launcher/ui/pages/instance/ServersPage.h
@@ -0,0 +1,94 @@
+/* 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 <QMainWindow>
+#include <QString>
+
+#include "ui/pages/BasePage.h"
+#include <Application.h>
+
+namespace Ui
+{
+class ServersPage;
+}
+
+struct Server;
+class ServersModel;
+class MinecraftInstance;
+
+class ServersPage : public QMainWindow, public BasePage
+{
+ Q_OBJECT
+
+public:
+ explicit ServersPage(InstancePtr inst, QWidget *parent = 0);
+ virtual ~ServersPage();
+
+ void openedImpl() override;
+ void closedImpl() override;
+
+ virtual QString displayName() const override
+ {
+ return tr("Servers");
+ }
+ virtual QIcon icon() const override
+ {
+ return APPLICATION->getThemedIcon("unknown_server");
+ }
+ virtual QString id() const override
+ {
+ return "servers";
+ }
+ virtual QString helpPage() const override
+ {
+ return "Servers-management";
+ }
+
+protected:
+ QMenu * createPopupMenu() override;
+
+private:
+ void updateState();
+ void scheduleSave();
+ bool saveIsScheduled() const;
+
+private slots:
+ void currentChanged(const QModelIndex &current, const QModelIndex &previous);
+ void rowsRemoved(const QModelIndex &parent, int first, int last);
+
+ void on_actionAdd_triggered();
+ void on_actionRemove_triggered();
+ void on_actionMove_Up_triggered();
+ void on_actionMove_Down_triggered();
+ void on_actionJoin_triggered();
+
+ void on_RunningState_changed(bool running);
+
+ void nameEdited(const QString & name);
+ void addressEdited(const QString & address);
+ void resourceIndexChanged(int index);\
+
+ void ShowContextMenu(const QPoint &pos);
+
+private: // data
+ int currentServer = -1;
+ bool m_locked = true;
+ Ui::ServersPage *ui = nullptr;
+ ServersModel * m_model = nullptr;
+ InstancePtr m_inst = nullptr;
+};
+
diff --git a/launcher/ui/pages/instance/ServersPage.ui b/launcher/ui/pages/instance/ServersPage.ui
new file mode 100644
index 00000000..e8f79cf2
--- /dev/null
+++ b/launcher/ui/pages/instance/ServersPage.ui
@@ -0,0 +1,194 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>ServersPage</class>
+ <widget class="QMainWindow" name="ServersPage">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>1318</width>
+ <height>879</height>
+ </rect>
+ </property>
+ <widget class="QWidget" name="centralwidget">
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QTreeView" name="serversView">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="acceptDrops">
+ <bool>true</bool>
+ </property>
+ <property name="alternatingRowColors">
+ <bool>true</bool>
+ </property>
+ <property name="selectionMode">
+ <enum>QAbstractItemView::SingleSelection</enum>
+ </property>
+ <property name="selectionBehavior">
+ <enum>QAbstractItemView::SelectRows</enum>
+ </property>
+ <property name="iconSize">
+ <size>
+ <width>64</width>
+ <height>64</height>
+ </size>
+ </property>
+ <property name="rootIsDecorated">
+ <bool>false</bool>
+ </property>
+ <attribute name="headerStretchLastSection">
+ <bool>false</bool>
+ </attribute>
+ </widget>
+ </item>
+ <item>
+ <layout class="QGridLayout" name="gridLayout_2">
+ <property name="leftMargin">
+ <number>6</number>
+ </property>
+ <property name="rightMargin">
+ <number>6</number>
+ </property>
+ <item row="0" column="0">
+ <widget class="QLabel" name="nameLabel">
+ <property name="text">
+ <string>&amp;Name</string>
+ </property>
+ <property name="buddy">
+ <cstring>nameLine</cstring>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QLineEdit" name="nameLine"/>
+ </item>
+ <item row="1" column="0">
+ <widget class="QLabel" name="addressLabel">
+ <property name="text">
+ <string>Address</string>
+ </property>
+ <property name="buddy">
+ <cstring>addressLine</cstring>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QLineEdit" name="addressLine"/>
+ </item>
+ <item row="2" column="0">
+ <widget class="QLabel" name="resourcesLabel">
+ <property name="text">
+ <string>Reso&amp;urces</string>
+ </property>
+ <property name="buddy">
+ <cstring>resourceComboBox</cstring>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="1">
+ <widget class="QComboBox" name="resourceComboBox">
+ <item>
+ <property name="text">
+ <string>Ask to download</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Always download</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Never download</string>
+ </property>
+ </item>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ <widget class="WideBar" name="toolBar">
+ <property name="windowTitle">
+ <string>Actions</string>
+ </property>
+ <property name="allowedAreas">
+ <set>Qt::LeftToolBarArea|Qt::RightToolBarArea</set>
+ </property>
+ <property name="toolButtonStyle">
+ <enum>Qt::ToolButtonTextOnly</enum>
+ </property>
+ <property name="floatable">
+ <bool>false</bool>
+ </property>
+ <attribute name="toolBarArea">
+ <enum>RightToolBarArea</enum>
+ </attribute>
+ <attribute name="toolBarBreak">
+ <bool>false</bool>
+ </attribute>
+ <addaction name="actionAdd"/>
+ <addaction name="actionRemove"/>
+ <addaction name="actionMove_Up"/>
+ <addaction name="actionMove_Down"/>
+ <addaction name="actionJoin"/>
+ </widget>
+ <action name="actionAdd">
+ <property name="text">
+ <string>Add</string>
+ </property>
+ </action>
+ <action name="actionRemove">
+ <property name="text">
+ <string>Remove</string>
+ </property>
+ </action>
+ <action name="actionMove_Up">
+ <property name="text">
+ <string>Move Up</string>
+ </property>
+ </action>
+ <action name="actionMove_Down">
+ <property name="text">
+ <string>Move Down</string>
+ </property>
+ </action>
+ <action name="actionJoin">
+ <property name="text">
+ <string>Join</string>
+ </property>
+ </action>
+ </widget>
+ <customwidgets>
+ <customwidget>
+ <class>WideBar</class>
+ <extends>QToolBar</extends>
+ <header>ui/widgets/WideBar.h</header>
+ </customwidget>
+ </customwidgets>
+ <tabstops>
+ <tabstop>serversView</tabstop>
+ <tabstop>nameLine</tabstop>
+ <tabstop>addressLine</tabstop>
+ <tabstop>resourceComboBox</tabstop>
+ </tabstops>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/launcher/ui/pages/instance/ShaderPackPage.h b/launcher/ui/pages/instance/ShaderPackPage.h
new file mode 100644
index 00000000..36724992
--- /dev/null
+++ b/launcher/ui/pages/instance/ShaderPackPage.h
@@ -0,0 +1,22 @@
+#pragma once
+
+#include "ModFolderPage.h"
+#include "ui_ModFolderPage.h"
+
+class ShaderPackPage : public ModFolderPage
+{
+ Q_OBJECT
+public:
+ explicit ShaderPackPage(MinecraftInstance *instance, QWidget *parent = 0)
+ : ModFolderPage(instance, instance->shaderPackList(), "shaderpacks",
+ "shaderpacks", tr("Shader packs"), "Resource-packs", parent)
+ {
+ ui->actionView_configs->setVisible(false);
+ }
+ virtual ~ShaderPackPage() {}
+
+ virtual bool shouldDisplay() const override
+ {
+ return true;
+ }
+};
diff --git a/launcher/ui/pages/instance/TexturePackPage.h b/launcher/ui/pages/instance/TexturePackPage.h
new file mode 100644
index 00000000..3f04997d
--- /dev/null
+++ b/launcher/ui/pages/instance/TexturePackPage.h
@@ -0,0 +1,22 @@
+#pragma once
+
+#include "ModFolderPage.h"
+#include "ui_ModFolderPage.h"
+
+class TexturePackPage : public ModFolderPage
+{
+ Q_OBJECT
+public:
+ explicit TexturePackPage(MinecraftInstance *instance, QWidget *parent = 0)
+ : ModFolderPage(instance, instance->texturePackList(), "texturepacks", "resourcepacks",
+ tr("Texture packs"), "Texture-packs", parent)
+ {
+ ui->actionView_configs->setVisible(false);
+ }
+ virtual ~TexturePackPage() {}
+
+ virtual bool shouldDisplay() const override
+ {
+ return m_inst->traits().contains("texturepacks");
+ }
+};
diff --git a/launcher/ui/pages/instance/VersionPage.cpp b/launcher/ui/pages/instance/VersionPage.cpp
new file mode 100644
index 00000000..715059ff
--- /dev/null
+++ b/launcher/ui/pages/instance/VersionPage.cpp
@@ -0,0 +1,641 @@
+/* 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 "Application.h"
+
+#include <QMessageBox>
+#include <QLabel>
+#include <QEvent>
+#include <QKeyEvent>
+#include <QMenu>
+#include <QAbstractItemModel>
+#include <QMessageBox>
+#include <QListView>
+#include <QString>
+#include <QUrl>
+
+#include "VersionPage.h"
+#include "ui_VersionPage.h"
+
+#include "ui/dialogs/CustomMessageBox.h"
+#include "ui/dialogs/VersionSelectDialog.h"
+#include "ui/dialogs/NewComponentDialog.h"
+#include "ui/dialogs/ProgressDialog.h"
+
+#include "ui/GuiUtil.h"
+
+#include "minecraft/PackProfile.h"
+#include "minecraft/auth/AccountList.h"
+#include "minecraft/mod/Mod.h"
+#include "icons/IconList.h"
+#include "Exception.h"
+#include "Version.h"
+#include "DesktopServices.h"
+
+#include "meta/Index.h"
+#include "meta/VersionList.h"
+
+class IconProxy : public QIdentityProxyModel
+{
+ Q_OBJECT
+public:
+
+ IconProxy(QWidget *parentWidget) : QIdentityProxyModel(parentWidget)
+ {
+ connect(parentWidget, &QObject::destroyed, this, &IconProxy::widgetGone);
+ m_parentWidget = parentWidget;
+ }
+
+ virtual QVariant data(const QModelIndex &proxyIndex, int role = Qt::DisplayRole) const override
+ {
+ QVariant var = QIdentityProxyModel::data(proxyIndex, role);
+ int column = proxyIndex.column();
+ if(column == 0 && role == Qt::DecorationRole && m_parentWidget)
+ {
+ if(!var.isNull())
+ {
+ auto string = var.toString();
+ if(string == "warning")
+ {
+ return APPLICATION->getThemedIcon("status-yellow");
+ }
+ else if(string == "error")
+ {
+ return APPLICATION->getThemedIcon("status-bad");
+ }
+ }
+ return APPLICATION->getThemedIcon("status-good");
+ }
+ return var;
+ }
+private slots:
+ void widgetGone()
+ {
+ m_parentWidget = nullptr;
+ }
+
+private:
+ QWidget *m_parentWidget = nullptr;
+};
+
+QIcon VersionPage::icon() const
+{
+ return APPLICATION->icons()->getIcon(m_inst->iconKey());
+}
+bool VersionPage::shouldDisplay() const
+{
+ return true;
+}
+
+QMenu * VersionPage::createPopupMenu()
+{
+ QMenu* filteredMenu = QMainWindow::createPopupMenu();
+ filteredMenu->removeAction( ui->toolBar->toggleViewAction() );
+ return filteredMenu;
+}
+
+VersionPage::VersionPage(MinecraftInstance *inst, QWidget *parent)
+ : QMainWindow(parent), ui(new Ui::VersionPage), m_inst(inst)
+{
+ ui->setupUi(this);
+
+ ui->toolBar->insertSpacer(ui->actionReload);
+
+ m_profile = m_inst->getPackProfile();
+
+ reloadPackProfile();
+
+ auto proxy = new IconProxy(ui->packageView);
+ proxy->setSourceModel(m_profile.get());
+
+ m_filterModel = new QSortFilterProxyModel();
+ m_filterModel->setDynamicSortFilter(true);
+ m_filterModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
+ m_filterModel->setSortCaseSensitivity(Qt::CaseInsensitive);
+ m_filterModel->setSourceModel(proxy);
+ m_filterModel->setFilterKeyColumn(-1);
+
+ ui->packageView->setModel(m_filterModel);
+ ui->packageView->installEventFilter(this);
+ ui->packageView->setSelectionMode(QAbstractItemView::SingleSelection);
+ ui->packageView->setContextMenuPolicy(Qt::CustomContextMenu);
+
+ connect(ui->packageView->selectionModel(), &QItemSelectionModel::currentChanged, this, &VersionPage::versionCurrent);
+ auto smodel = ui->packageView->selectionModel();
+ connect(smodel, &QItemSelectionModel::currentChanged, this, &VersionPage::packageCurrent);
+
+ connect(m_profile.get(), &PackProfile::minecraftChanged, this, &VersionPage::updateVersionControls);
+ controlsEnabled = !m_inst->isRunning();
+ updateVersionControls();
+ preselect(0);
+ connect(m_inst, &BaseInstance::runningStatusChanged, this, &VersionPage::updateRunningStatus);
+ connect(ui->packageView, &ModListView::customContextMenuRequested, this, &VersionPage::showContextMenu);
+ connect(ui->filterEdit, &QLineEdit::textChanged, this, &VersionPage::onFilterTextChanged);
+}
+
+VersionPage::~VersionPage()
+{
+ delete ui;
+}
+
+void VersionPage::showContextMenu(const QPoint& pos)
+{
+ auto menu = ui->toolBar->createContextMenu(this, tr("Context menu"));
+ menu->exec(ui->packageView->mapToGlobal(pos));
+ delete menu;
+}
+
+void VersionPage::packageCurrent(const QModelIndex &current, const QModelIndex &previous)
+{
+ if (!current.isValid())
+ {
+ ui->frame->clear();
+ return;
+ }
+ int row = current.row();
+ auto patch = m_profile->getComponent(row);
+ auto severity = patch->getProblemSeverity();
+ switch(severity)
+ {
+ case ProblemSeverity::Warning:
+ ui->frame->setModText(tr("%1 possibly has issues.").arg(patch->getName()));
+ break;
+ case ProblemSeverity::Error:
+ ui->frame->setModText(tr("%1 has issues!").arg(patch->getName()));
+ break;
+ default:
+ case ProblemSeverity::None:
+ ui->frame->clear();
+ return;
+ }
+
+ auto &problems = patch->getProblems();
+ QString problemOut;
+ for (auto &problem: problems)
+ {
+ if(problem.m_severity == ProblemSeverity::Error)
+ {
+ problemOut += tr("Error: ");
+ }
+ else if(problem.m_severity == ProblemSeverity::Warning)
+ {
+ problemOut += tr("Warning: ");
+ }
+ problemOut += problem.m_description;
+ problemOut += "\n";
+ }
+ ui->frame->setModDescription(problemOut);
+}
+
+void VersionPage::updateRunningStatus(bool running)
+{
+ if(controlsEnabled == running) {
+ controlsEnabled = !running;
+ updateVersionControls();
+ }
+}
+
+void VersionPage::updateVersionControls()
+{
+ // FIXME: this is a dirty hack
+ auto minecraftVersion = Version(m_profile->getComponentVersion("net.minecraft"));
+
+ bool supportsFabric = minecraftVersion >= Version("1.14");
+ ui->actionInstall_Fabric->setEnabled(controlsEnabled && supportsFabric);
+
+ bool supportsForge = minecraftVersion <= Version("1.16.5");
+ ui->actionInstall_Forge->setEnabled(controlsEnabled && supportsForge);
+
+ bool supportsLiteLoader = minecraftVersion <= Version("1.12.2");
+ ui->actionInstall_LiteLoader->setEnabled(controlsEnabled && supportsLiteLoader);
+
+ updateButtons();
+}
+
+void VersionPage::updateButtons(int row)
+{
+ if(row == -1)
+ row = currentRow();
+ auto patch = m_profile->getComponent(row);
+ ui->actionRemove->setEnabled(controlsEnabled && patch && patch->isRemovable());
+ ui->actionMove_down->setEnabled(controlsEnabled && patch && patch->isMoveable());
+ ui->actionMove_up->setEnabled(controlsEnabled && patch && patch->isMoveable());
+ ui->actionChange_version->setEnabled(controlsEnabled && patch && patch->isVersionChangeable());
+ ui->actionEdit->setEnabled(controlsEnabled && patch && patch->isCustom());
+ ui->actionCustomize->setEnabled(controlsEnabled && patch && patch->isCustomizable());
+ ui->actionRevert->setEnabled(controlsEnabled && patch && patch->isRevertible());
+ ui->actionDownload_All->setEnabled(controlsEnabled);
+ ui->actionAdd_Empty->setEnabled(controlsEnabled);
+ ui->actionReload->setEnabled(controlsEnabled);
+ ui->actionInstall_mods->setEnabled(controlsEnabled);
+ ui->actionReplace_Minecraft_jar->setEnabled(controlsEnabled);
+ ui->actionAdd_to_Minecraft_jar->setEnabled(controlsEnabled);
+}
+
+bool VersionPage::reloadPackProfile()
+{
+ try
+ {
+ m_profile->reload(Net::Mode::Online);
+ return true;
+ }
+ catch (const Exception &e)
+ {
+ QMessageBox::critical(this, tr("Error"), e.cause());
+ return false;
+ }
+ catch (...)
+ {
+ QMessageBox::critical(
+ this, tr("Error"),
+ tr("Couldn't load the instance profile."));
+ return false;
+ }
+}
+
+void VersionPage::on_actionReload_triggered()
+{
+ reloadPackProfile();
+ m_container->refreshContainer();
+}
+
+void VersionPage::on_actionRemove_triggered()
+{
+ if (ui->packageView->currentIndex().isValid())
+ {
+ // FIXME: use actual model, not reloading.
+ if (!m_profile->remove(ui->packageView->currentIndex().row()))
+ {
+ QMessageBox::critical(this, tr("Error"), tr("Couldn't remove file"));
+ }
+ }
+ updateButtons();
+ reloadPackProfile();
+ m_container->refreshContainer();
+}
+
+void VersionPage::on_actionInstall_mods_triggered()
+{
+ if(m_container)
+ {
+ m_container->selectPage("mods");
+ }
+}
+
+void VersionPage::on_actionAdd_to_Minecraft_jar_triggered()
+{
+ auto list = GuiUtil::BrowseForFiles("jarmod", tr("Select jar mods"), tr("Minecraft.jar mods (*.zip *.jar)"), APPLICATION->settings()->get("CentralModsDir").toString(), this->parentWidget());
+ if(!list.empty())
+ {
+ m_profile->installJarMods(list);
+ }
+ updateButtons();
+}
+
+void VersionPage::on_actionReplace_Minecraft_jar_triggered()
+{
+ auto jarPath = GuiUtil::BrowseForFile("jar", tr("Select jar"), tr("Minecraft.jar replacement (*.jar)"), APPLICATION->settings()->get("CentralModsDir").toString(), this->parentWidget());
+ if(!jarPath.isEmpty())
+ {
+ m_profile->installCustomJar(jarPath);
+ }
+ updateButtons();
+}
+
+void VersionPage::on_actionMove_up_triggered()
+{
+ try
+ {
+ m_profile->move(currentRow(), PackProfile::MoveUp);
+ }
+ catch (const Exception &e)
+ {
+ QMessageBox::critical(this, tr("Error"), e.cause());
+ }
+ updateButtons();
+}
+
+void VersionPage::on_actionMove_down_triggered()
+{
+ try
+ {
+ m_profile->move(currentRow(), PackProfile::MoveDown);
+ }
+ catch (const Exception &e)
+ {
+ QMessageBox::critical(this, tr("Error"), e.cause());
+ }
+ updateButtons();
+}
+
+void VersionPage::on_actionChange_version_triggered()
+{
+ auto versionRow = currentRow();
+ if(versionRow == -1)
+ {
+ return;
+ }
+ auto patch = m_profile->getComponent(versionRow);
+ auto name = patch->getName();
+ auto list = patch->getVersionList();
+ if(!list)
+ {
+ return;
+ }
+ auto uid = list->uid();
+ // FIXME: this is a horrible HACK. Get version filtering information from the actual metadata...
+ if(uid == "net.minecraftforge")
+ {
+ on_actionInstall_Forge_triggered();
+ return;
+ }
+ else if (uid == "com.mumfrey.liteloader")
+ {
+ on_actionInstall_LiteLoader_triggered();
+ return;
+ }
+ VersionSelectDialog vselect(list.get(), tr("Change %1 version").arg(name), this);
+ if (uid == "net.fabricmc.intermediary")
+ {
+ vselect.setEmptyString(tr("No intermediary mappings versions are currently available."));
+ vselect.setEmptyErrorString(tr("Couldn't load or download the intermediary mappings version lists!"));
+ vselect.setExactFilter(BaseVersionList::ParentVersionRole, m_profile->getComponentVersion("net.minecraft"));
+ }
+ auto currentVersion = patch->getVersion();
+ if(!currentVersion.isEmpty())
+ {
+ vselect.setCurrentVersion(currentVersion);
+ }
+ if (!vselect.exec() || !vselect.selectedVersion())
+ return;
+
+ qDebug() << "Change" << uid << "to" << vselect.selectedVersion()->descriptor();
+ bool important = false;
+ if(uid == "net.minecraft")
+ {
+ important = true;
+ }
+ m_profile->setComponentVersion(uid, vselect.selectedVersion()->descriptor(), important);
+ m_profile->resolve(Net::Mode::Online);
+ m_container->refreshContainer();
+}
+
+void VersionPage::on_actionDownload_All_triggered()
+{
+ if (!APPLICATION->accounts()->anyAccountIsValid())
+ {
+ CustomMessageBox::selectable(
+ this, tr("Error"),
+ tr("MultiMC cannot download Minecraft or update instances unless you have at least "
+ "one account added.\nPlease add your Mojang or Minecraft account."),
+ QMessageBox::Warning)->show();
+ return;
+ }
+
+ auto updateTask = m_inst->createUpdateTask(Net::Mode::Online);
+ if (!updateTask)
+ {
+ return;
+ }
+ ProgressDialog tDialog(this);
+ connect(updateTask.get(), SIGNAL(failed(QString)), SLOT(onGameUpdateError(QString)));
+ // FIXME: unused return value
+ tDialog.execWithTask(updateTask.get());
+ updateButtons();
+ m_container->refreshContainer();
+}
+
+void VersionPage::on_actionInstall_Forge_triggered()
+{
+ auto vlist = APPLICATION->metadataIndex()->get("net.minecraftforge");
+ if(!vlist)
+ {
+ return;
+ }
+ VersionSelectDialog vselect(vlist.get(), tr("Select Forge version"), this);
+ vselect.setExactFilter(BaseVersionList::ParentVersionRole, m_profile->getComponentVersion("net.minecraft"));
+ vselect.setEmptyString(tr("No Forge versions are currently available for Minecraft ") + m_profile->getComponentVersion("net.minecraft"));
+ vselect.setEmptyErrorString(tr("Couldn't load or download the Forge version lists!"));
+
+ auto currentVersion = m_profile->getComponentVersion("net.minecraftforge");
+ if(!currentVersion.isEmpty())
+ {
+ vselect.setCurrentVersion(currentVersion);
+ }
+
+ if (vselect.exec() && vselect.selectedVersion())
+ {
+ auto vsn = vselect.selectedVersion();
+ m_profile->setComponentVersion("net.minecraftforge", vsn->descriptor());
+ m_profile->resolve(Net::Mode::Online);
+ // m_profile->installVersion();
+ preselect(m_profile->rowCount(QModelIndex())-1);
+ m_container->refreshContainer();
+ }
+}
+
+void VersionPage::on_actionInstall_Fabric_triggered()
+{
+ auto vlist = APPLICATION->metadataIndex()->get("net.fabricmc.fabric-loader");
+ if(!vlist)
+ {
+ return;
+ }
+ VersionSelectDialog vselect(vlist.get(), tr("Select Fabric Loader version"), this);
+ vselect.setEmptyString(tr("No Fabric Loader versions are currently available."));
+ vselect.setEmptyErrorString(tr("Couldn't load or download the Fabric Loader version lists!"));
+
+ auto currentVersion = m_profile->getComponentVersion("net.fabricmc.fabric-loader");
+ if(!currentVersion.isEmpty())
+ {
+ vselect.setCurrentVersion(currentVersion);
+ }
+
+ if (vselect.exec() && vselect.selectedVersion())
+ {
+ auto vsn = vselect.selectedVersion();
+ m_profile->setComponentVersion("net.fabricmc.fabric-loader", vsn->descriptor());
+ m_profile->resolve(Net::Mode::Online);
+ preselect(m_profile->rowCount(QModelIndex())-1);
+ m_container->refreshContainer();
+ }
+}
+
+void VersionPage::on_actionAdd_Empty_triggered()
+{
+ NewComponentDialog compdialog(QString(), QString(), this);
+ QStringList blacklist;
+ for(int i = 0; i < m_profile->rowCount(); i++)
+ {
+ auto comp = m_profile->getComponent(i);
+ blacklist.push_back(comp->getID());
+ }
+ compdialog.setBlacklist(blacklist);
+ if (compdialog.exec())
+ {
+ qDebug() << "name:" << compdialog.name();
+ qDebug() << "uid:" << compdialog.uid();
+ m_profile->installEmpty(compdialog.uid(), compdialog.name());
+ }
+}
+
+void VersionPage::on_actionInstall_LiteLoader_triggered()
+{
+ auto vlist = APPLICATION->metadataIndex()->get("com.mumfrey.liteloader");
+ if(!vlist)
+ {
+ return;
+ }
+ VersionSelectDialog vselect(vlist.get(), tr("Select LiteLoader version"), this);
+ vselect.setExactFilter(BaseVersionList::ParentVersionRole, m_profile->getComponentVersion("net.minecraft"));
+ vselect.setEmptyString(tr("No LiteLoader versions are currently available for Minecraft ") + m_profile->getComponentVersion("net.minecraft"));
+ vselect.setEmptyErrorString(tr("Couldn't load or download the LiteLoader version lists!"));
+
+ auto currentVersion = m_profile->getComponentVersion("com.mumfrey.liteloader");
+ if(!currentVersion.isEmpty())
+ {
+ vselect.setCurrentVersion(currentVersion);
+ }
+
+ if (vselect.exec() && vselect.selectedVersion())
+ {
+ auto vsn = vselect.selectedVersion();
+ m_profile->setComponentVersion("com.mumfrey.liteloader", vsn->descriptor());
+ m_profile->resolve(Net::Mode::Online);
+ // m_profile->installVersion(vselect.selectedVersion());
+ preselect(m_profile->rowCount(QModelIndex())-1);
+ m_container->refreshContainer();
+ }
+}
+
+void VersionPage::on_actionLibrariesFolder_triggered()
+{
+ DesktopServices::openDirectory(m_inst->getLocalLibraryPath(), true);
+}
+
+void VersionPage::on_actionMinecraftFolder_triggered()
+{
+ DesktopServices::openDirectory(m_inst->gameRoot(), true);
+}
+
+void VersionPage::versionCurrent(const QModelIndex &current, const QModelIndex &previous)
+{
+ currentIdx = current.row();
+ updateButtons(currentIdx);
+}
+
+void VersionPage::preselect(int row)
+{
+ if(row < 0)
+ {
+ row = 0;
+ }
+ if(row >= m_profile->rowCount(QModelIndex()))
+ {
+ row = m_profile->rowCount(QModelIndex()) - 1;
+ }
+ if(row < 0)
+ {
+ return;
+ }
+ auto model_index = m_profile->index(row);
+ ui->packageView->selectionModel()->select(model_index, QItemSelectionModel::SelectCurrent | QItemSelectionModel::Rows);
+ updateButtons(row);
+}
+
+void VersionPage::onGameUpdateError(QString error)
+{
+ CustomMessageBox::selectable(this, tr("Error updating instance"), error, QMessageBox::Warning)->show();
+}
+
+Component * VersionPage::current()
+{
+ auto row = currentRow();
+ if(row < 0)
+ {
+ return nullptr;
+ }
+ return m_profile->getComponent(row);
+}
+
+int VersionPage::currentRow()
+{
+ if (ui->packageView->selectionModel()->selectedRows().isEmpty())
+ {
+ return -1;
+ }
+ return ui->packageView->selectionModel()->selectedRows().first().row();
+}
+
+void VersionPage::on_actionCustomize_triggered()
+{
+ auto version = currentRow();
+ if(version == -1)
+ {
+ return;
+ }
+ auto patch = m_profile->getComponent(version);
+ if(!patch->getVersionFile())
+ {
+ // TODO: wait for the update task to finish here...
+ return;
+ }
+ if(!m_profile->customize(version))
+ {
+ // TODO: some error box here
+ }
+ updateButtons();
+ preselect(currentIdx);
+}
+
+void VersionPage::on_actionEdit_triggered()
+{
+ auto version = current();
+ if(!version)
+ {
+ return;
+ }
+ auto filename = version->getFilename();
+ if(!QFileInfo::exists(filename))
+ {
+ qWarning() << "file" << filename << "can't be opened for editing, doesn't exist!";
+ return;
+ }
+ APPLICATION->openJsonEditor(filename);
+}
+
+void VersionPage::on_actionRevert_triggered()
+{
+ auto version = currentRow();
+ if(version == -1)
+ {
+ return;
+ }
+ if(!m_profile->revertToBase(version))
+ {
+ // TODO: some error box here
+ }
+ updateButtons();
+ preselect(currentIdx);
+ m_container->refreshContainer();
+}
+
+void VersionPage::onFilterTextChanged(const QString &newContents)
+{
+ m_filterModel->setFilterFixedString(newContents);
+}
+
+#include "VersionPage.moc"
+
diff --git a/launcher/ui/pages/instance/VersionPage.h b/launcher/ui/pages/instance/VersionPage.h
new file mode 100644
index 00000000..b5ce4064
--- /dev/null
+++ b/launcher/ui/pages/instance/VersionPage.h
@@ -0,0 +1,104 @@
+/* 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 <QMainWindow>
+
+#include "minecraft/MinecraftInstance.h"
+#include "minecraft/PackProfile.h"
+#include "ui/pages/BasePage.h"
+
+namespace Ui
+{
+class VersionPage;
+}
+
+class VersionPage : public QMainWindow, public BasePage
+{
+ Q_OBJECT
+
+public:
+ explicit VersionPage(MinecraftInstance *inst, QWidget *parent = 0);
+ virtual ~VersionPage();
+ virtual QString displayName() const override
+ {
+ return tr("Version");
+ }
+ virtual QIcon icon() const override;
+ virtual QString id() const override
+ {
+ return "version";
+ }
+ virtual QString helpPage() const override
+ {
+ return "Instance-Version";
+ }
+ virtual bool shouldDisplay() const override;
+
+private slots:
+ void on_actionChange_version_triggered();
+ void on_actionInstall_Forge_triggered();
+ void on_actionInstall_Fabric_triggered();
+ void on_actionAdd_Empty_triggered();
+ void on_actionInstall_LiteLoader_triggered();
+ void on_actionReload_triggered();
+ void on_actionRemove_triggered();
+ void on_actionMove_up_triggered();
+ void on_actionMove_down_triggered();
+ void on_actionAdd_to_Minecraft_jar_triggered();
+ void on_actionReplace_Minecraft_jar_triggered();
+ void on_actionRevert_triggered();
+ void on_actionEdit_triggered();
+ void on_actionInstall_mods_triggered();
+ void on_actionCustomize_triggered();
+ void on_actionDownload_All_triggered();
+
+ void on_actionMinecraftFolder_triggered();
+ void on_actionLibrariesFolder_triggered();
+
+ void updateVersionControls();
+
+private:
+ Component * current();
+ int currentRow();
+ void updateButtons(int row = -1);
+ void preselect(int row = 0);
+ int doUpdate();
+
+protected:
+ QMenu * createPopupMenu() override;
+
+ /// FIXME: this shouldn't be necessary!
+ bool reloadPackProfile();
+
+private:
+ Ui::VersionPage *ui;
+ QSortFilterProxyModel *m_filterModel;
+ std::shared_ptr<PackProfile> m_profile;
+ MinecraftInstance *m_inst;
+ int currentIdx = 0;
+ bool controlsEnabled = false;
+
+public slots:
+ void versionCurrent(const QModelIndex &current, const QModelIndex &previous);
+
+private slots:
+ void updateRunningStatus(bool running);
+ void onGameUpdateError(QString error);
+ void packageCurrent(const QModelIndex &current, const QModelIndex &previous);
+ void showContextMenu(const QPoint &pos);
+ void onFilterTextChanged(const QString & newContents);
+};
diff --git a/launcher/ui/pages/instance/VersionPage.ui b/launcher/ui/pages/instance/VersionPage.ui
new file mode 100644
index 00000000..a4990ff3
--- /dev/null
+++ b/launcher/ui/pages/instance/VersionPage.ui
@@ -0,0 +1,285 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>VersionPage</class>
+ <widget class="QMainWindow" name="VersionPage">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>961</width>
+ <height>1091</height>
+ </rect>
+ </property>
+ <widget class="QWidget" name="centralwidget">
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <widget class="ModListView" name="packageView">
+ <property name="verticalScrollBarPolicy">
+ <enum>Qt::ScrollBarAlwaysOn</enum>
+ </property>
+ <property name="horizontalScrollBarPolicy">
+ <enum>Qt::ScrollBarAlwaysOff</enum>
+ </property>
+ <property name="sortingEnabled">
+ <bool>false</bool>
+ </property>
+ <property name="headerHidden">
+ <bool>false</bool>
+ </property>
+ <attribute name="headerVisible">
+ <bool>true</bool>
+ </attribute>
+ </widget>
+ </item>
+ <item>
+ <layout class="QGridLayout" name="gridLayout_2">
+ <item row="0" column="1">
+ <widget class="QLineEdit" name="filterEdit">
+ <property name="clearButtonEnabled">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="0">
+ <widget class="QLabel" name="filterLabel">
+ <property name="text">
+ <string>Filter:</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="MCModInfoFrame" name="frame">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ <widget class="WideBar" name="toolBar">
+ <property name="windowTitle">
+ <string>Actions</string>
+ </property>
+ <property name="allowedAreas">
+ <set>Qt::LeftToolBarArea|Qt::RightToolBarArea</set>
+ </property>
+ <property name="toolButtonStyle">
+ <enum>Qt::ToolButtonTextOnly</enum>
+ </property>
+ <property name="floatable">
+ <bool>false</bool>
+ </property>
+ <attribute name="toolBarArea">
+ <enum>RightToolBarArea</enum>
+ </attribute>
+ <attribute name="toolBarBreak">
+ <bool>false</bool>
+ </attribute>
+ <addaction name="actionChange_version"/>
+ <addaction name="actionMove_up"/>
+ <addaction name="actionMove_down"/>
+ <addaction name="actionRemove"/>
+ <addaction name="separator"/>
+ <addaction name="actionCustomize"/>
+ <addaction name="actionEdit"/>
+ <addaction name="actionRevert"/>
+ <addaction name="separator"/>
+ <addaction name="actionInstall_Forge"/>
+ <addaction name="actionInstall_Fabric"/>
+ <addaction name="actionInstall_LiteLoader"/>
+ <addaction name="actionInstall_mods"/>
+ <addaction name="separator"/>
+ <addaction name="actionAdd_to_Minecraft_jar"/>
+ <addaction name="actionReplace_Minecraft_jar"/>
+ <addaction name="actionAdd_Empty"/>
+ <addaction name="separator"/>
+ <addaction name="actionMinecraftFolder"/>
+ <addaction name="actionLibrariesFolder"/>
+ <addaction name="separator"/>
+ <addaction name="actionReload"/>
+ <addaction name="actionDownload_All"/>
+ </widget>
+ <action name="actionChange_version">
+ <property name="text">
+ <string>Change version</string>
+ </property>
+ <property name="toolTip">
+ <string>Change version of the selected package.</string>
+ </property>
+ </action>
+ <action name="actionMove_up">
+ <property name="text">
+ <string>Move up</string>
+ </property>
+ <property name="toolTip">
+ <string>Make the selected package apply sooner.</string>
+ </property>
+ </action>
+ <action name="actionMove_down">
+ <property name="text">
+ <string>Move down</string>
+ </property>
+ <property name="toolTip">
+ <string>Make the selected package apply later.</string>
+ </property>
+ </action>
+ <action name="actionRemove">
+ <property name="text">
+ <string>Remove</string>
+ </property>
+ <property name="toolTip">
+ <string>Remove selected package from the instance.</string>
+ </property>
+ </action>
+ <action name="actionCustomize">
+ <property name="text">
+ <string>Customize</string>
+ </property>
+ <property name="toolTip">
+ <string>Customize selected package.</string>
+ </property>
+ </action>
+ <action name="actionEdit">
+ <property name="text">
+ <string>Edit</string>
+ </property>
+ <property name="toolTip">
+ <string>Edit selected package.</string>
+ </property>
+ </action>
+ <action name="actionRevert">
+ <property name="text">
+ <string>Revert</string>
+ </property>
+ <property name="toolTip">
+ <string>Revert the selected package to default.</string>
+ </property>
+ </action>
+ <action name="actionInstall_Forge">
+ <property name="text">
+ <string>Install Forge</string>
+ </property>
+ <property name="toolTip">
+ <string>Install the Minecraft Forge package.</string>
+ </property>
+ </action>
+ <action name="actionInstall_Fabric">
+ <property name="text">
+ <string>Install Fabric</string>
+ </property>
+ <property name="toolTip">
+ <string>Install the Fabric Loader package.</string>
+ </property>
+ </action>
+ <action name="actionInstall_LiteLoader">
+ <property name="text">
+ <string>Install LiteLoader</string>
+ </property>
+ <property name="toolTip">
+ <string>Install the LiteLoader package.</string>
+ </property>
+ </action>
+ <action name="actionInstall_mods">
+ <property name="text">
+ <string>Install mods</string>
+ </property>
+ <property name="toolTip">
+ <string>Install normal mods.</string>
+ </property>
+ </action>
+ <action name="actionAdd_to_Minecraft_jar">
+ <property name="text">
+ <string>Add to Minecraft.jar</string>
+ </property>
+ <property name="toolTip">
+ <string>Add a mod into the Minecraft jar file.</string>
+ </property>
+ </action>
+ <action name="actionReplace_Minecraft_jar">
+ <property name="text">
+ <string>Replace Minecraft.jar</string>
+ </property>
+ </action>
+ <action name="actionAdd_Empty">
+ <property name="text">
+ <string>Add Empty</string>
+ </property>
+ <property name="toolTip">
+ <string>Add an empty custom package.</string>
+ </property>
+ </action>
+ <action name="actionReload">
+ <property name="text">
+ <string>Reload</string>
+ </property>
+ <property name="toolTip">
+ <string>Reload all packages.</string>
+ </property>
+ </action>
+ <action name="actionDownload_All">
+ <property name="text">
+ <string>Download All</string>
+ </property>
+ <property name="toolTip">
+ <string>Download the files needed to launch the instance now.</string>
+ </property>
+ </action>
+ <action name="actionMinecraftFolder">
+ <property name="text">
+ <string>Open .minecraft</string>
+ </property>
+ <property name="toolTip">
+ <string>Open the instance's .minecraft folder.</string>
+ </property>
+ </action>
+ <action name="actionLibrariesFolder">
+ <property name="text">
+ <string>Open libraries</string>
+ </property>
+ <property name="toolTip">
+ <string>Open the instance's local libraries folder.</string>
+ </property>
+ </action>
+ </widget>
+ <customwidgets>
+ <customwidget>
+ <class>ModListView</class>
+ <extends>QTreeView</extends>
+ <header>ui/widgets/ModListView.h</header>
+ </customwidget>
+ <customwidget>
+ <class>MCModInfoFrame</class>
+ <extends>QFrame</extends>
+ <header>ui/widgets/MCModInfoFrame.h</header>
+ <container>1</container>
+ </customwidget>
+ <customwidget>
+ <class>WideBar</class>
+ <extends>QToolBar</extends>
+ <header>ui/widgets/WideBar.h</header>
+ </customwidget>
+ </customwidgets>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/launcher/ui/pages/instance/WorldListPage.cpp b/launcher/ui/pages/instance/WorldListPage.cpp
new file mode 100644
index 00000000..d2bf63bd
--- /dev/null
+++ b/launcher/ui/pages/instance/WorldListPage.cpp
@@ -0,0 +1,412 @@
+/* Copyright 2015-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 "WorldListPage.h"
+#include "ui_WorldListPage.h"
+#include "minecraft/WorldList.h"
+
+#include <QEvent>
+#include <QMenu>
+#include <QKeyEvent>
+#include <QClipboard>
+#include <QMessageBox>
+#include <QTreeView>
+#include <QInputDialog>
+#include <QProcess>
+
+#include "tools/MCEditTool.h"
+#include "FileSystem.h"
+
+#include "ui/GuiUtil.h"
+#include "DesktopServices.h"
+
+#include "Application.h"
+
+
+class WorldListProxyModel : public QSortFilterProxyModel
+{
+ Q_OBJECT
+
+public:
+ WorldListProxyModel(QObject *parent) : QSortFilterProxyModel(parent) {}
+
+ virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const
+ {
+ QModelIndex sourceIndex = mapToSource(index);
+
+ if (index.column() == 0 && role == Qt::DecorationRole)
+ {
+ WorldList *worlds = qobject_cast<WorldList *>(sourceModel());
+ auto iconFile = worlds->data(sourceIndex, WorldList::IconFileRole).toString();
+ if(iconFile.isNull()) {
+ // NOTE: Minecraft uses the same placeholder for servers AND worlds
+ return APPLICATION->getThemedIcon("unknown_server");
+ }
+ return QIcon(iconFile);
+ }
+
+ return sourceIndex.data(role);
+ }
+};
+
+
+WorldListPage::WorldListPage(BaseInstance *inst, std::shared_ptr<WorldList> worlds, QWidget *parent)
+ : QMainWindow(parent), m_inst(inst), ui(new Ui::WorldListPage), m_worlds(worlds)
+{
+ ui->setupUi(this);
+
+ ui->toolBar->insertSpacer(ui->actionRefresh);
+
+ WorldListProxyModel * proxy = new WorldListProxyModel(this);
+ proxy->setSortCaseSensitivity(Qt::CaseInsensitive);
+ proxy->setSourceModel(m_worlds.get());
+ ui->worldTreeView->setSortingEnabled(true);
+ ui->worldTreeView->setModel(proxy);
+ ui->worldTreeView->installEventFilter(this);
+ ui->worldTreeView->setContextMenuPolicy(Qt::CustomContextMenu);
+ ui->worldTreeView->setIconSize(QSize(64,64));
+ connect(ui->worldTreeView, &QTreeView::customContextMenuRequested, this, &WorldListPage::ShowContextMenu);
+
+ auto head = ui->worldTreeView->header();
+ head->setSectionResizeMode(0, QHeaderView::Stretch);
+ head->setSectionResizeMode(1, QHeaderView::ResizeToContents);
+
+ connect(ui->worldTreeView->selectionModel(), &QItemSelectionModel::currentChanged, this, &WorldListPage::worldChanged);
+ worldChanged(QModelIndex(), QModelIndex());
+}
+
+void WorldListPage::openedImpl()
+{
+ m_worlds->startWatching();
+}
+
+void WorldListPage::closedImpl()
+{
+ m_worlds->stopWatching();
+}
+
+WorldListPage::~WorldListPage()
+{
+ m_worlds->stopWatching();
+ delete ui;
+}
+
+void WorldListPage::ShowContextMenu(const QPoint& pos)
+{
+ auto menu = ui->toolBar->createContextMenu(this, tr("Context menu"));
+ menu->exec(ui->worldTreeView->mapToGlobal(pos));
+ delete menu;
+}
+
+QMenu * WorldListPage::createPopupMenu()
+{
+ QMenu* filteredMenu = QMainWindow::createPopupMenu();
+ filteredMenu->removeAction( ui->toolBar->toggleViewAction() );
+ return filteredMenu;
+}
+
+bool WorldListPage::shouldDisplay() const
+{
+ return true;
+}
+
+bool WorldListPage::worldListFilter(QKeyEvent *keyEvent)
+{
+ switch (keyEvent->key())
+ {
+ case Qt::Key_Delete:
+ on_actionRemove_triggered();
+ return true;
+ default:
+ break;
+ }
+ return QWidget::eventFilter(ui->worldTreeView, keyEvent);
+}
+
+bool WorldListPage::eventFilter(QObject *obj, QEvent *ev)
+{
+ if (ev->type() != QEvent::KeyPress)
+ {
+ return QWidget::eventFilter(obj, ev);
+ }
+ QKeyEvent *keyEvent = static_cast<QKeyEvent *>(ev);
+ if (obj == ui->worldTreeView)
+ return worldListFilter(keyEvent);
+ return QWidget::eventFilter(obj, ev);
+}
+
+void WorldListPage::on_actionRemove_triggered()
+{
+ auto proxiedIndex = getSelectedWorld();
+
+ if(!proxiedIndex.isValid())
+ return;
+
+ auto result = QMessageBox::question(this,
+ tr("Are you sure?"),
+ tr("This will remove the selected world permenantly.\n"
+ "The world will be gone forever (A LONG TIME).\n"
+ "\n"
+ "Do you want to continue?"));
+ if(result != QMessageBox::Yes)
+ {
+ return;
+ }
+ m_worlds->stopWatching();
+ m_worlds->deleteWorld(proxiedIndex.row());
+ m_worlds->startWatching();
+}
+
+void WorldListPage::on_actionView_Folder_triggered()
+{
+ DesktopServices::openDirectory(m_worlds->dir().absolutePath(), true);
+}
+
+void WorldListPage::on_actionDatapacks_triggered()
+{
+ QModelIndex index = getSelectedWorld();
+
+ if (!index.isValid())
+ {
+ return;
+ }
+
+ if(!worldSafetyNagQuestion())
+ return;
+
+ auto fullPath = m_worlds->data(index, WorldList::FolderRole).toString();
+
+ DesktopServices::openDirectory(FS::PathCombine(fullPath, "datapacks"), true);
+}
+
+
+void WorldListPage::on_actionReset_Icon_triggered()
+{
+ auto proxiedIndex = getSelectedWorld();
+
+ if(!proxiedIndex.isValid())
+ return;
+
+ if(m_worlds->resetIcon(proxiedIndex.row())) {
+ ui->actionReset_Icon->setEnabled(false);
+ }
+}
+
+
+QModelIndex WorldListPage::getSelectedWorld()
+{
+ auto index = ui->worldTreeView->selectionModel()->currentIndex();
+
+ auto proxy = (QSortFilterProxyModel *) ui->worldTreeView->model();
+ return proxy->mapToSource(index);
+}
+
+void WorldListPage::on_actionCopy_Seed_triggered()
+{
+ QModelIndex index = getSelectedWorld();
+
+ if (!index.isValid())
+ {
+ return;
+ }
+ int64_t seed = m_worlds->data(index, WorldList::SeedRole).toLongLong();
+ APPLICATION->clipboard()->setText(QString::number(seed));
+}
+
+void WorldListPage::on_actionMCEdit_triggered()
+{
+ if(m_mceditStarting)
+ return;
+
+ auto mcedit = APPLICATION->mcedit();
+
+ const QString mceditPath = mcedit->path();
+
+ QModelIndex index = getSelectedWorld();
+
+ if (!index.isValid())
+ {
+ return;
+ }
+
+ if(!worldSafetyNagQuestion())
+ return;
+
+ auto fullPath = m_worlds->data(index, WorldList::FolderRole).toString();
+
+ auto program = mcedit->getProgramPath();
+ if(program.size())
+ {
+#ifdef Q_OS_WIN32
+ if(!QProcess::startDetached(program, {fullPath}, mceditPath))
+ {
+ mceditError();
+ }
+#else
+ m_mceditProcess.reset(new LoggedProcess());
+ m_mceditProcess->setDetachable(true);
+ connect(m_mceditProcess.get(), &LoggedProcess::stateChanged, this, &WorldListPage::mceditState);
+ m_mceditProcess->start(program, {fullPath});
+ m_mceditProcess->setWorkingDirectory(mceditPath);
+ m_mceditStarting = true;
+#endif
+ }
+ else
+ {
+ QMessageBox::warning(
+ this->parentWidget(),
+ tr("No MCEdit found or set up!"),
+ tr("You do not have MCEdit set up or it was moved.\nYou can set it up in the global settings.")
+ );
+ }
+}
+
+void WorldListPage::mceditError()
+{
+ QMessageBox::warning(
+ this->parentWidget(),
+ tr("MCEdit failed to start!"),
+ tr("MCEdit failed to start.\nIt may be necessary to reinstall it.")
+ );
+}
+
+void WorldListPage::mceditState(LoggedProcess::State state)
+{
+ bool failed = false;
+ switch(state)
+ {
+ case LoggedProcess::NotRunning:
+ case LoggedProcess::Starting:
+ return;
+ case LoggedProcess::FailedToStart:
+ case LoggedProcess::Crashed:
+ case LoggedProcess::Aborted:
+ {
+ failed = true;
+ }
+ case LoggedProcess::Running:
+ case LoggedProcess::Finished:
+ {
+ m_mceditStarting = false;
+ break;
+ }
+ }
+ if(failed)
+ {
+ mceditError();
+ }
+}
+
+void WorldListPage::worldChanged(const QModelIndex &current, const QModelIndex &previous)
+{
+ QModelIndex index = getSelectedWorld();
+ bool enable = index.isValid();
+ ui->actionCopy_Seed->setEnabled(enable);
+ ui->actionMCEdit->setEnabled(enable);
+ ui->actionRemove->setEnabled(enable);
+ ui->actionCopy->setEnabled(enable);
+ ui->actionRename->setEnabled(enable);
+ ui->actionDatapacks->setEnabled(enable);
+ bool hasIcon = !index.data(WorldList::IconFileRole).isNull();
+ ui->actionReset_Icon->setEnabled(enable && hasIcon);
+}
+
+void WorldListPage::on_actionAdd_triggered()
+{
+ auto list = GuiUtil::BrowseForFiles(
+ displayName(),
+ tr("Select a Minecraft world zip"),
+ tr("Minecraft World Zip File (*.zip)"), QString(), this->parentWidget());
+ if (!list.empty())
+ {
+ m_worlds->stopWatching();
+ for (auto filename : list)
+ {
+ m_worlds->installWorld(QFileInfo(filename));
+ }
+ m_worlds->startWatching();
+ }
+}
+
+bool WorldListPage::isWorldSafe(QModelIndex)
+{
+ return !m_inst->isRunning();
+}
+
+bool WorldListPage::worldSafetyNagQuestion()
+{
+ if(!isWorldSafe(getSelectedWorld()))
+ {
+ auto result = QMessageBox::question(this, tr("Copy World"), tr("Changing a world while Minecraft is running is potentially unsafe.\nDo you wish to proceed?"));
+ if(result == QMessageBox::No)
+ {
+ return false;
+ }
+ }
+ return true;
+}
+
+
+void WorldListPage::on_actionCopy_triggered()
+{
+ QModelIndex index = getSelectedWorld();
+ if (!index.isValid())
+ {
+ return;
+ }
+
+ if(!worldSafetyNagQuestion())
+ return;
+
+ auto worldVariant = m_worlds->data(index, WorldList::ObjectRole);
+ auto world = (World *) worldVariant.value<void *>();
+ bool ok = false;
+ QString name = QInputDialog::getText(this, tr("World name"), tr("Enter a new name for the copy."), QLineEdit::Normal, world->name(), &ok);
+
+ if (ok && name.length() > 0)
+ {
+ world->install(m_worlds->dir().absolutePath(), name);
+ }
+}
+
+void WorldListPage::on_actionRename_triggered()
+{
+ QModelIndex index = getSelectedWorld();
+ if (!index.isValid())
+ {
+ return;
+ }
+
+ if(!worldSafetyNagQuestion())
+ return;
+
+ auto worldVariant = m_worlds->data(index, WorldList::ObjectRole);
+ auto world = (World *) worldVariant.value<void *>();
+
+ bool ok = false;
+ QString name = QInputDialog::getText(this, tr("World name"), tr("Enter a new world name."), QLineEdit::Normal, world->name(), &ok);
+
+ if (ok && name.length() > 0)
+ {
+ world->rename(name);
+ }
+}
+
+void WorldListPage::on_actionRefresh_triggered()
+{
+ m_worlds->update();
+}
+
+#include "WorldListPage.moc"
diff --git a/launcher/ui/pages/instance/WorldListPage.h b/launcher/ui/pages/instance/WorldListPage.h
new file mode 100644
index 00000000..e07d5794
--- /dev/null
+++ b/launcher/ui/pages/instance/WorldListPage.h
@@ -0,0 +1,99 @@
+/* Copyright 2015-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 <QMainWindow>
+
+#include "minecraft/MinecraftInstance.h"
+#include "ui/pages/BasePage.h"
+#include <Application.h>
+#include <LoggedProcess.h>
+
+class WorldList;
+namespace Ui
+{
+class WorldListPage;
+}
+
+class WorldListPage : public QMainWindow, public BasePage
+{
+ Q_OBJECT
+
+public:
+ explicit WorldListPage(
+ BaseInstance *inst,
+ std::shared_ptr<WorldList> worlds,
+ QWidget *parent = 0
+ );
+ virtual ~WorldListPage();
+
+ virtual QString displayName() const override
+ {
+ return tr("Worlds");
+ }
+ virtual QIcon icon() const override
+ {
+ return APPLICATION->getThemedIcon("worlds");
+ }
+ virtual QString id() const override
+ {
+ return "worlds";
+ }
+ virtual QString helpPage() const override
+ {
+ return "Worlds";
+ }
+ virtual bool shouldDisplay() const override;
+
+ virtual void openedImpl() override;
+ virtual void closedImpl() override;
+
+protected:
+ bool eventFilter(QObject *obj, QEvent *ev) override;
+ bool worldListFilter(QKeyEvent *ev);
+ QMenu * createPopupMenu() override;
+
+protected:
+ BaseInstance *m_inst;
+
+private:
+ QModelIndex getSelectedWorld();
+ bool isWorldSafe(QModelIndex index);
+ bool worldSafetyNagQuestion();
+ void mceditError();
+
+private:
+ Ui::WorldListPage *ui;
+ std::shared_ptr<WorldList> m_worlds;
+ unique_qobject_ptr<LoggedProcess> m_mceditProcess;
+ bool m_mceditStarting = false;
+
+private slots:
+ void on_actionCopy_Seed_triggered();
+ void on_actionMCEdit_triggered();
+ void on_actionRemove_triggered();
+ void on_actionAdd_triggered();
+ void on_actionCopy_triggered();
+ void on_actionRename_triggered();
+ void on_actionRefresh_triggered();
+ void on_actionView_Folder_triggered();
+ void on_actionDatapacks_triggered();
+ void on_actionReset_Icon_triggered();
+ void worldChanged(const QModelIndex &current, const QModelIndex &previous);
+ void mceditState(LoggedProcess::State state);
+
+ void ShowContextMenu(const QPoint &pos);
+};
diff --git a/launcher/ui/pages/instance/WorldListPage.ui b/launcher/ui/pages/instance/WorldListPage.ui
new file mode 100644
index 00000000..7c68bfae
--- /dev/null
+++ b/launcher/ui/pages/instance/WorldListPage.ui
@@ -0,0 +1,161 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>WorldListPage</class>
+ <widget class="QMainWindow" name="WorldListPage">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>800</width>
+ <height>600</height>
+ </rect>
+ </property>
+ <widget class="QWidget" name="centralwidget">
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QTreeView" name="worldTreeView">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="acceptDrops">
+ <bool>true</bool>
+ </property>
+ <property name="dragDropMode">
+ <enum>QAbstractItemView::DragDrop</enum>
+ </property>
+ <property name="alternatingRowColors">
+ <bool>true</bool>
+ </property>
+ <property name="rootIsDecorated">
+ <bool>false</bool>
+ </property>
+ <property name="itemsExpandable">
+ <bool>false</bool>
+ </property>
+ <property name="sortingEnabled">
+ <bool>true</bool>
+ </property>
+ <property name="allColumnsShowFocus">
+ <bool>true</bool>
+ </property>
+ <attribute name="headerStretchLastSection">
+ <bool>false</bool>
+ </attribute>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <widget class="WideBar" name="toolBar">
+ <property name="windowTitle">
+ <string>Actions</string>
+ </property>
+ <property name="allowedAreas">
+ <set>Qt::LeftToolBarArea|Qt::RightToolBarArea</set>
+ </property>
+ <property name="toolButtonStyle">
+ <enum>Qt::ToolButtonTextOnly</enum>
+ </property>
+ <property name="floatable">
+ <bool>false</bool>
+ </property>
+ <attribute name="toolBarArea">
+ <enum>RightToolBarArea</enum>
+ </attribute>
+ <attribute name="toolBarBreak">
+ <bool>false</bool>
+ </attribute>
+ <addaction name="actionAdd"/>
+ <addaction name="separator"/>
+ <addaction name="actionRename"/>
+ <addaction name="actionCopy"/>
+ <addaction name="actionRemove"/>
+ <addaction name="actionMCEdit"/>
+ <addaction name="actionDatapacks"/>
+ <addaction name="actionReset_Icon"/>
+ <addaction name="separator"/>
+ <addaction name="actionCopy_Seed"/>
+ <addaction name="actionRefresh"/>
+ <addaction name="actionView_Folder"/>
+ </widget>
+ <action name="actionAdd">
+ <property name="text">
+ <string>Add</string>
+ </property>
+ </action>
+ <action name="actionRename">
+ <property name="text">
+ <string>Rename</string>
+ </property>
+ </action>
+ <action name="actionCopy">
+ <property name="text">
+ <string>Copy</string>
+ </property>
+ </action>
+ <action name="actionRemove">
+ <property name="text">
+ <string>Remove</string>
+ </property>
+ </action>
+ <action name="actionMCEdit">
+ <property name="text">
+ <string>MCEdit</string>
+ </property>
+ </action>
+ <action name="actionCopy_Seed">
+ <property name="text">
+ <string>Copy Seed</string>
+ </property>
+ </action>
+ <action name="actionRefresh">
+ <property name="text">
+ <string>Refresh</string>
+ </property>
+ </action>
+ <action name="actionView_Folder">
+ <property name="text">
+ <string>View Folder</string>
+ </property>
+ </action>
+ <action name="actionReset_Icon">
+ <property name="text">
+ <string>Reset Icon</string>
+ </property>
+ <property name="toolTip">
+ <string>Remove world icon to make the game re-generate it on next load.</string>
+ </property>
+ </action>
+ <action name="actionDatapacks">
+ <property name="text">
+ <string>Datapacks</string>
+ </property>
+ <property name="toolTip">
+ <string>Manage datapacks inside the world.</string>
+ </property>
+ </action>
+ </widget>
+ <customwidgets>
+ <customwidget>
+ <class>WideBar</class>
+ <extends>QToolBar</extends>
+ <header>ui/widgets/WideBar.h</header>
+ </customwidget>
+ </customwidgets>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/launcher/ui/pages/modplatform/ImportPage.cpp b/launcher/ui/pages/modplatform/ImportPage.cpp
new file mode 100644
index 00000000..c9e24ead
--- /dev/null
+++ b/launcher/ui/pages/modplatform/ImportPage.cpp
@@ -0,0 +1,132 @@
+#include "ImportPage.h"
+#include "ui_ImportPage.h"
+
+#include <QFileDialog>
+#include <QValidator>
+
+#include "ui/dialogs/NewInstanceDialog.h"
+
+#include "InstanceImportTask.h"
+
+
+class UrlValidator : public QValidator
+{
+public:
+ using QValidator::QValidator;
+
+ State validate(QString &in, int &pos) const
+ {
+ const QUrl url(in);
+ if (url.isValid() && !url.isRelative() && !url.isEmpty())
+ {
+ return Acceptable;
+ }
+ else if (QFile::exists(in))
+ {
+ return Acceptable;
+ }
+ else
+ {
+ return Intermediate;
+ }
+ }
+};
+
+ImportPage::ImportPage(NewInstanceDialog* dialog, QWidget *parent)
+ : QWidget(parent), ui(new Ui::ImportPage), dialog(dialog)
+{
+ ui->setupUi(this);
+ ui->modpackEdit->setValidator(new UrlValidator(ui->modpackEdit));
+ connect(ui->modpackEdit, &QLineEdit::textChanged, this, &ImportPage::updateState);
+}
+
+ImportPage::~ImportPage()
+{
+ delete ui;
+}
+
+bool ImportPage::shouldDisplay() const
+{
+ return true;
+}
+
+void ImportPage::openedImpl()
+{
+ updateState();
+}
+
+void ImportPage::updateState()
+{
+ if(!isOpened)
+ {
+ return;
+ }
+ if(ui->modpackEdit->hasAcceptableInput())
+ {
+ QString input = ui->modpackEdit->text();
+ auto url = QUrl::fromUserInput(input);
+ if(url.isLocalFile())
+ {
+ // FIXME: actually do some validation of what's inside here... this is fake AF
+ QFileInfo fi(input);
+ if(fi.exists() && fi.suffix() == "zip")
+ {
+ QFileInfo fi(url.fileName());
+ dialog->setSuggestedPack(fi.completeBaseName(), new InstanceImportTask(url));
+ dialog->setSuggestedIcon("default");
+ }
+ }
+ else
+ {
+ if(input.endsWith("?client=y")) {
+ input.chop(9);
+ input.append("/file");
+ url = QUrl::fromUserInput(input);
+ }
+ // hook, line and sinker.
+ QFileInfo fi(url.fileName());
+ dialog->setSuggestedPack(fi.completeBaseName(), new InstanceImportTask(url));
+ dialog->setSuggestedIcon("default");
+ }
+ }
+ else
+ {
+ dialog->setSuggestedPack();
+ }
+}
+
+void ImportPage::setUrl(const QString& url)
+{
+ ui->modpackEdit->setText(url);
+ updateState();
+}
+
+void ImportPage::on_modpackBtn_clicked()
+{
+ const QUrl url = QFileDialog::getOpenFileUrl(this, tr("Choose modpack"), modpackUrl(), tr("Zip (*.zip)"));
+ if (url.isValid())
+ {
+ if (url.isLocalFile())
+ {
+ ui->modpackEdit->setText(url.toLocalFile());
+ }
+ else
+ {
+ ui->modpackEdit->setText(url.toString());
+ }
+ }
+}
+
+
+QUrl ImportPage::modpackUrl() const
+{
+ const QUrl url(ui->modpackEdit->text());
+ if (url.isValid() && !url.isRelative() && !url.host().isEmpty())
+ {
+ return url;
+ }
+ else
+ {
+ return QUrl::fromLocalFile(ui->modpackEdit->text());
+ }
+}
diff --git a/launcher/ui/pages/modplatform/ImportPage.h b/launcher/ui/pages/modplatform/ImportPage.h
new file mode 100644
index 00000000..aba4def0
--- /dev/null
+++ b/launcher/ui/pages/modplatform/ImportPage.h
@@ -0,0 +1,70 @@
+/* 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 <QWidget>
+
+#include "ui/pages/BasePage.h"
+#include <Application.h>
+#include "tasks/Task.h"
+
+namespace Ui
+{
+class ImportPage;
+}
+
+class NewInstanceDialog;
+
+class ImportPage : public QWidget, public BasePage
+{
+ Q_OBJECT
+
+public:
+ explicit ImportPage(NewInstanceDialog* dialog, QWidget *parent = 0);
+ virtual ~ImportPage();
+ virtual QString displayName() const override
+ {
+ return tr("Import from zip");
+ }
+ virtual QIcon icon() const override
+ {
+ return APPLICATION->getThemedIcon("viewfolder");
+ }
+ virtual QString id() const override
+ {
+ return "import";
+ }
+ virtual QString helpPage() const override
+ {
+ return "Zip-import";
+ }
+ virtual bool shouldDisplay() const override;
+
+ void setUrl(const QString & url);
+ void openedImpl() override;
+
+private slots:
+ void on_modpackBtn_clicked();
+ void updateState();
+
+private:
+ QUrl modpackUrl() const;
+
+private:
+ Ui::ImportPage *ui = nullptr;
+ NewInstanceDialog* dialog = nullptr;
+};
+
diff --git a/launcher/ui/pages/modplatform/ImportPage.ui b/launcher/ui/pages/modplatform/ImportPage.ui
new file mode 100644
index 00000000..eb63cbe9
--- /dev/null
+++ b/launcher/ui/pages/modplatform/ImportPage.ui
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>ImportPage</class>
+ <widget class="QWidget" name="ImportPage">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>546</width>
+ <height>405</height>
+ </rect>
+ </property>
+ <layout class="QGridLayout" name="gridLayout">
+ <item row="1" column="1">
+ <widget class="QPushButton" name="modpackBtn">
+ <property name="text">
+ <string>Browse</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0">
+ <widget class="QLineEdit" name="modpackEdit">
+ <property name="placeholderText">
+ <string notr="true">http://</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="0" colspan="2">
+ <widget class="QLabel" name="modpackLabel">
+ <property name="text">
+ <string>Local file or link to a direct download:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="0" colspan="2">
+ <spacer name="verticalSpacer">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>40</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/launcher/ui/pages/modplatform/VanillaPage.cpp b/launcher/ui/pages/modplatform/VanillaPage.cpp
new file mode 100644
index 00000000..5c58c1f1
--- /dev/null
+++ b/launcher/ui/pages/modplatform/VanillaPage.cpp
@@ -0,0 +1,103 @@
+#include "VanillaPage.h"
+#include "ui_VanillaPage.h"
+
+#include <QTabBar>
+
+#include "Application.h"
+#include "meta/Index.h"
+#include "meta/VersionList.h"
+#include "ui/dialogs/NewInstanceDialog.h"
+#include "Filter.h"
+#include "InstanceCreationTask.h"
+
+VanillaPage::VanillaPage(NewInstanceDialog *dialog, QWidget *parent)
+ : QWidget(parent), dialog(dialog), ui(new Ui::VanillaPage)
+{
+ ui->setupUi(this);
+ ui->tabWidget->tabBar()->hide();
+ connect(ui->versionList, &VersionSelectWidget::selectedVersionChanged, this, &VanillaPage::setSelectedVersion);
+ filterChanged();
+ connect(ui->alphaFilter, &QCheckBox::stateChanged, this, &VanillaPage::filterChanged);
+ connect(ui->betaFilter, &QCheckBox::stateChanged, this, &VanillaPage::filterChanged);
+ connect(ui->snapshotFilter, &QCheckBox::stateChanged, this, &VanillaPage::filterChanged);
+ connect(ui->oldSnapshotFilter, &QCheckBox::stateChanged, this, &VanillaPage::filterChanged);
+ connect(ui->releaseFilter, &QCheckBox::stateChanged, this, &VanillaPage::filterChanged);
+ connect(ui->experimentsFilter, &QCheckBox::stateChanged, this, &VanillaPage::filterChanged);
+ connect(ui->refreshBtn, &QPushButton::clicked, this, &VanillaPage::refresh);
+}
+
+void VanillaPage::openedImpl()
+{
+ if(!initialized)
+ {
+ auto vlist = APPLICATION->metadataIndex()->get("net.minecraft");
+ ui->versionList->initialize(vlist.get());
+ initialized = true;
+ }
+ else
+ {
+ suggestCurrent();
+ }
+}
+
+void VanillaPage::refresh()
+{
+ ui->versionList->loadList();
+}
+
+void VanillaPage::filterChanged()
+{
+ QStringList out;
+ if(ui->alphaFilter->isChecked())
+ out << "(old_alpha)";
+ if(ui->betaFilter->isChecked())
+ out << "(old_beta)";
+ if(ui->snapshotFilter->isChecked())
+ out << "(snapshot)";
+ if(ui->oldSnapshotFilter->isChecked())
+ out << "(old_snapshot)";
+ if(ui->releaseFilter->isChecked())
+ out << "(release)";
+ if(ui->experimentsFilter->isChecked())
+ out << "(experiment)";
+ auto regexp = out.join('|');
+ ui->versionList->setFilter(BaseVersionList::TypeRole, new RegexpFilter(regexp, false));
+}
+
+VanillaPage::~VanillaPage()
+{
+ delete ui;
+}
+
+bool VanillaPage::shouldDisplay() const
+{
+ return true;
+}
+
+BaseVersionPtr VanillaPage::selectedVersion() const
+{
+ return m_selectedVersion;
+}
+
+void VanillaPage::suggestCurrent()
+{
+ if (!isOpened)
+ {
+ return;
+ }
+
+ if(!m_selectedVersion)
+ {
+ dialog->setSuggestedPack();
+ return;
+ }
+
+ dialog->setSuggestedPack(m_selectedVersion->descriptor(), new InstanceCreationTask(m_selectedVersion));
+ dialog->setSuggestedIcon("default");
+}
+
+void VanillaPage::setSelectedVersion(BaseVersionPtr version)
+{
+ m_selectedVersion = version;
+ suggestCurrent();
+}
diff --git a/launcher/ui/pages/modplatform/VanillaPage.h b/launcher/ui/pages/modplatform/VanillaPage.h
new file mode 100644
index 00000000..fd4c2daa
--- /dev/null
+++ b/launcher/ui/pages/modplatform/VanillaPage.h
@@ -0,0 +1,75 @@
+/* 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 <QWidget>
+
+#include "ui/pages/BasePage.h"
+#include <Application.h>
+#include "tasks/Task.h"
+
+namespace Ui
+{
+class VanillaPage;
+}
+
+class NewInstanceDialog;
+
+class VanillaPage : public QWidget, public BasePage
+{
+ Q_OBJECT
+
+public:
+ explicit VanillaPage(NewInstanceDialog *dialog, QWidget *parent = 0);
+ virtual ~VanillaPage();
+ virtual QString displayName() const override
+ {
+ return tr("Vanilla");
+ }
+ virtual QIcon icon() const override
+ {
+ return APPLICATION->getThemedIcon("minecraft");
+ }
+ virtual QString id() const override
+ {
+ return "vanilla";
+ }
+ virtual QString helpPage() const override
+ {
+ return "Vanilla-platform";
+ }
+ virtual bool shouldDisplay() const override;
+ void openedImpl() override;
+
+ BaseVersionPtr selectedVersion() const;
+
+public slots:
+ void setSelectedVersion(BaseVersionPtr version);
+
+private slots:
+ void filterChanged();
+
+private:
+ void refresh();
+ void suggestCurrent();
+
+private:
+ bool initialized = false;
+ NewInstanceDialog *dialog = nullptr;
+ Ui::VanillaPage *ui = nullptr;
+ bool m_versionSetByUser = false;
+ BaseVersionPtr m_selectedVersion;
+};
diff --git a/launcher/ui/pages/modplatform/VanillaPage.ui b/launcher/ui/pages/modplatform/VanillaPage.ui
new file mode 100644
index 00000000..870ff161
--- /dev/null
+++ b/launcher/ui/pages/modplatform/VanillaPage.ui
@@ -0,0 +1,169 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>VanillaPage</class>
+ <widget class="QWidget" name="VanillaPage">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>815</width>
+ <height>607</height>
+ </rect>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_5">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QTabWidget" name="tabWidget">
+ <property name="currentIndex">
+ <number>0</number>
+ </property>
+ <widget class="QWidget" name="tab">
+ <attribute name="title">
+ <string notr="true"/>
+ </attribute>
+ <layout class="QGridLayout" name="gridLayout_2">
+ <item row="0" column="1">
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <widget class="QLabel" name="label">
+ <property name="text">
+ <string>Filter</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="releaseFilter">
+ <property name="text">
+ <string>Releases</string>
+ </property>
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ <property name="checked">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="snapshotFilter">
+ <property name="text">
+ <string>Snapshots</string>
+ </property>
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="oldSnapshotFilter">
+ <property name="text">
+ <string>Old Snapshots</string>
+ </property>
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="betaFilter">
+ <property name="text">
+ <string>Betas</string>
+ </property>
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="alphaFilter">
+ <property name="text">
+ <string>Alphas</string>
+ </property>
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="experimentsFilter">
+ <property name="text">
+ <string>Experiments</string>
+ </property>
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="verticalSpacer">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>40</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QPushButton" name="refreshBtn">
+ <property name="text">
+ <string>Refresh</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item row="0" column="0">
+ <widget class="VersionSelectWidget" name="versionList" native="true">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <customwidgets>
+ <customwidget>
+ <class>VersionSelectWidget</class>
+ <extends>QWidget</extends>
+ <header>ui/widgets/VersionSelectWidget.h</header>
+ <container>1</container>
+ </customwidget>
+ </customwidgets>
+ <tabstops>
+ <tabstop>tabWidget</tabstop>
+ <tabstop>releaseFilter</tabstop>
+ <tabstop>snapshotFilter</tabstop>
+ <tabstop>oldSnapshotFilter</tabstop>
+ <tabstop>betaFilter</tabstop>
+ <tabstop>alphaFilter</tabstop>
+ <tabstop>experimentsFilter</tabstop>
+ <tabstop>refreshBtn</tabstop>
+ </tabstops>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlFilterModel.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlFilterModel.cpp
new file mode 100644
index 00000000..b5d8f22b
--- /dev/null
+++ b/launcher/ui/pages/modplatform/atlauncher/AtlFilterModel.cpp
@@ -0,0 +1,81 @@
+#include "AtlFilterModel.h"
+
+#include <QDebug>
+
+#include <modplatform/atlauncher/ATLPackIndex.h>
+#include <Version.h>
+#include <MMCStrings.h>
+
+namespace Atl {
+
+FilterModel::FilterModel(QObject *parent) : QSortFilterProxyModel(parent)
+{
+ currentSorting = Sorting::ByPopularity;
+ sortings.insert(tr("Sort by popularity"), Sorting::ByPopularity);
+ sortings.insert(tr("Sort by name"), Sorting::ByName);
+ sortings.insert(tr("Sort by game version"), Sorting::ByGameVersion);
+
+ searchTerm = "";
+}
+
+const QMap<QString, FilterModel::Sorting> FilterModel::getAvailableSortings()
+{
+ return sortings;
+}
+
+QString FilterModel::translateCurrentSorting()
+{
+ return sortings.key(currentSorting);
+}
+
+void FilterModel::setSorting(Sorting sorting)
+{
+ currentSorting = sorting;
+ invalidate();
+}
+
+FilterModel::Sorting FilterModel::getCurrentSorting()
+{
+ return currentSorting;
+}
+
+void FilterModel::setSearchTerm(const QString term)
+{
+ searchTerm = term.trimmed();
+ invalidate();
+}
+
+bool FilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
+{
+ if (searchTerm.isEmpty()) {
+ return true;
+ }
+
+ QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent);
+ ATLauncher::IndexedPack pack = sourceModel()->data(index, Qt::UserRole).value<ATLauncher::IndexedPack>();
+ return pack.name.contains(searchTerm, Qt::CaseInsensitive);
+}
+
+bool FilterModel::lessThan(const QModelIndex &left, const QModelIndex &right) const
+{
+ ATLauncher::IndexedPack leftPack = sourceModel()->data(left, Qt::UserRole).value<ATLauncher::IndexedPack>();
+ ATLauncher::IndexedPack rightPack = sourceModel()->data(right, Qt::UserRole).value<ATLauncher::IndexedPack>();
+
+ if (currentSorting == ByPopularity) {
+ return leftPack.position > rightPack.position;
+ }
+ else if (currentSorting == ByGameVersion) {
+ Version lv(leftPack.versions.at(0).minecraft);
+ Version rv(rightPack.versions.at(0).minecraft);
+ return lv < rv;
+ }
+ else if (currentSorting == ByName) {
+ return Strings::naturalCompare(leftPack.name, rightPack.name, Qt::CaseSensitive) >= 0;
+ }
+
+ // Invalid sorting set, somehow...
+ qWarning() << "Invalid sorting set!";
+ return true;
+}
+
+}
diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlFilterModel.h b/launcher/ui/pages/modplatform/atlauncher/AtlFilterModel.h
new file mode 100644
index 00000000..bd72ad91
--- /dev/null
+++ b/launcher/ui/pages/modplatform/atlauncher/AtlFilterModel.h
@@ -0,0 +1,34 @@
+#pragma once
+
+#include <QtCore/QSortFilterProxyModel>
+
+namespace Atl {
+
+class FilterModel : public QSortFilterProxyModel
+{
+ Q_OBJECT
+public:
+ FilterModel(QObject* parent = Q_NULLPTR);
+ enum Sorting {
+ ByPopularity,
+ ByGameVersion,
+ ByName,
+ };
+ const QMap<QString, Sorting> getAvailableSortings();
+ QString translateCurrentSorting();
+ void setSorting(Sorting sorting);
+ Sorting getCurrentSorting();
+ void setSearchTerm(QString term);
+
+protected:
+ bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
+ bool lessThan(const QModelIndex &left, const QModelIndex &right) const override;
+
+private:
+ QMap<QString, Sorting> sortings;
+ Sorting currentSorting;
+ QString searchTerm;
+
+};
+
+}
diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp
new file mode 100644
index 00000000..e8c6deee
--- /dev/null
+++ b/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp
@@ -0,0 +1,193 @@
+#include "AtlListModel.h"
+
+#include <BuildConfig.h>
+#include <Application.h>
+#include <Json.h>
+
+namespace Atl {
+
+ListModel::ListModel(QObject *parent) : QAbstractListModel(parent)
+{
+}
+
+ListModel::~ListModel()
+{
+}
+
+int ListModel::rowCount(const QModelIndex &parent) const
+{
+ return modpacks.size();
+}
+
+int ListModel::columnCount(const QModelIndex &parent) const
+{
+ return 1;
+}
+
+QVariant ListModel::data(const QModelIndex &index, int role) const
+{
+ int pos = index.row();
+ if(pos >= modpacks.size() || pos < 0 || !index.isValid())
+ {
+ return QString("INVALID INDEX %1").arg(pos);
+ }
+
+ ATLauncher::IndexedPack pack = modpacks.at(pos);
+ if(role == Qt::DisplayRole)
+ {
+ return pack.name;
+ }
+ else if (role == Qt::ToolTipRole)
+ {
+ return pack.name;
+ }
+ else if(role == Qt::DecorationRole)
+ {
+ if(m_logoMap.contains(pack.safeName))
+ {
+ return (m_logoMap.value(pack.safeName));
+ }
+ auto icon = APPLICATION->getThemedIcon("atlauncher-placeholder");
+
+ auto url = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "launcher/images/%1.png").arg(pack.safeName.toLower());
+ ((ListModel *)this)->requestLogo(pack.safeName, url);
+
+ return icon;
+ }
+ else if(role == Qt::UserRole)
+ {
+ QVariant v;
+ v.setValue(pack);
+ return v;
+ }
+
+ return QVariant();
+}
+
+void ListModel::request()
+{
+ beginResetModel();
+ modpacks.clear();
+ endResetModel();
+
+ auto *netJob = new NetJob("Atl::Request");
+ auto url = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "launcher/json/packsnew.json");
+ netJob->addNetAction(Net::Download::makeByteArray(QUrl(url), &response));
+ jobPtr = netJob;
+ jobPtr->start(APPLICATION->network());
+
+ QObject::connect(netJob, &NetJob::succeeded, this, &ListModel::requestFinished);
+ QObject::connect(netJob, &NetJob::failed, this, &ListModel::requestFailed);
+}
+
+void ListModel::requestFinished()
+{
+ jobPtr.reset();
+
+ QJsonParseError parse_error;
+ QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error);
+ if(parse_error.error != QJsonParseError::NoError) {
+ qWarning() << "Error while parsing JSON response from ATL at " << parse_error.offset << " reason: " << parse_error.errorString();
+ qWarning() << response;
+ return;
+ }
+
+ QList<ATLauncher::IndexedPack> newList;
+
+ auto packs = doc.array();
+ for(auto packRaw : packs) {
+ auto packObj = packRaw.toObject();
+
+ ATLauncher::IndexedPack pack;
+
+ try {
+ ATLauncher::loadIndexedPack(pack, packObj);
+ }
+ catch (const JSONValidationError &e) {
+ qDebug() << QString::fromUtf8(response);
+ qWarning() << "Error while reading pack manifest from ATLauncher: " << e.cause();
+ return;
+ }
+
+ // ignore packs without a published version
+ if(pack.versions.length() == 0) continue;
+ // only display public packs (for now)
+ if(pack.type != ATLauncher::PackType::Public) continue;
+ // ignore "system" packs (Vanilla, Vanilla with Forge, etc)
+ if(pack.system) continue;
+
+ newList.append(pack);
+ }
+
+ beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size() + newList.size() - 1);
+ modpacks.append(newList);
+ endInsertRows();
+}
+
+void ListModel::requestFailed(QString reason)
+{
+ jobPtr.reset();
+}
+
+void ListModel::getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback)
+{
+ if(m_logoMap.contains(logo))
+ {
+ callback(APPLICATION->metacache()->resolveEntry("ATLauncherPacks", QString("logos/%1").arg(logo.section(".", 0, 0)))->getFullPath());
+ }
+ else
+ {
+ requestLogo(logo, logoUrl);
+ }
+}
+
+void ListModel::logoFailed(QString logo)
+{
+ m_failedLogos.append(logo);
+ m_loadingLogos.removeAll(logo);
+}
+
+void ListModel::logoLoaded(QString logo, QIcon out)
+{
+ m_loadingLogos.removeAll(logo);
+ m_logoMap.insert(logo, out);
+
+ for(int i = 0; i < modpacks.size(); i++) {
+ if(modpacks[i].safeName == logo) {
+ emit dataChanged(createIndex(i, 0), createIndex(i, 0), {Qt::DecorationRole});
+ }
+ }
+}
+
+void ListModel::requestLogo(QString file, QString url)
+{
+ if(m_loadingLogos.contains(file) || m_failedLogos.contains(file))
+ {
+ return;
+ }
+
+ MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("ATLauncherPacks", QString("logos/%1").arg(file.section(".", 0, 0)));
+ NetJob *job = new NetJob(QString("ATLauncher Icon Download %1").arg(file));
+ job->addNetAction(Net::Download::makeCached(QUrl(url), entry));
+
+ auto fullPath = entry->getFullPath();
+ QObject::connect(job, &NetJob::succeeded, this, [this, file, fullPath]
+ {
+ emit logoLoaded(file, QIcon(fullPath));
+ if(waitingCallbacks.contains(file))
+ {
+ waitingCallbacks.value(file)(fullPath);
+ }
+ });
+
+ QObject::connect(job, &NetJob::failed, this, [this, file]
+ {
+ emit logoFailed(file);
+ });
+
+ job->start(APPLICATION->network());
+
+ m_loadingLogos.append(file);
+}
+
+}
diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlListModel.h b/launcher/ui/pages/modplatform/atlauncher/AtlListModel.h
new file mode 100644
index 00000000..79aa8180
--- /dev/null
+++ b/launcher/ui/pages/modplatform/atlauncher/AtlListModel.h
@@ -0,0 +1,52 @@
+#pragma once
+
+#include <QAbstractListModel>
+
+#include "net/NetJob.h"
+#include <QIcon>
+#include <modplatform/atlauncher/ATLPackIndex.h>
+
+namespace Atl {
+
+typedef QMap<QString, QIcon> LogoMap;
+typedef std::function<void(QString)> LogoCallback;
+
+class ListModel : public QAbstractListModel
+{
+ Q_OBJECT
+
+public:
+ ListModel(QObject *parent);
+ virtual ~ListModel();
+
+ int rowCount(const QModelIndex &parent) const override;
+ int columnCount(const QModelIndex &parent) const override;
+ QVariant data(const QModelIndex &index, int role) const override;
+
+ void request();
+
+ void getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback);
+
+private slots:
+ void requestFinished();
+ void requestFailed(QString reason);
+
+ void logoFailed(QString logo);
+ void logoLoaded(QString logo, QIcon out);
+
+private:
+ void requestLogo(QString file, QString url);
+
+private:
+ QList<ATLauncher::IndexedPack> modpacks;
+
+ QStringList m_failedLogos;
+ QStringList m_loadingLogos;
+ LogoMap m_logoMap;
+ QMap<QString, LogoCallback> waitingCallbacks;
+
+ NetJob::Ptr jobPtr;
+ QByteArray response;
+};
+
+}
diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp
new file mode 100644
index 00000000..14bbd18b
--- /dev/null
+++ b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp
@@ -0,0 +1,209 @@
+#include "AtlOptionalModDialog.h"
+#include "ui_AtlOptionalModDialog.h"
+
+AtlOptionalModListModel::AtlOptionalModListModel(QWidget *parent, QVector<ATLauncher::VersionMod> mods)
+ : QAbstractListModel(parent), m_mods(mods) {
+
+ // fill mod index
+ for (int i = 0; i < m_mods.size(); i++) {
+ auto mod = m_mods.at(i);
+ m_index[mod.name] = i;
+ }
+ // set initial state
+ for (int i = 0; i < m_mods.size(); i++) {
+ auto mod = m_mods.at(i);
+ m_selection[mod.name] = false;
+ setMod(mod, i, mod.selected, false);
+ }
+}
+
+QVector<QString> AtlOptionalModListModel::getResult() {
+ QVector<QString> result;
+
+ for (const auto& mod : m_mods) {
+ if (m_selection[mod.name]) {
+ result.push_back(mod.name);
+ }
+ }
+
+ return result;
+}
+
+int AtlOptionalModListModel::rowCount(const QModelIndex &parent) const {
+ return m_mods.size();
+}
+
+int AtlOptionalModListModel::columnCount(const QModelIndex &parent) const {
+ // Enabled, Name, Description
+ return 3;
+}
+
+QVariant AtlOptionalModListModel::data(const QModelIndex &index, int role) const {
+ auto row = index.row();
+ auto mod = m_mods.at(row);
+
+ if (role == Qt::DisplayRole) {
+ if (index.column() == NameColumn) {
+ return mod.name;
+ }
+ if (index.column() == DescriptionColumn) {
+ return mod.description;
+ }
+ }
+ else if (role == Qt::ToolTipRole) {
+ if (index.column() == DescriptionColumn) {
+ return mod.description;
+ }
+ }
+ else if (role == Qt::CheckStateRole) {
+ if (index.column() == EnabledColumn) {
+ return m_selection[mod.name] ? Qt::Checked : Qt::Unchecked;
+ }
+ }
+
+ return QVariant();
+}
+
+bool AtlOptionalModListModel::setData(const QModelIndex &index, const QVariant &value, int role) {
+ if (role == Qt::CheckStateRole) {
+ auto row = index.row();
+ auto mod = m_mods.at(row);
+
+ toggleMod(mod, row);
+ return true;
+ }
+
+ return false;
+}
+
+QVariant AtlOptionalModListModel::headerData(int section, Qt::Orientation orientation, int role) const {
+ if (role == Qt::DisplayRole && orientation == Qt::Horizontal) {
+ switch (section) {
+ case EnabledColumn:
+ return QString();
+ case NameColumn:
+ return QString("Name");
+ case DescriptionColumn:
+ return QString("Description");
+ }
+ }
+
+ return QVariant();
+}
+
+Qt::ItemFlags AtlOptionalModListModel::flags(const QModelIndex &index) const {
+ auto flags = QAbstractListModel::flags(index);
+ if (index.isValid() && index.column() == EnabledColumn) {
+ flags |= Qt::ItemIsUserCheckable;
+ }
+ return flags;
+}
+
+void AtlOptionalModListModel::selectRecommended() {
+ for (const auto& mod : m_mods) {
+ m_selection[mod.name] = mod.recommended;
+ }
+
+ emit dataChanged(AtlOptionalModListModel::index(0, EnabledColumn),
+ AtlOptionalModListModel::index(m_mods.size() - 1, EnabledColumn));
+}
+
+void AtlOptionalModListModel::clearAll() {
+ for (const auto& mod : m_mods) {
+ m_selection[mod.name] = false;
+ }
+
+ emit dataChanged(AtlOptionalModListModel::index(0, EnabledColumn),
+ AtlOptionalModListModel::index(m_mods.size() - 1, EnabledColumn));
+}
+
+void AtlOptionalModListModel::toggleMod(ATLauncher::VersionMod mod, int index) {
+ setMod(mod, index, !m_selection[mod.name]);
+}
+
+void AtlOptionalModListModel::setMod(ATLauncher::VersionMod mod, int index, bool enable, bool shouldEmit) {
+ if (m_selection[mod.name] == enable) return;
+
+ m_selection[mod.name] = enable;
+
+ // disable other mods in the group, if applicable
+ if (enable && !mod.group.isEmpty()) {
+ for (int i = 0; i < m_mods.size(); i++) {
+ if (index == i) continue;
+ auto other = m_mods.at(i);
+
+ if (mod.group == other.group) {
+ setMod(other, i, false, shouldEmit);
+ }
+ }
+ }
+
+ for (const auto& dependencyName : mod.depends) {
+ auto dependencyIndex = m_index[dependencyName];
+ auto dependencyMod = m_mods.at(dependencyIndex);
+
+ // enable/disable dependencies
+ if (enable) {
+ setMod(dependencyMod, dependencyIndex, true, shouldEmit);
+ }
+
+ // if the dependency is 'effectively hidden', then track which mods
+ // depend on it - so we can efficiently disable it when no more dependents
+ // depend on it.
+ auto dependants = m_dependants[dependencyName];
+
+ if (enable) {
+ dependants.append(mod.name);
+ }
+ else {
+ dependants.removeAll(mod.name);
+
+ // if there are no longer any dependents, let's disable the mod
+ if (dependencyMod.effectively_hidden && dependants.isEmpty()) {
+ setMod(dependencyMod, dependencyIndex, false, shouldEmit);
+ }
+ }
+ }
+
+ // disable mods that depend on this one, if disabling
+ if (!enable) {
+ auto dependants = m_dependants[mod.name];
+ for (const auto& dependencyName : dependants) {
+ auto dependencyIndex = m_index[dependencyName];
+ auto dependencyMod = m_mods.at(dependencyIndex);
+
+ setMod(dependencyMod, dependencyIndex, false, shouldEmit);
+ }
+ }
+
+ if (shouldEmit) {
+ emit dataChanged(AtlOptionalModListModel::index(index, EnabledColumn),
+ AtlOptionalModListModel::index(index, EnabledColumn));
+ }
+}
+
+
+AtlOptionalModDialog::AtlOptionalModDialog(QWidget *parent, QVector<ATLauncher::VersionMod> mods)
+ : QDialog(parent), ui(new Ui::AtlOptionalModDialog) {
+ ui->setupUi(this);
+
+ listModel = new AtlOptionalModListModel(this, mods);
+ ui->treeView->setModel(listModel);
+
+ ui->treeView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+ ui->treeView->header()->setSectionResizeMode(
+ AtlOptionalModListModel::NameColumn, QHeaderView::ResizeToContents);
+ ui->treeView->header()->setSectionResizeMode(
+ AtlOptionalModListModel::DescriptionColumn, QHeaderView::Stretch);
+
+ connect(ui->selectRecommendedButton, &QPushButton::pressed,
+ listModel, &AtlOptionalModListModel::selectRecommended);
+ connect(ui->clearAllButton, &QPushButton::pressed,
+ listModel, &AtlOptionalModListModel::clearAll);
+ connect(ui->installButton, &QPushButton::pressed,
+ this, &QDialog::close);
+}
+
+AtlOptionalModDialog::~AtlOptionalModDialog() {
+ delete ui;
+}
diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h
new file mode 100644
index 00000000..a1df43f6
--- /dev/null
+++ b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h
@@ -0,0 +1,66 @@
+#pragma once
+
+#include <QDialog>
+#include <QAbstractListModel>
+
+#include "modplatform/atlauncher/ATLPackIndex.h"
+
+namespace Ui {
+class AtlOptionalModDialog;
+}
+
+class AtlOptionalModListModel : public QAbstractListModel {
+ Q_OBJECT
+
+public:
+ enum Columns
+ {
+ EnabledColumn = 0,
+ NameColumn,
+ DescriptionColumn,
+ };
+
+ AtlOptionalModListModel(QWidget *parent, QVector<ATLauncher::VersionMod> mods);
+
+ QVector<QString> getResult();
+
+ int rowCount(const QModelIndex &parent) const override;
+ int columnCount(const QModelIndex &parent) const override;
+
+ QVariant data(const QModelIndex &index, int role) const override;
+ bool setData(const QModelIndex &index, const QVariant &value, int role) override;
+ QVariant headerData(int section, Qt::Orientation orientation, int role) const override;
+
+ Qt::ItemFlags flags(const QModelIndex &index) const override;
+
+public slots:
+ void selectRecommended();
+ void clearAll();
+
+private:
+ void toggleMod(ATLauncher::VersionMod mod, int index);
+ void setMod(ATLauncher::VersionMod mod, int index, bool enable, bool shouldEmit = true);
+
+private:
+ QVector<ATLauncher::VersionMod> m_mods;
+ QMap<QString, bool> m_selection;
+ QMap<QString, int> m_index;
+ QMap<QString, QVector<QString>> m_dependants;
+};
+
+class AtlOptionalModDialog : public QDialog {
+ Q_OBJECT
+
+public:
+ AtlOptionalModDialog(QWidget *parent, QVector<ATLauncher::VersionMod> mods);
+ ~AtlOptionalModDialog() override;
+
+ QVector<QString> getResult() {
+ return listModel->getResult();
+ }
+
+private:
+ Ui::AtlOptionalModDialog *ui;
+
+ AtlOptionalModListModel *listModel;
+};
diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.ui b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.ui
new file mode 100644
index 00000000..4c5c2ec5
--- /dev/null
+++ b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.ui
@@ -0,0 +1,65 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>AtlOptionalModDialog</class>
+ <widget class="QDialog" name="AtlOptionalModDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>550</width>
+ <height>310</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Select Mods To Install</string>
+ </property>
+ <layout class="QGridLayout" name="gridLayout_2">
+ <item row="1" column="3">
+ <widget class="QPushButton" name="installButton">
+ <property name="text">
+ <string>Install</string>
+ </property>
+ <property name="default">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QPushButton" name="selectRecommendedButton">
+ <property name="text">
+ <string>Select Recommended</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0">
+ <widget class="QPushButton" name="shareCodeButton">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ <property name="text">
+ <string>Use Share Code</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="2">
+ <widget class="QPushButton" name="clearAllButton">
+ <property name="text">
+ <string>Clear All</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="0" colspan="4">
+ <widget class="ModListView" name="treeView"/>
+ </item>
+ </layout>
+ </widget>
+ <customwidgets>
+ <customwidget>
+ <class>ModListView</class>
+ <extends>QTreeView</extends>
+ <header>ui/widgets/ModListView.h</header>
+ </customwidget>
+ </customwidgets>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp
new file mode 100644
index 00000000..5f6a1396
--- /dev/null
+++ b/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp
@@ -0,0 +1,171 @@
+#include "AtlPage.h"
+#include "ui_AtlPage.h"
+
+#include "modplatform/atlauncher/ATLPackInstallTask.h"
+
+#include "AtlOptionalModDialog.h"
+#include "ui/dialogs/NewInstanceDialog.h"
+#include "ui/dialogs/VersionSelectDialog.h"
+
+#include <BuildConfig.h>
+
+AtlPage::AtlPage(NewInstanceDialog* dialog, QWidget *parent)
+ : QWidget(parent), ui(new Ui::AtlPage), dialog(dialog)
+{
+ ui->setupUi(this);
+
+ filterModel = new Atl::FilterModel(this);
+ listModel = new Atl::ListModel(this);
+ filterModel->setSourceModel(listModel);
+ ui->packView->setModel(filterModel);
+ ui->packView->setSortingEnabled(true);
+
+ ui->packView->header()->hide();
+ ui->packView->setIndentation(0);
+
+ ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
+ ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300);
+
+ for(int i = 0; i < filterModel->getAvailableSortings().size(); i++)
+ {
+ ui->sortByBox->addItem(filterModel->getAvailableSortings().keys().at(i));
+ }
+ ui->sortByBox->setCurrentText(filterModel->translateCurrentSorting());
+
+ connect(ui->searchEdit, &QLineEdit::textChanged, this, &AtlPage::triggerSearch);
+ connect(ui->sortByBox, &QComboBox::currentTextChanged, this, &AtlPage::onSortingSelectionChanged);
+ connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &AtlPage::onSelectionChanged);
+ connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &AtlPage::onVersionSelectionChanged);
+}
+
+AtlPage::~AtlPage()
+{
+ delete ui;
+}
+
+bool AtlPage::shouldDisplay() const
+{
+ return true;
+}
+
+void AtlPage::openedImpl()
+{
+ if(!initialized)
+ {
+ listModel->request();
+ initialized = true;
+ }
+
+ suggestCurrent();
+}
+
+void AtlPage::suggestCurrent()
+{
+ if(!isOpened)
+ {
+ return;
+ }
+
+ if (selectedVersion.isEmpty())
+ {
+ dialog->setSuggestedPack();
+ return;
+ }
+
+ dialog->setSuggestedPack(selected.name + " " + selectedVersion, new ATLauncher::PackInstallTask(this, selected.safeName, selectedVersion));
+ auto editedLogoName = selected.safeName;
+ auto url = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "launcher/images/%1.png").arg(selected.safeName.toLower());
+ listModel->getLogo(selected.safeName, url, [this, editedLogoName](QString logo)
+ {
+ dialog->setSuggestedIconFromFile(logo, editedLogoName);
+ });
+}
+
+void AtlPage::triggerSearch()
+{
+ filterModel->setSearchTerm(ui->searchEdit->text());
+}
+
+void AtlPage::onSortingSelectionChanged(QString data)
+{
+ auto toSet = filterModel->getAvailableSortings().value(data);
+ filterModel->setSorting(toSet);
+}
+
+void AtlPage::onSelectionChanged(QModelIndex first, QModelIndex second)
+{
+ ui->versionSelectionBox->clear();
+
+ if(!first.isValid())
+ {
+ if(isOpened)
+ {
+ dialog->setSuggestedPack();
+ }
+ return;
+ }
+
+ selected = filterModel->data(first, Qt::UserRole).value<ATLauncher::IndexedPack>();
+
+ ui->packDescription->setHtml(selected.description.replace("\n", "<br>"));
+
+ for(const auto& version : selected.versions) {
+ ui->versionSelectionBox->addItem(version.version);
+ }
+
+ suggestCurrent();
+}
+
+void AtlPage::onVersionSelectionChanged(QString data)
+{
+ if(data.isNull() || data.isEmpty())
+ {
+ selectedVersion = "";
+ return;
+ }
+
+ selectedVersion = data;
+ suggestCurrent();
+}
+
+QVector<QString> AtlPage::chooseOptionalMods(QVector<ATLauncher::VersionMod> mods) {
+ AtlOptionalModDialog optionalModDialog(this, mods);
+ optionalModDialog.exec();
+ return optionalModDialog.getResult();
+}
+
+QString AtlPage::chooseVersion(Meta::VersionListPtr vlist, QString minecraftVersion) {
+ VersionSelectDialog vselect(vlist.get(), "Choose Version", APPLICATION->activeWindow(), false);
+ if (minecraftVersion != Q_NULLPTR) {
+ vselect.setExactFilter(BaseVersionList::ParentVersionRole, minecraftVersion);
+ vselect.setEmptyString(tr("No versions are currently available for Minecraft %1").arg(minecraftVersion));
+ }
+ else {
+ vselect.setEmptyString(tr("No versions are currently available"));
+ }
+ vselect.setEmptyErrorString(tr("Couldn't load or download the version lists!"));
+
+ // select recommended build
+ for (int i = 0; i < vlist->versions().size(); i++) {
+ auto version = vlist->versions().at(i);
+ auto reqs = version->requires();
+
+ // filter by minecraft version, if the loader depends on a certain version.
+ if (minecraftVersion != Q_NULLPTR) {
+ auto iter = std::find_if(reqs.begin(), reqs.end(), [](const Meta::Require &req) {
+ return req.uid == "net.minecraft";
+ });
+ if (iter == reqs.end()) continue;
+ if (iter->equalsVersion != minecraftVersion) continue;
+ }
+
+ // first recommended build we find, we use.
+ if (version->isRecommended()) {
+ vselect.setCurrentVersion(version->descriptor());
+ break;
+ }
+ }
+
+ vselect.exec();
+ return vselect.selectedVersion()->descriptor();
+}
diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlPage.h b/launcher/ui/pages/modplatform/atlauncher/AtlPage.h
new file mode 100644
index 00000000..b95b3d9e
--- /dev/null
+++ b/launcher/ui/pages/modplatform/atlauncher/AtlPage.h
@@ -0,0 +1,86 @@
+/* Copyright 2013-2019 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 "AtlFilterModel.h"
+#include "AtlListModel.h"
+
+#include <QWidget>
+#include <modplatform/atlauncher/ATLPackInstallTask.h>
+
+#include "Application.h"
+#include "ui/pages/BasePage.h"
+#include "tasks/Task.h"
+
+namespace Ui
+{
+ class AtlPage;
+}
+
+class NewInstanceDialog;
+
+class AtlPage : public QWidget, public BasePage, public ATLauncher::UserInteractionSupport
+{
+Q_OBJECT
+
+public:
+ explicit AtlPage(NewInstanceDialog* dialog, QWidget *parent = 0);
+ virtual ~AtlPage();
+ virtual QString displayName() const override
+ {
+ return tr("ATLauncher");
+ }
+ virtual QIcon icon() const override
+ {
+ return APPLICATION->getThemedIcon("atlauncher");
+ }
+ virtual QString id() const override
+ {
+ return "atl";
+ }
+ virtual QString helpPage() const override
+ {
+ return "ATL-platform";
+ }
+ virtual bool shouldDisplay() const override;
+
+ void openedImpl() override;
+
+private:
+ void suggestCurrent();
+
+ QString chooseVersion(Meta::VersionListPtr vlist, QString minecraftVersion) override;
+ QVector<QString> chooseOptionalMods(QVector<ATLauncher::VersionMod> mods) override;
+
+private slots:
+ void triggerSearch();
+
+ void onSortingSelectionChanged(QString data);
+
+ void onSelectionChanged(QModelIndex first, QModelIndex second);
+ void onVersionSelectionChanged(QString data);
+
+private:
+ Ui::AtlPage *ui = nullptr;
+ NewInstanceDialog* dialog = nullptr;
+ Atl::ListModel* listModel = nullptr;
+ Atl::FilterModel* filterModel = nullptr;
+
+ ATLauncher::IndexedPack selected;
+ QString selectedVersion;
+
+ bool initialized = false;
+};
diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlPage.ui b/launcher/ui/pages/modplatform/atlauncher/AtlPage.ui
new file mode 100644
index 00000000..9085766a
--- /dev/null
+++ b/launcher/ui/pages/modplatform/atlauncher/AtlPage.ui
@@ -0,0 +1,92 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>AtlPage</class>
+ <widget class="QWidget" name="AtlPage">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>837</width>
+ <height>685</height>
+ </rect>
+ </property>
+ <layout class="QGridLayout" name="gridLayout">
+ <item row="1" column="0" colspan="2">
+ <layout class="QGridLayout" name="gridLayout_3">
+ <item row="1" column="0">
+ <widget class="QTreeView" name="packView">
+ <property name="alternatingRowColors">
+ <bool>true</bool>
+ </property>
+ <property name="iconSize">
+ <size>
+ <width>96</width>
+ <height>48</height>
+ </size>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QTextBrowser" name="packDescription">
+ <property name="openExternalLinks">
+ <bool>true</bool>
+ </property>
+ <property name="openLinks">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="0" colspan="2">
+ <widget class="QLabel" name="label_2">
+ <property name="text">
+ <string>Warning: This is still a work in progress. If you run into issues with the imported modpack, it may be a bug.</string>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item row="2" column="0" colspan="2">
+ <layout class="QGridLayout" name="gridLayout_4" columnstretch="0,0,0" rowminimumheight="0" columnminimumwidth="0,0,0">
+ <item row="0" column="2">
+ <widget class="QComboBox" name="versionSelectionBox"/>
+ </item>
+ <item row="0" column="1">
+ <widget class="QLabel" name="label">
+ <property name="text">
+ <string>Version selected:</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="0">
+ <widget class="QComboBox" name="sortByBox"/>
+ </item>
+ </layout>
+ </item>
+ <item row="0" column="0">
+ <widget class="QLineEdit" name="searchEdit">
+ <property name="placeholderText">
+ <string>Search and filter ...</string>
+ </property>
+ <property name="clearButtonEnabled">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <tabstops>
+ <tabstop>searchEdit</tabstop>
+ <tabstop>packView</tabstop>
+ <tabstop>packDescription</tabstop>
+ <tabstop>sortByBox</tabstop>
+ <tabstop>versionSelectionBox</tabstop>
+ </tabstops>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/launcher/ui/pages/modplatform/flame/FlameModel.cpp b/launcher/ui/pages/modplatform/flame/FlameModel.cpp
new file mode 100644
index 00000000..a05ab641
--- /dev/null
+++ b/launcher/ui/pages/modplatform/flame/FlameModel.cpp
@@ -0,0 +1,258 @@
+#include "FlameModel.h"
+#include "Application.h"
+#include <Json.h>
+
+#include <MMCStrings.h>
+#include <Version.h>
+
+#include <QtMath>
+#include <QLabel>
+
+#include <RWStorage.h>
+
+namespace Flame {
+
+ListModel::ListModel(QObject *parent) : QAbstractListModel(parent)
+{
+}
+
+ListModel::~ListModel()
+{
+}
+
+int ListModel::rowCount(const QModelIndex &parent) const
+{
+ return modpacks.size();
+}
+
+int ListModel::columnCount(const QModelIndex &parent) const
+{
+ return 1;
+}
+
+QVariant ListModel::data(const QModelIndex &index, int role) const
+{
+ int pos = index.row();
+ if(pos >= modpacks.size() || pos < 0 || !index.isValid())
+ {
+ return QString("INVALID INDEX %1").arg(pos);
+ }
+
+ IndexedPack pack = modpacks.at(pos);
+ if(role == Qt::DisplayRole)
+ {
+ return pack.name;
+ }
+ else if (role == Qt::ToolTipRole)
+ {
+ if(pack.description.length() > 100)
+ {
+ //some magic to prevent to long tooltips and replace html linebreaks
+ QString edit = pack.description.left(97);
+ edit = edit.left(edit.lastIndexOf("<br>")).left(edit.lastIndexOf(" ")).append("...");
+ return edit;
+
+ }
+ return pack.description;
+ }
+ else if(role == Qt::DecorationRole)
+ {
+ if(m_logoMap.contains(pack.logoName))
+ {
+ return (m_logoMap.value(pack.logoName));
+ }
+ QIcon icon = APPLICATION->getThemedIcon("screenshot-placeholder");
+ ((ListModel *)this)->requestLogo(pack.logoName, pack.logoUrl);
+ return icon;
+ }
+ else if(role == Qt::UserRole)
+ {
+ QVariant v;
+ v.setValue(pack);
+ return v;
+ }
+
+ return QVariant();
+}
+
+void ListModel::logoLoaded(QString logo, QIcon out)
+{
+ m_loadingLogos.removeAll(logo);
+ m_logoMap.insert(logo, out);
+ for(int i = 0; i < modpacks.size(); i++) {
+ if(modpacks[i].logoName == logo) {
+ emit dataChanged(createIndex(i, 0), createIndex(i, 0), {Qt::DecorationRole});
+ }
+ }
+}
+
+void ListModel::logoFailed(QString logo)
+{
+ m_failedLogos.append(logo);
+ m_loadingLogos.removeAll(logo);
+}
+
+void ListModel::requestLogo(QString logo, QString url)
+{
+ if(m_loadingLogos.contains(logo) || m_failedLogos.contains(logo))
+ {
+ return;
+ }
+
+ MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("FlamePacks", QString("logos/%1").arg(logo.section(".", 0, 0)));
+ NetJob *job = new NetJob(QString("Flame Icon Download %1").arg(logo));
+ job->addNetAction(Net::Download::makeCached(QUrl(url), entry));
+
+ auto fullPath = entry->getFullPath();
+ QObject::connect(job, &NetJob::succeeded, this, [this, logo, fullPath]
+ {
+ emit logoLoaded(logo, QIcon(fullPath));
+ if(waitingCallbacks.contains(logo))
+ {
+ waitingCallbacks.value(logo)(fullPath);
+ }
+ });
+
+ QObject::connect(job, &NetJob::failed, this, [this, logo]
+ {
+ emit logoFailed(logo);
+ });
+
+ job->start(APPLICATION->network());
+
+ m_loadingLogos.append(logo);
+}
+
+void ListModel::getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback)
+{
+ if(m_logoMap.contains(logo))
+ {
+ callback(APPLICATION->metacache()->resolveEntry("FlamePacks", QString("logos/%1").arg(logo.section(".", 0, 0)))->getFullPath());
+ }
+ else
+ {
+ requestLogo(logo, logoUrl);
+ }
+}
+
+Qt::ItemFlags ListModel::flags(const QModelIndex &index) const
+{
+ return QAbstractListModel::flags(index);
+}
+
+bool ListModel::canFetchMore(const QModelIndex& parent) const
+{
+ return searchState == CanPossiblyFetchMore;
+}
+
+void ListModel::fetchMore(const QModelIndex& parent)
+{
+ if (parent.isValid())
+ return;
+ if(nextSearchOffset == 0) {
+ qWarning() << "fetchMore with 0 offset is wrong...";
+ return;
+ }
+ performPaginatedSearch();
+}
+
+void ListModel::performPaginatedSearch()
+{
+ NetJob *netJob = new NetJob("Flame::Search");
+ auto searchUrl = QString(
+ "https://addons-ecs.forgesvc.net/api/v2/addon/search?"
+ "categoryId=0&"
+ "gameId=432&"
+ "index=%1&"
+ "pageSize=25&"
+ "searchFilter=%2&"
+ "sectionId=4471&"
+ "sort=%3"
+ ).arg(nextSearchOffset).arg(currentSearchTerm).arg(currentSort);
+ netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response));
+ jobPtr = netJob;
+ jobPtr->start(APPLICATION->network());
+ QObject::connect(netJob, &NetJob::succeeded, this, &ListModel::searchRequestFinished);
+ QObject::connect(netJob, &NetJob::failed, this, &ListModel::searchRequestFailed);
+}
+
+void ListModel::searchWithTerm(const QString& term, int sort)
+{
+ if(currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull() && currentSort == sort) {
+ return;
+ }
+ currentSearchTerm = term;
+ currentSort = sort;
+ if(jobPtr) {
+ jobPtr->abort();
+ searchState = ResetRequested;
+ return;
+ }
+ else {
+ beginResetModel();
+ modpacks.clear();
+ endResetModel();
+ searchState = None;
+ }
+ nextSearchOffset = 0;
+ performPaginatedSearch();
+}
+
+void Flame::ListModel::searchRequestFinished()
+{
+ jobPtr.reset();
+
+ QJsonParseError parse_error;
+ QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error);
+ if(parse_error.error != QJsonParseError::NoError) {
+ qWarning() << "Error while parsing JSON response from CurseForge at " << parse_error.offset << " reason: " << parse_error.errorString();
+ qWarning() << response;
+ return;
+ }
+
+ QList<Flame::IndexedPack> newList;
+ auto packs = doc.array();
+ for(auto packRaw : packs) {
+ auto packObj = packRaw.toObject();
+
+ Flame::IndexedPack pack;
+ try
+ {
+ Flame::loadIndexedPack(pack, packObj);
+ newList.append(pack);
+ }
+ catch(const JSONValidationError &e)
+ {
+ qWarning() << "Error while loading pack from CurseForge: " << e.cause();
+ continue;
+ }
+ }
+ if(packs.size() < 25) {
+ searchState = Finished;
+ } else {
+ nextSearchOffset += 25;
+ searchState = CanPossiblyFetchMore;
+ }
+ beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size() + newList.size() - 1);
+ modpacks.append(newList);
+ endInsertRows();
+}
+
+void Flame::ListModel::searchRequestFailed(QString reason)
+{
+ jobPtr.reset();
+
+ if(searchState == ResetRequested) {
+ beginResetModel();
+ modpacks.clear();
+ endResetModel();
+
+ nextSearchOffset = 0;
+ performPaginatedSearch();
+ } else {
+ searchState = Finished;
+ }
+}
+
+}
+
diff --git a/launcher/ui/pages/modplatform/flame/FlameModel.h b/launcher/ui/pages/modplatform/flame/FlameModel.h
new file mode 100644
index 00000000..536f6add
--- /dev/null
+++ b/launcher/ui/pages/modplatform/flame/FlameModel.h
@@ -0,0 +1,76 @@
+#pragma once
+
+#include <RWStorage.h>
+
+#include <QAbstractListModel>
+#include <QSortFilterProxyModel>
+#include <QThreadPool>
+#include <QIcon>
+#include <QStyledItemDelegate>
+#include <QList>
+#include <QString>
+#include <QStringList>
+#include <QMetaType>
+
+#include <functional>
+#include <net/NetJob.h>
+
+#include <modplatform/flame/FlamePackIndex.h>
+
+namespace Flame {
+
+
+typedef QMap<QString, QIcon> LogoMap;
+typedef std::function<void(QString)> LogoCallback;
+
+class ListModel : public QAbstractListModel
+{
+ Q_OBJECT
+
+public:
+ ListModel(QObject *parent);
+ virtual ~ListModel();
+
+ int rowCount(const QModelIndex &parent) const override;
+ int columnCount(const QModelIndex &parent) const override;
+ QVariant data(const QModelIndex &index, int role) const override;
+ Qt::ItemFlags flags(const QModelIndex &index) const override;
+ bool canFetchMore(const QModelIndex & parent) const override;
+ void fetchMore(const QModelIndex & parent) override;
+
+ void getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback);
+ void searchWithTerm(const QString & term, const int sort);
+
+private slots:
+ void performPaginatedSearch();
+
+ void logoFailed(QString logo);
+ void logoLoaded(QString logo, QIcon out);
+
+ void searchRequestFinished();
+ void searchRequestFailed(QString reason);
+
+private:
+ void requestLogo(QString file, QString url);
+
+private:
+ QList<IndexedPack> modpacks;
+ QStringList m_failedLogos;
+ QStringList m_loadingLogos;
+ LogoMap m_logoMap;
+ QMap<QString, LogoCallback> waitingCallbacks;
+
+ QString currentSearchTerm;
+ int currentSort = 0;
+ int nextSearchOffset = 0;
+ enum SearchState {
+ None,
+ CanPossiblyFetchMore,
+ ResetRequested,
+ Finished
+ } searchState = None;
+ NetJob::Ptr jobPtr;
+ QByteArray response;
+};
+
+}
diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.cpp b/launcher/ui/pages/modplatform/flame/FlamePage.cpp
new file mode 100644
index 00000000..cb1185f7
--- /dev/null
+++ b/launcher/ui/pages/modplatform/flame/FlamePage.cpp
@@ -0,0 +1,186 @@
+#include "FlamePage.h"
+#include "ui_FlamePage.h"
+
+#include <QKeyEvent>
+
+#include "Application.h"
+#include "Json.h"
+#include "ui/dialogs/NewInstanceDialog.h"
+#include "InstanceImportTask.h"
+#include "FlameModel.h"
+
+FlamePage::FlamePage(NewInstanceDialog* dialog, QWidget *parent)
+ : QWidget(parent), ui(new Ui::FlamePage), dialog(dialog)
+{
+ ui->setupUi(this);
+ connect(ui->searchButton, &QPushButton::clicked, this, &FlamePage::triggerSearch);
+ ui->searchEdit->installEventFilter(this);
+ listModel = new Flame::ListModel(this);
+ ui->packView->setModel(listModel);
+
+ ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
+ ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300);
+
+ // index is used to set the sorting with the curseforge api
+ ui->sortByBox->addItem(tr("Sort by featured"));
+ ui->sortByBox->addItem(tr("Sort by popularity"));
+ ui->sortByBox->addItem(tr("Sort by last updated"));
+ ui->sortByBox->addItem(tr("Sort by name"));
+ ui->sortByBox->addItem(tr("Sort by author"));
+ ui->sortByBox->addItem(tr("Sort by total downloads"));
+
+ connect(ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch()));
+ connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FlamePage::onSelectionChanged);
+ connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &FlamePage::onVersionSelectionChanged);
+}
+
+FlamePage::~FlamePage()
+{
+ delete ui;
+}
+
+bool FlamePage::eventFilter(QObject* watched, QEvent* event)
+{
+ if (watched == ui->searchEdit && event->type() == QEvent::KeyPress) {
+ QKeyEvent* keyEvent = static_cast<QKeyEvent*>(event);
+ if (keyEvent->key() == Qt::Key_Return) {
+ triggerSearch();
+ keyEvent->accept();
+ return true;
+ }
+ }
+ return QWidget::eventFilter(watched, event);
+}
+
+bool FlamePage::shouldDisplay() const
+{
+ return true;
+}
+
+void FlamePage::openedImpl()
+{
+ suggestCurrent();
+ triggerSearch();
+}
+
+void FlamePage::triggerSearch()
+{
+ listModel->searchWithTerm(ui->searchEdit->text(), ui->sortByBox->currentIndex());
+}
+
+void FlamePage::onSelectionChanged(QModelIndex first, QModelIndex second)
+{
+ ui->versionSelectionBox->clear();
+
+ if(!first.isValid())
+ {
+ if(isOpened)
+ {
+ dialog->setSuggestedPack();
+ }
+ return;
+ }
+
+ current = listModel->data(first, Qt::UserRole).value<Flame::IndexedPack>();
+ QString text = "";
+ QString name = current.name;
+
+ if (current.websiteUrl.isEmpty())
+ text = name;
+ else
+ text = "<a href=\"" + current.websiteUrl + "\">" + name + "</a>";
+ if (!current.authors.empty()) {
+ auto authorToStr = [](Flame::ModpackAuthor & author) {
+ if(author.url.isEmpty()) {
+ return author.name;
+ }
+ return QString("<a href=\"%1\">%2</a>").arg(author.url, author.name);
+ };
+ QStringList authorStrs;
+ for(auto & author: current.authors) {
+ authorStrs.push_back(authorToStr(author));
+ }
+ text += "<br>" + tr(" by ") + authorStrs.join(", ");
+ }
+ text += "<br><br>";
+
+ ui->packDescription->setHtml(text + current.description);
+
+ if (current.versionsLoaded == false)
+ {
+ qDebug() << "Loading flame modpack versions";
+ NetJob *netJob = new NetJob(QString("Flame::PackVersions(%1)").arg(current.name));
+ std::shared_ptr<QByteArray> response = std::make_shared<QByteArray>();
+ int addonId = current.addonId;
+ netJob->addNetAction(Net::Download::makeByteArray(QString("https://addons-ecs.forgesvc.net/api/v2/addon/%1/files").arg(addonId), response.get()));
+
+ QObject::connect(netJob, &NetJob::succeeded, this, [this, response]
+ {
+ QJsonParseError parse_error;
+ QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
+ if(parse_error.error != QJsonParseError::NoError) {
+ qWarning() << "Error while parsing JSON response from CurseForge at " << parse_error.offset << " reason: " << parse_error.errorString();
+ qWarning() << *response;
+ return;
+ }
+ QJsonArray arr = doc.array();
+ try
+ {
+ Flame::loadIndexedPackVersions(current, arr);
+ }
+ catch(const JSONValidationError &e)
+ {
+ qDebug() << *response;
+ qWarning() << "Error while reading flame modpack version: " << e.cause();
+ }
+
+ for(auto version : current.versions) {
+ ui->versionSelectionBox->addItem(version.version, QVariant(version.downloadUrl));
+ }
+
+ suggestCurrent();
+ });
+ netJob->start(APPLICATION->network());
+ }
+ else
+ {
+ for(auto version : current.versions) {
+ ui->versionSelectionBox->addItem(version.version, QVariant(version.downloadUrl));
+ }
+
+ suggestCurrent();
+ }
+}
+
+void FlamePage::suggestCurrent()
+{
+ if(!isOpened)
+ {
+ return;
+ }
+
+ if (selectedVersion.isEmpty())
+ {
+ dialog->setSuggestedPack();
+ return;
+ }
+
+ dialog->setSuggestedPack(current.name, new InstanceImportTask(selectedVersion));
+ QString editedLogoName;
+ editedLogoName = "curseforge_" + current.logoName.section(".", 0, 0);
+ listModel->getLogo(current.logoName, current.logoUrl, [this, editedLogoName](QString logo)
+ {
+ dialog->setSuggestedIconFromFile(logo, editedLogoName);
+ });
+}
+
+void FlamePage::onVersionSelectionChanged(QString data)
+{
+ if(data.isNull() || data.isEmpty())
+ {
+ selectedVersion = "";
+ return;
+ }
+ selectedVersion = ui->versionSelectionBox->currentData().toString();
+ suggestCurrent();
+}
diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.h b/launcher/ui/pages/modplatform/flame/FlamePage.h
new file mode 100644
index 00000000..5cfe21dc
--- /dev/null
+++ b/launcher/ui/pages/modplatform/flame/FlamePage.h
@@ -0,0 +1,80 @@
+/* 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 <QWidget>
+
+#include "ui/pages/BasePage.h"
+#include <Application.h>
+#include "tasks/Task.h"
+#include <modplatform/flame/FlamePackIndex.h>
+
+namespace Ui
+{
+class FlamePage;
+}
+
+class NewInstanceDialog;
+
+namespace Flame {
+ class ListModel;
+}
+
+class FlamePage : public QWidget, public BasePage
+{
+ Q_OBJECT
+
+public:
+ explicit FlamePage(NewInstanceDialog* dialog, QWidget *parent = 0);
+ virtual ~FlamePage();
+ virtual QString displayName() const override
+ {
+ return tr("CurseForge");
+ }
+ virtual QIcon icon() const override
+ {
+ return APPLICATION->getThemedIcon("flame");
+ }
+ virtual QString id() const override
+ {
+ return "flame";
+ }
+ virtual QString helpPage() const override
+ {
+ return "Flame-platform";
+ }
+ virtual bool shouldDisplay() const override;
+
+ void openedImpl() override;
+
+ bool eventFilter(QObject * watched, QEvent * event) override;
+
+private:
+ void suggestCurrent();
+
+private slots:
+ void triggerSearch();
+ void onSelectionChanged(QModelIndex first, QModelIndex second);
+ void onVersionSelectionChanged(QString data);
+
+private:
+ Ui::FlamePage *ui = nullptr;
+ NewInstanceDialog* dialog = nullptr;
+ Flame::ListModel* listModel = nullptr;
+ Flame::IndexedPack current;
+
+ QString selectedVersion;
+};
diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.ui b/launcher/ui/pages/modplatform/flame/FlamePage.ui
new file mode 100644
index 00000000..9723815a
--- /dev/null
+++ b/launcher/ui/pages/modplatform/flame/FlamePage.ui
@@ -0,0 +1,90 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>FlamePage</class>
+ <widget class="QWidget" name="FlamePage">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>837</width>
+ <height>685</height>
+ </rect>
+ </property>
+ <layout class="QGridLayout" name="gridLayout">
+ <item row="1" column="0" colspan="2">
+ <layout class="QGridLayout" name="gridLayout_3">
+ <item row="1" column="0">
+ <widget class="QListView" name="packView">
+ <property name="iconSize">
+ <size>
+ <width>48</width>
+ <height>48</height>
+ </size>
+ </property>
+ <property name="horizontalScrollBarPolicy">
+ <enum>Qt::ScrollBarAlwaysOff</enum>
+ </property>
+ <property name="alternatingRowColors">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QTextBrowser" name="packDescription">
+ <property name="openExternalLinks">
+ <bool>true</bool>
+ </property>
+ <property name="openLinks">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item row="2" column="0" colspan="2">
+ <layout class="QGridLayout" name="gridLayout_4" columnstretch="0,0,0" rowminimumheight="0" columnminimumwidth="0,0,0">
+ <item row="0" column="2">
+ <widget class="QComboBox" name="versionSelectionBox"/>
+ </item>
+ <item row="0" column="1">
+ <widget class="QLabel" name="label">
+ <property name="text">
+ <string>Version selected:</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="0">
+ <widget class="QComboBox" name="sortByBox"/>
+ </item>
+ </layout>
+ </item>
+ <item row="0" column="1">
+ <widget class="QPushButton" name="searchButton">
+ <property name="text">
+ <string>Search</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="0">
+ <widget class="QLineEdit" name="searchEdit">
+ <property name="placeholderText">
+ <string>Search and filter ...</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <tabstops>
+ <tabstop>searchEdit</tabstop>
+ <tabstop>searchButton</tabstop>
+ <tabstop>packView</tabstop>
+ <tabstop>packDescription</tabstop>
+ <tabstop>sortByBox</tabstop>
+ <tabstop>versionSelectionBox</tabstop>
+ </tabstops>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/launcher/ui/pages/modplatform/ftb/FtbFilterModel.cpp b/launcher/ui/pages/modplatform/ftb/FtbFilterModel.cpp
new file mode 100644
index 00000000..793b8769
--- /dev/null
+++ b/launcher/ui/pages/modplatform/ftb/FtbFilterModel.cpp
@@ -0,0 +1,76 @@
+#include "FtbFilterModel.h"
+
+#include <QDebug>
+
+#include "modplatform/modpacksch/FTBPackManifest.h"
+#include <MMCStrings.h>
+
+namespace Ftb {
+
+FilterModel::FilterModel(QObject *parent) : QSortFilterProxyModel(parent)
+{
+ currentSorting = Sorting::ByPlays;
+ sortings.insert(tr("Sort by plays"), Sorting::ByPlays);
+ sortings.insert(tr("Sort by installs"), Sorting::ByInstalls);
+ sortings.insert(tr("Sort by name"), Sorting::ByName);
+}
+
+const QMap<QString, FilterModel::Sorting> FilterModel::getAvailableSortings()
+{
+ return sortings;
+}
+
+QString FilterModel::translateCurrentSorting()
+{
+ return sortings.key(currentSorting);
+}
+
+void FilterModel::setSorting(Sorting sorting)
+{
+ currentSorting = sorting;
+ invalidate();
+}
+
+FilterModel::Sorting FilterModel::getCurrentSorting()
+{
+ return currentSorting;
+}
+
+void FilterModel::setSearchTerm(const QString& term)
+{
+ searchTerm = term.trimmed();
+ invalidate();
+}
+
+bool FilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
+{
+ if (searchTerm.isEmpty()) {
+ return true;
+ }
+
+ auto index = sourceModel()->index(sourceRow, 0, sourceParent);
+ auto pack = sourceModel()->data(index, Qt::UserRole).value<ModpacksCH::Modpack>();
+ return pack.name.contains(searchTerm, Qt::CaseInsensitive);
+}
+
+bool FilterModel::lessThan(const QModelIndex &left, const QModelIndex &right) const
+{
+ ModpacksCH::Modpack leftPack = sourceModel()->data(left, Qt::UserRole).value<ModpacksCH::Modpack>();
+ ModpacksCH::Modpack rightPack = sourceModel()->data(right, Qt::UserRole).value<ModpacksCH::Modpack>();
+
+ if (currentSorting == ByPlays) {
+ return leftPack.plays < rightPack.plays;
+ }
+ else if (currentSorting == ByInstalls) {
+ return leftPack.installs < rightPack.installs;
+ }
+ else if (currentSorting == ByName) {
+ return Strings::naturalCompare(leftPack.name, rightPack.name, Qt::CaseSensitive) >= 0;
+ }
+
+ // Invalid sorting set, somehow...
+ qWarning() << "Invalid sorting set!";
+ return true;
+}
+
+}
diff --git a/launcher/ui/pages/modplatform/ftb/FtbFilterModel.h b/launcher/ui/pages/modplatform/ftb/FtbFilterModel.h
new file mode 100644
index 00000000..2e712c7d
--- /dev/null
+++ b/launcher/ui/pages/modplatform/ftb/FtbFilterModel.h
@@ -0,0 +1,35 @@
+#pragma once
+
+#include <QtCore/QSortFilterProxyModel>
+
+namespace Ftb {
+
+class FilterModel : public QSortFilterProxyModel
+{
+ Q_OBJECT
+
+public:
+ FilterModel(QObject* parent = Q_NULLPTR);
+ enum Sorting {
+ ByPlays,
+ ByInstalls,
+ ByName,
+ };
+ const QMap<QString, Sorting> getAvailableSortings();
+ QString translateCurrentSorting();
+ void setSorting(Sorting sorting);
+ Sorting getCurrentSorting();
+ void setSearchTerm(const QString& term);
+
+protected:
+ bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
+ bool lessThan(const QModelIndex &left, const QModelIndex &right) const override;
+
+private:
+ QMap<QString, Sorting> sortings;
+ Sorting currentSorting;
+ QString searchTerm { "" };
+
+};
+
+}
diff --git a/launcher/ui/pages/modplatform/ftb/FtbListModel.cpp b/launcher/ui/pages/modplatform/ftb/FtbListModel.cpp
new file mode 100644
index 00000000..59cd0b85
--- /dev/null
+++ b/launcher/ui/pages/modplatform/ftb/FtbListModel.cpp
@@ -0,0 +1,278 @@
+#include "FtbListModel.h"
+
+#include "BuildConfig.h"
+#include "Application.h"
+#include "Json.h"
+
+#include <QPainter>
+
+namespace Ftb {
+
+ListModel::ListModel(QObject *parent) : QAbstractListModel(parent)
+{
+}
+
+ListModel::~ListModel()
+{
+}
+
+int ListModel::rowCount(const QModelIndex &parent) const
+{
+ return modpacks.size();
+}
+
+int ListModel::columnCount(const QModelIndex &parent) const
+{
+ return 1;
+}
+
+QVariant ListModel::data(const QModelIndex &index, int role) const
+{
+ int pos = index.row();
+ if(pos >= modpacks.size() || pos < 0 || !index.isValid())
+ {
+ return QString("INVALID INDEX %1").arg(pos);
+ }
+
+ ModpacksCH::Modpack pack = modpacks.at(pos);
+ if(role == Qt::DisplayRole)
+ {
+ return pack.name;
+ }
+ else if (role == Qt::ToolTipRole)
+ {
+ return pack.synopsis;
+ }
+ else if(role == Qt::DecorationRole)
+ {
+ QIcon placeholder = APPLICATION->getThemedIcon("screenshot-placeholder");
+
+ auto iter = m_logoMap.find(pack.name);
+ if (iter != m_logoMap.end()) {
+ auto & logo = *iter;
+ if(!logo.result.isNull()) {
+ return logo.result;
+ }
+ return placeholder;
+ }
+
+ for(auto art : pack.art) {
+ if(art.type == "square") {
+ ((ListModel *)this)->requestLogo(pack.name, art.url);
+ }
+ }
+ return placeholder;
+ }
+ else if(role == Qt::UserRole)
+ {
+ QVariant v;
+ v.setValue(pack);
+ return v;
+ }
+
+ return QVariant();
+}
+
+void ListModel::getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback)
+{
+ if(m_logoMap.contains(logo))
+ {
+ callback(APPLICATION->metacache()->resolveEntry("ModpacksCHPacks", QString("logos/%1").arg(logo.section(".", 0, 0)))->getFullPath());
+ }
+ else
+ {
+ requestLogo(logo, logoUrl);
+ }
+}
+
+void ListModel::request()
+{
+ beginResetModel();
+ modpacks.clear();
+ endResetModel();
+
+ auto *netJob = new NetJob("Ftb::Request");
+ auto url = QString(BuildConfig.MODPACKSCH_API_BASE_URL + "public/modpack/all");
+ netJob->addNetAction(Net::Download::makeByteArray(QUrl(url), &response));
+ jobPtr = netJob;
+ jobPtr->start(APPLICATION->network());
+
+ QObject::connect(netJob, &NetJob::succeeded, this, &ListModel::requestFinished);
+ QObject::connect(netJob, &NetJob::failed, this, &ListModel::requestFailed);
+}
+
+void ListModel::requestFinished()
+{
+ jobPtr.reset();
+ remainingPacks.clear();
+
+ QJsonParseError parse_error;
+ QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error);
+ if(parse_error.error != QJsonParseError::NoError) {
+ qWarning() << "Error while parsing JSON response from FTB at " << parse_error.offset << " reason: " << parse_error.errorString();
+ qWarning() << response;
+ return;
+ }
+
+ auto packs = doc.object().value("packs").toArray();
+ for(auto pack : packs) {
+ auto packId = pack.toInt();
+ remainingPacks.append(packId);
+ }
+
+ if(!remainingPacks.isEmpty()) {
+ currentPack = remainingPacks.at(0);
+ requestPack();
+ }
+}
+
+void ListModel::requestFailed(QString reason)
+{
+ jobPtr.reset();
+ remainingPacks.clear();
+}
+
+void ListModel::requestPack()
+{
+ auto *netJob = new NetJob("Ftb::Search");
+ auto searchUrl = QString(BuildConfig.MODPACKSCH_API_BASE_URL + "public/modpack/%1")
+ .arg(currentPack);
+ netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response));
+ jobPtr = netJob;
+ jobPtr->start(APPLICATION->network());
+
+ QObject::connect(netJob, &NetJob::succeeded, this, &ListModel::packRequestFinished);
+ QObject::connect(netJob, &NetJob::failed, this, &ListModel::packRequestFailed);
+}
+
+void ListModel::packRequestFinished()
+{
+ jobPtr.reset();
+ remainingPacks.removeOne(currentPack);
+
+ QJsonParseError parse_error;
+ QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error);
+
+ if(parse_error.error != QJsonParseError::NoError) {
+ qWarning() << "Error while parsing JSON response from FTB at " << parse_error.offset << " reason: " << parse_error.errorString();
+ qWarning() << response;
+ return;
+ }
+
+ auto obj = doc.object();
+
+ ModpacksCH::Modpack pack;
+ try
+ {
+ ModpacksCH::loadModpack(pack, obj);
+ }
+ catch (const JSONValidationError &e)
+ {
+ qDebug() << QString::fromUtf8(response);
+ qWarning() << "Error while reading pack manifest from FTB: " << e.cause();
+ return;
+ }
+
+ // Since there is no guarantee that packs have a version, this will just
+ // ignore those "dud" packs.
+ if (pack.versions.empty())
+ {
+ qWarning() << "FTB Pack " << pack.id << " ignored. reason: lacking any versions";
+ }
+ else
+ {
+ beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size());
+ modpacks.append(pack);
+ endInsertRows();
+ }
+
+ if(!remainingPacks.isEmpty()) {
+ currentPack = remainingPacks.at(0);
+ requestPack();
+ }
+}
+
+void ListModel::packRequestFailed(QString reason)
+{
+ jobPtr.reset();
+ remainingPacks.removeOne(currentPack);
+}
+
+void ListModel::logoLoaded(QString logo, bool stale)
+{
+ auto & logoObj = m_logoMap[logo];
+ logoObj.downloadJob.reset();
+ QString smallPath = logoObj.fullpath + ".small";
+
+ QFileInfo smallInfo(smallPath);
+
+ if(stale || !smallInfo.exists()) {
+ QImage image(logoObj.fullpath);
+ if (image.isNull())
+ {
+ logoObj.failed = true;
+ return;
+ }
+ QImage small;
+ if (image.width() > image.height()) {
+ small = image.scaledToWidth(512).scaledToWidth(256, Qt::SmoothTransformation);
+ }
+ else {
+ small = image.scaledToHeight(512).scaledToHeight(256, Qt::SmoothTransformation);
+ }
+ QPoint offset((256 - small.width()) / 2, (256 - small.height()) / 2);
+ QImage square(QSize(256, 256), QImage::Format_ARGB32);
+ square.fill(Qt::transparent);
+
+ QPainter painter(&square);
+ painter.drawImage(offset, small);
+ painter.end();
+
+ square.save(logoObj.fullpath + ".small", "PNG");
+ }
+
+ logoObj.result = QIcon(logoObj.fullpath + ".small");
+ for(int i = 0; i < modpacks.size(); i++) {
+ if(modpacks[i].name == logo) {
+ emit dataChanged(createIndex(i, 0), createIndex(i, 0), {Qt::DecorationRole});
+ }
+ }
+}
+
+void ListModel::logoFailed(QString logo)
+{
+ m_logoMap[logo].failed = true;
+ m_logoMap[logo].downloadJob.reset();
+}
+
+void ListModel::requestLogo(QString logo, QString url)
+{
+ if(m_logoMap.contains(logo)) {
+ return;
+ }
+
+ MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("ModpacksCHPacks", QString("logos/%1").arg(logo.section(".", 0, 0)));
+
+ bool stale = entry->isStale();
+
+ NetJob *job = new NetJob(QString("FTB Icon Download %1").arg(logo));
+ job->addNetAction(Net::Download::makeCached(QUrl(url), entry));
+
+ auto fullPath = entry->getFullPath();
+ QObject::connect(job, &NetJob::finished, this, [this, logo, fullPath, stale]
+ {
+ logoLoaded(logo, stale);
+ });
+
+ QObject::connect(job, &NetJob::failed, this, [this, logo]
+ {
+ logoFailed(logo);
+ });
+
+ auto &newLogoEntry = m_logoMap[logo];
+ newLogoEntry.downloadJob = job;
+ newLogoEntry.fullpath = fullPath;
+ job->start(APPLICATION->network());
+}
+
+}
diff --git a/launcher/ui/pages/modplatform/ftb/FtbListModel.h b/launcher/ui/pages/modplatform/ftb/FtbListModel.h
new file mode 100644
index 00000000..e2b73c25
--- /dev/null
+++ b/launcher/ui/pages/modplatform/ftb/FtbListModel.h
@@ -0,0 +1,61 @@
+#pragma once
+
+#include <QAbstractListModel>
+
+#include "modplatform/modpacksch/FTBPackManifest.h"
+#include "net/NetJob.h"
+#include <QIcon>
+
+namespace Ftb {
+
+struct Logo {
+ QString fullpath;
+ NetJob::Ptr downloadJob;
+ QIcon result;
+ bool failed = false;
+};
+
+typedef QMap<QString, Logo> LogoMap;
+typedef std::function<void(QString)> LogoCallback;
+
+class ListModel : public QAbstractListModel
+{
+ Q_OBJECT
+
+public:
+ ListModel(QObject *parent);
+ virtual ~ListModel();
+
+ int rowCount(const QModelIndex &parent) const override;
+ int columnCount(const QModelIndex &parent) const override;
+ QVariant data(const QModelIndex &index, int role) const override;
+
+ void request();
+
+ void getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback);
+
+private slots:
+ void requestFinished();
+ void requestFailed(QString reason);
+
+ void requestPack();
+ void packRequestFinished();
+ void packRequestFailed(QString reason);
+
+ void logoFailed(QString logo);
+ void logoLoaded(QString logo, bool stale);
+
+private:
+ void requestLogo(QString file, QString url);
+
+private:
+ QList<ModpacksCH::Modpack> modpacks;
+ LogoMap m_logoMap;
+
+ NetJob::Ptr jobPtr;
+ int currentPack;
+ QList<int> remainingPacks;
+ QByteArray response;
+};
+
+}
diff --git a/launcher/ui/pages/modplatform/ftb/FtbPage.cpp b/launcher/ui/pages/modplatform/ftb/FtbPage.cpp
new file mode 100644
index 00000000..a82de1d6
--- /dev/null
+++ b/launcher/ui/pages/modplatform/ftb/FtbPage.cpp
@@ -0,0 +1,150 @@
+#include "FtbPage.h"
+#include "ui_FtbPage.h"
+
+#include <QKeyEvent>
+
+#include "ui/dialogs/NewInstanceDialog.h"
+#include "modplatform/modpacksch/FTBPackInstallTask.h"
+
+#include "HoeDown.h"
+
+FtbPage::FtbPage(NewInstanceDialog* dialog, QWidget *parent)
+ : QWidget(parent), ui(new Ui::FtbPage), dialog(dialog)
+{
+ ui->setupUi(this);
+
+ filterModel = new Ftb::FilterModel(this);
+ listModel = new Ftb::ListModel(this);
+ filterModel->setSourceModel(listModel);
+ ui->packView->setModel(filterModel);
+ ui->packView->setSortingEnabled(true);
+ ui->packView->header()->hide();
+ ui->packView->setIndentation(0);
+
+ ui->searchEdit->installEventFilter(this);
+
+ ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
+ ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300);
+
+ for(int i = 0; i < filterModel->getAvailableSortings().size(); i++)
+ {
+ ui->sortByBox->addItem(filterModel->getAvailableSortings().keys().at(i));
+ }
+ ui->sortByBox->setCurrentText(filterModel->translateCurrentSorting());
+
+ connect(ui->searchEdit, &QLineEdit::textChanged, this, &FtbPage::triggerSearch);
+ connect(ui->sortByBox, &QComboBox::currentTextChanged, this, &FtbPage::onSortingSelectionChanged);
+ connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FtbPage::onSelectionChanged);
+ connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &FtbPage::onVersionSelectionChanged);
+}
+
+FtbPage::~FtbPage()
+{
+ delete ui;
+}
+
+bool FtbPage::eventFilter(QObject* watched, QEvent* event)
+{
+ if (watched == ui->searchEdit && event->type() == QEvent::KeyPress) {
+ QKeyEvent* keyEvent = static_cast<QKeyEvent*>(event);
+ if (keyEvent->key() == Qt::Key_Return) {
+ triggerSearch();
+ keyEvent->accept();
+ return true;
+ }
+ }
+ return QWidget::eventFilter(watched, event);
+}
+
+bool FtbPage::shouldDisplay() const
+{
+ return true;
+}
+
+void FtbPage::openedImpl()
+{
+ if(!initialised)
+ {
+ listModel->request();
+ initialised = true;
+ }
+
+ suggestCurrent();
+}
+
+void FtbPage::suggestCurrent()
+{
+ if(!isOpened)
+ {
+ return;
+ }
+
+ if (selectedVersion.isEmpty())
+ {
+ dialog->setSuggestedPack();
+ return;
+ }
+
+ dialog->setSuggestedPack(selected.name + " " + selectedVersion, new ModpacksCH::PackInstallTask(selected, selectedVersion));
+ for(auto art : selected.art) {
+ if(art.type == "square") {
+ QString editedLogoName;
+ editedLogoName = selected.name;
+
+ listModel->getLogo(selected.name, art.url, [this, editedLogoName](QString logo)
+ {
+ dialog->setSuggestedIconFromFile(logo + ".small", editedLogoName);
+ });
+ }
+ }
+}
+
+void FtbPage::triggerSearch()
+{
+ filterModel->setSearchTerm(ui->searchEdit->text());
+}
+
+void FtbPage::onSortingSelectionChanged(QString data)
+{
+ auto toSet = filterModel->getAvailableSortings().value(data);
+ filterModel->setSorting(toSet);
+}
+
+void FtbPage::onSelectionChanged(QModelIndex first, QModelIndex second)
+{
+ ui->versionSelectionBox->clear();
+
+ if(!first.isValid())
+ {
+ if(isOpened)
+ {
+ dialog->setSuggestedPack();
+ }
+ return;
+ }
+
+ selected = filterModel->data(first, Qt::UserRole).value<ModpacksCH::Modpack>();
+
+ HoeDown hoedown;
+ QString output = hoedown.process(selected.description.toUtf8());
+ ui->packDescription->setHtml(output);
+
+ // reverse foreach, so that the newest versions are first
+ for (auto i = selected.versions.size(); i--;) {
+ ui->versionSelectionBox->addItem(selected.versions.at(i).name);
+ }
+
+ suggestCurrent();
+}
+
+void FtbPage::onVersionSelectionChanged(QString data)
+{
+ if(data.isNull() || data.isEmpty())
+ {
+ selectedVersion = "";
+ return;
+ }
+
+ selectedVersion = data;
+ suggestCurrent();
+}
diff --git a/launcher/ui/pages/modplatform/ftb/FtbPage.h b/launcher/ui/pages/modplatform/ftb/FtbPage.h
new file mode 100644
index 00000000..28a189f0
--- /dev/null
+++ b/launcher/ui/pages/modplatform/ftb/FtbPage.h
@@ -0,0 +1,83 @@
+/* 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 "FtbFilterModel.h"
+#include "FtbListModel.h"
+
+#include <QWidget>
+
+#include "Application.h"
+#include "ui/pages/BasePage.h"
+#include "tasks/Task.h"
+
+namespace Ui
+{
+ class FtbPage;
+}
+
+class NewInstanceDialog;
+
+class FtbPage : public QWidget, public BasePage
+{
+Q_OBJECT
+
+public:
+ explicit FtbPage(NewInstanceDialog* dialog, QWidget *parent = 0);
+ virtual ~FtbPage();
+ virtual QString displayName() const override
+ {
+ return tr("FTB");
+ }
+ virtual QIcon icon() const override
+ {
+ return APPLICATION->getThemedIcon("ftb_logo");
+ }
+ virtual QString id() const override
+ {
+ return "ftb";
+ }
+ virtual QString helpPage() const override
+ {
+ return "FTB-platform";
+ }
+ virtual bool shouldDisplay() const override;
+
+ void openedImpl() override;
+
+ bool eventFilter(QObject * watched, QEvent * event) override;
+
+private:
+ void suggestCurrent();
+
+private slots:
+ void triggerSearch();
+
+ void onSortingSelectionChanged(QString data);
+ void onSelectionChanged(QModelIndex first, QModelIndex second);
+ void onVersionSelectionChanged(QString data);
+
+private:
+ Ui::FtbPage *ui = nullptr;
+ NewInstanceDialog* dialog = nullptr;
+ Ftb::ListModel* listModel = nullptr;
+ Ftb::FilterModel* filterModel = nullptr;
+
+ ModpacksCH::Modpack selected;
+ QString selectedVersion;
+
+ bool initialised { false };
+};
diff --git a/launcher/ui/pages/modplatform/ftb/FtbPage.ui b/launcher/ui/pages/modplatform/ftb/FtbPage.ui
new file mode 100644
index 00000000..e9c783e3
--- /dev/null
+++ b/launcher/ui/pages/modplatform/ftb/FtbPage.ui
@@ -0,0 +1,79 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>FtbPage</class>
+ <widget class="QWidget" name="FtbPage">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>875</width>
+ <height>745</height>
+ </rect>
+ </property>
+ <layout class="QGridLayout" name="gridLayout">
+ <item row="2" column="0" colspan="2">
+ <layout class="QGridLayout" name="gridLayout_4" columnstretch="0,0,0" rowminimumheight="0" columnminimumwidth="0,0,0">
+ <item row="0" column="2">
+ <widget class="QComboBox" name="versionSelectionBox"/>
+ </item>
+ <item row="0" column="1">
+ <widget class="QLabel" name="label">
+ <property name="text">
+ <string>Version selected:</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="0">
+ <widget class="QComboBox" name="sortByBox"/>
+ </item>
+ </layout>
+ </item>
+ <item row="0" column="0">
+ <widget class="QLineEdit" name="searchEdit">
+ <property name="placeholderText">
+ <string>Search and filter ...</string>
+ </property>
+ <property name="clearButtonEnabled">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0" colspan="2">
+ <layout class="QGridLayout" name="gridLayout_3">
+ <item row="0" column="0">
+ <widget class="QTreeView" name="packView">
+ <property name="alternatingRowColors">
+ <bool>true</bool>
+ </property>
+ <property name="iconSize">
+ <size>
+ <width>48</width>
+ <height>48</height>
+ </size>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QTextBrowser" name="packDescription">
+ <property name="openExternalLinks">
+ <bool>true</bool>
+ </property>
+ <property name="openLinks">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ <tabstops>
+ <tabstop>searchEdit</tabstop>
+ <tabstop>versionSelectionBox</tabstop>
+ </tabstops>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp b/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp
new file mode 100644
index 00000000..5fa932b7
--- /dev/null
+++ b/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp
@@ -0,0 +1,259 @@
+#include "ListModel.h"
+#include "Application.h"
+
+#include <MMCStrings.h>
+#include <Version.h>
+
+#include <QtMath>
+#include <QLabel>
+
+#include <RWStorage.h>
+
+#include <BuildConfig.h>
+
+namespace LegacyFTB {
+
+FilterModel::FilterModel(QObject *parent) : QSortFilterProxyModel(parent)
+{
+ currentSorting = Sorting::ByGameVersion;
+ sortings.insert(tr("Sort by name"), Sorting::ByName);
+ sortings.insert(tr("Sort by game version"), Sorting::ByGameVersion);
+}
+
+bool FilterModel::lessThan(const QModelIndex &left, const QModelIndex &right) const
+{
+ Modpack leftPack = sourceModel()->data(left, Qt::UserRole).value<Modpack>();
+ Modpack rightPack = sourceModel()->data(right, Qt::UserRole).value<Modpack>();
+
+ if(currentSorting == Sorting::ByGameVersion) {
+ Version lv(leftPack.mcVersion);
+ Version rv(rightPack.mcVersion);
+ return lv < rv;
+
+ } else if(currentSorting == Sorting::ByName) {
+ return Strings::naturalCompare(leftPack.name, rightPack.name, Qt::CaseSensitive) >= 0;
+ }
+
+ //UHM, some inavlid value set?!
+ qWarning() << "Invalid sorting set!";
+ return true;
+}
+
+bool FilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
+{
+ return true;
+}
+
+const QMap<QString, FilterModel::Sorting> FilterModel::getAvailableSortings()
+{
+ return sortings;
+}
+
+QString FilterModel::translateCurrentSorting()
+{
+ return sortings.key(currentSorting);
+}
+
+void FilterModel::setSorting(Sorting s)
+{
+ currentSorting = s;
+ invalidate();
+}
+
+FilterModel::Sorting FilterModel::getCurrentSorting()
+{
+ return currentSorting;
+}
+
+ListModel::ListModel(QObject *parent) : QAbstractListModel(parent)
+{
+}
+
+ListModel::~ListModel()
+{
+}
+
+QString ListModel::translatePackType(PackType type) const
+{
+ switch(type)
+ {
+ case PackType::Public:
+ return tr("Public Modpack");
+ case PackType::ThirdParty:
+ return tr("Third Party Modpack");
+ case PackType::Private:
+ return tr("Private Modpack");
+ }
+ qWarning() << "Unknown FTB modpack type:" << int(type);
+ return QString();
+}
+
+int ListModel::rowCount(const QModelIndex &parent) const
+{
+ return modpacks.size();
+}
+
+int ListModel::columnCount(const QModelIndex &parent) const
+{
+ return 1;
+}
+
+QVariant ListModel::data(const QModelIndex &index, int role) const
+{
+ int pos = index.row();
+ if(pos >= modpacks.size() || pos < 0 || !index.isValid())
+ {
+ return QString("INVALID INDEX %1").arg(pos);
+ }
+
+ Modpack pack = modpacks.at(pos);
+ if(role == Qt::DisplayRole)
+ {
+ return pack.name + "\n" + translatePackType(pack.type);
+ }
+ else if (role == Qt::ToolTipRole)
+ {
+ if(pack.description.length() > 100)
+ {
+ //some magic to prevent to long tooltips and replace html linebreaks
+ QString edit = pack.description.left(97);
+ edit = edit.left(edit.lastIndexOf("<br>")).left(edit.lastIndexOf(" ")).append("...");
+ return edit;
+
+ }
+ return pack.description;
+ }
+ else if(role == Qt::DecorationRole)
+ {
+ if(m_logoMap.contains(pack.logo))
+ {
+ return (m_logoMap.value(pack.logo));
+ }
+ QIcon icon = APPLICATION->getThemedIcon("screenshot-placeholder");
+ ((ListModel *)this)->requestLogo(pack.logo);
+ return icon;
+ }
+ else if(role == Qt::TextColorRole)
+ {
+ if(pack.broken)
+ {
+ //FIXME: Hardcoded color
+ return QColor(255, 0, 50);
+ }
+ else if(pack.bugged)
+ {
+ //FIXME: Hardcoded color
+ //bugged pack, currently only indicates bugged xml
+ return QColor(244, 229, 66);
+ }
+ }
+ else if(role == Qt::UserRole)
+ {
+ QVariant v;
+ v.setValue(pack);
+ return v;
+ }
+
+ return QVariant();
+}
+
+void ListModel::fill(ModpackList modpacks)
+{
+ beginResetModel();
+ this->modpacks = modpacks;
+ endResetModel();
+}
+
+void ListModel::addPack(Modpack modpack)
+{
+ beginResetModel();
+ this->modpacks.append(modpack);
+ endResetModel();
+}
+
+void ListModel::clear()
+{
+ beginResetModel();
+ modpacks.clear();
+ endResetModel();
+}
+
+Modpack ListModel::at(int row)
+{
+ return modpacks.at(row);
+}
+
+void ListModel::remove(int row)
+{
+ if(row < 0 || row >= modpacks.size())
+ {
+ qWarning() << "Attempt to remove FTB modpacks with invalid row" << row;
+ return;
+ }
+ beginRemoveRows(QModelIndex(), row, row);
+ modpacks.removeAt(row);
+ endRemoveRows();
+}
+
+void ListModel::logoLoaded(QString logo, QIcon out)
+{
+ m_loadingLogos.removeAll(logo);
+ m_logoMap.insert(logo, out);
+ emit dataChanged(createIndex(0, 0), createIndex(1, 0));
+}
+
+void ListModel::logoFailed(QString logo)
+{
+ m_failedLogos.append(logo);
+ m_loadingLogos.removeAll(logo);
+}
+
+void ListModel::requestLogo(QString file)
+{
+ if(m_loadingLogos.contains(file) || m_failedLogos.contains(file))
+ {
+ return;
+ }
+
+ MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("FTBPacks", QString("logos/%1").arg(file.section(".", 0, 0)));
+ NetJob *job = new NetJob(QString("FTB Icon Download for %1").arg(file));
+ job->addNetAction(Net::Download::makeCached(QUrl(QString(BuildConfig.LEGACY_FTB_CDN_BASE_URL + "static/%1").arg(file)), entry));
+
+ auto fullPath = entry->getFullPath();
+ QObject::connect(job, &NetJob::finished, this, [this, file, fullPath]
+ {
+ emit logoLoaded(file, QIcon(fullPath));
+ if(waitingCallbacks.contains(file))
+ {
+ waitingCallbacks.value(file)(fullPath);
+ }
+ });
+
+ QObject::connect(job, &NetJob::failed, this, [this, file]
+ {
+ emit logoFailed(file);
+ });
+
+ job->start(APPLICATION->network());
+
+ m_loadingLogos.append(file);
+}
+
+void ListModel::getLogo(const QString &logo, LogoCallback callback)
+{
+ if(m_logoMap.contains(logo))
+ {
+ callback(APPLICATION->metacache()->resolveEntry("FTBPacks", QString("logos/%1").arg(logo.section(".", 0, 0)))->getFullPath());
+ }
+ else
+ {
+ requestLogo(logo);
+ }
+}
+
+Qt::ItemFlags ListModel::flags(const QModelIndex &index) const
+{
+ return QAbstractListModel::flags(index);
+}
+
+}
diff --git a/launcher/ui/pages/modplatform/legacy_ftb/ListModel.h b/launcher/ui/pages/modplatform/legacy_ftb/ListModel.h
new file mode 100644
index 00000000..c55df000
--- /dev/null
+++ b/launcher/ui/pages/modplatform/legacy_ftb/ListModel.h
@@ -0,0 +1,78 @@
+#pragma once
+
+#include <modplatform/legacy_ftb/PackHelpers.h>
+#include <RWStorage.h>
+
+#include <QAbstractListModel>
+#include <QSortFilterProxyModel>
+#include <QThreadPool>
+#include <QIcon>
+#include <QStyledItemDelegate>
+
+#include <functional>
+
+namespace LegacyFTB {
+
+typedef QMap<QString, QIcon> FTBLogoMap;
+typedef std::function<void(QString)> LogoCallback;
+
+class FilterModel : public QSortFilterProxyModel
+{
+ Q_OBJECT
+public:
+ FilterModel(QObject* parent = Q_NULLPTR);
+ enum Sorting {
+ ByName,
+ ByGameVersion
+ };
+ const QMap<QString, Sorting> getAvailableSortings();
+ QString translateCurrentSorting();
+ void setSorting(Sorting sorting);
+ Sorting getCurrentSorting();
+
+protected:
+ bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
+ bool lessThan(const QModelIndex &left, const QModelIndex &right) const override;
+
+private:
+ QMap<QString, Sorting> sortings;
+ Sorting currentSorting;
+
+};
+
+class ListModel : public QAbstractListModel
+{
+ Q_OBJECT
+private:
+ ModpackList modpacks;
+ QStringList m_failedLogos;
+ QStringList m_loadingLogos;
+ FTBLogoMap m_logoMap;
+ QMap<QString, LogoCallback> waitingCallbacks;
+
+ void requestLogo(QString file);
+ QString translatePackType(PackType type) const;
+
+
+private slots:
+ void logoFailed(QString logo);
+ void logoLoaded(QString logo, QIcon out);
+
+public:
+ ListModel(QObject *parent);
+ ~ListModel();
+ int rowCount(const QModelIndex &parent) const override;
+ int columnCount(const QModelIndex &parent) const override;
+ QVariant data(const QModelIndex &index, int role) const override;
+ Qt::ItemFlags flags(const QModelIndex &index) const override;
+
+ void fill(ModpackList modpacks);
+ void addPack(Modpack modpack);
+ void clear();
+ void remove(int row);
+
+ Modpack at(int row);
+ void getLogo(const QString &logo, LogoCallback callback);
+};
+
+}
diff --git a/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp b/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp
new file mode 100644
index 00000000..891704de
--- /dev/null
+++ b/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp
@@ -0,0 +1,371 @@
+#include "Page.h"
+#include "ui_Page.h"
+
+#include <QInputDialog>
+
+#include "Application.h"
+
+#include "ui/dialogs/CustomMessageBox.h"
+#include "ui/dialogs/NewInstanceDialog.h"
+
+#include "modplatform/legacy_ftb/PackFetchTask.h"
+#include "modplatform/legacy_ftb/PackInstallTask.h"
+#include "modplatform/legacy_ftb/PrivatePackManager.h"
+#include "ListModel.h"
+
+namespace LegacyFTB {
+
+Page::Page(NewInstanceDialog* dialog, QWidget *parent)
+ : QWidget(parent), dialog(dialog), ui(new Ui::Page)
+{
+ ftbFetchTask.reset(new PackFetchTask(APPLICATION->network()));
+ ftbPrivatePacks.reset(new PrivatePackManager());
+
+ ui->setupUi(this);
+
+ {
+ publicFilterModel = new FilterModel(this);
+ publicListModel = new ListModel(this);
+ publicFilterModel->setSourceModel(publicListModel);
+
+ ui->publicPackList->setModel(publicFilterModel);
+ ui->publicPackList->setSortingEnabled(true);
+ ui->publicPackList->header()->hide();
+ ui->publicPackList->setIndentation(0);
+ ui->publicPackList->setIconSize(QSize(42, 42));
+
+ for(int i = 0; i < publicFilterModel->getAvailableSortings().size(); i++)
+ {
+ ui->sortByBox->addItem(publicFilterModel->getAvailableSortings().keys().at(i));
+ }
+
+ ui->sortByBox->setCurrentText(publicFilterModel->translateCurrentSorting());
+ }
+
+ {
+ thirdPartyFilterModel = new FilterModel(this);
+ thirdPartyModel = new ListModel(this);
+ thirdPartyFilterModel->setSourceModel(thirdPartyModel);
+
+ ui->thirdPartyPackList->setModel(thirdPartyFilterModel);
+ ui->thirdPartyPackList->setSortingEnabled(true);
+ ui->thirdPartyPackList->header()->hide();
+ ui->thirdPartyPackList->setIndentation(0);
+ ui->thirdPartyPackList->setIconSize(QSize(42, 42));
+
+ thirdPartyFilterModel->setSorting(publicFilterModel->getCurrentSorting());
+ }
+
+ {
+ privateFilterModel = new FilterModel(this);
+ privateListModel = new ListModel(this);
+ privateFilterModel->setSourceModel(privateListModel);
+
+ ui->privatePackList->setModel(privateFilterModel);
+ ui->privatePackList->setSortingEnabled(true);
+ ui->privatePackList->header()->hide();
+ ui->privatePackList->setIndentation(0);
+ ui->privatePackList->setIconSize(QSize(42, 42));
+
+ privateFilterModel->setSorting(publicFilterModel->getCurrentSorting());
+ }
+
+ ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
+ ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300);
+
+ connect(ui->sortByBox, &QComboBox::currentTextChanged, this, &Page::onSortingSelectionChanged);
+ connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &Page::onVersionSelectionItemChanged);
+
+ connect(ui->publicPackList->selectionModel(), &QItemSelectionModel::currentChanged, this, &Page::onPublicPackSelectionChanged);
+ connect(ui->thirdPartyPackList->selectionModel(), &QItemSelectionModel::currentChanged, this, &Page::onThirdPartyPackSelectionChanged);
+ connect(ui->privatePackList->selectionModel(), &QItemSelectionModel::currentChanged, this, &Page::onPrivatePackSelectionChanged);
+
+ connect(ui->addPackBtn, &QPushButton::pressed, this, &Page::onAddPackClicked);
+ connect(ui->removePackBtn, &QPushButton::pressed, this, &Page::onRemovePackClicked);
+
+ connect(ui->tabWidget, &QTabWidget::currentChanged, this, &Page::onTabChanged);
+
+ // ui->modpackInfo->setOpenExternalLinks(true);
+
+ ui->publicPackList->selectionModel()->reset();
+ ui->thirdPartyPackList->selectionModel()->reset();
+ ui->privatePackList->selectionModel()->reset();
+
+ onTabChanged(ui->tabWidget->currentIndex());
+}
+
+Page::~Page()
+{
+ delete ui;
+}
+
+bool Page::shouldDisplay() const
+{
+ return true;
+}
+
+void Page::openedImpl()
+{
+ if(!initialized)
+ {
+ connect(ftbFetchTask.get(), &PackFetchTask::finished, this, &Page::ftbPackDataDownloadSuccessfully);
+ connect(ftbFetchTask.get(), &PackFetchTask::failed, this, &Page::ftbPackDataDownloadFailed);
+
+ connect(ftbFetchTask.get(), &PackFetchTask::privateFileDownloadFinished, this, &Page::ftbPrivatePackDataDownloadSuccessfully);
+ connect(ftbFetchTask.get(), &PackFetchTask::privateFileDownloadFailed, this, &Page::ftbPrivatePackDataDownloadFailed);
+
+ ftbFetchTask->fetch();
+ ftbPrivatePacks->load();
+ ftbFetchTask->fetchPrivate(ftbPrivatePacks->getCurrentPackCodes().toList());
+ initialized = true;
+ }
+ suggestCurrent();
+}
+
+void Page::suggestCurrent()
+{
+ if(!isOpened)
+ {
+ return;
+ }
+
+ if(selected.broken || selectedVersion.isEmpty())
+ {
+ dialog->setSuggestedPack();
+ return;
+ }
+
+ dialog->setSuggestedPack(selected.name, new PackInstallTask(APPLICATION->network(), selected, selectedVersion));
+ QString editedLogoName;
+ if(selected.logo.toLower().startsWith("ftb"))
+ {
+ editedLogoName = selected.logo;
+ }
+ else
+ {
+ editedLogoName = "ftb_" + selected.logo;
+ }
+
+ editedLogoName = editedLogoName.left(editedLogoName.lastIndexOf(".png"));
+
+ if(selected.type == PackType::Public)
+ {
+ publicListModel->getLogo(selected.logo, [this, editedLogoName](QString logo)
+ {
+ dialog->setSuggestedIconFromFile(logo, editedLogoName);
+ });
+ }
+ else if (selected.type == PackType::ThirdParty)
+ {
+ thirdPartyModel->getLogo(selected.logo, [this, editedLogoName](QString logo)
+ {
+ dialog->setSuggestedIconFromFile(logo, editedLogoName);
+ });
+ }
+ else if (selected.type == PackType::Private)
+ {
+ privateListModel->getLogo(selected.logo, [this, editedLogoName](QString logo)
+ {
+ dialog->setSuggestedIconFromFile(logo, editedLogoName);
+ });
+ }
+}
+
+void Page::ftbPackDataDownloadSuccessfully(ModpackList publicPacks, ModpackList thirdPartyPacks)
+{
+ publicListModel->fill(publicPacks);
+ thirdPartyModel->fill(thirdPartyPacks);
+}
+
+void Page::ftbPackDataDownloadFailed(QString reason)
+{
+ //TODO: Display the error
+}
+
+void Page::ftbPrivatePackDataDownloadSuccessfully(Modpack pack)
+{
+ privateListModel->addPack(pack);
+}
+
+void Page::ftbPrivatePackDataDownloadFailed(QString reason, QString packCode)
+{
+ auto reply = QMessageBox::question(
+ this,
+ tr("FTB private packs"),
+ tr("Failed to download pack information for code %1.\nShould it be removed now?").arg(packCode)
+ );
+ if(reply == QMessageBox::Yes)
+ {
+ ftbPrivatePacks->remove(packCode);
+ }
+}
+
+void Page::onPublicPackSelectionChanged(QModelIndex now, QModelIndex prev)
+{
+ if(!now.isValid())
+ {
+ onPackSelectionChanged();
+ return;
+ }
+ Modpack selectedPack = publicFilterModel->data(now, Qt::UserRole).value<Modpack>();
+ onPackSelectionChanged(&selectedPack);
+}
+
+void Page::onThirdPartyPackSelectionChanged(QModelIndex now, QModelIndex prev)
+{
+ if(!now.isValid())
+ {
+ onPackSelectionChanged();
+ return;
+ }
+ Modpack selectedPack = thirdPartyFilterModel->data(now, Qt::UserRole).value<Modpack>();
+ onPackSelectionChanged(&selectedPack);
+}
+
+void Page::onPrivatePackSelectionChanged(QModelIndex now, QModelIndex prev)
+{
+ if(!now.isValid())
+ {
+ onPackSelectionChanged();
+ return;
+ }
+ Modpack selectedPack = privateFilterModel->data(now, Qt::UserRole).value<Modpack>();
+ onPackSelectionChanged(&selectedPack);
+}
+
+void Page::onPackSelectionChanged(Modpack* pack)
+{
+ ui->versionSelectionBox->clear();
+ if(pack)
+ {
+ currentModpackInfo->setHtml("Pack by <b>" + pack->author + "</b>" +
+ "<br>Minecraft " + pack->mcVersion + "<br>" + "<br>" + pack->description + "<ul><li>" + pack->mods.replace(";", "</li><li>")
+ + "</li></ul>");
+ bool currentAdded = false;
+
+ for(int i = 0; i < pack->oldVersions.size(); i++)
+ {
+ if(pack->currentVersion == pack->oldVersions.at(i))
+ {
+ currentAdded = true;
+ }
+ ui->versionSelectionBox->addItem(pack->oldVersions.at(i));
+ }
+
+ if(!currentAdded)
+ {
+ ui->versionSelectionBox->addItem(pack->currentVersion);
+ }
+ selected = *pack;
+ }
+ else
+ {
+ currentModpackInfo->setHtml("");
+ ui->versionSelectionBox->clear();
+ if(isOpened)
+ {
+ dialog->setSuggestedPack();
+ }
+ return;
+ }
+ suggestCurrent();
+}
+
+void Page::onVersionSelectionItemChanged(QString data)
+{
+ if(data.isNull() || data.isEmpty())
+ {
+ selectedVersion = "";
+ return;
+ }
+
+ selectedVersion = data;
+ suggestCurrent();
+}
+
+void Page::onSortingSelectionChanged(QString data)
+{
+ FilterModel::Sorting toSet = publicFilterModel->getAvailableSortings().value(data);
+ publicFilterModel->setSorting(toSet);
+ thirdPartyFilterModel->setSorting(toSet);
+ privateFilterModel->setSorting(toSet);
+}
+
+void Page::onTabChanged(int tab)
+{
+ if(tab == 1)
+ {
+ currentModel = thirdPartyFilterModel;
+ currentList = ui->thirdPartyPackList;
+ currentModpackInfo = ui->thirdPartyPackDescription;
+ }
+ else if(tab == 2)
+ {
+ currentModel = privateFilterModel;
+ currentList = ui->privatePackList;
+ currentModpackInfo = ui->privatePackDescription;
+ }
+ else
+ {
+ currentModel = publicFilterModel;
+ currentList = ui->publicPackList;
+ currentModpackInfo = ui->publicPackDescription;
+ }
+
+ currentList->selectionModel()->reset();
+ QModelIndex idx = currentList->currentIndex();
+ if(idx.isValid())
+ {
+ auto pack = currentModel->data(idx, Qt::UserRole).value<Modpack>();
+ onPackSelectionChanged(&pack);
+ }
+ else
+ {
+ onPackSelectionChanged();
+ }
+}
+
+void Page::onAddPackClicked()
+{
+ bool ok;
+ QString text = QInputDialog::getText(
+ this,
+ tr("Add FTB pack"),
+ tr("Enter pack code:"),
+ QLineEdit::Normal,
+ QString(),
+ &ok
+ );
+ if(ok && !text.isEmpty())
+ {
+ ftbPrivatePacks->add(text);
+ ftbFetchTask->fetchPrivate({text});
+ }
+}
+
+void Page::onRemovePackClicked()
+{
+ auto index = ui->privatePackList->currentIndex();
+ if(!index.isValid())
+ {
+ return;
+ }
+ auto row = index.row();
+ Modpack pack = privateListModel->at(row);
+ auto answer = QMessageBox::question(
+ this,
+ tr("Remove pack"),
+ tr("Are you sure you want to remove pack %1?").arg(pack.name),
+ QMessageBox::Yes | QMessageBox::No
+ );
+ if(answer != QMessageBox::Yes)
+ {
+ return;
+ }
+
+ ftbPrivatePacks->remove(pack.packCode);
+ privateListModel->remove(row);
+ onPackSelectionChanged();
+}
+
+}
diff --git a/launcher/ui/pages/modplatform/legacy_ftb/Page.h b/launcher/ui/pages/modplatform/legacy_ftb/Page.h
new file mode 100644
index 00000000..d8225e11
--- /dev/null
+++ b/launcher/ui/pages/modplatform/legacy_ftb/Page.h
@@ -0,0 +1,119 @@
+/* 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 <QWidget>
+#include <QTreeView>
+#include <QTextBrowser>
+
+#include "ui/pages/BasePage.h"
+#include <Application.h>
+#include "tasks/Task.h"
+#include "modplatform/legacy_ftb/PackHelpers.h"
+#include "modplatform/legacy_ftb/PackFetchTask.h"
+#include "QObjectPtr.h"
+
+class NewInstanceDialog;
+
+namespace LegacyFTB {
+
+namespace Ui
+{
+class Page;
+}
+
+class ListModel;
+class FilterModel;
+class PrivatePackListModel;
+class PrivatePackFilterModel;
+class PrivatePackManager;
+
+class Page : public QWidget, public BasePage
+{
+ Q_OBJECT
+
+public:
+ explicit Page(NewInstanceDialog * dialog, QWidget *parent = 0);
+ virtual ~Page();
+ QString displayName() const override
+ {
+ return tr("FTB Legacy");
+ }
+ QIcon icon() const override
+ {
+ return APPLICATION->getThemedIcon("ftb_logo");
+ }
+ QString id() const override
+ {
+ return "legacy_ftb";
+ }
+ QString helpPage() const override
+ {
+ return "FTB-platform";
+ }
+ bool shouldDisplay() const override;
+ void openedImpl() override;
+
+private:
+ void suggestCurrent();
+ void onPackSelectionChanged(Modpack *pack = nullptr);
+
+private slots:
+ void ftbPackDataDownloadSuccessfully(ModpackList publicPacks, ModpackList thirdPartyPacks);
+ void ftbPackDataDownloadFailed(QString reason);
+
+ void ftbPrivatePackDataDownloadSuccessfully(Modpack pack);
+ void ftbPrivatePackDataDownloadFailed(QString reason, QString packCode);
+
+ void onSortingSelectionChanged(QString data);
+ void onVersionSelectionItemChanged(QString data);
+
+ void onPublicPackSelectionChanged(QModelIndex first, QModelIndex second);
+ void onThirdPartyPackSelectionChanged(QModelIndex first, QModelIndex second);
+ void onPrivatePackSelectionChanged(QModelIndex first, QModelIndex second);
+
+ void onTabChanged(int tab);
+
+ void onAddPackClicked();
+ void onRemovePackClicked();
+
+private:
+ FilterModel* currentModel = nullptr;
+ QTreeView* currentList = nullptr;
+ QTextBrowser* currentModpackInfo = nullptr;
+
+ bool initialized = false;
+ Modpack selected;
+ QString selectedVersion;
+
+ ListModel* publicListModel = nullptr;
+ FilterModel* publicFilterModel = nullptr;
+
+ ListModel *thirdPartyModel = nullptr;
+ FilterModel *thirdPartyFilterModel = nullptr;
+
+ ListModel *privateListModel = nullptr;
+ FilterModel *privateFilterModel = nullptr;
+
+ unique_qobject_ptr<PackFetchTask> ftbFetchTask;
+ std::unique_ptr<PrivatePackManager> ftbPrivatePacks;
+
+ NewInstanceDialog* dialog = nullptr;
+
+ Ui::Page *ui = nullptr;
+};
+
+}
diff --git a/launcher/ui/pages/modplatform/legacy_ftb/Page.ui b/launcher/ui/pages/modplatform/legacy_ftb/Page.ui
new file mode 100644
index 00000000..15e5d432
--- /dev/null
+++ b/launcher/ui/pages/modplatform/legacy_ftb/Page.ui
@@ -0,0 +1,135 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>LegacyFTB::Page</class>
+ <widget class="QWidget" name="LegacyFTB::Page">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>709</width>
+ <height>602</height>
+ </rect>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <widget class="QTabWidget" name="tabWidget">
+ <property name="currentIndex">
+ <number>0</number>
+ </property>
+ <widget class="QWidget" name="tab">
+ <attribute name="title">
+ <string>Public</string>
+ </attribute>
+ <layout class="QGridLayout" name="gridLayout">
+ <item row="0" column="0">
+ <widget class="QTreeView" name="publicPackList">
+ <property name="maximumSize">
+ <size>
+ <width>250</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="alternatingRowColors">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QTextBrowser" name="publicPackDescription"/>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QWidget" name="tab_2">
+ <attribute name="title">
+ <string>3rd Party</string>
+ </attribute>
+ <layout class="QGridLayout" name="gridLayout_3">
+ <item row="0" column="1">
+ <widget class="QTextBrowser" name="thirdPartyPackDescription"/>
+ </item>
+ <item row="0" column="0">
+ <widget class="QTreeView" name="thirdPartyPackList">
+ <property name="maximumSize">
+ <size>
+ <width>250</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="alternatingRowColors">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QWidget" name="tab_3">
+ <attribute name="title">
+ <string>Private</string>
+ </attribute>
+ <layout class="QGridLayout" name="gridLayout_2">
+ <item row="0" column="0">
+ <widget class="QTreeView" name="privatePackList">
+ <property name="maximumSize">
+ <size>
+ <width>250</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="alternatingRowColors">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0">
+ <widget class="QPushButton" name="addPackBtn">
+ <property name="text">
+ <string>Add pack</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="0">
+ <widget class="QPushButton" name="removePackBtn">
+ <property name="text">
+ <string>Remove selected pack</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1" rowspan="3">
+ <widget class="QTextBrowser" name="privatePackDescription"/>
+ </item>
+ </layout>
+ </widget>
+ </widget>
+ </item>
+ <item>
+ <layout class="QGridLayout" name="gridLayout_4">
+ <item row="0" column="1">
+ <widget class="QLabel" name="label">
+ <property name="text">
+ <string>Version selected:</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="2">
+ <widget class="QComboBox" name="versionSelectionBox"/>
+ </item>
+ <item row="0" column="0">
+ <widget class="QComboBox" name="sortByBox">
+ <property name="minimumSize">
+ <size>
+ <width>265</width>
+ <height>0</height>
+ </size>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/launcher/ui/pages/modplatform/technic/TechnicData.h b/launcher/ui/pages/modplatform/technic/TechnicData.h
new file mode 100644
index 00000000..50fd75e8
--- /dev/null
+++ b/launcher/ui/pages/modplatform/technic/TechnicData.h
@@ -0,0 +1,42 @@
+/* Copyright 2020-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 <QList>
+#include <QString>
+
+namespace Technic {
+struct Modpack {
+ QString slug;
+
+ QString name;
+ QString logoUrl;
+ QString logoName;
+
+ bool broken = true;
+
+ QString url;
+ bool isSolder = false;
+ QString minecraftVersion;
+
+ bool metadataLoaded = false;
+ QString websiteUrl;
+ QString author;
+ QString description;
+};
+}
+
+Q_DECLARE_METATYPE(Technic::Modpack)
diff --git a/launcher/ui/pages/modplatform/technic/TechnicModel.cpp b/launcher/ui/pages/modplatform/technic/TechnicModel.cpp
new file mode 100644
index 00000000..63c2d4c4
--- /dev/null
+++ b/launcher/ui/pages/modplatform/technic/TechnicModel.cpp
@@ -0,0 +1,237 @@
+/* Copyright 2020-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 "TechnicModel.h"
+#include "Application.h"
+#include "Json.h"
+
+#include <QIcon>
+
+Technic::ListModel::ListModel(QObject *parent) : QAbstractListModel(parent)
+{
+}
+
+Technic::ListModel::~ListModel()
+{
+}
+
+QVariant Technic::ListModel::data(const QModelIndex& index, int role) const
+{
+ int pos = index.row();
+ if(pos >= modpacks.size() || pos < 0 || !index.isValid())
+ {
+ return QString("INVALID INDEX %1").arg(pos);
+ }
+
+ Modpack pack = modpacks.at(pos);
+ if(role == Qt::DisplayRole)
+ {
+ return pack.name;
+ }
+ else if(role == Qt::DecorationRole)
+ {
+ if(m_logoMap.contains(pack.logoName))
+ {
+ return (m_logoMap.value(pack.logoName));
+ }
+ QIcon icon = APPLICATION->getThemedIcon("screenshot-placeholder");
+ ((ListModel *)this)->requestLogo(pack.logoName, pack.logoUrl);
+ return icon;
+ }
+ else if(role == Qt::UserRole)
+ {
+ QVariant v;
+ v.setValue(pack);
+ return v;
+ }
+ return QVariant();
+}
+
+int Technic::ListModel::columnCount(const QModelIndex&) const
+{
+ return 1;
+}
+
+int Technic::ListModel::rowCount(const QModelIndex&) const
+{
+ return modpacks.size();
+}
+
+void Technic::ListModel::searchWithTerm(const QString& term)
+{
+ if(currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull()) {
+ return;
+ }
+ currentSearchTerm = term;
+ if(jobPtr) {
+ jobPtr->abort();
+ searchState = ResetRequested;
+ return;
+ }
+ else {
+ beginResetModel();
+ modpacks.clear();
+ endResetModel();
+ searchState = None;
+ }
+ performSearch();
+}
+
+void Technic::ListModel::performSearch()
+{
+ NetJob *netJob = new NetJob("Technic::Search");
+ QString searchUrl = "";
+ if (currentSearchTerm.isEmpty()) {
+ searchUrl = "https://api.technicpack.net/trending?build=multimc";
+ }
+ else
+ {
+ searchUrl = QString(
+ "https://api.technicpack.net/search?build=multimc&q=%1"
+ ).arg(currentSearchTerm);
+ }
+ netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response));
+ jobPtr = netJob;
+ jobPtr->start(APPLICATION->network());
+ QObject::connect(netJob, &NetJob::succeeded, this, &ListModel::searchRequestFinished);
+ QObject::connect(netJob, &NetJob::failed, this, &ListModel::searchRequestFailed);
+}
+
+void Technic::ListModel::searchRequestFinished()
+{
+ jobPtr.reset();
+
+ QJsonParseError parse_error;
+ QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error);
+ if(parse_error.error != QJsonParseError::NoError)
+ {
+ qWarning() << "Error while parsing JSON response from Technic at " << parse_error.offset << " reason: " << parse_error.errorString();
+ qWarning() << response;
+ return;
+ }
+
+ QList<Modpack> newList;
+ try {
+ auto root = Json::requireObject(doc);
+ auto objs = Json::requireArray(root, "modpacks");
+ for (auto technicPack: objs) {
+ Modpack pack;
+ auto technicPackObject = Json::requireObject(technicPack);
+ pack.name = Json::requireString(technicPackObject, "name");
+ pack.slug = Json::requireString(technicPackObject, "slug");
+ if (pack.slug == "vanilla")
+ continue;
+
+ auto rawURL = Json::ensureString(technicPackObject, "iconUrl", "null");
+ if(rawURL == "null") {
+ pack.logoUrl = "null";
+ pack.logoName = "null";
+ }
+ else {
+ pack.logoUrl = rawURL;
+ pack.logoName = rawURL.section(QLatin1Char('/'), -1).section(QLatin1Char('.'), 0, 0);
+ }
+ pack.broken = false;
+ newList.append(pack);
+ }
+ }
+ catch (const JSONValidationError &err)
+ {
+ qCritical() << "Couldn't parse technic search results:" << err.cause() ;
+ return;
+ }
+ searchState = Finished;
+ beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size() + newList.size() - 1);
+ modpacks.append(newList);
+ endInsertRows();
+}
+
+void Technic::ListModel::getLogo(const QString& logo, const QString& logoUrl, Technic::LogoCallback callback)
+{
+ if(m_logoMap.contains(logo))
+ {
+ callback(APPLICATION->metacache()->resolveEntry("TechnicPacks", QString("logos/%1").arg(logo))->getFullPath());
+ }
+ else
+ {
+ requestLogo(logo, logoUrl);
+ }
+}
+
+void Technic::ListModel::searchRequestFailed()
+{
+ jobPtr.reset();
+
+ if(searchState == ResetRequested)
+ {
+ beginResetModel();
+ modpacks.clear();
+ endResetModel();
+
+ performSearch();
+ }
+ else
+ {
+ searchState = Finished;
+ }
+}
+
+
+void Technic::ListModel::logoLoaded(QString logo, QString out)
+{
+ m_loadingLogos.removeAll(logo);
+ m_logoMap.insert(logo, QIcon(out));
+ for(int i = 0; i < modpacks.size(); i++)
+ {
+ if(modpacks[i].logoName == logo)
+ {
+ emit dataChanged(createIndex(i, 0), createIndex(i, 0), {Qt::DecorationRole});
+ }
+ }
+}
+
+void Technic::ListModel::logoFailed(QString logo)
+{
+ m_failedLogos.append(logo);
+ m_loadingLogos.removeAll(logo);
+}
+
+void Technic::ListModel::requestLogo(QString logo, QString url)
+{
+ if(m_loadingLogos.contains(logo) || m_failedLogos.contains(logo) || logo == "null")
+ {
+ return;
+ }
+
+ MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("TechnicPacks", QString("logos/%1").arg(logo));
+ NetJob *job = new NetJob(QString("Technic Icon Download %1").arg(logo));
+ job->addNetAction(Net::Download::makeCached(QUrl(url), entry));
+
+ auto fullPath = entry->getFullPath();
+
+ QObject::connect(job, &NetJob::succeeded, this, [this, logo, fullPath]
+ {
+ logoLoaded(logo, fullPath);
+ });
+
+ QObject::connect(job, &NetJob::failed, this, [this, logo]
+ {
+ logoFailed(logo);
+ });
+
+ job->start(APPLICATION->network());
+
+ m_loadingLogos.append(logo);
+}
diff --git a/launcher/ui/pages/modplatform/technic/TechnicModel.h b/launcher/ui/pages/modplatform/technic/TechnicModel.h
new file mode 100644
index 00000000..e80e6e7c
--- /dev/null
+++ b/launcher/ui/pages/modplatform/technic/TechnicModel.h
@@ -0,0 +1,70 @@
+/* Copyright 2020-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 <QModelIndex>
+
+#include "TechnicData.h"
+#include "net/NetJob.h"
+
+namespace Technic {
+
+typedef std::function<void(QString)> LogoCallback;
+
+class ListModel : public QAbstractListModel
+{
+ Q_OBJECT
+
+public:
+ ListModel(QObject *parent);
+ virtual ~ListModel();
+
+ virtual QVariant data(const QModelIndex& index, int role) const;
+ virtual int columnCount(const QModelIndex& parent) const;
+ virtual int rowCount(const QModelIndex& parent) const;
+
+ void getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback);
+ void searchWithTerm(const QString & term);
+
+private slots:
+ void searchRequestFinished();
+ void searchRequestFailed();
+
+ void logoFailed(QString logo);
+ void logoLoaded(QString logo, QString out);
+
+private:
+ void performSearch();
+ void requestLogo(QString logo, QString url);
+
+private:
+ QList<Modpack> modpacks;
+ QStringList m_failedLogos;
+ QStringList m_loadingLogos;
+ QMap<QString, QIcon> m_logoMap;
+ QMap<QString, LogoCallback> waitingCallbacks;
+
+ QString currentSearchTerm;
+ enum SearchState {
+ None,
+ ResetRequested,
+ Finished
+ } searchState = None;
+ NetJob::Ptr jobPtr;
+ QByteArray response;
+};
+
+}
diff --git a/launcher/ui/pages/modplatform/technic/TechnicPage.cpp b/launcher/ui/pages/modplatform/technic/TechnicPage.cpp
new file mode 100644
index 00000000..ac69675c
--- /dev/null
+++ b/launcher/ui/pages/modplatform/technic/TechnicPage.cpp
@@ -0,0 +1,201 @@
+/* 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 "TechnicPage.h"
+#include "ui_TechnicPage.h"
+
+#include <QKeyEvent>
+
+#include "ui/dialogs/NewInstanceDialog.h"
+
+#include "TechnicModel.h"
+#include "modplatform/technic/SingleZipPackInstallTask.h"
+#include "modplatform/technic/SolderPackInstallTask.h"
+#include "Json.h"
+
+#include "Application.h"
+
+TechnicPage::TechnicPage(NewInstanceDialog* dialog, QWidget *parent)
+ : QWidget(parent), ui(new Ui::TechnicPage), dialog(dialog)
+{
+ ui->setupUi(this);
+ connect(ui->searchButton, &QPushButton::clicked, this, &TechnicPage::triggerSearch);
+ ui->searchEdit->installEventFilter(this);
+ model = new Technic::ListModel(this);
+ ui->packView->setModel(model);
+ connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &TechnicPage::onSelectionChanged);
+}
+
+bool TechnicPage::eventFilter(QObject* watched, QEvent* event)
+{
+ if (watched == ui->searchEdit && event->type() == QEvent::KeyPress) {
+ QKeyEvent* keyEvent = static_cast<QKeyEvent*>(event);
+ if (keyEvent->key() == Qt::Key_Return) {
+ triggerSearch();
+ keyEvent->accept();
+ return true;
+ }
+ }
+ return QWidget::eventFilter(watched, event);
+}
+
+TechnicPage::~TechnicPage()
+{
+ delete ui;
+}
+
+bool TechnicPage::shouldDisplay() const
+{
+ return true;
+}
+
+void TechnicPage::openedImpl()
+{
+ suggestCurrent();
+ triggerSearch();
+}
+
+void TechnicPage::triggerSearch() {
+ model->searchWithTerm(ui->searchEdit->text());
+}
+
+void TechnicPage::onSelectionChanged(QModelIndex first, QModelIndex second)
+{
+ if(!first.isValid())
+ {
+ if(isOpened)
+ {
+ dialog->setSuggestedPack();
+ }
+ //ui->frame->clear();
+ return;
+ }
+
+ current = model->data(first, Qt::UserRole).value<Technic::Modpack>();
+ suggestCurrent();
+}
+
+void TechnicPage::suggestCurrent()
+{
+ if (!isOpened)
+ {
+ return;
+ }
+ if (current.broken)
+ {
+ dialog->setSuggestedPack();
+ return;
+ }
+
+ QString editedLogoName = "technic_" + current.logoName.section(".", 0, 0);
+ model->getLogo(current.logoName, current.logoUrl, [this, editedLogoName](QString logo)
+ {
+ dialog->setSuggestedIconFromFile(logo, editedLogoName);
+ });
+
+ if (current.metadataLoaded)
+ {
+ metadataLoaded();
+ return;
+ }
+
+ NetJob *netJob = new NetJob(QString("Technic::PackMeta(%1)").arg(current.name));
+ std::shared_ptr<QByteArray> response = std::make_shared<QByteArray>();
+ QString slug = current.slug;
+ netJob->addNetAction(Net::Download::makeByteArray(QString("https://api.technicpack.net/modpack/%1?build=multimc").arg(slug), response.get()));
+ QObject::connect(netJob, &NetJob::succeeded, this, [this, response, slug]
+ {
+ if (current.slug != slug)
+ {
+ return;
+ }
+ QJsonParseError parse_error;
+ QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
+ QJsonObject obj = doc.object();
+ if(parse_error.error != QJsonParseError::NoError)
+ {
+ qWarning() << "Error while parsing JSON response from Technic at " << parse_error.offset << " reason: " << parse_error.errorString();
+ qWarning() << *response;
+ return;
+ }
+ if (!obj.contains("url"))
+ {
+ qWarning() << "Json doesn't contain an url key";
+ return;
+ }
+ QJsonValueRef url = obj["url"];
+ if (url.isString())
+ {
+ current.url = url.toString();
+ }
+ else
+ {
+ if (!obj.contains("solder"))
+ {
+ qWarning() << "Json doesn't contain a valid url or solder key";
+ return;
+ }
+ QJsonValueRef solderUrl = obj["solder"];
+ if (solderUrl.isString())
+ {
+ current.url = solderUrl.toString();
+ current.isSolder = true;
+ }
+ else
+ {
+ qWarning() << "Json doesn't contain a valid url or solder key";
+ return;
+ }
+ }
+
+ current.minecraftVersion = Json::ensureString(obj, "minecraft", QString(), "__placeholder__");
+ current.websiteUrl = Json::ensureString(obj, "platformUrl", QString(), "__placeholder__");
+ current.author = Json::ensureString(obj, "user", QString(), "__placeholder__");
+ current.description = Json::ensureString(obj, "description", QString(), "__placeholder__");
+ current.metadataLoaded = true;
+ metadataLoaded();
+ });
+ netJob->start(APPLICATION->network());
+}
+
+// expects current.metadataLoaded to be true
+void TechnicPage::metadataLoaded()
+{
+ QString text = "";
+ QString name = current.name;
+
+ if (current.websiteUrl.isEmpty())
+ // This allows injecting HTML here.
+ text = name;
+ else
+ // URL not properly escaped for inclusion in HTML. The name allows for injecting HTML.
+ text = "<a href=\"" + current.websiteUrl + "\">" + name + "</a>";
+ if (!current.author.isEmpty()) {
+ // This allows injecting HTML here
+ text += tr(" by ") + current.author;
+ }
+
+ ui->frame->setModText(text);
+ ui->frame->setModDescription(current.description);
+ if (!current.isSolder)
+ {
+ dialog->setSuggestedPack(current.name, new Technic::SingleZipPackInstallTask(current.url, current.minecraftVersion));
+ }
+ else
+ {
+ while (current.url.endsWith('/')) current.url.chop(1);
+ dialog->setSuggestedPack(current.name, new Technic::SolderPackInstallTask(APPLICATION->network(), current.url + "/modpack/" + current.slug, current.minecraftVersion));
+ }
+}
diff --git a/launcher/ui/pages/modplatform/technic/TechnicPage.h b/launcher/ui/pages/modplatform/technic/TechnicPage.h
new file mode 100644
index 00000000..21695dd0
--- /dev/null
+++ b/launcher/ui/pages/modplatform/technic/TechnicPage.h
@@ -0,0 +1,78 @@
+/* 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 <QWidget>
+
+#include "ui/pages/BasePage.h"
+#include <Application.h>
+#include "tasks/Task.h"
+#include "TechnicData.h"
+
+namespace Ui
+{
+class TechnicPage;
+}
+
+class NewInstanceDialog;
+
+namespace Technic {
+ class ListModel;
+}
+
+class TechnicPage : public QWidget, public BasePage
+{
+ Q_OBJECT
+
+public:
+ explicit TechnicPage(NewInstanceDialog* dialog, QWidget *parent = 0);
+ virtual ~TechnicPage();
+ virtual QString displayName() const override
+ {
+ return tr("Technic");
+ }
+ virtual QIcon icon() const override
+ {
+ return APPLICATION->getThemedIcon("technic");
+ }
+ virtual QString id() const override
+ {
+ return "technic";
+ }
+ virtual QString helpPage() const override
+ {
+ return "Technic-platform";
+ }
+ virtual bool shouldDisplay() const override;
+
+ void openedImpl() override;
+
+ bool eventFilter(QObject* watched, QEvent* event) override;
+
+private:
+ void suggestCurrent();
+ void metadataLoaded();
+
+private slots:
+ void triggerSearch();
+ void onSelectionChanged(QModelIndex first, QModelIndex second);
+
+private:
+ Ui::TechnicPage *ui = nullptr;
+ NewInstanceDialog* dialog = nullptr;
+ Technic::ListModel* model = nullptr;
+ Technic::Modpack current;
+};
diff --git a/launcher/ui/pages/modplatform/technic/TechnicPage.ui b/launcher/ui/pages/modplatform/technic/TechnicPage.ui
new file mode 100644
index 00000000..dde685d9
--- /dev/null
+++ b/launcher/ui/pages/modplatform/technic/TechnicPage.ui
@@ -0,0 +1,95 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>TechnicPage</class>
+ <widget class="QWidget" name="TechnicPage">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>546</width>
+ <height>405</height>
+ </rect>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <widget class="QWidget" name="widget" native="true">
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QLineEdit" name="searchEdit">
+ <property name="placeholderText">
+ <string>Search and filter ...</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="searchButton">
+ <property name="text">
+ <string>Search</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QListView" name="packView">
+ <property name="horizontalScrollBarPolicy">
+ <enum>Qt::ScrollBarAlwaysOff</enum>
+ </property>
+ <property name="alternatingRowColors">
+ <bool>true</bool>
+ </property>
+ <property name="iconSize">
+ <size>
+ <width>48</width>
+ <height>48</height>
+ </size>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="MCModInfoFrame" name="frame">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="frameShape">
+ <enum>QFrame::StyledPanel</enum>
+ </property>
+ <property name="frameShadow">
+ <enum>QFrame::Raised</enum>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <customwidgets>
+ <customwidget>
+ <class>MCModInfoFrame</class>
+ <extends>QFrame</extends>
+ <header>ui/widgets/MCModInfoFrame.h</header>
+ <container>1</container>
+ </customwidget>
+ </customwidgets>
+ <tabstops>
+ <tabstop>searchEdit</tabstop>
+ <tabstop>searchButton</tabstop>
+ <tabstop>packView</tabstop>
+ </tabstops>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/launcher/ui/setupwizard/AnalyticsWizardPage.cpp b/launcher/ui/setupwizard/AnalyticsWizardPage.cpp
new file mode 100644
index 00000000..3db2f6dc
--- /dev/null
+++ b/launcher/ui/setupwizard/AnalyticsWizardPage.cpp
@@ -0,0 +1,63 @@
+#include "AnalyticsWizardPage.h"
+#include <Application.h>
+
+#include <QVBoxLayout>
+#include <QTextBrowser>
+#include <QCheckBox>
+
+#include <ganalytics.h>
+#include <BuildConfig.h>
+
+AnalyticsWizardPage::AnalyticsWizardPage(QWidget *parent)
+ : BaseWizardPage(parent)
+{
+ setObjectName(QStringLiteral("analyticsPage"));
+ verticalLayout_3 = new QVBoxLayout(this);
+ verticalLayout_3->setObjectName(QStringLiteral("verticalLayout_3"));
+ textBrowser = new QTextBrowser(this);
+ textBrowser->setObjectName(QStringLiteral("textBrowser"));
+ textBrowser->setAcceptRichText(false);
+ textBrowser->setOpenExternalLinks(true);
+ verticalLayout_3->addWidget(textBrowser);
+
+ checkBox = new QCheckBox(this);
+ checkBox->setObjectName(QStringLiteral("checkBox"));
+ checkBox->setChecked(true);
+ verticalLayout_3->addWidget(checkBox);
+ retranslate();
+}
+
+AnalyticsWizardPage::~AnalyticsWizardPage()
+{
+}
+
+bool AnalyticsWizardPage::validatePage()
+{
+ auto settings = APPLICATION->settings();
+ auto analytics = APPLICATION->analytics();
+ auto status = checkBox->isChecked();
+ settings->set("AnalyticsSeen", analytics->version());
+ settings->set("Analytics", status);
+ return true;
+}
+
+void AnalyticsWizardPage::retranslate()
+{
+ setTitle(tr("Analytics"));
+ setSubTitle(tr("We track some anonymous statistics about users."));
+ textBrowser->setHtml(tr(
+ "<html><body>"
+ "<p>The launcher sends anonymous usage statistics on every start of the application. This helps us decide what platforms and issues to focus on.</p>"
+ "<p>The data is processed by Google Analytics, see their <a href=\"https://support.google.com/analytics/answer/6004245?hl=en\">article on the "
+ "matter</a>.</p>"
+ "<p>The following data is collected:</p>"
+ "<ul><li>A random unique ID of the installation.<br />It is stored in the application settings file.</li>"
+ "<li>Anonymized (partial) IP address.</li>"
+ "<li>Launcher version.</li>"
+ "<li>Operating system name, version and architecture.</li>"
+ "<li>CPU architecture (kernel architecture on linux).</li>"
+ "<li>Size of system memory.</li>"
+ "<li>Java version, architecture and memory settings.</li></ul>"
+ "<p>If we change the tracked information, you will see this page again.</p></body></html>"));
+ checkBox->setText(tr("Enable Analytics"));
+}
diff --git a/launcher/ui/setupwizard/AnalyticsWizardPage.h b/launcher/ui/setupwizard/AnalyticsWizardPage.h
new file mode 100644
index 00000000..c451db2c
--- /dev/null
+++ b/launcher/ui/setupwizard/AnalyticsWizardPage.h
@@ -0,0 +1,25 @@
+#pragma once
+
+#include "BaseWizardPage.h"
+
+class QVBoxLayout;
+class QTextBrowser;
+class QCheckBox;
+
+class AnalyticsWizardPage : public BaseWizardPage
+{
+ Q_OBJECT
+public:
+ explicit AnalyticsWizardPage(QWidget *parent = Q_NULLPTR);
+ virtual ~AnalyticsWizardPage();
+
+ bool validatePage() override;
+
+protected:
+ void retranslate() override;
+
+private:
+ QVBoxLayout *verticalLayout_3 = nullptr;
+ QTextBrowser *textBrowser = nullptr;
+ QCheckBox *checkBox = nullptr;
+}; \ No newline at end of file
diff --git a/launcher/ui/setupwizard/BaseWizardPage.h b/launcher/ui/setupwizard/BaseWizardPage.h
new file mode 100644
index 00000000..72dbecfd
--- /dev/null
+++ b/launcher/ui/setupwizard/BaseWizardPage.h
@@ -0,0 +1,33 @@
+#pragma once
+
+#include <QWizardPage>
+#include <QEvent>
+
+class BaseWizardPage : public QWizardPage
+{
+public:
+ explicit BaseWizardPage(QWidget *parent = Q_NULLPTR)
+ : QWizardPage(parent)
+ {
+ }
+ virtual ~BaseWizardPage() {};
+
+ virtual bool wantsRefreshButton()
+ {
+ return false;
+ }
+ virtual void refresh()
+ {
+ }
+
+protected:
+ virtual void retranslate() = 0;
+ void changeEvent(QEvent * event) override
+ {
+ if (event->type() == QEvent::LanguageChange)
+ {
+ retranslate();
+ }
+ QWizardPage::changeEvent(event);
+ }
+};
diff --git a/launcher/ui/setupwizard/JavaWizardPage.cpp b/launcher/ui/setupwizard/JavaWizardPage.cpp
new file mode 100644
index 00000000..63b3d480
--- /dev/null
+++ b/launcher/ui/setupwizard/JavaWizardPage.cpp
@@ -0,0 +1,98 @@
+#include "JavaWizardPage.h"
+#include "Application.h"
+
+#include <QVBoxLayout>
+#include <QGroupBox>
+#include <QSpinBox>
+#include <QLabel>
+#include <QLineEdit>
+#include <QPushButton>
+#include <QToolButton>
+#include <QFileDialog>
+
+#include <sys.h>
+
+#include "FileSystem.h"
+#include "java/JavaInstall.h"
+#include "java/JavaUtils.h"
+#include "JavaCommon.h"
+
+#include "ui/widgets/VersionSelectWidget.h"
+#include "ui/dialogs/CustomMessageBox.h"
+#include "ui/widgets/JavaSettingsWidget.h"
+
+
+JavaWizardPage::JavaWizardPage(QWidget *parent)
+ :BaseWizardPage(parent)
+{
+ setupUi();
+}
+
+void JavaWizardPage::setupUi()
+{
+ setObjectName(QStringLiteral("javaPage"));
+ QVBoxLayout * layout = new QVBoxLayout(this);
+
+ m_java_widget = new JavaSettingsWidget(this);
+ layout->addWidget(m_java_widget);
+ setLayout(layout);
+
+ retranslate();
+}
+
+void JavaWizardPage::refresh()
+{
+ m_java_widget->refresh();
+}
+
+void JavaWizardPage::initializePage()
+{
+ m_java_widget->initialize();
+}
+
+bool JavaWizardPage::wantsRefreshButton()
+{
+ return true;
+}
+
+bool JavaWizardPage::validatePage()
+{
+ auto settings = APPLICATION->settings();
+ auto result = m_java_widget->validate();
+ switch(result)
+ {
+ default:
+ case JavaSettingsWidget::ValidationStatus::Bad:
+ {
+ return false;
+ }
+ case JavaSettingsWidget::ValidationStatus::AllOK:
+ {
+ settings->set("JavaPath", m_java_widget->javaPath());
+ }
+ case JavaSettingsWidget::ValidationStatus::JavaBad:
+ {
+ // Memory
+ auto s = APPLICATION->settings();
+ s->set("MinMemAlloc", m_java_widget->minHeapSize());
+ s->set("MaxMemAlloc", m_java_widget->maxHeapSize());
+ if (m_java_widget->permGenEnabled())
+ {
+ s->set("PermGen", m_java_widget->permGenSize());
+ }
+ else
+ {
+ s->reset("PermGen");
+ }
+ return true;
+ }
+ }
+}
+
+void JavaWizardPage::retranslate()
+{
+ setTitle(tr("Java"));
+ setSubTitle(tr("You do not have a working Java set up yet or it went missing.\n"
+ "Please select one of the following or browse for a java executable."));
+ m_java_widget->retranslate();
+}
diff --git a/launcher/ui/setupwizard/JavaWizardPage.h b/launcher/ui/setupwizard/JavaWizardPage.h
new file mode 100644
index 00000000..0d749039
--- /dev/null
+++ b/launcher/ui/setupwizard/JavaWizardPage.h
@@ -0,0 +1,29 @@
+#pragma once
+
+#include "BaseWizardPage.h"
+
+class JavaSettingsWidget;
+
+class JavaWizardPage : public BaseWizardPage
+{
+ Q_OBJECT
+public:
+ explicit JavaWizardPage(QWidget *parent = Q_NULLPTR);
+
+ virtual ~JavaWizardPage()
+ {
+ };
+
+ bool wantsRefreshButton() override;
+ void refresh() override;
+ void initializePage() override;
+ bool validatePage() override;
+
+protected: /* methods */
+ void setupUi();
+ void retranslate() override;
+
+private: /* data */
+ JavaSettingsWidget *m_java_widget = nullptr;
+};
+
diff --git a/launcher/ui/setupwizard/LanguageWizardPage.cpp b/launcher/ui/setupwizard/LanguageWizardPage.cpp
new file mode 100644
index 00000000..072df10d
--- /dev/null
+++ b/launcher/ui/setupwizard/LanguageWizardPage.cpp
@@ -0,0 +1,49 @@
+#include "LanguageWizardPage.h"
+#include <Application.h>
+#include <translations/TranslationsModel.h>
+
+#include "ui/widgets/LanguageSelectionWidget.h"
+#include <QVBoxLayout>
+#include <BuildConfig.h>
+
+LanguageWizardPage::LanguageWizardPage(QWidget *parent)
+ : BaseWizardPage(parent)
+{
+ setObjectName(QStringLiteral("languagePage"));
+ auto layout = new QVBoxLayout(this);
+ mainWidget = new LanguageSelectionWidget(this);
+ layout->setContentsMargins(0,0,0,0);
+ layout->addWidget(mainWidget);
+
+ retranslate();
+}
+
+LanguageWizardPage::~LanguageWizardPage()
+{
+}
+
+bool LanguageWizardPage::wantsRefreshButton()
+{
+ return true;
+}
+
+void LanguageWizardPage::refresh()
+{
+ auto translations = APPLICATION->translations();
+ translations->downloadIndex();
+}
+
+bool LanguageWizardPage::validatePage()
+{
+ auto settings = APPLICATION->settings();
+ QString key = mainWidget->getSelectedLanguageKey();
+ settings->set("Language", key);
+ return true;
+}
+
+void LanguageWizardPage::retranslate()
+{
+ setTitle(tr("Language"));
+ setSubTitle(tr("Select the language to use in %1").arg(BuildConfig.LAUNCHER_NAME));
+ mainWidget->retranslate();
+}
diff --git a/launcher/ui/setupwizard/LanguageWizardPage.h b/launcher/ui/setupwizard/LanguageWizardPage.h
new file mode 100644
index 00000000..45a0e5c0
--- /dev/null
+++ b/launcher/ui/setupwizard/LanguageWizardPage.h
@@ -0,0 +1,26 @@
+#pragma once
+
+#include "BaseWizardPage.h"
+
+class LanguageSelectionWidget;
+
+class LanguageWizardPage : public BaseWizardPage
+{
+ Q_OBJECT
+public:
+ explicit LanguageWizardPage(QWidget *parent = Q_NULLPTR);
+
+ virtual ~LanguageWizardPage();
+
+ bool wantsRefreshButton() override;
+
+ void refresh() override;
+
+ bool validatePage() override;
+
+protected:
+ void retranslate() override;
+
+private:
+ LanguageSelectionWidget *mainWidget = nullptr;
+};
diff --git a/launcher/ui/setupwizard/SetupWizard.cpp b/launcher/ui/setupwizard/SetupWizard.cpp
new file mode 100644
index 00000000..5af5ba91
--- /dev/null
+++ b/launcher/ui/setupwizard/SetupWizard.cpp
@@ -0,0 +1,89 @@
+#include "SetupWizard.h"
+
+#include "LanguageWizardPage.h"
+#include "JavaWizardPage.h"
+#include "AnalyticsWizardPage.h"
+
+#include "translations/TranslationsModel.h"
+#include <Application.h>
+#include <FileSystem.h>
+#include <ganalytics.h>
+
+#include <QAbstractButton>
+#include <BuildConfig.h>
+
+SetupWizard::SetupWizard(QWidget *parent) : QWizard(parent)
+{
+ setObjectName(QStringLiteral("SetupWizard"));
+ resize(615, 659);
+ // make it ugly everywhere to avoid variability in theming
+ setWizardStyle(QWizard::ClassicStyle);
+ setOptions(QWizard::NoCancelButton | QWizard::IndependentPages | QWizard::HaveCustomButton1);
+
+ retranslate();
+
+ connect(this, &QWizard::currentIdChanged, this, &SetupWizard::pageChanged);
+}
+
+void SetupWizard::retranslate()
+{
+ setButtonText(QWizard::NextButton, tr("&Next >"));
+ setButtonText(QWizard::BackButton, tr("< &Back"));
+ setButtonText(QWizard::FinishButton, tr("&Finish"));
+ setButtonText(QWizard::CustomButton1, tr("&Refresh"));
+ setWindowTitle(tr("%1 Quick Setup").arg(BuildConfig.LAUNCHER_NAME));
+}
+
+BaseWizardPage * SetupWizard::getBasePage(int id)
+{
+ if(id == -1)
+ return nullptr;
+ auto pagePtr = page(id);
+ if(!pagePtr)
+ return nullptr;
+ return dynamic_cast<BaseWizardPage *>(pagePtr);
+}
+
+BaseWizardPage * SetupWizard::getCurrentBasePage()
+{
+ return getBasePage(currentId());
+}
+
+void SetupWizard::pageChanged(int id)
+{
+ auto basePagePtr = getBasePage(id);
+ if(!basePagePtr)
+ {
+ return;
+ }
+ if(basePagePtr->wantsRefreshButton())
+ {
+ setButtonLayout({QWizard::CustomButton1, QWizard::Stretch, QWizard::BackButton, QWizard::NextButton, QWizard::FinishButton});
+ auto customButton = button(QWizard::CustomButton1);
+ connect(customButton, &QAbstractButton::pressed, [&](){
+ auto basePagePtr = getCurrentBasePage();
+ if(basePagePtr)
+ {
+ basePagePtr->refresh();
+ }
+ });
+ }
+ else
+ {
+ setButtonLayout({QWizard::Stretch, QWizard::BackButton, QWizard::NextButton, QWizard::FinishButton});
+ }
+}
+
+
+void SetupWizard::changeEvent(QEvent *event)
+{
+ if (event->type() == QEvent::LanguageChange)
+ {
+ retranslate();
+ }
+ QWizard::changeEvent(event);
+}
+
+SetupWizard::~SetupWizard()
+{
+}
diff --git a/launcher/ui/setupwizard/SetupWizard.h b/launcher/ui/setupwizard/SetupWizard.h
new file mode 100644
index 00000000..9b8adb4d
--- /dev/null
+++ b/launcher/ui/setupwizard/SetupWizard.h
@@ -0,0 +1,45 @@
+/* Copyright 2017-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 <QWizard>
+
+namespace Ui
+{
+class SetupWizard;
+}
+
+class BaseWizardPage;
+
+class SetupWizard : public QWizard
+{
+ Q_OBJECT
+
+public: /* con/destructors */
+ explicit SetupWizard(QWidget *parent = 0);
+ virtual ~SetupWizard();
+
+ void changeEvent(QEvent * event) override;
+ BaseWizardPage *getBasePage(int id);
+ BaseWizardPage *getCurrentBasePage();
+
+private slots:
+ void pageChanged(int id);
+
+private: /* methods */
+ void retranslate();
+};
+
diff --git a/launcher/ui/themes/BrightTheme.cpp b/launcher/ui/themes/BrightTheme.cpp
new file mode 100644
index 00000000..b9188bdd
--- /dev/null
+++ b/launcher/ui/themes/BrightTheme.cpp
@@ -0,0 +1,56 @@
+#include "BrightTheme.h"
+
+QString BrightTheme::id()
+{
+ return "bright";
+}
+
+QString BrightTheme::name()
+{
+ return QObject::tr("Bright");
+}
+
+bool BrightTheme::hasColorScheme()
+{
+ return true;
+}
+
+QPalette BrightTheme::colorScheme()
+{
+ QPalette brightPalette;
+ brightPalette.setColor(QPalette::Window, QColor(239,240,241));
+ brightPalette.setColor(QPalette::WindowText, QColor(49,54,59));
+ brightPalette.setColor(QPalette::Base, QColor(252,252,252));
+ brightPalette.setColor(QPalette::AlternateBase, QColor(239,240,241));
+ brightPalette.setColor(QPalette::ToolTipBase, QColor(49,54,59));
+ brightPalette.setColor(QPalette::ToolTipText, QColor(239,240,241));
+ brightPalette.setColor(QPalette::Text, QColor(49,54,59));
+ brightPalette.setColor(QPalette::Button, QColor(239,240,241));
+ brightPalette.setColor(QPalette::ButtonText, QColor(49,54,59));
+ brightPalette.setColor(QPalette::BrightText, Qt::red);
+ brightPalette.setColor(QPalette::Link, QColor(41, 128, 185));
+ brightPalette.setColor(QPalette::Highlight, QColor(61, 174, 233));
+ brightPalette.setColor(QPalette::HighlightedText, QColor(239,240,241));
+ return fadeInactive(brightPalette, fadeAmount(), fadeColor());
+}
+
+double BrightTheme::fadeAmount()
+{
+ return 0.5;
+}
+
+QColor BrightTheme::fadeColor()
+{
+ return QColor(239,240,241);
+}
+
+bool BrightTheme::hasStyleSheet()
+{
+ return false;
+}
+
+QString BrightTheme::appStyleSheet()
+{
+ return QString();
+}
+
diff --git a/launcher/ui/themes/BrightTheme.h b/launcher/ui/themes/BrightTheme.h
new file mode 100644
index 00000000..c61f52d5
--- /dev/null
+++ b/launcher/ui/themes/BrightTheme.h
@@ -0,0 +1,19 @@
+#pragma once
+
+#include "FusionTheme.h"
+
+class BrightTheme: public FusionTheme
+{
+public:
+ virtual ~BrightTheme() {}
+
+ QString id() override;
+ QString name() override;
+ bool hasStyleSheet() override;
+ QString appStyleSheet() override;
+ bool hasColorScheme() override;
+ QPalette colorScheme() override;
+ double fadeAmount() override;
+ QColor fadeColor() override;
+};
+
diff --git a/launcher/ui/themes/CustomTheme.cpp b/launcher/ui/themes/CustomTheme.cpp
new file mode 100644
index 00000000..3e3e27de
--- /dev/null
+++ b/launcher/ui/themes/CustomTheme.cpp
@@ -0,0 +1,244 @@
+#include "CustomTheme.h"
+#include <QDir>
+#include <Json.h>
+#include <FileSystem.h>
+
+const char * themeFile = "theme.json";
+const char * styleFile = "themeStyle.css";
+
+static bool readThemeJson(const QString &path, QPalette &palette, double &fadeAmount, QColor &fadeColor, QString &name, QString &widgets)
+{
+ QFileInfo pathInfo(path);
+ if(pathInfo.exists() && pathInfo.isFile())
+ {
+ try
+ {
+ auto doc = Json::requireDocument(path, "Theme JSON file");
+ const QJsonObject root = doc.object();
+ name = Json::requireString(root, "name", "Theme name");
+ widgets = Json::requireString(root, "widgets", "Qt widget theme");
+ auto colorsRoot = Json::requireObject(root, "colors", "colors object");
+ auto readColor = [&](QString colorName) -> QColor
+ {
+ auto colorValue = Json::ensureString(colorsRoot, colorName, QString());
+ if(!colorValue.isEmpty())
+ {
+ QColor color(colorValue);
+ if(!color.isValid())
+ {
+ qWarning() << "Color value" << colorValue << "for" << colorName << "was not recognized.";
+ return QColor();
+ }
+ return color;
+ }
+ return QColor();
+ };
+ auto readAndSetColor = [&](QPalette::ColorRole role, QString colorName)
+ {
+ auto color = readColor(colorName);
+ if(color.isValid())
+ {
+ palette.setColor(role, color);
+ }
+ else
+ {
+ qDebug() << "Color value for" << colorName << "was not present.";
+ }
+ };
+
+ // palette
+ readAndSetColor(QPalette::Window, "Window");
+ readAndSetColor(QPalette::WindowText, "WindowText");
+ readAndSetColor(QPalette::Base, "Base");
+ readAndSetColor(QPalette::AlternateBase, "AlternateBase");
+ readAndSetColor(QPalette::ToolTipBase, "ToolTipBase");
+ readAndSetColor(QPalette::ToolTipText, "ToolTipText");
+ readAndSetColor(QPalette::Text, "Text");
+ readAndSetColor(QPalette::Button, "Button");
+ readAndSetColor(QPalette::ButtonText, "ButtonText");
+ readAndSetColor(QPalette::BrightText, "BrightText");
+ readAndSetColor(QPalette::Link, "Link");
+ readAndSetColor(QPalette::Highlight, "Highlight");
+ readAndSetColor(QPalette::HighlightedText, "HighlightedText");
+
+ //fade
+ fadeColor = readColor("fadeColor");
+ fadeAmount = Json::ensureDouble(colorsRoot, "fadeAmount", 0.5, "fade amount");
+
+ }
+ catch (const Exception &e)
+ {
+ qWarning() << "Couldn't load theme json: " << e.cause();
+ return false;
+ }
+ }
+ else
+ {
+ qDebug() << "No theme json present.";
+ return false;
+ }
+ return true;
+}
+
+static bool writeThemeJson(const QString &path, const QPalette &palette, double fadeAmount, QColor fadeColor, QString name, QString widgets)
+{
+ QJsonObject rootObj;
+ rootObj.insert("name", name);
+ rootObj.insert("widgets", widgets);
+
+ QJsonObject colorsObj;
+ auto insertColor = [&](QPalette::ColorRole role, QString colorName)
+ {
+ colorsObj.insert(colorName, palette.color(role).name());
+ };
+
+ // palette
+ insertColor(QPalette::Window, "Window");
+ insertColor(QPalette::WindowText, "WindowText");
+ insertColor(QPalette::Base, "Base");
+ insertColor(QPalette::AlternateBase, "AlternateBase");
+ insertColor(QPalette::ToolTipBase, "ToolTipBase");
+ insertColor(QPalette::ToolTipText, "ToolTipText");
+ insertColor(QPalette::Text, "Text");
+ insertColor(QPalette::Button, "Button");
+ insertColor(QPalette::ButtonText, "ButtonText");
+ insertColor(QPalette::BrightText, "BrightText");
+ insertColor(QPalette::Link, "Link");
+ insertColor(QPalette::Highlight, "Highlight");
+ insertColor(QPalette::HighlightedText, "HighlightedText");
+
+ // fade
+ colorsObj.insert("fadeColor", fadeColor.name());
+ colorsObj.insert("fadeAmount", fadeAmount);
+
+ rootObj.insert("colors", colorsObj);
+ try
+ {
+ Json::write(rootObj, path);
+ return true;
+ }
+ catch (const Exception &e)
+ {
+ qWarning() << "Failed to write theme json to" << path;
+ return false;
+ }
+}
+
+CustomTheme::CustomTheme(ITheme* baseTheme, QString folder)
+{
+ m_id = folder;
+ QString path = FS::PathCombine("themes", m_id);
+ QString pathResources = FS::PathCombine("themes", m_id, "resources");
+
+ qDebug() << "Loading theme" << m_id;
+
+ if(!FS::ensureFolderPathExists(path) || !FS::ensureFolderPathExists(pathResources))
+ {
+ qWarning() << "couldn't create folder for theme!";
+ m_palette = baseTheme->colorScheme();
+ m_styleSheet = baseTheme->appStyleSheet();
+ return;
+ }
+
+ auto themeFilePath = FS::PathCombine(path, themeFile);
+
+ m_palette = baseTheme->colorScheme();
+ if (!readThemeJson(themeFilePath, m_palette, m_fadeAmount, m_fadeColor, m_name, m_widgets))
+ {
+ m_name = "Custom";
+ m_palette = baseTheme->colorScheme();
+ m_fadeColor = baseTheme->fadeColor();
+ m_fadeAmount = baseTheme->fadeAmount();
+ m_widgets = baseTheme->qtTheme();
+
+ QFileInfo info(themeFilePath);
+ if(!info.exists())
+ {
+ writeThemeJson(themeFilePath, m_palette, m_fadeAmount, m_fadeColor, "Custom", m_widgets);
+ }
+ }
+ else
+ {
+ m_palette = fadeInactive(m_palette, m_fadeAmount, m_fadeColor);
+ }
+
+ auto cssFilePath = FS::PathCombine(path, styleFile);
+ QFileInfo info (cssFilePath);
+ if(info.isFile())
+ {
+ try
+ {
+ // TODO: validate css?
+ m_styleSheet = QString::fromUtf8(FS::read(cssFilePath));
+ }
+ catch (const Exception &e)
+ {
+ qWarning() << "Couldn't load css:" << e.cause() << "from" << cssFilePath;
+ m_styleSheet = baseTheme->appStyleSheet();
+ }
+ }
+ else
+ {
+ qDebug() << "No theme css present.";
+ m_styleSheet = baseTheme->appStyleSheet();
+ try
+ {
+ FS::write(cssFilePath, m_styleSheet.toUtf8());
+ }
+ catch (const Exception &e)
+ {
+ qWarning() << "Couldn't write css:" << e.cause() << "to" << cssFilePath;
+ }
+ }
+}
+
+QStringList CustomTheme::searchPaths()
+{
+ return { FS::PathCombine("themes", m_id, "resources") };
+}
+
+
+QString CustomTheme::id()
+{
+ return m_id;
+}
+
+QString CustomTheme::name()
+{
+ return m_name;
+}
+
+bool CustomTheme::hasColorScheme()
+{
+ return true;
+}
+
+QPalette CustomTheme::colorScheme()
+{
+ return m_palette;
+}
+
+bool CustomTheme::hasStyleSheet()
+{
+ return true;
+}
+
+QString CustomTheme::appStyleSheet()
+{
+ return m_styleSheet;
+}
+
+double CustomTheme::fadeAmount()
+{
+ return m_fadeAmount;
+}
+
+QColor CustomTheme::fadeColor()
+{
+ return m_fadeColor;
+}
+
+QString CustomTheme::qtTheme()
+{
+ return m_widgets;
+}
diff --git a/launcher/ui/themes/CustomTheme.h b/launcher/ui/themes/CustomTheme.h
new file mode 100644
index 00000000..d216895d
--- /dev/null
+++ b/launcher/ui/themes/CustomTheme.h
@@ -0,0 +1,31 @@
+#pragma once
+
+#include "ITheme.h"
+
+class CustomTheme: public ITheme
+{
+public:
+ CustomTheme(ITheme * baseTheme, QString folder);
+ virtual ~CustomTheme() {}
+
+ QString id() override;
+ QString name() override;
+ bool hasStyleSheet() override;
+ QString appStyleSheet() override;
+ bool hasColorScheme() override;
+ QPalette colorScheme() override;
+ double fadeAmount() override;
+ QColor fadeColor() override;
+ QString qtTheme() override;
+ QStringList searchPaths() override;
+
+private: /* data */
+ QPalette m_palette;
+ QColor m_fadeColor;
+ double m_fadeAmount;
+ QString m_styleSheet;
+ QString m_name;
+ QString m_id;
+ QString m_widgets;
+};
+
diff --git a/launcher/ui/themes/DarkTheme.cpp b/launcher/ui/themes/DarkTheme.cpp
new file mode 100644
index 00000000..31ecd559
--- /dev/null
+++ b/launcher/ui/themes/DarkTheme.cpp
@@ -0,0 +1,55 @@
+#include "DarkTheme.h"
+
+QString DarkTheme::id()
+{
+ return "dark";
+}
+
+QString DarkTheme::name()
+{
+ return QObject::tr("Dark");
+}
+
+bool DarkTheme::hasColorScheme()
+{
+ return true;
+}
+
+QPalette DarkTheme::colorScheme()
+{
+ QPalette darkPalette;
+ darkPalette.setColor(QPalette::Window, QColor(49,54,59));
+ darkPalette.setColor(QPalette::WindowText, Qt::white);
+ darkPalette.setColor(QPalette::Base, QColor(35,38,41));
+ darkPalette.setColor(QPalette::AlternateBase, QColor(49,54,59));
+ darkPalette.setColor(QPalette::ToolTipBase, Qt::white);
+ darkPalette.setColor(QPalette::ToolTipText, Qt::white);
+ darkPalette.setColor(QPalette::Text, Qt::white);
+ darkPalette.setColor(QPalette::Button, QColor(49,54,59));
+ darkPalette.setColor(QPalette::ButtonText, Qt::white);
+ darkPalette.setColor(QPalette::BrightText, Qt::red);
+ darkPalette.setColor(QPalette::Link, QColor(42, 130, 218));
+ darkPalette.setColor(QPalette::Highlight, QColor(42, 130, 218));
+ darkPalette.setColor(QPalette::HighlightedText, Qt::black);
+ return fadeInactive(darkPalette, fadeAmount(), fadeColor());
+}
+
+double DarkTheme::fadeAmount()
+{
+ return 0.5;
+}
+
+QColor DarkTheme::fadeColor()
+{
+ return QColor(49,54,59);
+}
+
+bool DarkTheme::hasStyleSheet()
+{
+ return true;
+}
+
+QString DarkTheme::appStyleSheet()
+{
+ return "QToolTip { color: #ffffff; background-color: #2a82da; border: 1px solid white; }";
+}
diff --git a/launcher/ui/themes/DarkTheme.h b/launcher/ui/themes/DarkTheme.h
new file mode 100644
index 00000000..9bd2f343
--- /dev/null
+++ b/launcher/ui/themes/DarkTheme.h
@@ -0,0 +1,18 @@
+#pragma once
+
+#include "FusionTheme.h"
+
+class DarkTheme: public FusionTheme
+{
+public:
+ virtual ~DarkTheme() {}
+
+ QString id() override;
+ QString name() override;
+ bool hasStyleSheet() override;
+ QString appStyleSheet() override;
+ bool hasColorScheme() override;
+ QPalette colorScheme() override;
+ double fadeAmount() override;
+ QColor fadeColor() override;
+};
diff --git a/launcher/ui/themes/FusionTheme.cpp b/launcher/ui/themes/FusionTheme.cpp
new file mode 100644
index 00000000..cf3286ba
--- /dev/null
+++ b/launcher/ui/themes/FusionTheme.cpp
@@ -0,0 +1,6 @@
+#include "FusionTheme.h"
+
+QString FusionTheme::qtTheme()
+{
+ return "Fusion";
+}
diff --git a/launcher/ui/themes/FusionTheme.h b/launcher/ui/themes/FusionTheme.h
new file mode 100644
index 00000000..ee34245a
--- /dev/null
+++ b/launcher/ui/themes/FusionTheme.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include "ITheme.h"
+
+class FusionTheme: public ITheme
+{
+public:
+ virtual ~FusionTheme() {}
+
+ QString qtTheme() override;
+};
diff --git a/launcher/ui/themes/ITheme.cpp b/launcher/ui/themes/ITheme.cpp
new file mode 100644
index 00000000..7247b444
--- /dev/null
+++ b/launcher/ui/themes/ITheme.cpp
@@ -0,0 +1,47 @@
+#include "ITheme.h"
+#include "rainbow.h"
+#include <QStyleFactory>
+#include <QDir>
+#include "Application.h"
+
+void ITheme::apply(bool)
+{
+ QApplication::setStyle(QStyleFactory::create(qtTheme()));
+ if(hasColorScheme())
+ {
+ QApplication::setPalette(colorScheme());
+ }
+ if(hasStyleSheet())
+ {
+ APPLICATION->setStyleSheet(appStyleSheet());
+ }
+ else
+ {
+ APPLICATION->setStyleSheet(QString());
+ }
+ QDir::setSearchPaths("theme", searchPaths());
+}
+
+QPalette ITheme::fadeInactive(QPalette in, qreal bias, QColor color)
+{
+ auto blend = [&in, bias, color](QPalette::ColorRole role)
+ {
+ QColor from = in.color(QPalette::Active, role);
+ QColor blended = Rainbow::mix(from, color, bias);
+ in.setColor(QPalette::Disabled, role, blended);
+ };
+ blend(QPalette::Window);
+ blend(QPalette::WindowText);
+ blend(QPalette::Base);
+ blend(QPalette::AlternateBase);
+ blend(QPalette::ToolTipBase);
+ blend(QPalette::ToolTipText);
+ blend(QPalette::Text);
+ blend(QPalette::Button);
+ blend(QPalette::ButtonText);
+ blend(QPalette::BrightText);
+ blend(QPalette::Link);
+ blend(QPalette::Highlight);
+ blend(QPalette::HighlightedText);
+ return in;
+}
diff --git a/launcher/ui/themes/ITheme.h b/launcher/ui/themes/ITheme.h
new file mode 100644
index 00000000..c2347cf6
--- /dev/null
+++ b/launcher/ui/themes/ITheme.h
@@ -0,0 +1,27 @@
+#pragma once
+#include <QString>
+#include <QPalette>
+
+class QStyle;
+
+class ITheme
+{
+public:
+ virtual ~ITheme() {}
+ virtual void apply(bool initial);
+ virtual QString id() = 0;
+ virtual QString name() = 0;
+ virtual bool hasStyleSheet() = 0;
+ virtual QString appStyleSheet() = 0;
+ virtual QString qtTheme() = 0;
+ virtual bool hasColorScheme() = 0;
+ virtual QPalette colorScheme() = 0;
+ virtual QColor fadeColor() = 0;
+ virtual double fadeAmount() = 0;
+ virtual QStringList searchPaths()
+ {
+ return {};
+ }
+
+ static QPalette fadeInactive(QPalette in, qreal bias, QColor color);
+};
diff --git a/launcher/ui/themes/SystemTheme.cpp b/launcher/ui/themes/SystemTheme.cpp
new file mode 100644
index 00000000..49b1afaa
--- /dev/null
+++ b/launcher/ui/themes/SystemTheme.cpp
@@ -0,0 +1,83 @@
+#include "SystemTheme.h"
+#include <QApplication>
+#include <QStyle>
+#include <QStyleFactory>
+#include <QDebug>
+
+SystemTheme::SystemTheme()
+{
+ qDebug() << "Determining System Theme...";
+ const auto & style = QApplication::style();
+ systemPalette = style->standardPalette();
+ QString lowerThemeName = style->objectName();
+ qDebug() << "System theme seems to be:" << lowerThemeName;
+ QStringList styles = QStyleFactory::keys();
+ for(auto &st: styles)
+ {
+ qDebug() << "Considering theme from theme factory:" << st.toLower();
+ if(st.toLower() == lowerThemeName)
+ {
+ systemTheme = st;
+ qDebug() << "System theme has been determined to be:" << systemTheme;
+ return;
+ }
+ }
+ // fall back to fusion if we can't find the current theme.
+ systemTheme = "Fusion";
+ qDebug() << "System theme not found, defaulted to Fusion";
+}
+
+void SystemTheme::apply(bool initial)
+{
+ // if we are applying the system theme as the first theme, just don't touch anything. it's for the better...
+ if(initial)
+ {
+ return;
+ }
+ ITheme::apply(initial);
+}
+
+QString SystemTheme::id()
+{
+ return "system";
+}
+
+QString SystemTheme::name()
+{
+ return QObject::tr("System");
+}
+
+QString SystemTheme::qtTheme()
+{
+ return systemTheme;
+}
+
+QPalette SystemTheme::colorScheme()
+{
+ return systemPalette;
+}
+
+QString SystemTheme::appStyleSheet()
+{
+ return QString();
+}
+
+double SystemTheme::fadeAmount()
+{
+ return 0.5;
+}
+
+QColor SystemTheme::fadeColor()
+{
+ return QColor(128,128,128);
+}
+
+bool SystemTheme::hasStyleSheet()
+{
+ return false;
+}
+
+bool SystemTheme::hasColorScheme()
+{
+ return true;
+}
diff --git a/launcher/ui/themes/SystemTheme.h b/launcher/ui/themes/SystemTheme.h
new file mode 100644
index 00000000..fe450600
--- /dev/null
+++ b/launcher/ui/themes/SystemTheme.h
@@ -0,0 +1,24 @@
+#pragma once
+
+#include "ITheme.h"
+
+class SystemTheme: public ITheme
+{
+public:
+ SystemTheme();
+ virtual ~SystemTheme() {}
+ void apply(bool initial) override;
+
+ QString id() override;
+ QString name() override;
+ QString qtTheme() override;
+ bool hasStyleSheet() override;
+ QString appStyleSheet() override;
+ bool hasColorScheme() override;
+ QPalette colorScheme() override;
+ double fadeAmount() override;
+ QColor fadeColor() override;
+private:
+ QPalette systemPalette;
+ QString systemTheme;
+};
diff --git a/launcher/ui/widgets/Common.cpp b/launcher/ui/widgets/Common.cpp
new file mode 100644
index 00000000..f72f3596
--- /dev/null
+++ b/launcher/ui/widgets/Common.cpp
@@ -0,0 +1,27 @@
+#include "Common.h"
+
+// Origin: Qt
+QStringList viewItemTextLayout(QTextLayout &textLayout, int lineWidth, qreal &height,
+ qreal &widthUsed)
+{
+ QStringList lines;
+ height = 0;
+ widthUsed = 0;
+ textLayout.beginLayout();
+ QString str = textLayout.text();
+ while (true)
+ {
+ QTextLine line = textLayout.createLine();
+ if (!line.isValid())
+ break;
+ if (line.textLength() == 0)
+ break;
+ line.setLineWidth(lineWidth);
+ line.setPosition(QPointF(0, height));
+ height += line.height();
+ lines.append(str.mid(line.textStart(), line.textLength()));
+ widthUsed = qMax(widthUsed, line.naturalTextWidth());
+ }
+ textLayout.endLayout();
+ return lines;
+}
diff --git a/launcher/ui/widgets/Common.h b/launcher/ui/widgets/Common.h
new file mode 100644
index 00000000..b3fbe1a0
--- /dev/null
+++ b/launcher/ui/widgets/Common.h
@@ -0,0 +1,6 @@
+#pragma once
+#include <QStringList>
+#include <QTextLayout>
+
+QStringList viewItemTextLayout(QTextLayout &textLayout, int lineWidth, qreal &height,
+ qreal &widthUsed); \ No newline at end of file
diff --git a/launcher/ui/widgets/CustomCommands.cpp b/launcher/ui/widgets/CustomCommands.cpp
new file mode 100644
index 00000000..24bdc07d
--- /dev/null
+++ b/launcher/ui/widgets/CustomCommands.cpp
@@ -0,0 +1,49 @@
+#include "CustomCommands.h"
+#include "ui_CustomCommands.h"
+
+CustomCommands::~CustomCommands()
+{
+ delete ui;
+}
+
+CustomCommands::CustomCommands(QWidget* parent):
+ QWidget(parent),
+ ui(new Ui::CustomCommands)
+{
+ ui->setupUi(this);
+}
+
+void CustomCommands::initialize(bool checkable, bool checked, const QString& prelaunch, const QString& wrapper, const QString& postexit)
+{
+ ui->customCommandsGroupBox->setCheckable(checkable);
+ if(checkable)
+ {
+ ui->customCommandsGroupBox->setChecked(checked);
+ }
+ ui->preLaunchCmdTextBox->setText(prelaunch);
+ ui->wrapperCmdTextBox->setText(wrapper);
+ ui->postExitCmdTextBox->setText(postexit);
+}
+
+
+bool CustomCommands::checked() const
+{
+ if(!ui->customCommandsGroupBox->isCheckable())
+ return true;
+ return ui->customCommandsGroupBox->isChecked();
+}
+
+QString CustomCommands::prelaunchCommand() const
+{
+ return ui->preLaunchCmdTextBox->text();
+}
+
+QString CustomCommands::wrapperCommand() const
+{
+ return ui->wrapperCmdTextBox->text();
+}
+
+QString CustomCommands::postexitCommand() const
+{
+ return ui->postExitCmdTextBox->text();
+}
diff --git a/launcher/ui/widgets/CustomCommands.h b/launcher/ui/widgets/CustomCommands.h
new file mode 100644
index 00000000..8db991fa
--- /dev/null
+++ b/launcher/ui/widgets/CustomCommands.h
@@ -0,0 +1,43 @@
+/* Copyright 2018-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 <QWidget>
+
+namespace Ui
+{
+class CustomCommands;
+}
+
+class CustomCommands : public QWidget
+{
+ Q_OBJECT
+
+public:
+ explicit CustomCommands(QWidget *parent = 0);
+ virtual ~CustomCommands();
+ void initialize(bool checkable, bool checked, const QString & prelaunch, const QString & wrapper, const QString & postexit);
+
+ bool checked() const;
+ QString prelaunchCommand() const;
+ QString wrapperCommand() const;
+ QString postexitCommand() const;
+
+private:
+ Ui::CustomCommands *ui;
+};
+
+
diff --git a/launcher/ui/widgets/CustomCommands.ui b/launcher/ui/widgets/CustomCommands.ui
new file mode 100644
index 00000000..21964ad2
--- /dev/null
+++ b/launcher/ui/widgets/CustomCommands.ui
@@ -0,0 +1,107 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>CustomCommands</class>
+ <widget class="QWidget" name="CustomCommands">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>518</width>
+ <height>646</height>
+ </rect>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QGroupBox" name="customCommandsGroupBox">
+ <property name="enabled">
+ <bool>true</bool>
+ </property>
+ <property name="title">
+ <string>Cus&amp;tom Commands</string>
+ </property>
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ <property name="checked">
+ <bool>false</bool>
+ </property>
+ <layout class="QGridLayout" name="gridLayout_4">
+ <item row="2" column="0">
+ <widget class="QLabel" name="labelPostExitCmd">
+ <property name="text">
+ <string>Post-exit command:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QLineEdit" name="preLaunchCmdTextBox"/>
+ </item>
+ <item row="0" column="0">
+ <widget class="QLabel" name="labelPreLaunchCmd">
+ <property name="text">
+ <string>Pre-launch command:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="1">
+ <widget class="QLineEdit" name="postExitCmdTextBox"/>
+ </item>
+ <item row="1" column="0">
+ <widget class="QLabel" name="labelWrapperCmd">
+ <property name="text">
+ <string>Wrapper command:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QLineEdit" name="wrapperCmdTextBox"/>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="labelCustomCmdsDescription">
+ <property name="text">
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Pre-launch command runs before the instance launches and post-exit command runs after it exits.&lt;/p&gt;&lt;p&gt;Both will be run in the launcher's working folder with extra environment variables:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;$INST_NAME - Name of the instance&lt;/li&gt;&lt;li&gt;$INST_ID - ID of the instance (its folder name)&lt;/li&gt;&lt;li&gt;$INST_DIR - absolute path of the instance&lt;/li&gt;&lt;li&gt;$INST_MC_DIR - absolute path of minecraft&lt;/li&gt;&lt;li&gt;$INST_JAVA - java binary used for launch&lt;/li&gt;&lt;li&gt;$INST_JAVA_ARGS - command-line parameters used for launch&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Wrapper command allows launching using an extra wrapper program (like 'optirun' on Linux)&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ <property name="textInteractionFlags">
+ <set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="verticalSpacer">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>40</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/launcher/ui/widgets/DropLabel.cpp b/launcher/ui/widgets/DropLabel.cpp
new file mode 100644
index 00000000..a900e57c
--- /dev/null
+++ b/launcher/ui/widgets/DropLabel.cpp
@@ -0,0 +1,41 @@
+#include "DropLabel.h"
+
+#include <QMimeData>
+#include <QDropEvent>
+
+DropLabel::DropLabel(QWidget *parent) : QLabel(parent)
+{
+ setAcceptDrops(true);
+}
+
+void DropLabel::dragEnterEvent(QDragEnterEvent *event)
+{
+ event->acceptProposedAction();
+}
+
+void DropLabel::dragMoveEvent(QDragMoveEvent *event)
+{
+ event->acceptProposedAction();
+}
+
+void DropLabel::dragLeaveEvent(QDragLeaveEvent *event)
+{
+ event->accept();
+}
+
+void DropLabel::dropEvent(QDropEvent *event)
+{
+ const QMimeData *mimeData = event->mimeData();
+
+ if (!mimeData)
+ {
+ return;
+ }
+
+ if (mimeData->hasUrls()) {
+ auto urls = mimeData->urls();
+ emit droppedURLs(urls);
+ }
+
+ event->acceptProposedAction();
+}
diff --git a/launcher/ui/widgets/DropLabel.h b/launcher/ui/widgets/DropLabel.h
new file mode 100644
index 00000000..c5ca0bcc
--- /dev/null
+++ b/launcher/ui/widgets/DropLabel.h
@@ -0,0 +1,20 @@
+#pragma once
+
+#include <QLabel>
+
+class DropLabel : public QLabel
+{
+ Q_OBJECT
+
+public:
+ explicit DropLabel(QWidget *parent = nullptr);
+
+signals:
+ void droppedURLs(QList<QUrl> urls);
+
+protected:
+ void dropEvent(QDropEvent *event) override;
+ void dragEnterEvent(QDragEnterEvent *event) override;
+ void dragMoveEvent(QDragMoveEvent *event) override;
+ void dragLeaveEvent(QDragLeaveEvent *event) override;
+};
diff --git a/launcher/ui/widgets/FocusLineEdit.cpp b/launcher/ui/widgets/FocusLineEdit.cpp
new file mode 100644
index 00000000..b272100c
--- /dev/null
+++ b/launcher/ui/widgets/FocusLineEdit.cpp
@@ -0,0 +1,25 @@
+#include "FocusLineEdit.h"
+#include <QDebug>
+
+FocusLineEdit::FocusLineEdit(QWidget *parent) : QLineEdit(parent)
+{
+ _selectOnMousePress = false;
+}
+
+void FocusLineEdit::focusInEvent(QFocusEvent *e)
+{
+ QLineEdit::focusInEvent(e);
+ selectAll();
+ _selectOnMousePress = true;
+}
+
+void FocusLineEdit::mousePressEvent(QMouseEvent *me)
+{
+ QLineEdit::mousePressEvent(me);
+ if (_selectOnMousePress)
+ {
+ selectAll();
+ _selectOnMousePress = false;
+ }
+ qDebug() << selectedText();
+}
diff --git a/launcher/ui/widgets/FocusLineEdit.h b/launcher/ui/widgets/FocusLineEdit.h
new file mode 100644
index 00000000..71b4f140
--- /dev/null
+++ b/launcher/ui/widgets/FocusLineEdit.h
@@ -0,0 +1,17 @@
+#include <QLineEdit>
+
+class FocusLineEdit : public QLineEdit
+{
+ Q_OBJECT
+public:
+ FocusLineEdit(QWidget *parent);
+ virtual ~FocusLineEdit()
+ {
+ }
+
+protected:
+ void focusInEvent(QFocusEvent *e);
+ void mousePressEvent(QMouseEvent *me);
+
+ bool _selectOnMousePress;
+};
diff --git a/launcher/ui/widgets/IconLabel.cpp b/launcher/ui/widgets/IconLabel.cpp
new file mode 100644
index 00000000..bf1c2358
--- /dev/null
+++ b/launcher/ui/widgets/IconLabel.cpp
@@ -0,0 +1,43 @@
+#include "IconLabel.h"
+
+#include <QStyle>
+#include <QStyleOption>
+#include <QLayout>
+#include <QPainter>
+#include <QRect>
+
+IconLabel::IconLabel(QWidget *parent, QIcon icon, QSize size)
+ : QWidget(parent), m_size(size), m_icon(icon)
+{
+ setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
+}
+
+QSize IconLabel::sizeHint() const
+{
+ return m_size;
+}
+
+void IconLabel::setIcon(QIcon icon)
+{
+ m_icon = icon;
+ update();
+}
+
+void IconLabel::paintEvent(QPaintEvent *)
+{
+ QPainter p(this);
+ QRect rect = contentsRect();
+ int width = rect.width();
+ int height = rect.height();
+ if(width < height)
+ {
+ rect.setHeight(width);
+ rect.translate(0, (height - width) / 2);
+ }
+ else if (width > height)
+ {
+ rect.setWidth(height);
+ rect.translate((width - height) / 2, 0);
+ }
+ m_icon.paint(&p, rect);
+}
diff --git a/launcher/ui/widgets/IconLabel.h b/launcher/ui/widgets/IconLabel.h
new file mode 100644
index 00000000..6d212c4c
--- /dev/null
+++ b/launcher/ui/widgets/IconLabel.h
@@ -0,0 +1,26 @@
+#pragma once
+#include <QWidget>
+#include <QIcon>
+
+class QStyleOption;
+
+/**
+ * This is a trivial widget that paints a QIcon of the specified size.
+ */
+class IconLabel : public QWidget
+{
+ Q_OBJECT
+
+public:
+ /// Create a line separator. orientation is the orientation of the line.
+ explicit IconLabel(QWidget *parent, QIcon icon, QSize size);
+
+ virtual QSize sizeHint() const;
+ virtual void paintEvent(QPaintEvent *);
+
+ void setIcon(QIcon icon);
+
+private:
+ QSize m_size;
+ QIcon m_icon;
+};
diff --git a/launcher/ui/widgets/InstanceCardWidget.ui b/launcher/ui/widgets/InstanceCardWidget.ui
new file mode 100644
index 00000000..6eeeb076
--- /dev/null
+++ b/launcher/ui/widgets/InstanceCardWidget.ui
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>InstanceCardWidget</class>
+ <widget class="QWidget" name="InstanceCardWidget">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>473</width>
+ <height>118</height>
+ </rect>
+ </property>
+ <layout class="QGridLayout" name="gridLayout">
+ <item row="0" column="0" rowspan="2">
+ <widget class="QToolButton" name="iconButton">
+ <property name="iconSize">
+ <size>
+ <width>80</width>
+ <height>80</height>
+ </size>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QLabel" name="nameLabel">
+ <property name="text">
+ <string>&amp;Name:</string>
+ </property>
+ <property name="buddy">
+ <cstring>instNameTextBox</cstring>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="2">
+ <widget class="QLineEdit" name="instNameTextBox"/>
+ </item>
+ <item row="1" column="1">
+ <widget class="QLabel" name="groupLabel">
+ <property name="text">
+ <string>&amp;Group:</string>
+ </property>
+ <property name="buddy">
+ <cstring>groupBox</cstring>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="2">
+ <widget class="QComboBox" name="groupBox">
+ <property name="editable">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/launcher/ui/widgets/JavaSettingsWidget.cpp b/launcher/ui/widgets/JavaSettingsWidget.cpp
new file mode 100644
index 00000000..b9d7620c
--- /dev/null
+++ b/launcher/ui/widgets/JavaSettingsWidget.cpp
@@ -0,0 +1,432 @@
+#include "JavaSettingsWidget.h"
+
+#include <QVBoxLayout>
+#include <QGroupBox>
+#include <QSpinBox>
+#include <QLabel>
+#include <QLineEdit>
+#include <QPushButton>
+#include <QToolButton>
+#include <QFileDialog>
+
+#include <sys.h>
+
+#include "java/JavaInstall.h"
+#include "java/JavaUtils.h"
+#include "FileSystem.h"
+
+#include "ui/dialogs/CustomMessageBox.h"
+#include "ui/widgets/VersionSelectWidget.h"
+
+#include "Application.h"
+#include "BuildConfig.h"
+
+JavaSettingsWidget::JavaSettingsWidget(QWidget* parent) : QWidget(parent)
+{
+ m_availableMemory = Sys::getSystemRam() / Sys::mebibyte;
+
+ goodIcon = APPLICATION->getThemedIcon("status-good");
+ yellowIcon = APPLICATION->getThemedIcon("status-yellow");
+ badIcon = APPLICATION->getThemedIcon("status-bad");
+ setupUi();
+
+ connect(m_minMemSpinBox, SIGNAL(valueChanged(int)), this, SLOT(memoryValueChanged(int)));
+ connect(m_maxMemSpinBox, SIGNAL(valueChanged(int)), this, SLOT(memoryValueChanged(int)));
+ connect(m_permGenSpinBox, SIGNAL(valueChanged(int)), this, SLOT(memoryValueChanged(int)));
+ connect(m_versionWidget, &VersionSelectWidget::selectedVersionChanged, this, &JavaSettingsWidget::javaVersionSelected);
+ connect(m_javaBrowseBtn, &QPushButton::clicked, this, &JavaSettingsWidget::on_javaBrowseBtn_clicked);
+ connect(m_javaPathTextBox, &QLineEdit::textEdited, this, &JavaSettingsWidget::javaPathEdited);
+ connect(m_javaStatusBtn, &QToolButton::clicked, this, &JavaSettingsWidget::on_javaStatusBtn_clicked);
+}
+
+void JavaSettingsWidget::setupUi()
+{
+ setObjectName(QStringLiteral("javaSettingsWidget"));
+ m_verticalLayout = new QVBoxLayout(this);
+ m_verticalLayout->setObjectName(QStringLiteral("verticalLayout"));
+
+ m_versionWidget = new VersionSelectWidget(this);
+ m_verticalLayout->addWidget(m_versionWidget);
+
+ m_horizontalLayout = new QHBoxLayout();
+ m_horizontalLayout->setObjectName(QStringLiteral("horizontalLayout"));
+ m_javaPathTextBox = new QLineEdit(this);
+ m_javaPathTextBox->setObjectName(QStringLiteral("javaPathTextBox"));
+
+ m_horizontalLayout->addWidget(m_javaPathTextBox);
+
+ m_javaBrowseBtn = new QPushButton(this);
+ m_javaBrowseBtn->setObjectName(QStringLiteral("javaBrowseBtn"));
+
+ m_horizontalLayout->addWidget(m_javaBrowseBtn);
+
+ m_javaStatusBtn = new QToolButton(this);
+ m_javaStatusBtn->setIcon(yellowIcon);
+ m_horizontalLayout->addWidget(m_javaStatusBtn);
+
+ m_verticalLayout->addLayout(m_horizontalLayout);
+
+ m_memoryGroupBox = new QGroupBox(this);
+ m_memoryGroupBox->setObjectName(QStringLiteral("memoryGroupBox"));
+ m_gridLayout_2 = new QGridLayout(m_memoryGroupBox);
+ m_gridLayout_2->setObjectName(QStringLiteral("gridLayout_2"));
+
+ m_labelMinMem = new QLabel(m_memoryGroupBox);
+ m_labelMinMem->setObjectName(QStringLiteral("labelMinMem"));
+ m_gridLayout_2->addWidget(m_labelMinMem, 0, 0, 1, 1);
+
+ m_minMemSpinBox = new QSpinBox(m_memoryGroupBox);
+ m_minMemSpinBox->setObjectName(QStringLiteral("minMemSpinBox"));
+ m_minMemSpinBox->setSuffix(QStringLiteral(" MiB"));
+ m_minMemSpinBox->setMinimum(128);
+ m_minMemSpinBox->setMaximum(m_availableMemory);
+ m_minMemSpinBox->setSingleStep(128);
+ m_labelMinMem->setBuddy(m_minMemSpinBox);
+ m_gridLayout_2->addWidget(m_minMemSpinBox, 0, 1, 1, 1);
+
+ m_labelMaxMem = new QLabel(m_memoryGroupBox);
+ m_labelMaxMem->setObjectName(QStringLiteral("labelMaxMem"));
+ m_gridLayout_2->addWidget(m_labelMaxMem, 1, 0, 1, 1);
+
+ m_maxMemSpinBox = new QSpinBox(m_memoryGroupBox);
+ m_maxMemSpinBox->setObjectName(QStringLiteral("maxMemSpinBox"));
+ m_maxMemSpinBox->setSuffix(QStringLiteral(" MiB"));
+ m_maxMemSpinBox->setMinimum(128);
+ m_maxMemSpinBox->setMaximum(m_availableMemory);
+ m_maxMemSpinBox->setSingleStep(128);
+ m_labelMaxMem->setBuddy(m_maxMemSpinBox);
+ m_gridLayout_2->addWidget(m_maxMemSpinBox, 1, 1, 1, 1);
+
+ m_labelPermGen = new QLabel(m_memoryGroupBox);
+ m_labelPermGen->setObjectName(QStringLiteral("labelPermGen"));
+ m_labelPermGen->setText(QStringLiteral("PermGen:"));
+ m_gridLayout_2->addWidget(m_labelPermGen, 2, 0, 1, 1);
+ m_labelPermGen->setVisible(false);
+
+ m_permGenSpinBox = new QSpinBox(m_memoryGroupBox);
+ m_permGenSpinBox->setObjectName(QStringLiteral("permGenSpinBox"));
+ m_permGenSpinBox->setSuffix(QStringLiteral(" MiB"));
+ m_permGenSpinBox->setMinimum(64);
+ m_permGenSpinBox->setMaximum(m_availableMemory);
+ m_permGenSpinBox->setSingleStep(8);
+ m_gridLayout_2->addWidget(m_permGenSpinBox, 2, 1, 1, 1);
+ m_permGenSpinBox->setVisible(false);
+
+ m_verticalLayout->addWidget(m_memoryGroupBox);
+
+ retranslate();
+}
+
+void JavaSettingsWidget::initialize()
+{
+ m_versionWidget->initialize(APPLICATION->javalist().get());
+ m_versionWidget->setResizeOn(2);
+ auto s = APPLICATION->settings();
+ // Memory
+ observedMinMemory = s->get("MinMemAlloc").toInt();
+ observedMaxMemory = s->get("MaxMemAlloc").toInt();
+ observedPermGenMemory = s->get("PermGen").toInt();
+ m_minMemSpinBox->setValue(observedMinMemory);
+ m_maxMemSpinBox->setValue(observedMaxMemory);
+ m_permGenSpinBox->setValue(observedPermGenMemory);
+}
+
+void JavaSettingsWidget::refresh()
+{
+ m_versionWidget->loadList();
+}
+
+JavaSettingsWidget::ValidationStatus JavaSettingsWidget::validate()
+{
+ switch(javaStatus)
+ {
+ default:
+ case JavaStatus::NotSet:
+ case JavaStatus::DoesNotExist:
+ case JavaStatus::DoesNotStart:
+ case JavaStatus::ReturnedInvalidData:
+ {
+ int button = CustomMessageBox::selectable(
+ this,
+ tr("No Java version selected"),
+ tr("You didn't select a Java version or selected something that doesn't work.\n"
+ "%1 will not be able to start Minecraft.\n"
+ "Do you wish to proceed without any Java?"
+ "\n\n"
+ "You can change the Java version in the settings later.\n"
+ ).arg(BuildConfig.LAUNCHER_NAME),
+ QMessageBox::Warning,
+ QMessageBox::Yes | QMessageBox::No,
+ QMessageBox::NoButton
+ )->exec();
+ if(button == QMessageBox::No)
+ {
+ return ValidationStatus::Bad;
+ }
+ return ValidationStatus::JavaBad;
+ }
+ break;
+ case JavaStatus::Pending:
+ {
+ return ValidationStatus::Bad;
+ }
+ case JavaStatus::Good:
+ {
+ return ValidationStatus::AllOK;
+ }
+ }
+}
+
+QString JavaSettingsWidget::javaPath() const
+{
+ return m_javaPathTextBox->text();
+}
+
+int JavaSettingsWidget::maxHeapSize() const
+{
+ return m_maxMemSpinBox->value();
+}
+
+int JavaSettingsWidget::minHeapSize() const
+{
+ return m_minMemSpinBox->value();
+}
+
+bool JavaSettingsWidget::permGenEnabled() const
+{
+ return m_permGenSpinBox->isVisible();
+}
+
+int JavaSettingsWidget::permGenSize() const
+{
+ return m_permGenSpinBox->value();
+}
+
+void JavaSettingsWidget::memoryValueChanged(int)
+{
+ bool actuallyChanged = false;
+ int min = m_minMemSpinBox->value();
+ int max = m_maxMemSpinBox->value();
+ int permgen = m_permGenSpinBox->value();
+ QObject *obj = sender();
+ if (obj == m_minMemSpinBox && min != observedMinMemory)
+ {
+ observedMinMemory = min;
+ actuallyChanged = true;
+ if (min > max)
+ {
+ observedMaxMemory = min;
+ m_maxMemSpinBox->setValue(min);
+ }
+ }
+ else if (obj == m_maxMemSpinBox && max != observedMaxMemory)
+ {
+ observedMaxMemory = max;
+ actuallyChanged = true;
+ if (min > max)
+ {
+ observedMinMemory = max;
+ m_minMemSpinBox->setValue(max);
+ }
+ }
+ else if (obj == m_permGenSpinBox && permgen != observedPermGenMemory)
+ {
+ observedPermGenMemory = permgen;
+ actuallyChanged = true;
+ }
+ if(actuallyChanged)
+ {
+ checkJavaPathOnEdit(m_javaPathTextBox->text());
+ }
+}
+
+void JavaSettingsWidget::javaVersionSelected(BaseVersionPtr version)
+{
+ auto java = std::dynamic_pointer_cast<JavaInstall>(version);
+ if(!java)
+ {
+ return;
+ }
+ auto visible = java->id.requiresPermGen();
+ m_labelPermGen->setVisible(visible);
+ m_permGenSpinBox->setVisible(visible);
+ m_javaPathTextBox->setText(java->path);
+ checkJavaPath(java->path);
+}
+
+void JavaSettingsWidget::on_javaBrowseBtn_clicked()
+{
+ QString filter;
+#if defined Q_OS_WIN32
+ filter = "Java (javaw.exe)";
+#else
+ filter = "Java (java)";
+#endif
+ QString raw_path = QFileDialog::getOpenFileName(this, tr("Find Java executable"), QString(), filter);
+ if(raw_path.isEmpty())
+ {
+ return;
+ }
+ QString cooked_path = FS::NormalizePath(raw_path);
+ m_javaPathTextBox->setText(cooked_path);
+ checkJavaPath(cooked_path);
+}
+
+void JavaSettingsWidget::on_javaStatusBtn_clicked()
+{
+ QString text;
+ bool failed = false;
+ switch(javaStatus)
+ {
+ case JavaStatus::NotSet:
+ checkJavaPath(m_javaPathTextBox->text());
+ return;
+ case JavaStatus::DoesNotExist:
+ text += QObject::tr("The specified file either doesn't exist or is not a proper executable.");
+ failed = true;
+ break;
+ case JavaStatus::DoesNotStart:
+ {
+ text += QObject::tr("The specified java binary didn't start properly.<br />");
+ auto htmlError = m_result.errorLog;
+ if(!htmlError.isEmpty())
+ {
+ htmlError.replace('\n', "<br />");
+ text += QString("<font color=\"red\">%1</font>").arg(htmlError);
+ }
+ failed = true;
+ break;
+ }
+ case JavaStatus::ReturnedInvalidData:
+ {
+ text += QObject::tr("The specified java binary returned unexpected results:<br />");
+ auto htmlOut = m_result.outLog;
+ if(!htmlOut.isEmpty())
+ {
+ htmlOut.replace('\n', "<br />");
+ text += QString("<font color=\"red\">%1</font>").arg(htmlOut);
+ }
+ failed = true;
+ break;
+ }
+ case JavaStatus::Good:
+ text += QObject::tr("Java test succeeded!<br />Platform reported: %1<br />Java version "
+ "reported: %2<br />").arg(m_result.realPlatform, m_result.javaVersion.toString());
+ break;
+ case JavaStatus::Pending:
+ // TODO: abort here?
+ return;
+ }
+ CustomMessageBox::selectable(
+ this,
+ failed ? QObject::tr("Java test success") : QObject::tr("Java test failure"),
+ text,
+ failed ? QMessageBox::Critical : QMessageBox::Information
+ )->show();
+}
+
+void JavaSettingsWidget::setJavaStatus(JavaSettingsWidget::JavaStatus status)
+{
+ javaStatus = status;
+ switch(javaStatus)
+ {
+ case JavaStatus::Good:
+ m_javaStatusBtn->setIcon(goodIcon);
+ break;
+ case JavaStatus::NotSet:
+ case JavaStatus::Pending:
+ m_javaStatusBtn->setIcon(yellowIcon);
+ break;
+ default:
+ m_javaStatusBtn->setIcon(badIcon);
+ break;
+ }
+}
+
+void JavaSettingsWidget::javaPathEdited(const QString& path)
+{
+ checkJavaPathOnEdit(path);
+}
+
+void JavaSettingsWidget::checkJavaPathOnEdit(const QString& path)
+{
+ auto realPath = FS::ResolveExecutable(path);
+ QFileInfo pathInfo(realPath);
+ if (pathInfo.baseName().toLower().contains("java"))
+ {
+ checkJavaPath(path);
+ }
+ else
+ {
+ if(!m_checker)
+ {
+ setJavaStatus(JavaStatus::NotSet);
+ }
+ }
+}
+
+void JavaSettingsWidget::checkJavaPath(const QString &path)
+{
+ if(m_checker)
+ {
+ queuedCheck = path;
+ return;
+ }
+ auto realPath = FS::ResolveExecutable(path);
+ if(realPath.isNull())
+ {
+ setJavaStatus(JavaStatus::DoesNotExist);
+ return;
+ }
+ setJavaStatus(JavaStatus::Pending);
+ m_checker.reset(new JavaChecker());
+ m_checker->m_path = path;
+ m_checker->m_minMem = m_minMemSpinBox->value();
+ m_checker->m_maxMem = m_maxMemSpinBox->value();
+ if(m_permGenSpinBox->isVisible())
+ {
+ m_checker->m_permGen = m_permGenSpinBox->value();
+ }
+ connect(m_checker.get(), &JavaChecker::checkFinished, this, &JavaSettingsWidget::checkFinished);
+ m_checker->performCheck();
+}
+
+void JavaSettingsWidget::checkFinished(JavaCheckResult result)
+{
+ m_result = result;
+ switch(result.validity)
+ {
+ case JavaCheckResult::Validity::Valid:
+ {
+ setJavaStatus(JavaStatus::Good);
+ break;
+ }
+ case JavaCheckResult::Validity::ReturnedInvalidData:
+ {
+ setJavaStatus(JavaStatus::ReturnedInvalidData);
+ break;
+ }
+ case JavaCheckResult::Validity::Errored:
+ {
+ setJavaStatus(JavaStatus::DoesNotStart);
+ break;
+ }
+ }
+ m_checker.reset();
+ if(!queuedCheck.isNull())
+ {
+ checkJavaPath(queuedCheck);
+ queuedCheck.clear();
+ }
+}
+
+void JavaSettingsWidget::retranslate()
+{
+ m_memoryGroupBox->setTitle(tr("Memory"));
+ m_maxMemSpinBox->setToolTip(tr("The maximum amount of memory Minecraft is allowed to use."));
+ m_labelMinMem->setText(tr("Minimum memory allocation:"));
+ m_labelMaxMem->setText(tr("Maximum memory allocation:"));
+ m_minMemSpinBox->setToolTip(tr("The amount of memory Minecraft is started with."));
+ m_permGenSpinBox->setToolTip(tr("The amount of memory available to store loaded Java classes."));
+ m_javaBrowseBtn->setText(tr("Browse"));
+}
diff --git a/launcher/ui/widgets/JavaSettingsWidget.h b/launcher/ui/widgets/JavaSettingsWidget.h
new file mode 100644
index 00000000..0d280daf
--- /dev/null
+++ b/launcher/ui/widgets/JavaSettingsWidget.h
@@ -0,0 +1,102 @@
+#pragma once
+#include <QWidget>
+
+#include <java/JavaChecker.h>
+#include <BaseVersion.h>
+#include <QObjectPtr.h>
+#include <QIcon>
+
+class QLineEdit;
+class VersionSelectWidget;
+class QSpinBox;
+class QPushButton;
+class QVBoxLayout;
+class QHBoxLayout;
+class QGroupBox;
+class QGridLayout;
+class QLabel;
+class QToolButton;
+
+/**
+ * This is a widget for all the Java settings dialogs and pages.
+ */
+class JavaSettingsWidget : public QWidget
+{
+ Q_OBJECT
+
+public:
+ explicit JavaSettingsWidget(QWidget *parent);
+ virtual ~JavaSettingsWidget() {};
+
+ enum class JavaStatus
+ {
+ NotSet,
+ Pending,
+ Good,
+ DoesNotExist,
+ DoesNotStart,
+ ReturnedInvalidData
+ } javaStatus = JavaStatus::NotSet;
+
+ enum class ValidationStatus
+ {
+ Bad,
+ JavaBad,
+ AllOK
+ };
+
+ void refresh();
+ void initialize();
+ ValidationStatus validate();
+ void retranslate();
+
+ bool permGenEnabled() const;
+ int permGenSize() const;
+ int minHeapSize() const;
+ int maxHeapSize() const;
+ QString javaPath() const;
+
+
+protected slots:
+ void memoryValueChanged(int);
+ void javaPathEdited(const QString &path);
+ void javaVersionSelected(BaseVersionPtr version);
+ void on_javaBrowseBtn_clicked();
+ void on_javaStatusBtn_clicked();
+ void checkFinished(JavaCheckResult result);
+
+protected: /* methods */
+ void checkJavaPathOnEdit(const QString &path);
+ void checkJavaPath(const QString &path);
+ void setJavaStatus(JavaStatus status);
+ void setupUi();
+
+private: /* data */
+ VersionSelectWidget *m_versionWidget = nullptr;
+ QVBoxLayout *m_verticalLayout = nullptr;
+
+ QLineEdit * m_javaPathTextBox = nullptr;
+ QPushButton * m_javaBrowseBtn = nullptr;
+ QToolButton * m_javaStatusBtn = nullptr;
+ QHBoxLayout *m_horizontalLayout = nullptr;
+
+ QGroupBox *m_memoryGroupBox = nullptr;
+ QGridLayout *m_gridLayout_2 = nullptr;
+ QSpinBox *m_maxMemSpinBox = nullptr;
+ QLabel *m_labelMinMem = nullptr;
+ QLabel *m_labelMaxMem = nullptr;
+ QSpinBox *m_minMemSpinBox = nullptr;
+ QLabel *m_labelPermGen = nullptr;
+ QSpinBox *m_permGenSpinBox = nullptr;
+ QIcon goodIcon;
+ QIcon yellowIcon;
+ QIcon badIcon;
+
+ int observedMinMemory = 0;
+ int observedMaxMemory = 0;
+ int observedPermGenMemory = 0;
+ QString queuedCheck;
+ uint64_t m_availableMemory = 0ull;
+ shared_qobject_ptr<JavaChecker> m_checker;
+ JavaCheckResult m_result;
+};
diff --git a/launcher/ui/widgets/LabeledToolButton.cpp b/launcher/ui/widgets/LabeledToolButton.cpp
new file mode 100644
index 00000000..ab2d3278
--- /dev/null
+++ b/launcher/ui/widgets/LabeledToolButton.cpp
@@ -0,0 +1,115 @@
+/* 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 <QLabel>
+#include <QVBoxLayout>
+#include <QResizeEvent>
+#include <QStyleOption>
+#include "LabeledToolButton.h"
+#include <QApplication>
+#include <QDebug>
+
+/*
+ *
+ * Tool Button with a label on it, instead of the normal text rendering
+ *
+ */
+
+LabeledToolButton::LabeledToolButton(QWidget * parent)
+ : QToolButton(parent)
+ , m_label(new QLabel(this))
+{
+ //QToolButton::setText(" ");
+ m_label->setWordWrap(true);
+ m_label->setMouseTracking(false);
+ m_label->setAlignment(Qt::AlignCenter);
+ m_label->setTextInteractionFlags(Qt::NoTextInteraction);
+ // somehow, this makes word wrap work in the QLabel. yay.
+ //m_label->setMinimumWidth(100);
+}
+
+QString LabeledToolButton::text() const
+{
+ return m_label->text();
+}
+
+void LabeledToolButton::setText(const QString & text)
+{
+ m_label->setText(text);
+}
+
+void LabeledToolButton::setIcon(QIcon icon)
+{
+ m_icon = icon;
+ resetIcon();
+}
+
+
+/*!
+ \reimp
+*/
+QSize LabeledToolButton::sizeHint() const
+{
+ /*
+ Q_D(const QToolButton);
+ if (d->sizeHint.isValid())
+ return d->sizeHint;
+ */
+ ensurePolished();
+
+ int w = 0, h = 0;
+ QStyleOptionToolButton opt;
+ initStyleOption(&opt);
+ QSize sz =m_label->sizeHint();
+ w = sz.width();
+ h = sz.height();
+
+ opt.rect.setSize(QSize(w, h)); // PM_MenuButtonIndicator depends on the height
+ if (popupMode() == MenuButtonPopup)
+ w += style()->pixelMetric(QStyle::PM_MenuButtonIndicator, &opt, this);
+
+ QSize rawSize = style()->sizeFromContents(QStyle::CT_ToolButton, &opt, QSize(w, h), this);
+ QSize sizeHint = rawSize.expandedTo(QApplication::globalStrut());
+ return sizeHint;
+}
+
+
+
+void LabeledToolButton::resizeEvent(QResizeEvent * event)
+{
+ m_label->setGeometry(QRect(4, 4, width()-8, height()-8));
+ if(!m_icon.isNull())
+ {
+ resetIcon();
+ }
+ QWidget::resizeEvent(event);
+}
+
+void LabeledToolButton::resetIcon()
+{
+ auto iconSz = m_icon.actualSize(QSize(160, 80));
+ float w = iconSz.width();
+ float h = iconSz.height();
+ float ar = w/h;
+ // FIXME: hardcoded max size of 160x80
+ int newW = 80 * ar;
+ if(newW > 160)
+ newW = 160;
+ QSize newSz (newW, 80);
+ auto pixmap = m_icon.pixmap(newSz);
+ m_label->setPixmap(pixmap);
+ m_label->setMinimumHeight(80);
+ m_label->setSizePolicy( QSizePolicy::MinimumExpanding, QSizePolicy::Preferred );
+}
diff --git a/launcher/ui/widgets/LabeledToolButton.h b/launcher/ui/widgets/LabeledToolButton.h
new file mode 100644
index 00000000..51f99e9b
--- /dev/null
+++ b/launcher/ui/widgets/LabeledToolButton.h
@@ -0,0 +1,40 @@
+/* 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 <QPushButton>
+#include <QToolButton>
+
+class QLabel;
+
+class LabeledToolButton : public QToolButton
+{
+ Q_OBJECT
+
+ QLabel * m_label;
+ QIcon m_icon;
+
+public:
+ LabeledToolButton(QWidget * parent = 0);
+
+ QString text() const;
+ void setText(const QString & text);
+ void setIcon(QIcon icon);
+ virtual QSize sizeHint() const;
+protected:
+ void resizeEvent(QResizeEvent * event);
+ void resetIcon();
+};
diff --git a/launcher/ui/widgets/LanguageSelectionWidget.cpp b/launcher/ui/widgets/LanguageSelectionWidget.cpp
new file mode 100644
index 00000000..cf70c7b4
--- /dev/null
+++ b/launcher/ui/widgets/LanguageSelectionWidget.cpp
@@ -0,0 +1,66 @@
+#include "LanguageSelectionWidget.h"
+
+#include <QVBoxLayout>
+#include <QTreeView>
+#include <QHeaderView>
+#include <QLabel>
+#include "Application.h"
+#include "translations/TranslationsModel.h"
+
+LanguageSelectionWidget::LanguageSelectionWidget(QWidget *parent) :
+ QWidget(parent)
+{
+ verticalLayout = new QVBoxLayout(this);
+ verticalLayout->setObjectName(QStringLiteral("verticalLayout"));
+ languageView = new QTreeView(this);
+ languageView->setObjectName(QStringLiteral("languageView"));
+ languageView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+ languageView->setAlternatingRowColors(true);
+ languageView->setRootIsDecorated(false);
+ languageView->setItemsExpandable(false);
+ languageView->setWordWrap(true);
+ languageView->header()->setCascadingSectionResizes(true);
+ languageView->header()->setStretchLastSection(false);
+ verticalLayout->addWidget(languageView);
+ helpUsLabel = new QLabel(this);
+ helpUsLabel->setObjectName(QStringLiteral("helpUsLabel"));
+ helpUsLabel->setTextInteractionFlags(Qt::LinksAccessibleByMouse);
+ helpUsLabel->setOpenExternalLinks(true);
+ helpUsLabel->setWordWrap(true);
+ verticalLayout->addWidget(helpUsLabel);
+
+ auto translations = APPLICATION->translations();
+ auto index = translations->selectedIndex();
+ languageView->setModel(translations.get());
+ languageView->setCurrentIndex(index);
+ languageView->header()->setSectionResizeMode(QHeaderView::ResizeToContents);
+ languageView->header()->setSectionResizeMode(0, QHeaderView::Stretch);
+ connect(languageView->selectionModel(), &QItemSelectionModel::currentRowChanged, this, &LanguageSelectionWidget::languageRowChanged);
+ verticalLayout->setContentsMargins(0,0,0,0);
+}
+
+QString LanguageSelectionWidget::getSelectedLanguageKey() const
+{
+ auto translations = APPLICATION->translations();
+ return translations->data(languageView->currentIndex(), Qt::UserRole).toString();
+}
+
+void LanguageSelectionWidget::retranslate()
+{
+ QString text = tr("Don't see your language or the quality is poor?<br/><a href=\"%1\">Help us with translations!</a>")
+ .arg("https://github.com/MultiMC/Launcher/wiki/Translating-MultiMC");
+ helpUsLabel->setText(text);
+
+}
+
+void LanguageSelectionWidget::languageRowChanged(const QModelIndex& current, const QModelIndex& previous)
+{
+ if (current == previous)
+ {
+ return;
+ }
+ auto translations = APPLICATION->translations();
+ QString key = translations->data(current, Qt::UserRole).toString();
+ translations->selectLanguage(key);
+ translations->updateLanguage(key);
+}
diff --git a/launcher/ui/widgets/LanguageSelectionWidget.h b/launcher/ui/widgets/LanguageSelectionWidget.h
new file mode 100644
index 00000000..e65936db
--- /dev/null
+++ b/launcher/ui/widgets/LanguageSelectionWidget.h
@@ -0,0 +1,41 @@
+/* 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 <QWidget>
+
+class QVBoxLayout;
+class QTreeView;
+class QLabel;
+
+class LanguageSelectionWidget: public QWidget
+{
+ Q_OBJECT
+public:
+ explicit LanguageSelectionWidget(QWidget *parent = 0);
+ virtual ~LanguageSelectionWidget() { };
+
+ QString getSelectedLanguageKey() const;
+ void retranslate();
+
+protected slots:
+ void languageRowChanged(const QModelIndex &current, const QModelIndex &previous);
+
+private:
+ QVBoxLayout *verticalLayout = nullptr;
+ QTreeView *languageView = nullptr;
+ QLabel *helpUsLabel = nullptr;
+};
diff --git a/launcher/ui/widgets/LineSeparator.cpp b/launcher/ui/widgets/LineSeparator.cpp
new file mode 100644
index 00000000..d03e6762
--- /dev/null
+++ b/launcher/ui/widgets/LineSeparator.cpp
@@ -0,0 +1,37 @@
+#include "LineSeparator.h"
+
+#include <QStyle>
+#include <QStyleOption>
+#include <QLayout>
+#include <QPainter>
+
+void LineSeparator::initStyleOption(QStyleOption *option) const
+{
+ option->initFrom(this);
+ // in a horizontal layout, the line is vertical (and vice versa)
+ if (m_orientation == Qt::Vertical)
+ option->state |= QStyle::State_Horizontal;
+}
+
+LineSeparator::LineSeparator(QWidget *parent, Qt::Orientation orientation)
+ : QWidget(parent), m_orientation(orientation)
+{
+ setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
+}
+
+QSize LineSeparator::sizeHint() const
+{
+ QStyleOption opt;
+ initStyleOption(&opt);
+ const int extent =
+ style()->pixelMetric(QStyle::PM_ToolBarSeparatorExtent, &opt, parentWidget());
+ return QSize(extent, extent);
+}
+
+void LineSeparator::paintEvent(QPaintEvent *)
+{
+ QPainter p(this);
+ QStyleOption opt;
+ initStyleOption(&opt);
+ style()->drawPrimitive(QStyle::PE_IndicatorToolBarSeparator, &opt, &p, parentWidget());
+}
diff --git a/launcher/ui/widgets/LineSeparator.h b/launcher/ui/widgets/LineSeparator.h
new file mode 100644
index 00000000..22927b68
--- /dev/null
+++ b/launcher/ui/widgets/LineSeparator.h
@@ -0,0 +1,18 @@
+#pragma once
+#include <QWidget>
+
+class QStyleOption;
+
+class LineSeparator : public QWidget
+{
+ Q_OBJECT
+
+public:
+ /// Create a line separator. orientation is the orientation of the line.
+ explicit LineSeparator(QWidget *parent, Qt::Orientation orientation = Qt::Horizontal);
+ QSize sizeHint() const;
+ void paintEvent(QPaintEvent *);
+ void initStyleOption(QStyleOption *option) const;
+private:
+ Qt::Orientation m_orientation = Qt::Horizontal;
+};
diff --git a/launcher/ui/widgets/LogView.cpp b/launcher/ui/widgets/LogView.cpp
new file mode 100644
index 00000000..26a2a527
--- /dev/null
+++ b/launcher/ui/widgets/LogView.cpp
@@ -0,0 +1,144 @@
+#include "LogView.h"
+#include <QTextBlock>
+#include <QScrollBar>
+
+LogView::LogView(QWidget* parent) : QPlainTextEdit(parent)
+{
+ setWordWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere);
+ m_defaultFormat = new QTextCharFormat(currentCharFormat());
+}
+
+LogView::~LogView()
+{
+ delete m_defaultFormat;
+}
+
+void LogView::setWordWrap(bool wrapping)
+{
+ if(wrapping)
+ {
+ setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+ setLineWrapMode(QPlainTextEdit::WidgetWidth);
+ }
+ else
+ {
+ setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded);
+ setLineWrapMode(QPlainTextEdit::NoWrap);
+ }
+}
+
+void LogView::setModel(QAbstractItemModel* model)
+{
+ if(m_model)
+ {
+ disconnect(m_model, &QAbstractItemModel::modelReset, this, &LogView::repopulate);
+ disconnect(m_model, &QAbstractItemModel::rowsInserted, this, &LogView::rowsInserted);
+ disconnect(m_model, &QAbstractItemModel::rowsAboutToBeInserted, this, &LogView::rowsAboutToBeInserted);
+ disconnect(m_model, &QAbstractItemModel::rowsRemoved, this, &LogView::rowsRemoved);
+ }
+ m_model = model;
+ if(m_model)
+ {
+ connect(m_model, &QAbstractItemModel::modelReset, this, &LogView::repopulate);
+ connect(m_model, &QAbstractItemModel::rowsInserted, this, &LogView::rowsInserted);
+ connect(m_model, &QAbstractItemModel::rowsAboutToBeInserted, this, &LogView::rowsAboutToBeInserted);
+ connect(m_model, &QAbstractItemModel::rowsRemoved, this, &LogView::rowsRemoved);
+ connect(m_model, &QAbstractItemModel::destroyed, this, &LogView::modelDestroyed);
+ }
+ repopulate();
+}
+
+QAbstractItemModel * LogView::model() const
+{
+ return m_model;
+}
+
+void LogView::modelDestroyed(QObject* model)
+{
+ if(m_model == model)
+ {
+ setModel(nullptr);
+ }
+}
+
+void LogView::repopulate()
+{
+ auto doc = document();
+ doc->clear();
+ if(!m_model)
+ {
+ return;
+ }
+ rowsInserted(QModelIndex(), 0, m_model->rowCount() - 1);
+}
+
+void LogView::rowsAboutToBeInserted(const QModelIndex& parent, int first, int last)
+{
+ Q_UNUSED(parent)
+ Q_UNUSED(first)
+ Q_UNUSED(last)
+ QScrollBar *bar = verticalScrollBar();
+ int max_bar = bar->maximum();
+ int val_bar = bar->value();
+ if (m_scroll)
+ {
+ m_scroll = (max_bar - val_bar) <= 1;
+ }
+ else
+ {
+ m_scroll = val_bar == max_bar;
+ }
+}
+
+void LogView::rowsInserted(const QModelIndex& parent, int first, int last)
+{
+ for(int i = first; i <= last; i++)
+ {
+ auto idx = m_model->index(i, 0, parent);
+ auto text = m_model->data(idx, Qt::DisplayRole).toString();
+ QTextCharFormat format(*m_defaultFormat);
+ auto font = m_model->data(idx, Qt::FontRole);
+ if(font.isValid())
+ {
+ format.setFont(font.value<QFont>());
+ }
+ auto fg = m_model->data(idx, Qt::TextColorRole);
+ if(fg.isValid())
+ {
+ format.setForeground(fg.value<QColor>());
+ }
+ auto bg = m_model->data(idx, Qt::BackgroundRole);
+ if(bg.isValid())
+ {
+ format.setBackground(bg.value<QColor>());
+ }
+ auto workCursor = textCursor();
+ workCursor.movePosition(QTextCursor::End);
+ workCursor.insertText(text, format);
+ workCursor.insertBlock();
+ }
+ if(m_scroll && !m_scrolling)
+ {
+ m_scrolling = true;
+ QMetaObject::invokeMethod( this, "scrollToBottom", Qt::QueuedConnection);
+ }
+}
+
+void LogView::rowsRemoved(const QModelIndex& parent, int first, int last)
+{
+ // TODO: some day... maybe
+ Q_UNUSED(parent)
+ Q_UNUSED(first)
+ Q_UNUSED(last)
+}
+
+void LogView::scrollToBottom()
+{
+ m_scrolling = false;
+ verticalScrollBar()->setSliderPosition(verticalScrollBar()->maximum());
+}
+
+void LogView::findNext(const QString& what, bool reverse)
+{
+ find(what, reverse ? QTextDocument::FindFlag::FindBackward : QTextDocument::FindFlag(0));
+}
diff --git a/launcher/ui/widgets/LogView.h b/launcher/ui/widgets/LogView.h
new file mode 100644
index 00000000..3143360a
--- /dev/null
+++ b/launcher/ui/widgets/LogView.h
@@ -0,0 +1,36 @@
+#pragma once
+#include <QPlainTextEdit>
+#include <QAbstractItemView>
+
+class QAbstractItemModel;
+
+class LogView: public QPlainTextEdit
+{
+ Q_OBJECT
+public:
+ explicit LogView(QWidget *parent = nullptr);
+ virtual ~LogView();
+
+ virtual void setModel(QAbstractItemModel *model);
+ QAbstractItemModel *model() const;
+
+public slots:
+ void setWordWrap(bool wrapping);
+ void findNext(const QString & what, bool reverse);
+ void scrollToBottom();
+
+protected slots:
+ void repopulate();
+ // note: this supports only appending
+ void rowsInserted(const QModelIndex &parent, int first, int last);
+ void rowsAboutToBeInserted(const QModelIndex &parent, int first, int last);
+ // note: this supports only removing from front
+ void rowsRemoved(const QModelIndex &parent, int first, int last);
+ void modelDestroyed(QObject * model);
+
+protected:
+ QAbstractItemModel *m_model = nullptr;
+ QTextCharFormat *m_defaultFormat = nullptr;
+ bool m_scroll = false;
+ bool m_scrolling = false;
+};
diff --git a/launcher/ui/widgets/MCModInfoFrame.cpp b/launcher/ui/widgets/MCModInfoFrame.cpp
new file mode 100644
index 00000000..8c4bd690
--- /dev/null
+++ b/launcher/ui/widgets/MCModInfoFrame.cpp
@@ -0,0 +1,168 @@
+/* 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 <QMessageBox>
+#include <QtGui>
+
+#include "MCModInfoFrame.h"
+#include "ui_MCModInfoFrame.h"
+
+#include "ui/dialogs/CustomMessageBox.h"
+
+void MCModInfoFrame::updateWithMod(Mod &m)
+{
+ if (m.type() == m.MOD_FOLDER)
+ {
+ clear();
+ return;
+ }
+
+ QString text = "";
+ QString name = "";
+ if (m.name().isEmpty())
+ name = m.mmc_id();
+ else
+ name = m.name();
+
+ if (m.homeurl().isEmpty())
+ text = name;
+ else
+ text = "<a href=\"" + m.homeurl() + "\">" + name + "</a>";
+ if (!m.authors().isEmpty())
+ text += " by " + m.authors().join(", ");
+
+ setModText(text);
+
+ if (m.description().isEmpty())
+ {
+ setModDescription(QString());
+ }
+ else
+ {
+ setModDescription(m.description());
+ }
+}
+
+void MCModInfoFrame::clear()
+{
+ setModText(QString());
+ setModDescription(QString());
+}
+
+MCModInfoFrame::MCModInfoFrame(QWidget *parent) :
+ QFrame(parent),
+ ui(new Ui::MCModInfoFrame)
+{
+ ui->setupUi(this);
+ ui->label_ModDescription->setHidden(true);
+ ui->label_ModText->setHidden(true);
+ updateHiddenState();
+}
+
+MCModInfoFrame::~MCModInfoFrame()
+{
+ delete ui;
+}
+
+void MCModInfoFrame::updateHiddenState()
+{
+ if(ui->label_ModDescription->isHidden() && ui->label_ModText->isHidden())
+ {
+ setHidden(true);
+ }
+ else
+ {
+ setHidden(false);
+ }
+}
+
+void MCModInfoFrame::setModText(QString text)
+{
+ if(text.isEmpty())
+ {
+ ui->label_ModText->setHidden(true);
+ }
+ else
+ {
+ ui->label_ModText->setText(text);
+ ui->label_ModText->setHidden(false);
+ }
+ updateHiddenState();
+}
+
+void MCModInfoFrame::setModDescription(QString text)
+{
+ if(text.isEmpty())
+ {
+ ui->label_ModDescription->setHidden(true);
+ updateHiddenState();
+ return;
+ }
+ else
+ {
+ ui->label_ModDescription->setHidden(false);
+ updateHiddenState();
+ }
+ ui->label_ModDescription->setToolTip("");
+ QString intermediatetext = text.trimmed();
+ bool prev(false);
+ QChar rem('\n');
+ QString finaltext;
+ finaltext.reserve(intermediatetext.size());
+ foreach(const QChar& c, intermediatetext)
+ {
+ if(c == rem && prev){
+ continue;
+ }
+ prev = c == rem;
+ finaltext += c;
+ }
+ QString labeltext;
+ labeltext.reserve(300);
+ if(finaltext.length() > 290)
+ {
+ ui->label_ModDescription->setOpenExternalLinks(false);
+ ui->label_ModDescription->setTextFormat(Qt::TextFormat::RichText);
+ desc = text;
+ // This allows injecting HTML here.
+ labeltext.append("<html><body>" + finaltext.left(287) + "<a href=\"#mod_desc\">...</a></body></html>");
+ QObject::connect(ui->label_ModDescription, &QLabel::linkActivated, this, &MCModInfoFrame::modDescEllipsisHandler);
+ }
+ else
+ {
+ ui->label_ModDescription->setTextFormat(Qt::TextFormat::PlainText);
+ labeltext.append(finaltext);
+ }
+ ui->label_ModDescription->setText(labeltext);
+}
+
+void MCModInfoFrame::modDescEllipsisHandler(const QString &link)
+{
+ if(!currentBox)
+ {
+ currentBox = CustomMessageBox::selectable(this, QString(), desc);
+ connect(currentBox, &QMessageBox::finished, this, &MCModInfoFrame::boxClosed);
+ currentBox->show();
+ }
+ else
+ {
+ currentBox->setText(desc);
+ }
+}
+
+void MCModInfoFrame::boxClosed(int result)
+{
+ currentBox = nullptr;
+}
diff --git a/launcher/ui/widgets/MCModInfoFrame.h b/launcher/ui/widgets/MCModInfoFrame.h
new file mode 100644
index 00000000..0b7ef537
--- /dev/null
+++ b/launcher/ui/widgets/MCModInfoFrame.h
@@ -0,0 +1,52 @@
+/* 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 <QFrame>
+#include "minecraft/mod/Mod.h"
+
+namespace Ui
+{
+class MCModInfoFrame;
+}
+
+class MCModInfoFrame : public QFrame
+{
+ Q_OBJECT
+
+public:
+ explicit MCModInfoFrame(QWidget *parent = 0);
+ ~MCModInfoFrame();
+
+ void setModText(QString text);
+ void setModDescription(QString text);
+
+ void updateWithMod(Mod &m);
+ void clear();
+
+public slots:
+ void modDescEllipsisHandler(const QString& link );
+ void boxClosed(int result);
+
+private:
+ void updateHiddenState();
+
+private:
+ Ui::MCModInfoFrame *ui;
+ QString desc;
+ class QMessageBox * currentBox = nullptr;
+};
+
diff --git a/launcher/ui/widgets/MCModInfoFrame.ui b/launcher/ui/widgets/MCModInfoFrame.ui
new file mode 100644
index 00000000..5ef33379
--- /dev/null
+++ b/launcher/ui/widgets/MCModInfoFrame.ui
@@ -0,0 +1,92 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>MCModInfoFrame</class>
+ <widget class="QFrame" name="MCModInfoFrame">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>527</width>
+ <height>113</height>
+ </rect>
+ </property>
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>16777215</width>
+ <height>120</height>
+ </size>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_2">
+ <property name="spacing">
+ <number>6</number>
+ </property>
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QLabel" name="label_ModText">
+ <property name="text">
+ <string notr="true"/>
+ </property>
+ <property name="textFormat">
+ <enum>Qt::RichText</enum>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ <property name="openExternalLinks">
+ <bool>true</bool>
+ </property>
+ <property name="textInteractionFlags">
+ <set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="label_ModDescription">
+ <property name="toolTip">
+ <string notr="true"/>
+ </property>
+ <property name="text">
+ <string notr="true"/>
+ </property>
+ <property name="textFormat">
+ <enum>Qt::RichText</enum>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ <property name="openExternalLinks">
+ <bool>true</bool>
+ </property>
+ <property name="textInteractionFlags">
+ <set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/launcher/ui/widgets/ModListView.cpp b/launcher/ui/widgets/ModListView.cpp
new file mode 100644
index 00000000..c8ccd292
--- /dev/null
+++ b/launcher/ui/widgets/ModListView.cpp
@@ -0,0 +1,66 @@
+/* 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 "ModListView.h"
+#include <QHeaderView>
+#include <QMouseEvent>
+#include <QPainter>
+#include <QDrag>
+#include <QRect>
+
+ModListView::ModListView ( QWidget* parent )
+ :QTreeView ( parent )
+{
+ setAllColumnsShowFocus ( true );
+ setExpandsOnDoubleClick ( false );
+ setRootIsDecorated ( false );
+ setSortingEnabled ( true );
+ setAlternatingRowColors ( true );
+ setSelectionMode ( QAbstractItemView::ExtendedSelection );
+ setHeaderHidden ( false );
+ setSelectionBehavior(QAbstractItemView::SelectRows);
+ setVerticalScrollBarPolicy ( Qt::ScrollBarAlwaysOn );
+ setHorizontalScrollBarPolicy ( Qt::ScrollBarAsNeeded );
+ setDropIndicatorShown(true);
+ setDragEnabled(true);
+ setDragDropMode(QAbstractItemView::DropOnly);
+ viewport()->setAcceptDrops(true);
+}
+
+void ModListView::setModel ( QAbstractItemModel* model )
+{
+ QTreeView::setModel ( model );
+ auto head = header();
+ head->setStretchLastSection(false);
+ // HACK: this is true for the checkbox column of mod lists
+ auto string = model->headerData(0,head->orientation()).toString();
+ if(head->count() < 1)
+ {
+ return;
+ }
+ if(!string.size())
+ {
+ head->setSectionResizeMode(0, QHeaderView::ResizeToContents);
+ head->setSectionResizeMode(1, QHeaderView::Stretch);
+ for(int i = 2; i < head->count(); i++)
+ head->setSectionResizeMode(i, QHeaderView::ResizeToContents);
+ }
+ else
+ {
+ head->setSectionResizeMode(0, QHeaderView::Stretch);
+ for(int i = 1; i < head->count(); i++)
+ head->setSectionResizeMode(i, QHeaderView::ResizeToContents);
+ }
+}
diff --git a/launcher/ui/widgets/ModListView.h b/launcher/ui/widgets/ModListView.h
new file mode 100644
index 00000000..881e092f
--- /dev/null
+++ b/launcher/ui/widgets/ModListView.h
@@ -0,0 +1,25 @@
+/* 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 <QTreeView>
+
+class ModListView: public QTreeView
+{
+ Q_OBJECT
+public:
+ explicit ModListView ( QWidget* parent = 0 );
+ virtual void setModel ( QAbstractItemModel* model );
+};
diff --git a/launcher/ui/widgets/PageContainer.cpp b/launcher/ui/widgets/PageContainer.cpp
new file mode 100644
index 00000000..74a6dff3
--- /dev/null
+++ b/launcher/ui/widgets/PageContainer.cpp
@@ -0,0 +1,240 @@
+/* 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 "PageContainer.h"
+#include "PageContainer_p.h"
+
+#include <QStackedLayout>
+#include <QPushButton>
+#include <QSortFilterProxyModel>
+#include <QUrl>
+#include <QStyledItemDelegate>
+#include <QListView>
+#include <QLineEdit>
+#include <QLabel>
+#include <QDialogButtonBox>
+#include <QGridLayout>
+
+#include "settings/SettingsObject.h"
+
+#include "ui/widgets/IconLabel.h"
+
+#include "DesktopServices.h"
+#include "Application.h"
+
+class PageEntryFilterModel : public QSortFilterProxyModel
+{
+public:
+ explicit PageEntryFilterModel(QObject *parent = 0) : QSortFilterProxyModel(parent)
+ {
+ }
+
+protected:
+ bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
+ {
+ const QString pattern = filterRegExp().pattern();
+ const auto model = static_cast<PageModel *>(sourceModel());
+ const auto page = model->pages().at(sourceRow);
+ if (!page->shouldDisplay())
+ return false;
+ // Regular contents check, then check page-filter.
+ return QSortFilterProxyModel::filterAcceptsRow(sourceRow, sourceParent);
+ }
+};
+
+PageContainer::PageContainer(BasePageProvider *pageProvider, QString defaultId,
+ QWidget *parent)
+ : QWidget(parent)
+{
+ createUI();
+ m_model = new PageModel(this);
+ m_proxyModel = new PageEntryFilterModel(this);
+ int counter = 0;
+ auto pages = pageProvider->getPages();
+ for (auto page : pages)
+ {
+ page->stackIndex = m_pageStack->addWidget(dynamic_cast<QWidget *>(page));
+ page->listIndex = counter;
+ page->setParentContainer(this);
+ counter++;
+ }
+ m_model->setPages(pages);
+
+ m_proxyModel->setSourceModel(m_model);
+ m_proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
+
+ m_pageList->setIconSize(QSize(pageIconSize, pageIconSize));
+ m_pageList->setSelectionMode(QAbstractItemView::SingleSelection);
+ m_pageList->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
+ m_pageList->setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContents);
+ m_pageList->setModel(m_proxyModel);
+ connect(m_pageList->selectionModel(), SIGNAL(currentRowChanged(QModelIndex, QModelIndex)),
+ this, SLOT(currentChanged(QModelIndex)));
+ m_pageStack->setStackingMode(QStackedLayout::StackOne);
+ m_pageList->setFocus();
+ selectPage(defaultId);
+}
+
+bool PageContainer::selectPage(QString pageId)
+{
+ // now find what we want to have selected...
+ auto page = m_model->findPageEntryById(pageId);
+ QModelIndex index;
+ if (page)
+ {
+ index = m_proxyModel->mapFromSource(m_model->index(page->listIndex));
+ }
+ if(!index.isValid())
+ {
+ index = m_proxyModel->index(0, 0);
+ }
+ if (index.isValid())
+ {
+ m_pageList->setCurrentIndex(index);
+ return true;
+ }
+ return false;
+}
+
+void PageContainer::refreshContainer()
+{
+ m_proxyModel->invalidate();
+ if(!m_currentPage->shouldDisplay())
+ {
+ auto index = m_proxyModel->index(0, 0);
+ if(index.isValid())
+ {
+ m_pageList->setCurrentIndex(index);
+ }
+ else
+ {
+ // FIXME: unhandled corner case: what to do when there's no page to select?
+ }
+ }
+}
+
+void PageContainer::createUI()
+{
+ m_pageStack = new QStackedLayout;
+ m_pageList = new PageView;
+ m_header = new QLabel();
+ m_iconHeader = new IconLabel(this, QIcon(), QSize(24, 24));
+
+ QFont headerLabelFont = m_header->font();
+ headerLabelFont.setBold(true);
+ const int pointSize = headerLabelFont.pointSize();
+ if (pointSize > 0)
+ headerLabelFont.setPointSize(pointSize + 2);
+ m_header->setFont(headerLabelFont);
+
+ QHBoxLayout *headerHLayout = new QHBoxLayout;
+ const int leftMargin = APPLICATION->style()->pixelMetric(QStyle::PM_LayoutLeftMargin);
+ headerHLayout->addSpacerItem(new QSpacerItem(leftMargin, 0, QSizePolicy::Fixed, QSizePolicy::Ignored));
+ headerHLayout->addWidget(m_header);
+ headerHLayout->addSpacerItem(new QSpacerItem(0, 0, QSizePolicy::Expanding, QSizePolicy::Ignored));
+ headerHLayout->addWidget(m_iconHeader);
+ const int rightMargin = APPLICATION->style()->pixelMetric(QStyle::PM_LayoutRightMargin);
+ headerHLayout->addSpacerItem(new QSpacerItem(rightMargin, 0, QSizePolicy::Fixed, QSizePolicy::Ignored));
+ headerHLayout->setContentsMargins(0, 6, 0, 0);
+
+ m_pageStack->setMargin(0);
+ m_pageStack->addWidget(new QWidget(this));
+
+ m_layout = new QGridLayout;
+ m_layout->addLayout(headerHLayout, 0, 1, 1, 1);
+ m_layout->addWidget(m_pageList, 0, 0, 2, 1);
+ m_layout->addLayout(m_pageStack, 1, 1, 1, 1);
+ m_layout->setColumnStretch(1, 4);
+ m_layout->setContentsMargins(0,0,0,6);
+ setLayout(m_layout);
+}
+
+void PageContainer::addButtons(QWidget *buttons)
+{
+ m_layout->addWidget(buttons, 2, 0, 1, 2);
+}
+
+void PageContainer::addButtons(QLayout *buttons)
+{
+ m_layout->addLayout(buttons, 2, 0, 1, 2);
+}
+
+void PageContainer::showPage(int row)
+{
+ if (m_currentPage)
+ {
+ m_currentPage->closed();
+ }
+ if (row != -1)
+ {
+ m_currentPage = m_model->pages().at(row);
+ }
+ else
+ {
+ m_currentPage = nullptr;
+ }
+ if (m_currentPage)
+ {
+ m_pageStack->setCurrentIndex(m_currentPage->stackIndex);
+ m_header->setText(m_currentPage->displayName());
+ m_iconHeader->setIcon(m_currentPage->icon());
+ m_currentPage->opened();
+ }
+ else
+ {
+ m_pageStack->setCurrentIndex(0);
+ m_header->setText(QString());
+ m_iconHeader->setIcon(APPLICATION->getThemedIcon("bug"));
+ }
+}
+
+void PageContainer::help()
+{
+ if (m_currentPage)
+ {
+ QString pageId = m_currentPage->helpPage();
+ if (pageId.isEmpty())
+ return;
+ DesktopServices::openUrl(QUrl("https://github.com/MultiMC/Launcher/wiki/" + pageId));
+ }
+}
+
+void PageContainer::currentChanged(const QModelIndex &current)
+{
+ showPage(current.isValid() ? m_proxyModel->mapToSource(current).row() : -1);
+}
+
+bool PageContainer::prepareToClose()
+{
+ if(!saveAll())
+ {
+ return false;
+ }
+ if (m_currentPage)
+ {
+ m_currentPage->closed();
+ }
+ return true;
+}
+
+bool PageContainer::saveAll()
+{
+ for (auto page : m_model->pages())
+ {
+ if (!page->apply())
+ return false;
+ }
+ return true;
+}
diff --git a/launcher/ui/widgets/PageContainer.h b/launcher/ui/widgets/PageContainer.h
new file mode 100644
index 00000000..8d2172db
--- /dev/null
+++ b/launcher/ui/widgets/PageContainer.h
@@ -0,0 +1,89 @@
+/* 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 <QWidget>
+#include <QModelIndex>
+
+#include "ui/pages/BasePageProvider.h"
+#include "ui/pages/BasePageContainer.h"
+
+class QLayout;
+class IconLabel;
+class QSortFilterProxyModel;
+class PageModel;
+class QLabel;
+class QListView;
+class QLineEdit;
+class QStackedLayout;
+class QGridLayout;
+
+class PageContainer : public QWidget, public BasePageContainer
+{
+ Q_OBJECT
+public:
+ explicit PageContainer(BasePageProvider *pageProvider, QString defaultId = QString(),
+ QWidget *parent = 0);
+ virtual ~PageContainer() {}
+
+ void addButtons(QWidget * buttons);
+ void addButtons(QLayout * buttons);
+ /*
+ * Save any unsaved state and prepare to be closed.
+ * @return true if everything can be saved, false if there is something that requires attention
+ */
+ bool prepareToClose();
+ bool saveAll();
+
+ /* request close - used by individual pages */
+ bool requestClose() override
+ {
+ if(m_container)
+ {
+ return m_container->requestClose();
+ }
+ return false;
+ }
+
+ virtual bool selectPage(QString pageId) override;
+
+ void refreshContainer() override;
+ virtual void setParentContainer(BasePageContainer * container)
+ {
+ m_container = container;
+ };
+
+private:
+ void createUI();
+
+public slots:
+ void help();
+
+private slots:
+ void currentChanged(const QModelIndex &current);
+ void showPage(int row);
+
+private:
+ BasePageContainer * m_container = nullptr;
+ BasePage * m_currentPage = 0;
+ QSortFilterProxyModel *m_proxyModel;
+ PageModel *m_model;
+ QStackedLayout *m_pageStack;
+ QListView *m_pageList;
+ QLabel *m_header;
+ IconLabel *m_iconHeader;
+ QGridLayout *m_layout;
+};
diff --git a/launcher/ui/widgets/PageContainer_p.h b/launcher/ui/widgets/PageContainer_p.h
new file mode 100644
index 00000000..da1a66f4
--- /dev/null
+++ b/launcher/ui/widgets/PageContainer_p.h
@@ -0,0 +1,123 @@
+/* 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 <QListView>
+#include <QStyledItemDelegate>
+#include <QEvent>
+#include <QScrollBar>
+
+class BasePage;
+const int pageIconSize = 24;
+
+class PageViewDelegate : public QStyledItemDelegate
+{
+public:
+ PageViewDelegate(QObject *parent) : QStyledItemDelegate(parent)
+ {
+ }
+ QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const
+ {
+ QSize size = QStyledItemDelegate::sizeHint(option, index);
+ size.setHeight(qMax(size.height(), 32));
+ return size;
+ }
+};
+
+class PageModel : public QAbstractListModel
+{
+public:
+ PageModel(QObject *parent = 0) : QAbstractListModel(parent)
+ {
+ QPixmap empty(pageIconSize, pageIconSize);
+ empty.fill(Qt::transparent);
+ m_emptyIcon = QIcon(empty);
+ }
+ virtual ~PageModel() {}
+
+ int rowCount(const QModelIndex &parent = QModelIndex()) const
+ {
+ return parent.isValid() ? 0 : m_pages.size();
+ }
+ QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const
+ {
+ switch (role)
+ {
+ case Qt::DisplayRole:
+ return m_pages.at(index.row())->displayName();
+ case Qt::DecorationRole:
+ {
+ QIcon icon = m_pages.at(index.row())->icon();
+ if (icon.isNull())
+ icon = m_emptyIcon;
+ // HACK: fixes icon stretching on windows. TODO: report Qt bug for this
+ return QIcon(icon.pixmap(QSize(48,48)));
+ }
+ }
+ return QVariant();
+ }
+
+ void setPages(const QList<BasePage *> &pages)
+ {
+ beginResetModel();
+ m_pages = pages;
+ endResetModel();
+ }
+ const QList<BasePage *> &pages() const
+ {
+ return m_pages;
+ }
+
+ BasePage * findPageEntryById(QString id)
+ {
+ for(auto page: m_pages)
+ {
+ if (page->id() == id)
+ return page;
+ }
+ return nullptr;
+ }
+
+ QList<BasePage *> m_pages;
+ QIcon m_emptyIcon;
+};
+
+class PageView : public QListView
+{
+public:
+ PageView(QWidget *parent = 0) : QListView(parent)
+ {
+ setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Expanding);
+ setItemDelegate(new PageViewDelegate(this));
+ setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+ }
+
+ virtual QSize sizeHint() const
+ {
+ int width = sizeHintForColumn(0) + frameWidth() * 2 + 5;
+ if (verticalScrollBar()->isVisible())
+ width += verticalScrollBar()->width();
+ return QSize(width, 100);
+ }
+
+ virtual bool eventFilter(QObject *obj, QEvent *event)
+ {
+ if (obj == verticalScrollBar() &&
+ (event->type() == QEvent::Show || event->type() == QEvent::Hide))
+ updateGeometry();
+ return QListView::eventFilter(obj, event);
+ }
+};
diff --git a/launcher/ui/widgets/ProgressWidget.cpp b/launcher/ui/widgets/ProgressWidget.cpp
new file mode 100644
index 00000000..911e555d
--- /dev/null
+++ b/launcher/ui/widgets/ProgressWidget.cpp
@@ -0,0 +1,73 @@
+// Licensed under the Apache-2.0 license. See README.md for details.
+
+#include "ProgressWidget.h"
+#include <QProgressBar>
+#include <QLabel>
+#include <QVBoxLayout>
+#include <QEventLoop>
+
+#include "tasks/Task.h"
+
+ProgressWidget::ProgressWidget(QWidget *parent)
+ : QWidget(parent)
+{
+ m_label = new QLabel(this);
+ m_label->setWordWrap(true);
+ m_bar = new QProgressBar(this);
+ m_bar->setMinimum(0);
+ m_bar->setMaximum(100);
+ QVBoxLayout *layout = new QVBoxLayout(this);
+ layout->addWidget(m_label);
+ layout->addWidget(m_bar);
+ layout->addStretch();
+ setLayout(layout);
+}
+
+void ProgressWidget::start(std::shared_ptr<Task> task)
+{
+ if (m_task)
+ {
+ disconnect(m_task.get(), 0, this, 0);
+ }
+ m_task = task;
+ connect(m_task.get(), &Task::finished, this, &ProgressWidget::handleTaskFinish);
+ connect(m_task.get(), &Task::status, this, &ProgressWidget::handleTaskStatus);
+ connect(m_task.get(), &Task::progress, this, &ProgressWidget::handleTaskProgress);
+ connect(m_task.get(), &Task::destroyed, this, &ProgressWidget::taskDestroyed);
+ if (!m_task->isRunning())
+ {
+ QMetaObject::invokeMethod(m_task.get(), "start", Qt::QueuedConnection);
+ }
+}
+bool ProgressWidget::exec(std::shared_ptr<Task> task)
+{
+ QEventLoop loop;
+ connect(task.get(), &Task::finished, &loop, &QEventLoop::quit);
+ start(task);
+ if (task->isRunning())
+ {
+ loop.exec();
+ }
+ return task->wasSuccessful();
+}
+
+void ProgressWidget::handleTaskFinish()
+{
+ if (!m_task->wasSuccessful())
+ {
+ m_label->setText(m_task->failReason());
+ }
+}
+void ProgressWidget::handleTaskStatus(const QString &status)
+{
+ m_label->setText(status);
+}
+void ProgressWidget::handleTaskProgress(qint64 current, qint64 total)
+{
+ m_bar->setMaximum(total);
+ m_bar->setValue(current);
+}
+void ProgressWidget::taskDestroyed()
+{
+ m_task = nullptr;
+}
diff --git a/launcher/ui/widgets/ProgressWidget.h b/launcher/ui/widgets/ProgressWidget.h
new file mode 100644
index 00000000..fa67748a
--- /dev/null
+++ b/launcher/ui/widgets/ProgressWidget.h
@@ -0,0 +1,32 @@
+// Licensed under the Apache-2.0 license. See README.md for details.
+
+#pragma once
+
+#include <QWidget>
+#include <memory>
+
+class Task;
+class QProgressBar;
+class QLabel;
+
+class ProgressWidget : public QWidget
+{
+ Q_OBJECT
+public:
+ explicit ProgressWidget(QWidget *parent = nullptr);
+
+public slots:
+ void start(std::shared_ptr<Task> task);
+ bool exec(std::shared_ptr<Task> task);
+
+private slots:
+ void handleTaskFinish();
+ void handleTaskStatus(const QString &status);
+ void handleTaskProgress(qint64 current, qint64 total);
+ void taskDestroyed();
+
+private:
+ QLabel *m_label;
+ QProgressBar *m_bar;
+ std::shared_ptr<Task> m_task;
+};
diff --git a/launcher/ui/widgets/VersionListView.cpp b/launcher/ui/widgets/VersionListView.cpp
new file mode 100644
index 00000000..8424fedd
--- /dev/null
+++ b/launcher/ui/widgets/VersionListView.cpp
@@ -0,0 +1,163 @@
+/* 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 <QHeaderView>
+#include <QApplication>
+#include <QMouseEvent>
+#include <QDrag>
+#include <QPainter>
+#include "VersionListView.h"
+#include "Common.h"
+
+VersionListView::VersionListView(QWidget *parent)
+ :QTreeView ( parent )
+{
+ m_emptyString = tr("No versions are currently available.");
+}
+
+void VersionListView::rowsInserted(const QModelIndex &parent, int start, int end)
+{
+ m_itemCount += end-start+1;
+ updateEmptyViewPort();
+ QTreeView::rowsInserted(parent, start, end);
+}
+
+
+void VersionListView::rowsAboutToBeRemoved(const QModelIndex &parent, int start, int end)
+{
+ m_itemCount -= end-start+1;
+ updateEmptyViewPort();
+ QTreeView::rowsInserted(parent, start, end);
+}
+
+void VersionListView::setModel(QAbstractItemModel *model)
+{
+ m_itemCount = model->rowCount();
+ updateEmptyViewPort();
+ QTreeView::setModel(model);
+}
+
+void VersionListView::reset()
+{
+ if(model())
+ {
+ m_itemCount = model()->rowCount();
+ }
+ else {
+ m_itemCount = 0;
+ }
+ updateEmptyViewPort();
+ QTreeView::reset();
+}
+
+void VersionListView::setEmptyString(QString emptyString)
+{
+ m_emptyString = emptyString;
+ updateEmptyViewPort();
+}
+
+void VersionListView::setEmptyErrorString(QString emptyErrorString)
+{
+ m_emptyErrorString = emptyErrorString;
+ updateEmptyViewPort();
+}
+
+void VersionListView::setEmptyMode(VersionListView::EmptyMode mode)
+{
+ m_emptyMode = mode;
+ updateEmptyViewPort();
+}
+
+void VersionListView::updateEmptyViewPort()
+{
+#ifndef QT_NO_ACCESSIBILITY
+ setAccessibleDescription(currentEmptyString());
+#endif /* !QT_NO_ACCESSIBILITY */
+
+ if(!m_itemCount)
+ {
+ viewport()->update();
+ }
+}
+
+void VersionListView::paintEvent(QPaintEvent *event)
+{
+ if(m_itemCount)
+ {
+ QTreeView::paintEvent(event);
+ }
+ else
+ {
+ paintInfoLabel(event);
+ }
+}
+
+QString VersionListView::currentEmptyString() const
+{
+ if(m_itemCount) {
+ return QString();
+ }
+ switch(m_emptyMode)
+ {
+ default:
+ case VersionListView::Empty:
+ return QString();
+ case VersionListView::String:
+ return m_emptyString;
+ case VersionListView::ErrorString:
+ return m_emptyErrorString;
+ }
+}
+
+
+void VersionListView::paintInfoLabel(QPaintEvent *event) const
+{
+ QString emptyString = currentEmptyString();
+
+ //calculate the rect for the overlay
+ QPainter painter(viewport());
+ painter.setRenderHint(QPainter::Antialiasing, true);
+ QFont font("sans", 20);
+ font.setBold(true);
+
+ QRect bounds = viewport()->geometry();
+ bounds.moveTop(0);
+ auto innerBounds = bounds;
+ innerBounds.adjust(10, 10, -10, -10);
+
+ QColor background = QApplication::palette().color(QPalette::Foreground);
+ QColor foreground = QApplication::palette().color(QPalette::Base);
+ foreground.setAlpha(190);
+ painter.setFont(font);
+ auto fontMetrics = painter.fontMetrics();
+ auto textRect = fontMetrics.boundingRect(innerBounds, Qt::AlignHCenter | Qt::TextWordWrap, emptyString);
+ textRect.moveCenter(bounds.center());
+
+ auto wrapRect = textRect;
+ wrapRect.adjust(-10, -10, 10, 10);
+
+ //check if we are allowed to draw in our area
+ if (!event->rect().intersects(wrapRect)) {
+ return;
+ }
+
+ painter.setBrush(QBrush(background));
+ painter.setPen(foreground);
+ painter.drawRoundedRect(wrapRect, 5.0, 5.0);
+
+ painter.setPen(foreground);
+ painter.setFont(font);
+ painter.drawText(textRect, Qt::AlignHCenter | Qt::TextWordWrap, emptyString);
+}
diff --git a/launcher/ui/widgets/VersionListView.h b/launcher/ui/widgets/VersionListView.h
new file mode 100644
index 00000000..4153b314
--- /dev/null
+++ b/launcher/ui/widgets/VersionListView.h
@@ -0,0 +1,56 @@
+/* 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 <QTreeView>
+
+class VersionListView : public QTreeView
+{
+ Q_OBJECT
+public:
+
+ explicit VersionListView(QWidget *parent = 0);
+ virtual void paintEvent(QPaintEvent *event) override;
+ virtual void setModel(QAbstractItemModel* model) override;
+
+ enum EmptyMode
+ {
+ Empty,
+ String,
+ ErrorString
+ };
+
+ void setEmptyString(QString emptyString);
+ void setEmptyErrorString(QString emptyErrorString);
+ void setEmptyMode(EmptyMode mode);
+
+public slots:
+ virtual void reset() override;
+
+protected slots:
+ virtual void rowsAboutToBeRemoved(const QModelIndex & parent, int start, int end) override;
+ virtual void rowsInserted(const QModelIndex &parent, int start, int end) override;
+
+private: /* methods */
+ void paintInfoLabel(QPaintEvent *event) const;
+ void updateEmptyViewPort();
+ QString currentEmptyString() const;
+
+private: /* variables */
+ int m_itemCount = 0;
+ QString m_emptyString;
+ QString m_emptyErrorString;
+ EmptyMode m_emptyMode = Empty;
+};
diff --git a/launcher/ui/widgets/VersionSelectWidget.cpp b/launcher/ui/widgets/VersionSelectWidget.cpp
new file mode 100644
index 00000000..1209f118
--- /dev/null
+++ b/launcher/ui/widgets/VersionSelectWidget.cpp
@@ -0,0 +1,205 @@
+#include "VersionSelectWidget.h"
+
+#include <QProgressBar>
+#include <QVBoxLayout>
+#include <QHeaderView>
+
+#include "VersionListView.h"
+#include "VersionProxyModel.h"
+
+#include "ui/dialogs/CustomMessageBox.h"
+
+VersionSelectWidget::VersionSelectWidget(QWidget* parent)
+ : QWidget(parent)
+{
+ setObjectName(QStringLiteral("VersionSelectWidget"));
+ verticalLayout = new QVBoxLayout(this);
+ verticalLayout->setObjectName(QStringLiteral("verticalLayout"));
+ verticalLayout->setContentsMargins(0, 0, 0, 0);
+
+ m_proxyModel = new VersionProxyModel(this);
+
+ listView = new VersionListView(this);
+ listView->setObjectName(QStringLiteral("listView"));
+ listView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+ listView->setAlternatingRowColors(true);
+ listView->setRootIsDecorated(false);
+ listView->setItemsExpandable(false);
+ listView->setWordWrap(true);
+ listView->header()->setCascadingSectionResizes(true);
+ listView->header()->setStretchLastSection(false);
+ listView->setModel(m_proxyModel);
+ verticalLayout->addWidget(listView);
+
+ sneakyProgressBar = new QProgressBar(this);
+ sneakyProgressBar->setObjectName(QStringLiteral("sneakyProgressBar"));
+ sneakyProgressBar->setFormat(QStringLiteral("%p%"));
+ verticalLayout->addWidget(sneakyProgressBar);
+ sneakyProgressBar->setHidden(true);
+ connect(listView->selectionModel(), &QItemSelectionModel::currentRowChanged, this, &VersionSelectWidget::currentRowChanged);
+
+ QMetaObject::connectSlotsByName(this);
+}
+
+void VersionSelectWidget::setCurrentVersion(const QString& version)
+{
+ m_currentVersion = version;
+ m_proxyModel->setCurrentVersion(version);
+}
+
+void VersionSelectWidget::setEmptyString(QString emptyString)
+{
+ listView->setEmptyString(emptyString);
+}
+
+void VersionSelectWidget::setEmptyErrorString(QString emptyErrorString)
+{
+ listView->setEmptyErrorString(emptyErrorString);
+}
+
+VersionSelectWidget::~VersionSelectWidget()
+{
+}
+
+void VersionSelectWidget::setResizeOn(int column)
+{
+ listView->header()->setSectionResizeMode(resizeOnColumn, QHeaderView::ResizeToContents);
+ resizeOnColumn = column;
+ listView->header()->setSectionResizeMode(resizeOnColumn, QHeaderView::Stretch);
+}
+
+void VersionSelectWidget::initialize(BaseVersionList *vlist)
+{
+ m_vlist = vlist;
+ m_proxyModel->setSourceModel(vlist);
+ listView->header()->setSectionResizeMode(QHeaderView::ResizeToContents);
+ listView->header()->setSectionResizeMode(resizeOnColumn, QHeaderView::Stretch);
+
+ if (!m_vlist->isLoaded())
+ {
+ loadList();
+ }
+ else
+ {
+ if (m_proxyModel->rowCount() == 0)
+ {
+ listView->setEmptyMode(VersionListView::String);
+ }
+ preselect();
+ }
+}
+
+void VersionSelectWidget::closeEvent(QCloseEvent * event)
+{
+ QWidget::closeEvent(event);
+}
+
+void VersionSelectWidget::loadList()
+{
+ auto newTask = m_vlist->getLoadTask();
+ if (!newTask)
+ {
+ return;
+ }
+ loadTask = newTask.get();
+ connect(loadTask, &Task::succeeded, this, &VersionSelectWidget::onTaskSucceeded);
+ connect(loadTask, &Task::failed, this, &VersionSelectWidget::onTaskFailed);
+ connect(loadTask, &Task::progress, this, &VersionSelectWidget::changeProgress);
+ if(!loadTask->isRunning())
+ {
+ loadTask->start();
+ }
+ sneakyProgressBar->setHidden(false);
+}
+
+void VersionSelectWidget::onTaskSucceeded()
+{
+ if (m_proxyModel->rowCount() == 0)
+ {
+ listView->setEmptyMode(VersionListView::String);
+ }
+ sneakyProgressBar->setHidden(true);
+ preselect();
+ loadTask = nullptr;
+}
+
+void VersionSelectWidget::onTaskFailed(const QString& reason)
+{
+ CustomMessageBox::selectable(this, tr("Error"), tr("List update failed:\n%1").arg(reason), QMessageBox::Warning)->show();
+ onTaskSucceeded();
+}
+
+void VersionSelectWidget::changeProgress(qint64 current, qint64 total)
+{
+ sneakyProgressBar->setMaximum(total);
+ sneakyProgressBar->setValue(current);
+}
+
+void VersionSelectWidget::currentRowChanged(const QModelIndex& current, const QModelIndex&)
+{
+ auto variant = m_proxyModel->data(current, BaseVersionList::VersionPointerRole);
+ emit selectedVersionChanged(variant.value<BaseVersionPtr>());
+}
+
+void VersionSelectWidget::preselect()
+{
+ if(preselectedAlready)
+ return;
+ selectCurrent();
+ if(preselectedAlready)
+ return;
+ selectRecommended();
+}
+
+void VersionSelectWidget::selectCurrent()
+{
+ if(m_currentVersion.isEmpty())
+ {
+ return;
+ }
+ auto idx = m_proxyModel->getVersion(m_currentVersion);
+ if(idx.isValid())
+ {
+ preselectedAlready = true;
+ listView->selectionModel()->setCurrentIndex(idx,QItemSelectionModel::SelectCurrent | QItemSelectionModel::Rows);
+ listView->scrollTo(idx, QAbstractItemView::PositionAtCenter);
+ }
+}
+
+void VersionSelectWidget::selectRecommended()
+{
+ auto idx = m_proxyModel->getRecommended();
+ if(idx.isValid())
+ {
+ preselectedAlready = true;
+ listView->selectionModel()->setCurrentIndex(idx,QItemSelectionModel::SelectCurrent | QItemSelectionModel::Rows);
+ listView->scrollTo(idx, QAbstractItemView::PositionAtCenter);
+ }
+}
+
+bool VersionSelectWidget::hasVersions() const
+{
+ return m_proxyModel->rowCount(QModelIndex()) != 0;
+}
+
+BaseVersionPtr VersionSelectWidget::selectedVersion() const
+{
+ auto currentIndex = listView->selectionModel()->currentIndex();
+ auto variant = m_proxyModel->data(currentIndex, BaseVersionList::VersionPointerRole);
+ return variant.value<BaseVersionPtr>();
+}
+
+void VersionSelectWidget::setExactFilter(BaseVersionList::ModelRoles role, QString filter)
+{
+ m_proxyModel->setFilter(role, new ExactFilter(filter));
+}
+
+void VersionSelectWidget::setFuzzyFilter(BaseVersionList::ModelRoles role, QString filter)
+{
+ m_proxyModel->setFilter(role, new ContainsFilter(filter));
+}
+
+void VersionSelectWidget::setFilter(BaseVersionList::ModelRoles role, Filter *filter)
+{
+ m_proxyModel->setFilter(role, filter);
+}
diff --git a/launcher/ui/widgets/VersionSelectWidget.h b/launcher/ui/widgets/VersionSelectWidget.h
new file mode 100644
index 00000000..0a649408
--- /dev/null
+++ b/launcher/ui/widgets/VersionSelectWidget.h
@@ -0,0 +1,81 @@
+/* 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 <QWidget>
+#include <QSortFilterProxyModel>
+#include "BaseVersionList.h"
+
+class VersionProxyModel;
+class VersionListView;
+class QVBoxLayout;
+class QProgressBar;
+class Filter;
+
+class VersionSelectWidget: public QWidget
+{
+ Q_OBJECT
+public:
+ explicit VersionSelectWidget(QWidget *parent = 0);
+ ~VersionSelectWidget();
+
+ //! loads the list if needed.
+ void initialize(BaseVersionList *vlist);
+
+ //! Starts a task that loads the list.
+ void loadList();
+
+ bool hasVersions() const;
+ BaseVersionPtr selectedVersion() const;
+ void selectRecommended();
+ void selectCurrent();
+
+ void setCurrentVersion(const QString & version);
+ void setFuzzyFilter(BaseVersionList::ModelRoles role, QString filter);
+ void setExactFilter(BaseVersionList::ModelRoles role, QString filter);
+ void setFilter(BaseVersionList::ModelRoles role, Filter *filter);
+ void setEmptyString(QString emptyString);
+ void setEmptyErrorString(QString emptyErrorString);
+ void setResizeOn(int column);
+
+signals:
+ void selectedVersionChanged(BaseVersionPtr version);
+
+protected:
+ virtual void closeEvent ( QCloseEvent* );
+
+private slots:
+ void onTaskSucceeded();
+ void onTaskFailed(const QString &reason);
+ void changeProgress(qint64 current, qint64 total);
+ void currentRowChanged(const QModelIndex &current, const QModelIndex &);
+
+private:
+ void preselect();
+
+private:
+ QString m_currentVersion;
+ BaseVersionList *m_vlist = nullptr;
+ VersionProxyModel *m_proxyModel = nullptr;
+ int resizeOnColumn = 0;
+ Task * loadTask;
+ bool preselectedAlready = false;
+
+private:
+ QVBoxLayout *verticalLayout = nullptr;
+ VersionListView *listView = nullptr;
+ QProgressBar *sneakyProgressBar = nullptr;
+};
diff --git a/launcher/ui/widgets/WideBar.cpp b/launcher/ui/widgets/WideBar.cpp
new file mode 100644
index 00000000..cbd6c617
--- /dev/null
+++ b/launcher/ui/widgets/WideBar.cpp
@@ -0,0 +1,116 @@
+#include "WideBar.h"
+#include <QToolButton>
+#include <QMenu>
+
+class ActionButton : public QToolButton
+{
+ Q_OBJECT
+public:
+ ActionButton(QAction * action, QWidget * parent = 0) : QToolButton(parent), m_action(action) {
+ setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
+ connect(action, &QAction::changed, this, &ActionButton::actionChanged);
+ connect(this, &ActionButton::clicked, action, &QAction::trigger);
+ actionChanged();
+ };
+private slots:
+ void actionChanged() {
+ setEnabled(m_action->isEnabled());
+ setChecked(m_action->isChecked());
+ setCheckable(m_action->isCheckable());
+ setText(m_action->text());
+ setIcon(m_action->icon());
+ setToolTip(m_action->toolTip());
+ setHidden(!m_action->isVisible());
+ setFocusPolicy(Qt::NoFocus);
+ }
+private:
+ QAction * m_action;
+};
+
+
+WideBar::WideBar(const QString& title, QWidget* parent) : QToolBar(title, parent)
+{
+ setFloatable(false);
+ setMovable(false);
+}
+
+WideBar::WideBar(QWidget* parent) : QToolBar(parent)
+{
+ setFloatable(false);
+ setMovable(false);
+}
+
+struct WideBar::BarEntry {
+ enum Type {
+ None,
+ Action,
+ Separator,
+ Spacer
+ } type = None;
+ QAction *qAction = nullptr;
+ QAction *wideAction = nullptr;
+};
+
+
+WideBar::~WideBar()
+{
+ for(auto *iter: m_entries) {
+ delete iter;
+ }
+}
+
+void WideBar::addAction(QAction* action)
+{
+ auto entry = new BarEntry();
+ entry->qAction = addWidget(new ActionButton(action, this));
+ entry->wideAction = action;
+ entry->type = BarEntry::Action;
+ m_entries.push_back(entry);
+}
+
+void WideBar::addSeparator()
+{
+ auto entry = new BarEntry();
+ entry->qAction = QToolBar::addSeparator();
+ entry->type = BarEntry::Separator;
+ m_entries.push_back(entry);
+}
+
+void WideBar::insertSpacer(QAction* action)
+{
+ auto iter = std::find_if(m_entries.begin(), m_entries.end(), [action](BarEntry * entry) {
+ return entry->wideAction == action;
+ });
+ if(iter == m_entries.end()) {
+ return;
+ }
+ QWidget* spacer = new QWidget();
+ spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
+
+ auto entry = new BarEntry();
+ entry->qAction = insertWidget((*iter)->qAction, spacer);
+ entry->type = BarEntry::Spacer;
+ m_entries.insert(iter, entry);
+}
+
+QMenu * WideBar::createContextMenu(QWidget *parent, const QString & title)
+{
+ QMenu *contextMenu = new QMenu(title, parent);
+ for(auto & item: m_entries) {
+ switch(item->type) {
+ default:
+ case BarEntry::None:
+ break;
+ case BarEntry::Separator:
+ case BarEntry::Spacer:
+ contextMenu->addSeparator();
+ break;
+ case BarEntry::Action:
+ contextMenu->addAction(item->wideAction);
+ break;
+ }
+ }
+ return contextMenu;
+}
+
+#include "WideBar.moc"
diff --git a/launcher/ui/widgets/WideBar.h b/launcher/ui/widgets/WideBar.h
new file mode 100644
index 00000000..d1b8cbe7
--- /dev/null
+++ b/launcher/ui/widgets/WideBar.h
@@ -0,0 +1,26 @@
+#pragma once
+
+#include <QToolBar>
+#include <QAction>
+#include <QMap>
+
+class QMenu;
+
+class WideBar : public QToolBar
+{
+ Q_OBJECT
+
+public:
+ explicit WideBar(const QString &title, QWidget * parent = nullptr);
+ explicit WideBar(QWidget * parent = nullptr);
+ virtual ~WideBar();
+
+ void addAction(QAction *action);
+ void addSeparator();
+ void insertSpacer(QAction *action);
+ QMenu *createContextMenu(QWidget *parent = nullptr, const QString & title = QString());
+
+private:
+ struct BarEntry;
+ QList<BarEntry *> m_entries;
+};