summaryrefslogtreecommitdiff
path: root/meshmc/launcher/ui/pages/global
diff options
context:
space:
mode:
authorMehmet Samet Duman <yongdohyun@projecttick.org>2026-04-02 18:45:07 +0300
committerMehmet Samet Duman <yongdohyun@projecttick.org>2026-04-02 18:45:07 +0300
commit31b9a8949ed0a288143e23bf739f2eb64fdc63be (patch)
tree8a984fa143c38fccad461a77792d6864f3e82cd3 /meshmc/launcher/ui/pages/global
parent934382c8a1ce738589dee9ee0f14e1cec812770e (diff)
parentfad6a1066616b69d7f5fef01178efdf014c59537 (diff)
downloadProject-Tick-31b9a8949ed0a288143e23bf739f2eb64fdc63be.tar.gz
Project-Tick-31b9a8949ed0a288143e23bf739f2eb64fdc63be.zip
Add 'meshmc/' from commit 'fad6a1066616b69d7f5fef01178efdf014c59537'
git-subtree-dir: meshmc git-subtree-mainline: 934382c8a1ce738589dee9ee0f14e1cec812770e git-subtree-split: fad6a1066616b69d7f5fef01178efdf014c59537
Diffstat (limited to 'meshmc/launcher/ui/pages/global')
-rw-r--r--meshmc/launcher/ui/pages/global/AccountListPage.cpp261
-rw-r--r--meshmc/launcher/ui/pages/global/AccountListPage.h106
-rw-r--r--meshmc/launcher/ui/pages/global/AccountListPage.ui123
-rw-r--r--meshmc/launcher/ui/pages/global/AppearancePage.cpp215
-rw-r--r--meshmc/launcher/ui/pages/global/AppearancePage.h71
-rw-r--r--meshmc/launcher/ui/pages/global/AppearancePage.ui307
-rw-r--r--meshmc/launcher/ui/pages/global/CustomCommandsPage.cpp66
-rw-r--r--meshmc/launcher/ui/pages/global/CustomCommandsPage.h78
-rw-r--r--meshmc/launcher/ui/pages/global/ExternalToolsPage.cpp251
-rw-r--r--meshmc/launcher/ui/pages/global/ExternalToolsPage.h96
-rw-r--r--meshmc/launcher/ui/pages/global/ExternalToolsPage.ui194
-rw-r--r--meshmc/launcher/ui/pages/global/JavaPage.cpp287
-rw-r--r--meshmc/launcher/ui/pages/global/JavaPage.h99
-rw-r--r--meshmc/launcher/ui/pages/global/JavaPage.ui346
-rw-r--r--meshmc/launcher/ui/pages/global/LanguagePage.cpp68
-rw-r--r--meshmc/launcher/ui/pages/global/LanguagePage.h83
-rw-r--r--meshmc/launcher/ui/pages/global/MeshMCPage.cpp318
-rw-r--r--meshmc/launcher/ui/pages/global/MeshMCPage.h120
-rw-r--r--meshmc/launcher/ui/pages/global/MeshMCPage.ui482
-rw-r--r--meshmc/launcher/ui/pages/global/MinecraftPage.cpp115
-rw-r--r--meshmc/launcher/ui/pages/global/MinecraftPage.h91
-rw-r--r--meshmc/launcher/ui/pages/global/MinecraftPage.ui196
-rw-r--r--meshmc/launcher/ui/pages/global/PasteEEPage.cpp100
-rw-r--r--meshmc/launcher/ui/pages/global/PasteEEPage.h86
-rw-r--r--meshmc/launcher/ui/pages/global/PasteEEPage.ui128
-rw-r--r--meshmc/launcher/ui/pages/global/ProxyPage.cpp126
-rw-r--r--meshmc/launcher/ui/pages/global/ProxyPage.h88
-rw-r--r--meshmc/launcher/ui/pages/global/ProxyPage.ui203
28 files changed, 4704 insertions, 0 deletions
diff --git a/meshmc/launcher/ui/pages/global/AccountListPage.cpp b/meshmc/launcher/ui/pages/global/AccountListPage.cpp
new file mode 100644
index 0000000000..520877a664
--- /dev/null
+++ b/meshmc/launcher/ui/pages/global/AccountListPage.cpp
@@ -0,0 +1,261 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "AccountListPage.h"
+#include "ui_AccountListPage.h"
+
+#include <QItemSelectionModel>
+#include <QMenu>
+
+#include <QDebug>
+
+#include "net/NetJob.h"
+
+#include "ui/dialogs/ProgressDialog.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"
+
+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 Microsoft\" button to "
+ "add your Microsoft account."));
+ ui->listView->setEmptyMode(VersionListView::String);
+ ui->listView->setContextMenuPolicy(Qt::CustomContextMenu);
+
+ m_accounts = APPLICATION->accounts();
+
+ ui->listView->setModel(m_accounts.get());
+ // Expand the account column
+ ui->listView->header()->setSectionResizeMode(0, QHeaderView::Stretch);
+ ui->listView->header()->setSectionResizeMode(1, QHeaderView::Stretch);
+ ui->listView->header()->setSectionResizeMode(2,
+ QHeaderView::ResizeToContents);
+
+ 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(!BuildConfig.MSAClientID.isEmpty());
+}
+
+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_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 MeshMC.\n\n"
+ "Please update both your operating system and MeshMC."),
+ QMessageBox::Warning)
+ ->exec();
+ return;
+ }
+ MinecraftAccountPtr account = MSALoginDialog::newAccount(
+ this, tr("Log in with your Microsoft account to add it."));
+
+ 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>();
+ m_accounts->requestRefresh(account->internalId());
+ }
+}
+
+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();
+ MinecraftAccountPtr account =
+ selected.data(AccountList::PointerRole).value<MinecraftAccountPtr>();
+ ProgressDialog prog(this);
+ auto deleteSkinTask =
+ std::make_shared<SkinDelete>(this, account->accessToken());
+ 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/meshmc/launcher/ui/pages/global/AccountListPage.h b/meshmc/launcher/ui/pages/global/AccountListPage.h
new file mode 100644
index 0000000000..2e00e0d22a
--- /dev/null
+++ b/meshmc/launcher/ui/pages/global/AccountListPage.h
@@ -0,0 +1,106 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <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_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/meshmc/launcher/ui/pages/global/AccountListPage.ui b/meshmc/launcher/ui/pages/global/AccountListPage.ui
new file mode 100644
index 0000000000..96d0dc7518
--- /dev/null
+++ b/meshmc/launcher/ui/pages/global/AccountListPage.ui
@@ -0,0 +1,123 @@
+<?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="actionRefresh"/>
+ <addaction name="actionRemove"/>
+ <addaction name="actionSetDefault"/>
+ <addaction name="actionNoDefault"/>
+ <addaction name="separator"/>
+ <addaction name="actionUploadSkin"/>
+ <addaction name="actionDeleteSkin"/>
+ </widget>
+ <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/meshmc/launcher/ui/pages/global/AppearancePage.cpp b/meshmc/launcher/ui/pages/global/AppearancePage.cpp
new file mode 100644
index 0000000000..dc3838f55b
--- /dev/null
+++ b/meshmc/launcher/ui/pages/global/AppearancePage.cpp
@@ -0,0 +1,215 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "AppearancePage.h"
+#include "ui_AppearancePage.h"
+
+#include "Application.h"
+#include "ui/themes/ITheme.h"
+#include "ui/themes/ThemeManager.h"
+#include "ui/themes/CatPack.h"
+
+#include <QGraphicsOpacityEffect>
+
+static const QStringList previewIconNames = {
+ "new", "centralmods", "viewfolder", "launch",
+ "copy", "about", "settings", "accounts"};
+
+AppearancePage::AppearancePage(QWidget* parent)
+ : QWidget(parent), ui(new Ui::AppearancePage)
+{
+ ui->setupUi(this);
+
+ ui->catPreview->setGraphicsEffect(new QGraphicsOpacityEffect(this));
+
+ connect(ui->widgetStyleComboBox,
+ QOverload<int>::of(&QComboBox::currentIndexChanged), this,
+ &AppearancePage::applyWidgetTheme);
+ connect(ui->iconsComboBox,
+ QOverload<int>::of(&QComboBox::currentIndexChanged), this,
+ &AppearancePage::applyIconTheme);
+ connect(ui->catPackComboBox,
+ QOverload<int>::of(&QComboBox::currentIndexChanged), this,
+ &AppearancePage::applyCatTheme);
+
+ loadSettings();
+}
+
+AppearancePage::~AppearancePage()
+{
+ delete ui;
+}
+
+bool AppearancePage::apply()
+{
+ applySettings();
+ return true;
+}
+
+void AppearancePage::applyWidgetTheme(int index)
+{
+ auto settings = APPLICATION->settings();
+ auto originalTheme = settings->get("ApplicationTheme").toString();
+ auto newTheme = ui->widgetStyleComboBox->itemData(index).toString();
+ if (originalTheme != newTheme) {
+ settings->set("ApplicationTheme", newTheme);
+ APPLICATION->themeManager()->applyCurrentlySelectedTheme();
+
+ // Sync icon combo to the auto-resolved icon theme
+ auto resolvedIcon = settings->get("IconTheme").toString();
+ ui->iconsComboBox->blockSignals(true);
+ for (int i = 0; i < ui->iconsComboBox->count(); i++) {
+ if (ui->iconsComboBox->itemData(i).toString() == resolvedIcon) {
+ ui->iconsComboBox->setCurrentIndex(i);
+ break;
+ }
+ }
+ ui->iconsComboBox->blockSignals(false);
+ }
+ updateIconPreview();
+}
+
+void AppearancePage::applyIconTheme(int index)
+{
+ auto settings = APPLICATION->settings();
+ auto originalIconTheme = settings->get("IconTheme").toString();
+ auto newIconTheme = ui->iconsComboBox->itemData(index).toString();
+ if (originalIconTheme != newIconTheme) {
+ settings->set("IconTheme", newIconTheme);
+ APPLICATION->themeManager()->applyCurrentlySelectedTheme();
+ }
+ updateIconPreview();
+}
+
+void AppearancePage::applySettings()
+{
+ // Theme and icon changes are already persisted live via
+ // applyWidgetTheme/applyIconTheme. This is intentionally minimal — settings
+ // are saved on combo change.
+}
+
+void AppearancePage::loadSettings()
+{
+ auto settings = APPLICATION->settings();
+ auto tm = APPLICATION->themeManager();
+
+ // Block signals during population
+ ui->widgetStyleComboBox->blockSignals(true);
+ ui->iconsComboBox->blockSignals(true);
+ ui->catPackComboBox->blockSignals(true);
+
+ // --- Widget themes (flat list) ---
+ ui->widgetStyleComboBox->clear();
+ auto currentThemeId = settings->get("ApplicationTheme").toString();
+ auto themes = tm->allThemes();
+ int themeIdx = 0;
+
+ for (size_t i = 0; i < themes.size(); i++) {
+ auto* theme = themes[i];
+ ui->widgetStyleComboBox->addItem(theme->name(), theme->id());
+ if (!theme->tooltip().isEmpty()) {
+ ui->widgetStyleComboBox->setItemData(
+ static_cast<int>(i), theme->tooltip(), Qt::ToolTipRole);
+ }
+ if (theme->id() == currentThemeId) {
+ themeIdx = static_cast<int>(i);
+ }
+ }
+
+ ui->widgetStyleComboBox->setCurrentIndex(themeIdx);
+
+ // --- Icon themes (flat list) ---
+ ui->iconsComboBox->clear();
+ auto currentIconTheme = settings->get("IconTheme").toString();
+ auto iconThemeList = tm->iconThemes();
+ int iconIdx = 0;
+
+ for (int i = 0; i < iconThemeList.size(); i++) {
+ const auto& entry = iconThemeList[i];
+ ui->iconsComboBox->addItem(entry.name, entry.id);
+ if (entry.id == currentIconTheme) {
+ iconIdx = i;
+ }
+ }
+
+ ui->iconsComboBox->setCurrentIndex(iconIdx);
+
+ // --- Cat Packs ---
+ ui->catPackComboBox->clear();
+ auto currentCat = settings->get("BackgroundCat").toString();
+ auto cats = tm->getValidCatPacks();
+ int catIdx = 0;
+
+ for (int i = 0; i < cats.size(); i++) {
+ auto* cat = cats[i];
+ QIcon catIcon(cat->path());
+ ui->catPackComboBox->addItem(catIcon, cat->name(), cat->id());
+ if (cat->id() == currentCat) {
+ catIdx = i;
+ }
+ }
+
+ ui->catPackComboBox->setCurrentIndex(catIdx);
+
+ // Unblock signals
+ ui->widgetStyleComboBox->blockSignals(false);
+ ui->iconsComboBox->blockSignals(false);
+ ui->catPackComboBox->blockSignals(false);
+
+ // Initial previews
+ updateIconPreview();
+ updateCatPreview();
+}
+
+void AppearancePage::updateIconPreview()
+{
+ QList<QToolButton*> previewButtons = {ui->icon1, ui->icon2, ui->icon3,
+ ui->icon4, ui->icon5, ui->icon6,
+ ui->icon7, ui->icon8};
+
+ for (int i = 0; i < previewButtons.size() && i < previewIconNames.size();
+ i++) {
+ previewButtons[i]->setIcon(
+ APPLICATION->getThemedIcon(previewIconNames[i]));
+ }
+}
+
+void AppearancePage::applyCatTheme(int index)
+{
+ auto settings = APPLICATION->settings();
+ auto originalCat = settings->get("BackgroundCat").toString();
+ auto newCat = ui->catPackComboBox->itemData(index).toString();
+ if (originalCat != newCat) {
+ settings->set("BackgroundCat", newCat);
+ }
+ updateCatPreview();
+}
+
+void AppearancePage::updateCatPreview()
+{
+ QIcon catPackIcon(APPLICATION->themeManager()->getCatPack());
+ ui->catPreview->setIcon(catPackIcon);
+
+ auto effect =
+ dynamic_cast<QGraphicsOpacityEffect*>(ui->catPreview->graphicsEffect());
+ if (effect)
+ effect->setOpacity(1.0);
+}
diff --git a/meshmc/launcher/ui/pages/global/AppearancePage.h b/meshmc/launcher/ui/pages/global/AppearancePage.h
new file mode 100644
index 0000000000..5d83a3a82e
--- /dev/null
+++ b/meshmc/launcher/ui/pages/global/AppearancePage.h
@@ -0,0 +1,71 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <QWidget>
+#include "ui/pages/BasePage.h"
+#include <Application.h>
+
+namespace Ui
+{
+ class AppearancePage;
+}
+
+class AppearancePage : public QWidget, public BasePage
+{
+ Q_OBJECT
+
+ public:
+ explicit AppearancePage(QWidget* parent = nullptr);
+ ~AppearancePage();
+
+ QString displayName() const override
+ {
+ return tr("Appearance");
+ }
+ QIcon icon() const override
+ {
+ return APPLICATION->getThemedIcon("appearance");
+ }
+ QString id() const override
+ {
+ return "appearance-settings";
+ }
+ QString helpPage() const override
+ {
+ return "Appearance-settings";
+ }
+ bool apply() override;
+
+ private slots:
+ void applyWidgetTheme(int index);
+ void applyIconTheme(int index);
+ void applyCatTheme(int index);
+
+ private:
+ void applySettings();
+ void loadSettings();
+ void updateIconPreview();
+ void updateCatPreview();
+
+ Ui::AppearancePage* ui;
+};
diff --git a/meshmc/launcher/ui/pages/global/AppearancePage.ui b/meshmc/launcher/ui/pages/global/AppearancePage.ui
new file mode 100644
index 0000000000..edb9bead8a
--- /dev/null
+++ b/meshmc/launcher/ui/pages/global/AppearancePage.ui
@@ -0,0 +1,307 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>AppearancePage</class>
+ <widget class="QWidget" name="AppearancePage">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>514</width>
+ <height>400</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="QScrollArea" name="scrollArea">
+ <property name="widgetResizable">
+ <bool>true</bool>
+ </property>
+ <widget class="QWidget" name="scrollAreaContents">
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <layout class="QGridLayout" name="themeGridLayout">
+ <item row="0" column="0">
+ <widget class="QLabel" name="widgetStyleLabel">
+ <property name="text">
+ <string>&amp;Theme:</string>
+ </property>
+ <property name="buddy">
+ <cstring>widgetStyleComboBox</cstring>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QComboBox" name="widgetStyleComboBox">
+ <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="iconsLabel">
+ <property name="text">
+ <string>&amp;Icons:</string>
+ </property>
+ <property name="buddy">
+ <cstring>iconsComboBox</cstring>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QComboBox" name="iconsComboBox">
+ <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="2" column="0">
+ <widget class="QLabel" name="catPackLabel">
+ <property name="text">
+ <string>&amp;Cat:</string>
+ </property>
+ <property name="buddy">
+ <cstring>catPackComboBox</cstring>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="1">
+ <widget class="QComboBox" name="catPackComboBox">
+ <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>
+ </layout>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="previewBox">
+ <property name="title">
+ <string>Preview</string>
+ </property>
+ <layout class="QHBoxLayout" name="previewLayout">
+ <item>
+ <widget class="QToolButton" name="icon1">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ <property name="iconSize">
+ <size>
+ <width>24</width>
+ <height>24</height>
+ </size>
+ </property>
+ <property name="autoRaise">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QToolButton" name="icon2">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ <property name="iconSize">
+ <size>
+ <width>24</width>
+ <height>24</height>
+ </size>
+ </property>
+ <property name="autoRaise">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QToolButton" name="icon3">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ <property name="iconSize">
+ <size>
+ <width>24</width>
+ <height>24</height>
+ </size>
+ </property>
+ <property name="autoRaise">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QToolButton" name="icon4">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ <property name="iconSize">
+ <size>
+ <width>24</width>
+ <height>24</height>
+ </size>
+ </property>
+ <property name="autoRaise">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QToolButton" name="icon5">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ <property name="iconSize">
+ <size>
+ <width>24</width>
+ <height>24</height>
+ </size>
+ </property>
+ <property name="autoRaise">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QToolButton" name="icon6">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ <property name="iconSize">
+ <size>
+ <width>24</width>
+ <height>24</height>
+ </size>
+ </property>
+ <property name="autoRaise">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QToolButton" name="icon7">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ <property name="iconSize">
+ <size>
+ <width>24</width>
+ <height>24</height>
+ </size>
+ </property>
+ <property name="autoRaise">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QToolButton" name="icon8">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ <property name="iconSize">
+ <size>
+ <width>24</width>
+ <height>24</height>
+ </size>
+ </property>
+ <property name="autoRaise">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="previewSpacer">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>0</width>
+ <height>0</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QPushButton" name="catPreview">
+ <property name="focusPolicy">
+ <enum>Qt::NoFocus</enum>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ <property name="flat">
+ <bool>true</bool>
+ </property>
+ <property name="iconSize">
+ <size>
+ <width>64</width>
+ <height>128</height>
+ </size>
+ </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>0</width>
+ <height>0</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <tabstops>
+ <tabstop>widgetStyleComboBox</tabstop>
+ <tabstop>iconsComboBox</tabstop>
+ <tabstop>catPackComboBox</tabstop>
+ </tabstops>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/meshmc/launcher/ui/pages/global/CustomCommandsPage.cpp b/meshmc/launcher/ui/pages/global/CustomCommandsPage.cpp
new file mode 100644
index 0000000000..f17e0f7454
--- /dev/null
+++ b/meshmc/launcher/ui/pages/global/CustomCommandsPage.cpp
@@ -0,0 +1,66 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#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/meshmc/launcher/ui/pages/global/CustomCommandsPage.h b/meshmc/launcher/ui/pages/global/CustomCommandsPage.h
new file mode 100644
index 0000000000..5419e9ecff
--- /dev/null
+++ b/meshmc/launcher/ui/pages/global/CustomCommandsPage.h
@@ -0,0 +1,78 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 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/meshmc/launcher/ui/pages/global/ExternalToolsPage.cpp b/meshmc/launcher/ui/pages/global/ExternalToolsPage.cpp
new file mode 100644
index 0000000000..e8303b842b
--- /dev/null
+++ b/meshmc/launcher/ui/pages/global/ExternalToolsPage.cpp
@@ -0,0 +1,251 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "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_MACOS
+ 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/meshmc/launcher/ui/pages/global/ExternalToolsPage.h b/meshmc/launcher/ui/pages/global/ExternalToolsPage.h
new file mode 100644
index 0000000000..ad28d39ab0
--- /dev/null
+++ b/meshmc/launcher/ui/pages/global/ExternalToolsPage.h
@@ -0,0 +1,96 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <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/meshmc/launcher/ui/pages/global/ExternalToolsPage.ui b/meshmc/launcher/ui/pages/global/ExternalToolsPage.ui
new file mode 100644
index 0000000000..e79e938894
--- /dev/null
+++ b/meshmc/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/meshmc/launcher/ui/pages/global/JavaPage.cpp b/meshmc/launcher/ui/pages/global/JavaPage.cpp
new file mode 100644
index 0000000000..3b3d6b16b9
--- /dev/null
+++ b/meshmc/launcher/ui/pages/global/JavaPage.cpp
@@ -0,0 +1,287 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "JavaPage.h"
+#include "JavaCommon.h"
+#include "ui_JavaPage.h"
+
+#include <QFileDialog>
+#include <QMessageBox>
+#include <QDir>
+#include <QTabBar>
+#include <QTreeWidgetItem>
+#include <QDirIterator>
+
+#include "ui/dialogs/VersionSelectDialog.h"
+#ifndef MeshMC_DISABLE_JAVA_DOWNLOADER
+#include "ui/dialogs/JavaDownloadDialog.h"
+#endif
+
+#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);
+
+ auto sysMiB = Sys::getSystemRam() / Sys::mebibyte;
+ ui->maxMemSpinBox->setMaximum(sysMiB);
+ loadSettings();
+#ifdef MeshMC_DISABLE_JAVA_DOWNLOADER
+ // Hide the entire Installations tab when Java downloader is disabled
+ int idx = ui->tabWidget->indexOf(ui->tabInstallations);
+ if (idx != -1)
+ ui->tabWidget->removeTab(idx);
+#else
+ refreshInstalledJavas();
+#endif
+}
+
+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();
+}
+
+void JavaPage::on_javaDownloadBtn_clicked()
+{
+#ifndef MeshMC_DISABLE_JAVA_DOWNLOADER
+ JavaDownloadDialog dlg(this);
+ if (dlg.exec() == QDialog::Accepted) {
+ refreshInstalledJavas();
+ }
+#endif
+}
+
+void JavaPage::on_javaRefreshBtn_clicked()
+{
+ refreshInstalledJavas();
+}
+
+void JavaPage::on_javaRemoveBtn_clicked()
+{
+ auto* item = ui->installedJavaTree->currentItem();
+ if (!item)
+ return;
+
+ QString path = item->text(2);
+ if (path.isEmpty())
+ return;
+
+ // Find the java installation root directory (parent of bin/)
+ QFileInfo fi(path);
+ QDir javaDir = fi.dir(); // bin/
+ javaDir.cdUp(); // java root
+
+ auto result = QMessageBox::question(
+ this, tr("Remove Java Installation"),
+ tr("Are you sure you want to remove this Java installation?\n\n%1")
+ .arg(javaDir.absolutePath()),
+ QMessageBox::Yes | QMessageBox::No);
+
+ if (result != QMessageBox::Yes)
+ return;
+
+ javaDir.removeRecursively();
+ refreshInstalledJavas();
+}
+
+void JavaPage::on_javaUseBtn_clicked()
+{
+ auto* item = ui->installedJavaTree->currentItem();
+ if (!item)
+ return;
+
+ QString path = item->text(2);
+ if (!path.isEmpty()) {
+ ui->javaPathTextBox->setText(path);
+ ui->tabWidget->setCurrentIndex(0); // Switch to Settings tab
+ }
+}
+
+void JavaPage::refreshInstalledJavas()
+{
+ ui->installedJavaTree->clear();
+
+ QString javaBaseDir = JavaUtils::managedJavaRoot();
+ QDir baseDir(javaBaseDir);
+ if (!baseDir.exists())
+ return;
+
+ // Scan for java binaries under java/{vendor}/{version}/
+ QDirIterator vendorIt(javaBaseDir, QDir::Dirs | QDir::NoDotAndDotDot);
+ while (vendorIt.hasNext()) {
+ vendorIt.next();
+ QString vendorName = vendorIt.fileName();
+ QString vendorPath = vendorIt.filePath();
+
+ QDirIterator versionIt(vendorPath, QDir::Dirs | QDir::NoDotAndDotDot);
+ while (versionIt.hasNext()) {
+ versionIt.next();
+ QString versionPath = versionIt.filePath();
+
+ // Look for java binary
+#if defined(Q_OS_WIN)
+ QString binaryName = "javaw.exe";
+#else
+ QString binaryName = "java";
+#endif
+ QDirIterator binIt(versionPath, QStringList() << binaryName,
+ QDir::Files, QDirIterator::Subdirectories);
+ while (binIt.hasNext()) {
+ binIt.next();
+ QString javaPath = binIt.filePath();
+ if (javaPath.contains("/bin/")) {
+ auto* item = new QTreeWidgetItem(ui->installedJavaTree);
+ item->setText(0, versionIt.fileName());
+ item->setText(1, vendorName);
+ item->setText(2, javaPath);
+ break; // Only first binary per version dir
+ }
+ }
+ }
+ }
+
+ ui->installedJavaTree->resizeColumnToContents(0);
+ ui->installedJavaTree->resizeColumnToContents(1);
+}
diff --git a/meshmc/launcher/ui/pages/global/JavaPage.h b/meshmc/launcher/ui/pages/global/JavaPage.h
new file mode 100644
index 0000000000..5cd2f99fc4
--- /dev/null
+++ b/meshmc/launcher/ui/pages/global/JavaPage.h
@@ -0,0 +1,99 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <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();
+ void refreshInstalledJavas();
+
+ private slots:
+ void on_javaDetectBtn_clicked();
+ void on_javaTestBtn_clicked();
+ void on_javaBrowseBtn_clicked();
+ void on_javaDownloadBtn_clicked();
+ void on_javaRefreshBtn_clicked();
+ void on_javaRemoveBtn_clicked();
+ void on_javaUseBtn_clicked();
+ void checkerFinished();
+
+ private:
+ Ui::JavaPage* ui;
+ unique_qobject_ptr<JavaCommon::TestCheck> checker;
+};
diff --git a/meshmc/launcher/ui/pages/global/JavaPage.ui b/meshmc/launcher/ui/pages/global/JavaPage.ui
new file mode 100644
index 0000000000..8222a29679
--- /dev/null
+++ b/meshmc/launcher/ui/pages/global/JavaPage.ui
@@ -0,0 +1,346 @@
+<?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>Settings</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 class="QWidget" name="tabInstallations">
+ <attribute name="title">
+ <string>Installations</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="installationsLayout">
+ <item>
+ <widget class="QGroupBox" name="installedGroupBox">
+ <property name="title">
+ <string>Installed Java Runtimes</string>
+ </property>
+ <layout class="QVBoxLayout" name="installedLayout">
+ <item>
+ <widget class="QTreeWidget" name="installedJavaTree">
+ <property name="rootIsDecorated">
+ <bool>false</bool>
+ </property>
+ <property name="alternatingRowColors">
+ <bool>true</bool>
+ </property>
+ <column>
+ <property name="text">
+ <string>Version</string>
+ </property>
+ </column>
+ <column>
+ <property name="text">
+ <string>Vendor</string>
+ </property>
+ </column>
+ <column>
+ <property name="text">
+ <string>Path</string>
+ </property>
+ </column>
+ </widget>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="installBtnLayout">
+ <item>
+ <widget class="QPushButton" name="javaDownloadBtn">
+ <property name="text">
+ <string>Download Java...</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="javaRefreshBtn">
+ <property name="text">
+ <string>Refresh</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="javaRemoveBtn">
+ <property name="text">
+ <string>Remove</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="installBtnSpacer">
+ <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="javaUseBtn">
+ <property name="text">
+ <string>Use Selected</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ </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/meshmc/launcher/ui/pages/global/LanguagePage.cpp b/meshmc/launcher/ui/pages/global/LanguagePage.cpp
new file mode 100644
index 0000000000..f52c9429ac
--- /dev/null
+++ b/meshmc/launcher/ui/pages/global/LanguagePage.cpp
@@ -0,0 +1,68 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#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/meshmc/launcher/ui/pages/global/LanguagePage.h b/meshmc/launcher/ui/pages/global/LanguagePage.h
new file mode 100644
index 0000000000..50d5198490
--- /dev/null
+++ b/meshmc/launcher/ui/pages/global/LanguagePage.h
@@ -0,0 +1,83 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <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/meshmc/launcher/ui/pages/global/MeshMCPage.cpp b/meshmc/launcher/ui/pages/global/MeshMCPage.cpp
new file mode 100644
index 0000000000..fc5d406974
--- /dev/null
+++ b/meshmc/launcher/ui/pages/global/MeshMCPage.cpp
@@ -0,0 +1,318 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "MeshMCPage.h"
+#include "ui_MeshMCPage.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 <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
+};
+
+MeshMCPage::MeshMCPage(QWidget* parent)
+ : QWidget(parent), ui(new Ui::MeshMCPage)
+{
+ 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 && UpdateChecker::isUpdaterSupported()) {
+ // New updater: hide the legacy channel selector (no channel selection
+ // in the new system).
+ ui->updateChannelComboBox->setVisible(false);
+ ui->updateChannelLabel->setVisible(false);
+ ui->updateChannelDescLabel->setVisible(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()));
+
+ ui->migrateDataFolderMacBtn->setVisible(false);
+}
+
+MeshMCPage::~MeshMCPage()
+{
+ delete ui;
+ delete defaultFormat;
+}
+
+bool MeshMCPage::apply()
+{
+ applySettings();
+ return true;
+}
+
+void MeshMCPage::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 MeshMCPage::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 MeshMCPage::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 MeshMCPage::on_migrateDataFolderMacBtn_clicked()
+{
+ QMessageBox::information(
+ this, tr("Automatic macOS Migration"),
+ tr("%1 now stores macOS data under your Library/Application Support "
+ "folder automatically.")
+ .arg(BuildConfig.MESHMC_DISPLAYNAME));
+}
+
+void MeshMCPage::refreshUpdateChannelList()
+{
+ // No-op: the new updater does not use named channels.
+}
+
+void MeshMCPage::updateChannelSelectionChanged(int)
+{
+ // No-op.
+}
+
+void MeshMCPage::refreshUpdateChannelDesc()
+{
+ // No-op.
+}
+
+void MeshMCPage::applySettings()
+{
+ auto s = APPLICATION->settings();
+
+ if (ui->resetNotificationsBtn->isChecked()) {
+ s->set("ShownNotifications", QString());
+ }
+
+ // Updates
+ s->set("AutoUpdate", ui->autoUpdateCheckBox->isChecked());
+ // (UpdateChannel setting removed - the new updater always checks the stable
+ // feed)
+
+ // 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 MeshMCPage::loadSettings()
+{
+ auto s = APPLICATION->settings();
+ // Updates
+ ui->autoUpdateCheckBox->setChecked(s->get("AutoUpdate").toBool());
+ // (no channel to read in the new updater system)
+
+ // 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 MeshMCPage::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/meshmc/launcher/ui/pages/global/MeshMCPage.h b/meshmc/launcher/ui/pages/global/MeshMCPage.h
new file mode 100644
index 0000000000..f2cdb58b8e
--- /dev/null
+++ b/meshmc/launcher/ui/pages/global/MeshMCPage.h
@@ -0,0 +1,120 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <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 MeshMCPage;
+}
+
+class MeshMCPage : public QWidget, public BasePage
+{
+ Q_OBJECT
+
+ public:
+ explicit MeshMCPage(QWidget* parent = 0);
+ ~MeshMCPage();
+
+ QString displayName() const override
+ {
+ return "MeshMC";
+ }
+ QIcon icon() const override
+ {
+ return APPLICATION->getThemedIcon("launcher");
+ }
+ QString id() const override
+ {
+ return "launcher-settings";
+ }
+ QString helpPage() const override
+ {
+ return "MeshMC-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::MeshMCPage* ui;
+
+ // default format for the font preview...
+ QTextCharFormat* defaultFormat;
+
+ std::unique_ptr<LogColorCache> m_colors;
+
+ std::shared_ptr<TranslationsModel> m_languageModel;
+};
diff --git a/meshmc/launcher/ui/pages/global/MeshMCPage.ui b/meshmc/launcher/ui/pages/global/MeshMCPage.ui
new file mode 100644
index 0000000000..9d822b2952
--- /dev/null
+++ b/meshmc/launcher/ui/pages/global/MeshMCPage.ui
@@ -0,0 +1,482 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>MeshMCPage</class>
+ <widget class="QWidget" name="MeshMCPage">
+ <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 MeshMC)</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>MeshMC 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>
+ <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;MeshMC 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;MeshMC 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>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/meshmc/launcher/ui/pages/global/MinecraftPage.cpp b/meshmc/launcher/ui/pages/global/MinecraftPage.cpp
new file mode 100644
index 0000000000..1ec9fcba1a
--- /dev/null
+++ b/meshmc/launcher/ui/pages/global/MinecraftPage.cpp
@@ -0,0 +1,115 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "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/meshmc/launcher/ui/pages/global/MinecraftPage.h b/meshmc/launcher/ui/pages/global/MinecraftPage.h
new file mode 100644
index 0000000000..7e437a2af7
--- /dev/null
+++ b/meshmc/launcher/ui/pages/global/MinecraftPage.h
@@ -0,0 +1,91 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <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/meshmc/launcher/ui/pages/global/MinecraftPage.ui b/meshmc/launcher/ui/pages/global/MinecraftPage.ui
new file mode 100644
index 0000000000..857b8cfb12
--- /dev/null
+++ b/meshmc/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/meshmc/launcher/ui/pages/global/PasteEEPage.cpp b/meshmc/launcher/ui/pages/global/PasteEEPage.cpp
new file mode 100644
index 0000000000..d52d15f75d
--- /dev/null
+++ b/meshmc/launcher/ui/pages/global/PasteEEPage.cpp
@@ -0,0 +1,100 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "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 == "meshmc") {
+ ui->meshmcButton->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 = "meshmc";
+ }
+ s->set("PasteEEAPIKey", pasteKeyToUse);
+}
+
+bool PasteEEPage::apply()
+{
+ applySettings();
+ return true;
+}
+
+void PasteEEPage::textEdited(const QString& text)
+{
+ ui->customButton->setChecked(true);
+}
diff --git a/meshmc/launcher/ui/pages/global/PasteEEPage.h b/meshmc/launcher/ui/pages/global/PasteEEPage.h
new file mode 100644
index 0000000000..3eb0aade3a
--- /dev/null
+++ b/meshmc/launcher/ui/pages/global/PasteEEPage.h
@@ -0,0 +1,86 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <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/meshmc/launcher/ui/pages/global/PasteEEPage.ui b/meshmc/launcher/ui/pages/global/PasteEEPage.ui
new file mode 100644
index 0000000000..e81a6da78c
--- /dev/null
+++ b/meshmc/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="meshmcButton">
+ <property name="text">
+ <string>MeshMC 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 MeshMC 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>meshmcButton</tabstop>
+ <tabstop>customButton</tabstop>
+ <tabstop>customAPIkeyEdit</tabstop>
+ </tabstops>
+ <resources/>
+ <connections/>
+ <buttongroups>
+ <buttongroup name="pasteButtonGroup"/>
+ </buttongroups>
+</ui>
diff --git a/meshmc/launcher/ui/pages/global/ProxyPage.cpp b/meshmc/launcher/ui/pages/global/ProxyPage.cpp
new file mode 100644
index 0000000000..774d894ff7
--- /dev/null
+++ b/meshmc/launcher/ui/pages/global/ProxyPage.cpp
@@ -0,0 +1,126 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "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, &QButtonGroup::idClicked, this,
+ &ProxyPage::proxyChanged);
+}
+
+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/meshmc/launcher/ui/pages/global/ProxyPage.h b/meshmc/launcher/ui/pages/global/ProxyPage.h
new file mode 100644
index 0000000000..a6bb9d3b04
--- /dev/null
+++ b/meshmc/launcher/ui/pages/global/ProxyPage.h
@@ -0,0 +1,88 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <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/meshmc/launcher/ui/pages/global/ProxyPage.ui b/meshmc/launcher/ui/pages/global/ProxyPage.ui
new file mode 100644
index 0000000000..d7ab0bd38b
--- /dev/null
+++ b/meshmc/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 MeshMC. 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 MeshMC'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>