summaryrefslogtreecommitdiff
path: root/meshmc/launcher/ui/widgets
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/widgets
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/widgets')
-rw-r--r--meshmc/launcher/ui/widgets/Common.cpp47
-rw-r--r--meshmc/launcher/ui/widgets/Common.h27
-rw-r--r--meshmc/launcher/ui/widgets/CustomCommands.cpp69
-rw-r--r--meshmc/launcher/ui/widgets/CustomCommands.h65
-rw-r--r--meshmc/launcher/ui/widgets/CustomCommands.ui107
-rw-r--r--meshmc/launcher/ui/widgets/DropLabel.cpp61
-rw-r--r--meshmc/launcher/ui/widgets/DropLabel.h41
-rw-r--r--meshmc/launcher/ui/widgets/ErrorFrame.cpp142
-rw-r--r--meshmc/launcher/ui/widgets/ErrorFrame.h72
-rw-r--r--meshmc/launcher/ui/widgets/ErrorFrame.ui92
-rw-r--r--meshmc/launcher/ui/widgets/FocusLineEdit.cpp45
-rw-r--r--meshmc/launcher/ui/widgets/FocusLineEdit.h36
-rw-r--r--meshmc/launcher/ui/widgets/IconLabel.cpp61
-rw-r--r--meshmc/launcher/ui/widgets/IconLabel.h47
-rw-r--r--meshmc/launcher/ui/widgets/InstanceCardWidget.ui58
-rw-r--r--meshmc/launcher/ui/widgets/JavaSettingsWidget.cpp438
-rw-r--r--meshmc/launcher/ui/widgets/JavaSettingsWidget.h116
-rw-r--r--meshmc/launcher/ui/widgets/LabeledToolButton.cpp136
-rw-r--r--meshmc/launcher/ui/widgets/LabeledToolButton.h64
-rw-r--r--meshmc/launcher/ui/widgets/LanguageSelectionWidget.cpp91
-rw-r--r--meshmc/launcher/ui/widgets/LanguageSelectionWidget.h65
-rw-r--r--meshmc/launcher/ui/widgets/LineSeparator.cpp59
-rw-r--r--meshmc/launcher/ui/widgets/LineSeparator.h41
-rw-r--r--meshmc/launcher/ui/widgets/LogView.cpp161
-rw-r--r--meshmc/launcher/ui/widgets/LogView.h57
-rw-r--r--meshmc/launcher/ui/widgets/MCModInfoFrame.cpp173
-rw-r--r--meshmc/launcher/ui/widgets/MCModInfoFrame.h74
-rw-r--r--meshmc/launcher/ui/widgets/MCModInfoFrame.ui92
-rw-r--r--meshmc/launcher/ui/widgets/ModListView.cpp84
-rw-r--r--meshmc/launcher/ui/widgets/ModListView.h48
-rw-r--r--meshmc/launcher/ui/widgets/PageContainer.cpp253
-rw-r--r--meshmc/launcher/ui/widgets/PageContainer.h112
-rw-r--r--meshmc/launcher/ui/widgets/PageContainer_p.h143
-rw-r--r--meshmc/launcher/ui/widgets/ProgressWidget.cpp95
-rw-r--r--meshmc/launcher/ui/widgets/ProgressWidget.h55
-rw-r--r--meshmc/launcher/ui/widgets/VersionListView.cpp179
-rw-r--r--meshmc/launcher/ui/widgets/VersionListView.h75
-rw-r--r--meshmc/launcher/ui/widgets/VersionSelectWidget.cpp232
-rw-r--r--meshmc/launcher/ui/widgets/VersionSelectWidget.h104
-rw-r--r--meshmc/launcher/ui/widgets/WideBar.cpp135
-rw-r--r--meshmc/launcher/ui/widgets/WideBar.h48
41 files changed, 4100 insertions, 0 deletions
diff --git a/meshmc/launcher/ui/widgets/Common.cpp b/meshmc/launcher/ui/widgets/Common.cpp
new file mode 100644
index 0000000000..abbcc67cb7
--- /dev/null
+++ b/meshmc/launcher/ui/widgets/Common.cpp
@@ -0,0 +1,47 @@
+/* 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 "Common.h"
+
+// Origin: Qt
+QStringList viewItemTextLayout(QTextLayout& textLayout, int lineWidth,
+ qreal& height, qreal& widthUsed)
+{
+ QStringList lines;
+ height = 0;
+ widthUsed = 0;
+ textLayout.beginLayout();
+ QString str = textLayout.text();
+ while (true) {
+ QTextLine line = textLayout.createLine();
+ if (!line.isValid())
+ break;
+ if (line.textLength() == 0)
+ break;
+ line.setLineWidth(lineWidth);
+ line.setPosition(QPointF(0, height));
+ height += line.height();
+ lines.append(str.mid(line.textStart(), line.textLength()));
+ widthUsed = qMax(widthUsed, line.naturalTextWidth());
+ }
+ textLayout.endLayout();
+ return lines;
+}
diff --git a/meshmc/launcher/ui/widgets/Common.h b/meshmc/launcher/ui/widgets/Common.h
new file mode 100644
index 0000000000..d1f08ee7e7
--- /dev/null
+++ b/meshmc/launcher/ui/widgets/Common.h
@@ -0,0 +1,27 @@
+/* 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 <QStringList>
+#include <QTextLayout>
+
+QStringList viewItemTextLayout(QTextLayout& textLayout, int lineWidth,
+ qreal& height, qreal& widthUsed); \ No newline at end of file
diff --git a/meshmc/launcher/ui/widgets/CustomCommands.cpp b/meshmc/launcher/ui/widgets/CustomCommands.cpp
new file mode 100644
index 0000000000..6356f3804e
--- /dev/null
+++ b/meshmc/launcher/ui/widgets/CustomCommands.cpp
@@ -0,0 +1,69 @@
+/* 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 "CustomCommands.h"
+#include "ui_CustomCommands.h"
+
+CustomCommands::~CustomCommands()
+{
+ delete ui;
+}
+
+CustomCommands::CustomCommands(QWidget* parent)
+ : QWidget(parent), ui(new Ui::CustomCommands)
+{
+ ui->setupUi(this);
+}
+
+void CustomCommands::initialize(bool checkable, bool checked,
+ const QString& prelaunch,
+ const QString& wrapper, const QString& postexit)
+{
+ ui->customCommandsGroupBox->setCheckable(checkable);
+ if (checkable) {
+ ui->customCommandsGroupBox->setChecked(checked);
+ }
+ ui->preLaunchCmdTextBox->setText(prelaunch);
+ ui->wrapperCmdTextBox->setText(wrapper);
+ ui->postExitCmdTextBox->setText(postexit);
+}
+
+bool CustomCommands::checked() const
+{
+ if (!ui->customCommandsGroupBox->isCheckable())
+ return true;
+ return ui->customCommandsGroupBox->isChecked();
+}
+
+QString CustomCommands::prelaunchCommand() const
+{
+ return ui->preLaunchCmdTextBox->text();
+}
+
+QString CustomCommands::wrapperCommand() const
+{
+ return ui->wrapperCmdTextBox->text();
+}
+
+QString CustomCommands::postexitCommand() const
+{
+ return ui->postExitCmdTextBox->text();
+}
diff --git a/meshmc/launcher/ui/widgets/CustomCommands.h b/meshmc/launcher/ui/widgets/CustomCommands.h
new file mode 100644
index 0000000000..7da48221ca
--- /dev/null
+++ b/meshmc/launcher/ui/widgets/CustomCommands.h
@@ -0,0 +1,65 @@
+/* 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 <QWidget>
+
+namespace Ui
+{
+ class CustomCommands;
+}
+
+class CustomCommands : public QWidget
+{
+ Q_OBJECT
+
+ public:
+ explicit CustomCommands(QWidget* parent = 0);
+ virtual ~CustomCommands();
+ void initialize(bool checkable, bool checked, const QString& prelaunch,
+ const QString& wrapper, const QString& postexit);
+
+ bool checked() const;
+ QString prelaunchCommand() const;
+ QString wrapperCommand() const;
+ QString postexitCommand() const;
+
+ private:
+ Ui::CustomCommands* ui;
+};
diff --git a/meshmc/launcher/ui/widgets/CustomCommands.ui b/meshmc/launcher/ui/widgets/CustomCommands.ui
new file mode 100644
index 0000000000..a5d27faf83
--- /dev/null
+++ b/meshmc/launcher/ui/widgets/CustomCommands.ui
@@ -0,0 +1,107 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>CustomCommands</class>
+ <widget class="QWidget" name="CustomCommands">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>518</width>
+ <height>646</height>
+ </rect>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QGroupBox" name="customCommandsGroupBox">
+ <property name="enabled">
+ <bool>true</bool>
+ </property>
+ <property name="title">
+ <string>Cus&amp;tom Commands</string>
+ </property>
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ <property name="checked">
+ <bool>false</bool>
+ </property>
+ <layout class="QGridLayout" name="gridLayout_4">
+ <item row="2" column="0">
+ <widget class="QLabel" name="labelPostExitCmd">
+ <property name="text">
+ <string>Post-exit command:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QLineEdit" name="preLaunchCmdTextBox"/>
+ </item>
+ <item row="0" column="0">
+ <widget class="QLabel" name="labelPreLaunchCmd">
+ <property name="text">
+ <string>Pre-launch command:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="1">
+ <widget class="QLineEdit" name="postExitCmdTextBox"/>
+ </item>
+ <item row="1" column="0">
+ <widget class="QLabel" name="labelWrapperCmd">
+ <property name="text">
+ <string>Wrapper command:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QLineEdit" name="wrapperCmdTextBox"/>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="labelCustomCmdsDescription">
+ <property name="text">
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Pre-launch command runs before the instance launches and post-exit command runs after it exits.&lt;/p&gt;&lt;p&gt;Both will be run in MeshMC's working folder with extra environment variables:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;$INST_NAME - Name of the instance&lt;/li&gt;&lt;li&gt;$INST_ID - ID of the instance (its folder name)&lt;/li&gt;&lt;li&gt;$INST_DIR - absolute path of the instance&lt;/li&gt;&lt;li&gt;$INST_MC_DIR - absolute path of minecraft&lt;/li&gt;&lt;li&gt;$INST_JAVA - java binary used for launch&lt;/li&gt;&lt;li&gt;$INST_JAVA_ARGS - command-line parameters used for launch&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Wrapper command allows launching using an extra wrapper program (like 'optirun' on Linux)&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ <property name="textInteractionFlags">
+ <set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="verticalSpacer">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>40</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/meshmc/launcher/ui/widgets/DropLabel.cpp b/meshmc/launcher/ui/widgets/DropLabel.cpp
new file mode 100644
index 0000000000..abc7570168
--- /dev/null
+++ b/meshmc/launcher/ui/widgets/DropLabel.cpp
@@ -0,0 +1,61 @@
+/* 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 "DropLabel.h"
+
+#include <QMimeData>
+#include <QDropEvent>
+
+DropLabel::DropLabel(QWidget* parent) : QLabel(parent)
+{
+ setAcceptDrops(true);
+}
+
+void DropLabel::dragEnterEvent(QDragEnterEvent* event)
+{
+ event->acceptProposedAction();
+}
+
+void DropLabel::dragMoveEvent(QDragMoveEvent* event)
+{
+ event->acceptProposedAction();
+}
+
+void DropLabel::dragLeaveEvent(QDragLeaveEvent* event)
+{
+ event->accept();
+}
+
+void DropLabel::dropEvent(QDropEvent* event)
+{
+ const QMimeData* mimeData = event->mimeData();
+
+ if (!mimeData) {
+ return;
+ }
+
+ if (mimeData->hasUrls()) {
+ auto urls = mimeData->urls();
+ emit droppedURLs(urls);
+ }
+
+ event->acceptProposedAction();
+}
diff --git a/meshmc/launcher/ui/widgets/DropLabel.h b/meshmc/launcher/ui/widgets/DropLabel.h
new file mode 100644
index 0000000000..a3cef5af31
--- /dev/null
+++ b/meshmc/launcher/ui/widgets/DropLabel.h
@@ -0,0 +1,41 @@
+/* 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 <QLabel>
+
+class DropLabel : public QLabel
+{
+ Q_OBJECT
+
+ public:
+ explicit DropLabel(QWidget* parent = nullptr);
+
+ signals:
+ void droppedURLs(QList<QUrl> urls);
+
+ protected:
+ void dropEvent(QDropEvent* event) override;
+ void dragEnterEvent(QDragEnterEvent* event) override;
+ void dragMoveEvent(QDragMoveEvent* event) override;
+ void dragLeaveEvent(QDragLeaveEvent* event) override;
+};
diff --git a/meshmc/launcher/ui/widgets/ErrorFrame.cpp b/meshmc/launcher/ui/widgets/ErrorFrame.cpp
new file mode 100644
index 0000000000..eb786149bc
--- /dev/null
+++ b/meshmc/launcher/ui/widgets/ErrorFrame.cpp
@@ -0,0 +1,142 @@
+/* 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 <QMessageBox>
+#include <QtGui>
+
+#include "ErrorFrame.h"
+#include "ui_ErrorFrame.h"
+
+#include "ui/dialogs/CustomMessageBox.h"
+
+void ErrorFrame::clear()
+{
+ setTitle(QString());
+ setDescription(QString());
+}
+
+ErrorFrame::ErrorFrame(QWidget* parent) : QFrame(parent), ui(new Ui::ErrorFrame)
+{
+ ui->setupUi(this);
+ ui->label_Description->setHidden(true);
+ ui->label_Title->setHidden(true);
+ updateHiddenState();
+}
+
+ErrorFrame::~ErrorFrame()
+{
+ delete ui;
+}
+
+void ErrorFrame::updateHiddenState()
+{
+ if (ui->label_Description->isHidden() && ui->label_Title->isHidden()) {
+ setHidden(true);
+ } else {
+ setHidden(false);
+ }
+}
+
+void ErrorFrame::setTitle(QString text)
+{
+ if (text.isEmpty()) {
+ ui->label_Title->setHidden(true);
+ } else {
+ ui->label_Title->setText(text);
+ ui->label_Title->setHidden(false);
+ }
+ updateHiddenState();
+}
+
+void ErrorFrame::setDescription(QString text)
+{
+ if (text.isEmpty()) {
+ ui->label_Description->setHidden(true);
+ updateHiddenState();
+ return;
+ } else {
+ ui->label_Description->setHidden(false);
+ updateHiddenState();
+ }
+ ui->label_Description->setToolTip("");
+ QString intermediatetext = text.trimmed();
+ bool prev(false);
+ QChar rem('\n');
+ QString finaltext;
+ finaltext.reserve(intermediatetext.size());
+ foreach (const QChar& c, intermediatetext) {
+ if (c == rem && prev) {
+ continue;
+ }
+ prev = c == rem;
+ finaltext += c;
+ }
+ QString labeltext;
+ labeltext.reserve(300);
+ if (finaltext.length() > 290) {
+ ui->label_Description->setOpenExternalLinks(false);
+ ui->label_Description->setTextFormat(Qt::TextFormat::RichText);
+ desc = text;
+ // This allows injecting HTML here.
+ labeltext.append("<html><body>" + finaltext.left(287) +
+ "<a href=\"#mod_desc\">...</a></body></html>");
+ QObject::connect(ui->label_Description, &QLabel::linkActivated, this,
+ &ErrorFrame::ellipsisHandler);
+ } else {
+ ui->label_Description->setTextFormat(Qt::TextFormat::PlainText);
+ labeltext.append(finaltext);
+ }
+ ui->label_Description->setText(labeltext);
+}
+
+void ErrorFrame::ellipsisHandler(const QString& link)
+{
+ if (!currentBox) {
+ currentBox = CustomMessageBox::selectable(this, QString(), desc);
+ connect(currentBox, &QMessageBox::finished, this,
+ &ErrorFrame::boxClosed);
+ currentBox->show();
+ } else {
+ currentBox->setText(desc);
+ }
+}
+
+void ErrorFrame::boxClosed(int result)
+{
+ currentBox = nullptr;
+}
diff --git a/meshmc/launcher/ui/widgets/ErrorFrame.h b/meshmc/launcher/ui/widgets/ErrorFrame.h
new file mode 100644
index 0000000000..e1d94e2a89
--- /dev/null
+++ b/meshmc/launcher/ui/widgets/ErrorFrame.h
@@ -0,0 +1,72 @@
+/* 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 <QFrame>
+
+namespace Ui
+{
+ class ErrorFrame;
+}
+
+class ErrorFrame : public QFrame
+{
+ Q_OBJECT
+
+ public:
+ explicit ErrorFrame(QWidget* parent = 0);
+ ~ErrorFrame();
+
+ void setTitle(QString text);
+ void setDescription(QString text);
+
+ void clear();
+
+ public slots:
+ void ellipsisHandler(const QString& link);
+ void boxClosed(int result);
+
+ private:
+ void updateHiddenState();
+
+ private:
+ Ui::ErrorFrame* ui;
+ QString desc;
+ class QMessageBox* currentBox = nullptr;
+};
diff --git a/meshmc/launcher/ui/widgets/ErrorFrame.ui b/meshmc/launcher/ui/widgets/ErrorFrame.ui
new file mode 100644
index 0000000000..0bb5674395
--- /dev/null
+++ b/meshmc/launcher/ui/widgets/ErrorFrame.ui
@@ -0,0 +1,92 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>ErrorFrame</class>
+ <widget class="QFrame" name="ErrorFrame">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>527</width>
+ <height>113</height>
+ </rect>
+ </property>
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>16777215</width>
+ <height>120</height>
+ </size>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_2">
+ <property name="spacing">
+ <number>6</number>
+ </property>
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QLabel" name="label_Title">
+ <property name="text">
+ <string notr="true"/>
+ </property>
+ <property name="textFormat">
+ <enum>Qt::RichText</enum>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ <property name="openExternalLinks">
+ <bool>true</bool>
+ </property>
+ <property name="textInteractionFlags">
+ <set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="label_Description">
+ <property name="toolTip">
+ <string notr="true"/>
+ </property>
+ <property name="text">
+ <string notr="true"/>
+ </property>
+ <property name="textFormat">
+ <enum>Qt::RichText</enum>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ <property name="openExternalLinks">
+ <bool>true</bool>
+ </property>
+ <property name="textInteractionFlags">
+ <set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/meshmc/launcher/ui/widgets/FocusLineEdit.cpp b/meshmc/launcher/ui/widgets/FocusLineEdit.cpp
new file mode 100644
index 0000000000..18db9ba3e1
--- /dev/null
+++ b/meshmc/launcher/ui/widgets/FocusLineEdit.cpp
@@ -0,0 +1,45 @@
+/* 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 "FocusLineEdit.h"
+#include <QDebug>
+
+FocusLineEdit::FocusLineEdit(QWidget* parent) : QLineEdit(parent)
+{
+ _selectOnMousePress = false;
+}
+
+void FocusLineEdit::focusInEvent(QFocusEvent* e)
+{
+ QLineEdit::focusInEvent(e);
+ selectAll();
+ _selectOnMousePress = true;
+}
+
+void FocusLineEdit::mousePressEvent(QMouseEvent* me)
+{
+ QLineEdit::mousePressEvent(me);
+ if (_selectOnMousePress) {
+ selectAll();
+ _selectOnMousePress = false;
+ }
+ qDebug() << selectedText();
+}
diff --git a/meshmc/launcher/ui/widgets/FocusLineEdit.h b/meshmc/launcher/ui/widgets/FocusLineEdit.h
new file mode 100644
index 0000000000..024811b6d5
--- /dev/null
+++ b/meshmc/launcher/ui/widgets/FocusLineEdit.h
@@ -0,0 +1,36 @@
+/* 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 <QLineEdit>
+
+class FocusLineEdit : public QLineEdit
+{
+ Q_OBJECT
+ public:
+ FocusLineEdit(QWidget* parent);
+ virtual ~FocusLineEdit() {}
+
+ protected:
+ void focusInEvent(QFocusEvent* e);
+ void mousePressEvent(QMouseEvent* me);
+
+ bool _selectOnMousePress;
+};
diff --git a/meshmc/launcher/ui/widgets/IconLabel.cpp b/meshmc/launcher/ui/widgets/IconLabel.cpp
new file mode 100644
index 0000000000..11de955257
--- /dev/null
+++ b/meshmc/launcher/ui/widgets/IconLabel.cpp
@@ -0,0 +1,61 @@
+/* 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 "IconLabel.h"
+
+#include <QStyle>
+#include <QStyleOption>
+#include <QLayout>
+#include <QPainter>
+#include <QRect>
+
+IconLabel::IconLabel(QWidget* parent, QIcon icon, QSize size)
+ : QWidget(parent), m_size(size), m_icon(icon)
+{
+ setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
+}
+
+QSize IconLabel::sizeHint() const
+{
+ return m_size;
+}
+
+void IconLabel::setIcon(QIcon icon)
+{
+ m_icon = icon;
+ update();
+}
+
+void IconLabel::paintEvent(QPaintEvent*)
+{
+ QPainter p(this);
+ QRect rect = contentsRect();
+ int width = rect.width();
+ int height = rect.height();
+ if (width < height) {
+ rect.setHeight(width);
+ rect.translate(0, (height - width) / 2);
+ } else if (width > height) {
+ rect.setWidth(height);
+ rect.translate((width - height) / 2, 0);
+ }
+ m_icon.paint(&p, rect);
+}
diff --git a/meshmc/launcher/ui/widgets/IconLabel.h b/meshmc/launcher/ui/widgets/IconLabel.h
new file mode 100644
index 0000000000..e7792d3da9
--- /dev/null
+++ b/meshmc/launcher/ui/widgets/IconLabel.h
@@ -0,0 +1,47 @@
+/* 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 <QIcon>
+
+class QStyleOption;
+
+/**
+ * This is a trivial widget that paints a QIcon of the specified size.
+ */
+class IconLabel : public QWidget
+{
+ Q_OBJECT
+
+ public:
+ /// Create a line separator. orientation is the orientation of the line.
+ explicit IconLabel(QWidget* parent, QIcon icon, QSize size);
+
+ virtual QSize sizeHint() const;
+ virtual void paintEvent(QPaintEvent*);
+
+ void setIcon(QIcon icon);
+
+ private:
+ QSize m_size;
+ QIcon m_icon;
+};
diff --git a/meshmc/launcher/ui/widgets/InstanceCardWidget.ui b/meshmc/launcher/ui/widgets/InstanceCardWidget.ui
new file mode 100644
index 0000000000..6eeeb07692
--- /dev/null
+++ b/meshmc/launcher/ui/widgets/InstanceCardWidget.ui
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>InstanceCardWidget</class>
+ <widget class="QWidget" name="InstanceCardWidget">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>473</width>
+ <height>118</height>
+ </rect>
+ </property>
+ <layout class="QGridLayout" name="gridLayout">
+ <item row="0" column="0" rowspan="2">
+ <widget class="QToolButton" name="iconButton">
+ <property name="iconSize">
+ <size>
+ <width>80</width>
+ <height>80</height>
+ </size>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QLabel" name="nameLabel">
+ <property name="text">
+ <string>&amp;Name:</string>
+ </property>
+ <property name="buddy">
+ <cstring>instNameTextBox</cstring>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="2">
+ <widget class="QLineEdit" name="instNameTextBox"/>
+ </item>
+ <item row="1" column="1">
+ <widget class="QLabel" name="groupLabel">
+ <property name="text">
+ <string>&amp;Group:</string>
+ </property>
+ <property name="buddy">
+ <cstring>groupBox</cstring>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="2">
+ <widget class="QComboBox" name="groupBox">
+ <property name="editable">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/meshmc/launcher/ui/widgets/JavaSettingsWidget.cpp b/meshmc/launcher/ui/widgets/JavaSettingsWidget.cpp
new file mode 100644
index 0000000000..731f0166d7
--- /dev/null
+++ b/meshmc/launcher/ui/widgets/JavaSettingsWidget.cpp
@@ -0,0 +1,438 @@
+/* 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 "JavaSettingsWidget.h"
+
+#include <QVBoxLayout>
+#include <QGroupBox>
+#include <QSpinBox>
+#include <QLabel>
+#include <QLineEdit>
+#include <QPushButton>
+#include <QToolButton>
+#include <QFileDialog>
+
+#include <sys.h>
+
+#include "java/JavaInstall.h"
+#include "java/JavaUtils.h"
+#include "FileSystem.h"
+
+#include "ui/dialogs/CustomMessageBox.h"
+#include "ui/widgets/VersionSelectWidget.h"
+
+#include "Application.h"
+#include "BuildConfig.h"
+
+JavaSettingsWidget::JavaSettingsWidget(QWidget* parent) : QWidget(parent)
+{
+ m_availableMemory = Sys::getSystemRam() / Sys::mebibyte;
+
+ goodIcon = APPLICATION->getThemedIcon("status-good");
+ yellowIcon = APPLICATION->getThemedIcon("status-yellow");
+ badIcon = APPLICATION->getThemedIcon("status-bad");
+ setupUi();
+
+ connect(m_minMemSpinBox, SIGNAL(valueChanged(int)), this,
+ SLOT(memoryValueChanged(int)));
+ connect(m_maxMemSpinBox, SIGNAL(valueChanged(int)), this,
+ SLOT(memoryValueChanged(int)));
+ connect(m_permGenSpinBox, SIGNAL(valueChanged(int)), this,
+ SLOT(memoryValueChanged(int)));
+ connect(m_versionWidget, &VersionSelectWidget::selectedVersionChanged, this,
+ &JavaSettingsWidget::javaVersionSelected);
+ connect(m_javaBrowseBtn, &QPushButton::clicked, this,
+ &JavaSettingsWidget::on_javaBrowseBtn_clicked);
+ connect(m_javaPathTextBox, &QLineEdit::textEdited, this,
+ &JavaSettingsWidget::javaPathEdited);
+ connect(m_javaStatusBtn, &QToolButton::clicked, this,
+ &JavaSettingsWidget::on_javaStatusBtn_clicked);
+}
+
+void JavaSettingsWidget::setupUi()
+{
+ setObjectName(QStringLiteral("javaSettingsWidget"));
+ m_verticalLayout = new QVBoxLayout(this);
+ m_verticalLayout->setObjectName(QStringLiteral("verticalLayout"));
+
+ m_versionWidget = new VersionSelectWidget(this);
+ m_verticalLayout->addWidget(m_versionWidget);
+
+ m_horizontalLayout = new QHBoxLayout();
+ m_horizontalLayout->setObjectName(QStringLiteral("horizontalLayout"));
+ m_javaPathTextBox = new QLineEdit(this);
+ m_javaPathTextBox->setObjectName(QStringLiteral("javaPathTextBox"));
+
+ m_horizontalLayout->addWidget(m_javaPathTextBox);
+
+ m_javaBrowseBtn = new QPushButton(this);
+ m_javaBrowseBtn->setObjectName(QStringLiteral("javaBrowseBtn"));
+
+ m_horizontalLayout->addWidget(m_javaBrowseBtn);
+
+ m_javaStatusBtn = new QToolButton(this);
+ m_javaStatusBtn->setIcon(yellowIcon);
+ m_horizontalLayout->addWidget(m_javaStatusBtn);
+
+ m_verticalLayout->addLayout(m_horizontalLayout);
+
+ m_memoryGroupBox = new QGroupBox(this);
+ m_memoryGroupBox->setObjectName(QStringLiteral("memoryGroupBox"));
+ m_gridLayout_2 = new QGridLayout(m_memoryGroupBox);
+ m_gridLayout_2->setObjectName(QStringLiteral("gridLayout_2"));
+
+ m_labelMinMem = new QLabel(m_memoryGroupBox);
+ m_labelMinMem->setObjectName(QStringLiteral("labelMinMem"));
+ m_gridLayout_2->addWidget(m_labelMinMem, 0, 0, 1, 1);
+
+ m_minMemSpinBox = new QSpinBox(m_memoryGroupBox);
+ m_minMemSpinBox->setObjectName(QStringLiteral("minMemSpinBox"));
+ m_minMemSpinBox->setSuffix(QStringLiteral(" MiB"));
+ m_minMemSpinBox->setMinimum(128);
+ m_minMemSpinBox->setMaximum(m_availableMemory);
+ m_minMemSpinBox->setSingleStep(128);
+ m_labelMinMem->setBuddy(m_minMemSpinBox);
+ m_gridLayout_2->addWidget(m_minMemSpinBox, 0, 1, 1, 1);
+
+ m_labelMaxMem = new QLabel(m_memoryGroupBox);
+ m_labelMaxMem->setObjectName(QStringLiteral("labelMaxMem"));
+ m_gridLayout_2->addWidget(m_labelMaxMem, 1, 0, 1, 1);
+
+ m_maxMemSpinBox = new QSpinBox(m_memoryGroupBox);
+ m_maxMemSpinBox->setObjectName(QStringLiteral("maxMemSpinBox"));
+ m_maxMemSpinBox->setSuffix(QStringLiteral(" MiB"));
+ m_maxMemSpinBox->setMinimum(128);
+ m_maxMemSpinBox->setMaximum(m_availableMemory);
+ m_maxMemSpinBox->setSingleStep(128);
+ m_labelMaxMem->setBuddy(m_maxMemSpinBox);
+ m_gridLayout_2->addWidget(m_maxMemSpinBox, 1, 1, 1, 1);
+
+ m_labelPermGen = new QLabel(m_memoryGroupBox);
+ m_labelPermGen->setObjectName(QStringLiteral("labelPermGen"));
+ m_labelPermGen->setText(QStringLiteral("PermGen:"));
+ m_gridLayout_2->addWidget(m_labelPermGen, 2, 0, 1, 1);
+ m_labelPermGen->setVisible(false);
+
+ m_permGenSpinBox = new QSpinBox(m_memoryGroupBox);
+ m_permGenSpinBox->setObjectName(QStringLiteral("permGenSpinBox"));
+ m_permGenSpinBox->setSuffix(QStringLiteral(" MiB"));
+ m_permGenSpinBox->setMinimum(64);
+ m_permGenSpinBox->setMaximum(m_availableMemory);
+ m_permGenSpinBox->setSingleStep(8);
+ m_gridLayout_2->addWidget(m_permGenSpinBox, 2, 1, 1, 1);
+ m_permGenSpinBox->setVisible(false);
+
+ m_verticalLayout->addWidget(m_memoryGroupBox);
+
+ retranslate();
+}
+
+void JavaSettingsWidget::initialize()
+{
+ m_versionWidget->initialize(APPLICATION->javalist().get());
+ m_versionWidget->setResizeOn(2);
+ auto s = APPLICATION->settings();
+ // Memory
+ observedMinMemory = s->get("MinMemAlloc").toInt();
+ observedMaxMemory = s->get("MaxMemAlloc").toInt();
+ observedPermGenMemory = s->get("PermGen").toInt();
+ m_minMemSpinBox->setValue(observedMinMemory);
+ m_maxMemSpinBox->setValue(observedMaxMemory);
+ m_permGenSpinBox->setValue(observedPermGenMemory);
+}
+
+void JavaSettingsWidget::refresh()
+{
+ m_versionWidget->loadList();
+}
+
+JavaSettingsWidget::ValidationStatus JavaSettingsWidget::validate()
+{
+ switch (javaStatus) {
+ default:
+ case JavaStatus::NotSet:
+ case JavaStatus::DoesNotExist:
+ case JavaStatus::DoesNotStart:
+ case JavaStatus::ReturnedInvalidData: {
+ int button =
+ CustomMessageBox::selectable(
+ this, tr("No Java version selected"),
+ tr("You didn't select a Java version or selected something "
+ "that doesn't work.\n"
+ "%1 will not be able to start Minecraft.\n"
+ "Do you wish to proceed without any Java?"
+ "\n\n"
+ "You can change the Java version in the settings "
+ "later.\n")
+ .arg(BuildConfig.MESHMC_NAME),
+ QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No,
+ QMessageBox::NoButton)
+ ->exec();
+ if (button == QMessageBox::No) {
+ return ValidationStatus::Bad;
+ }
+ return ValidationStatus::JavaBad;
+ } break;
+ case JavaStatus::Pending: {
+ return ValidationStatus::Bad;
+ }
+ case JavaStatus::Good: {
+ return ValidationStatus::AllOK;
+ }
+ }
+}
+
+QString JavaSettingsWidget::javaPath() const
+{
+ return m_javaPathTextBox->text();
+}
+
+int JavaSettingsWidget::maxHeapSize() const
+{
+ return m_maxMemSpinBox->value();
+}
+
+int JavaSettingsWidget::minHeapSize() const
+{
+ return m_minMemSpinBox->value();
+}
+
+bool JavaSettingsWidget::permGenEnabled() const
+{
+ return m_permGenSpinBox->isVisible();
+}
+
+int JavaSettingsWidget::permGenSize() const
+{
+ return m_permGenSpinBox->value();
+}
+
+void JavaSettingsWidget::memoryValueChanged(int)
+{
+ bool actuallyChanged = false;
+ int min = m_minMemSpinBox->value();
+ int max = m_maxMemSpinBox->value();
+ int permgen = m_permGenSpinBox->value();
+ QObject* obj = sender();
+ if (obj == m_minMemSpinBox && min != observedMinMemory) {
+ observedMinMemory = min;
+ actuallyChanged = true;
+ if (min > max) {
+ observedMaxMemory = min;
+ m_maxMemSpinBox->setValue(min);
+ }
+ } else if (obj == m_maxMemSpinBox && max != observedMaxMemory) {
+ observedMaxMemory = max;
+ actuallyChanged = true;
+ if (min > max) {
+ observedMinMemory = max;
+ m_minMemSpinBox->setValue(max);
+ }
+ } else if (obj == m_permGenSpinBox && permgen != observedPermGenMemory) {
+ observedPermGenMemory = permgen;
+ actuallyChanged = true;
+ }
+ if (actuallyChanged) {
+ checkJavaPathOnEdit(m_javaPathTextBox->text());
+ }
+}
+
+void JavaSettingsWidget::javaVersionSelected(BaseVersionPtr version)
+{
+ auto java = std::dynamic_pointer_cast<JavaInstall>(version);
+ if (!java) {
+ return;
+ }
+ auto visible = java->id.requiresPermGen();
+ m_labelPermGen->setVisible(visible);
+ m_permGenSpinBox->setVisible(visible);
+ m_javaPathTextBox->setText(java->path);
+ checkJavaPath(java->path);
+}
+
+void JavaSettingsWidget::on_javaBrowseBtn_clicked()
+{
+ QString filter;
+#if defined Q_OS_WIN32
+ filter = "Java (javaw.exe)";
+#else
+ filter = "Java (java)";
+#endif
+ QString raw_path = QFileDialog::getOpenFileName(
+ this, tr("Find Java executable"), QString(), filter);
+ if (raw_path.isEmpty()) {
+ return;
+ }
+ QString cooked_path = FS::NormalizePath(raw_path);
+ m_javaPathTextBox->setText(cooked_path);
+ checkJavaPath(cooked_path);
+}
+
+void JavaSettingsWidget::on_javaStatusBtn_clicked()
+{
+ QString text;
+ bool failed = false;
+ switch (javaStatus) {
+ case JavaStatus::NotSet:
+ checkJavaPath(m_javaPathTextBox->text());
+ return;
+ case JavaStatus::DoesNotExist:
+ text += QObject::tr("The specified file either doesn't exist or is "
+ "not a proper executable.");
+ failed = true;
+ break;
+ case JavaStatus::DoesNotStart: {
+ text += QObject::tr(
+ "The specified java binary didn't start properly.<br />");
+ auto htmlError = m_result.errorLog;
+ if (!htmlError.isEmpty()) {
+ htmlError.replace('\n', "<br />");
+ text += QString("<font color=\"red\">%1</font>").arg(htmlError);
+ }
+ failed = true;
+ break;
+ }
+ case JavaStatus::ReturnedInvalidData: {
+ text += QObject::tr(
+ "The specified java binary returned unexpected results:<br />");
+ auto htmlOut = m_result.outLog;
+ if (!htmlOut.isEmpty()) {
+ htmlOut.replace('\n', "<br />");
+ text += QString("<font color=\"red\">%1</font>").arg(htmlOut);
+ }
+ failed = true;
+ break;
+ }
+ case JavaStatus::Good:
+ text += QObject::tr("Java test succeeded!<br />Platform reported: "
+ "%1<br />Java version "
+ "reported: %2<br />")
+ .arg(m_result.realPlatform,
+ m_result.javaVersion.toString());
+ break;
+ case JavaStatus::Pending:
+ // TODO: abort here?
+ return;
+ }
+ CustomMessageBox::selectable(
+ this,
+ failed ? QObject::tr("Java test failure")
+ : QObject::tr("Java test success"),
+ text, failed ? QMessageBox::Critical : QMessageBox::Information)
+ ->show();
+}
+
+void JavaSettingsWidget::setJavaStatus(JavaSettingsWidget::JavaStatus status)
+{
+ javaStatus = status;
+ switch (javaStatus) {
+ case JavaStatus::Good:
+ m_javaStatusBtn->setIcon(goodIcon);
+ break;
+ case JavaStatus::NotSet:
+ case JavaStatus::Pending:
+ m_javaStatusBtn->setIcon(yellowIcon);
+ break;
+ default:
+ m_javaStatusBtn->setIcon(badIcon);
+ break;
+ }
+}
+
+void JavaSettingsWidget::javaPathEdited(const QString& path)
+{
+ checkJavaPathOnEdit(path);
+}
+
+void JavaSettingsWidget::checkJavaPathOnEdit(const QString& path)
+{
+ auto realPath = FS::ResolveExecutable(path);
+ QFileInfo pathInfo(realPath);
+ if (pathInfo.baseName().toLower().contains("java")) {
+ checkJavaPath(path);
+ } else {
+ if (!m_checker) {
+ setJavaStatus(JavaStatus::NotSet);
+ }
+ }
+}
+
+void JavaSettingsWidget::checkJavaPath(const QString& path)
+{
+ if (m_checker) {
+ queuedCheck = path;
+ return;
+ }
+ auto realPath = FS::ResolveExecutable(path);
+ if (realPath.isNull()) {
+ setJavaStatus(JavaStatus::DoesNotExist);
+ return;
+ }
+ setJavaStatus(JavaStatus::Pending);
+ m_checker.reset(new JavaChecker());
+ m_checker->m_path = path;
+ m_checker->m_minMem = m_minMemSpinBox->value();
+ m_checker->m_maxMem = m_maxMemSpinBox->value();
+ if (m_permGenSpinBox->isVisible()) {
+ m_checker->m_permGen = m_permGenSpinBox->value();
+ }
+ connect(m_checker.get(), &JavaChecker::checkFinished, this,
+ &JavaSettingsWidget::checkFinished);
+ m_checker->performCheck();
+}
+
+void JavaSettingsWidget::checkFinished(JavaCheckResult result)
+{
+ m_result = result;
+ switch (result.validity) {
+ case JavaCheckResult::Validity::Valid: {
+ setJavaStatus(JavaStatus::Good);
+ break;
+ }
+ case JavaCheckResult::Validity::ReturnedInvalidData: {
+ setJavaStatus(JavaStatus::ReturnedInvalidData);
+ break;
+ }
+ case JavaCheckResult::Validity::Errored: {
+ setJavaStatus(JavaStatus::DoesNotStart);
+ break;
+ }
+ }
+ m_checker.reset();
+ if (!queuedCheck.isNull()) {
+ checkJavaPath(queuedCheck);
+ queuedCheck.clear();
+ }
+}
+
+void JavaSettingsWidget::retranslate()
+{
+ m_memoryGroupBox->setTitle(tr("Memory"));
+ m_maxMemSpinBox->setToolTip(
+ tr("The maximum amount of memory Minecraft is allowed to use."));
+ m_labelMinMem->setText(tr("Minimum memory allocation:"));
+ m_labelMaxMem->setText(tr("Maximum memory allocation:"));
+ m_minMemSpinBox->setToolTip(
+ tr("The amount of memory Minecraft is started with."));
+ m_permGenSpinBox->setToolTip(
+ tr("The amount of memory available to store loaded Java classes."));
+ m_javaBrowseBtn->setText(tr("Browse"));
+}
diff --git a/meshmc/launcher/ui/widgets/JavaSettingsWidget.h b/meshmc/launcher/ui/widgets/JavaSettingsWidget.h
new file mode 100644
index 0000000000..aa4b88f698
--- /dev/null
+++ b/meshmc/launcher/ui/widgets/JavaSettingsWidget.h
@@ -0,0 +1,116 @@
+/* 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 <java/JavaChecker.h>
+#include <BaseVersion.h>
+#include <QObjectPtr.h>
+#include <QIcon>
+
+class QLineEdit;
+class VersionSelectWidget;
+class QSpinBox;
+class QPushButton;
+class QVBoxLayout;
+class QHBoxLayout;
+class QGroupBox;
+class QGridLayout;
+class QLabel;
+class QToolButton;
+
+/**
+ * This is a widget for all the Java settings dialogs and pages.
+ */
+class JavaSettingsWidget : public QWidget
+{
+ Q_OBJECT
+
+ public:
+ explicit JavaSettingsWidget(QWidget* parent);
+ virtual ~JavaSettingsWidget() {};
+
+ enum class JavaStatus {
+ NotSet,
+ Pending,
+ Good,
+ DoesNotExist,
+ DoesNotStart,
+ ReturnedInvalidData
+ } javaStatus = JavaStatus::NotSet;
+
+ enum class ValidationStatus { Bad, JavaBad, AllOK };
+
+ void refresh();
+ void initialize();
+ ValidationStatus validate();
+ void retranslate();
+
+ bool permGenEnabled() const;
+ int permGenSize() const;
+ int minHeapSize() const;
+ int maxHeapSize() const;
+ QString javaPath() const;
+
+ protected slots:
+ void memoryValueChanged(int);
+ void javaPathEdited(const QString& path);
+ void javaVersionSelected(BaseVersionPtr version);
+ void on_javaBrowseBtn_clicked();
+ void on_javaStatusBtn_clicked();
+ void checkFinished(JavaCheckResult result);
+
+ protected: /* methods */
+ void checkJavaPathOnEdit(const QString& path);
+ void checkJavaPath(const QString& path);
+ void setJavaStatus(JavaStatus status);
+ void setupUi();
+
+ private: /* data */
+ VersionSelectWidget* m_versionWidget = nullptr;
+ QVBoxLayout* m_verticalLayout = nullptr;
+
+ QLineEdit* m_javaPathTextBox = nullptr;
+ QPushButton* m_javaBrowseBtn = nullptr;
+ QToolButton* m_javaStatusBtn = nullptr;
+ QHBoxLayout* m_horizontalLayout = nullptr;
+
+ QGroupBox* m_memoryGroupBox = nullptr;
+ QGridLayout* m_gridLayout_2 = nullptr;
+ QSpinBox* m_maxMemSpinBox = nullptr;
+ QLabel* m_labelMinMem = nullptr;
+ QLabel* m_labelMaxMem = nullptr;
+ QSpinBox* m_minMemSpinBox = nullptr;
+ QLabel* m_labelPermGen = nullptr;
+ QSpinBox* m_permGenSpinBox = nullptr;
+ QIcon goodIcon;
+ QIcon yellowIcon;
+ QIcon badIcon;
+
+ int observedMinMemory = 0;
+ int observedMaxMemory = 0;
+ int observedPermGenMemory = 0;
+ QString queuedCheck;
+ uint64_t m_availableMemory = 0ull;
+ shared_qobject_ptr<JavaChecker> m_checker;
+ JavaCheckResult m_result;
+};
diff --git a/meshmc/launcher/ui/widgets/LabeledToolButton.cpp b/meshmc/launcher/ui/widgets/LabeledToolButton.cpp
new file mode 100644
index 0000000000..7829f3de46
--- /dev/null
+++ b/meshmc/launcher/ui/widgets/LabeledToolButton.cpp
@@ -0,0 +1,136 @@
+/* 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 <QLabel>
+#include <QVBoxLayout>
+#include <QResizeEvent>
+#include <QStyleOption>
+#include "LabeledToolButton.h"
+#include <QApplication>
+#include <QDebug>
+
+/*
+ *
+ * Tool Button with a label on it, instead of the normal text rendering
+ *
+ */
+
+LabeledToolButton::LabeledToolButton(QWidget* parent)
+ : QToolButton(parent), m_label(new QLabel(this))
+{
+ // QToolButton::setText(" ");
+ m_label->setWordWrap(true);
+ m_label->setMouseTracking(false);
+ m_label->setAlignment(Qt::AlignCenter);
+ m_label->setTextInteractionFlags(Qt::NoTextInteraction);
+ // somehow, this makes word wrap work in the QLabel. yay.
+ // m_label->setMinimumWidth(100);
+}
+
+QString LabeledToolButton::text() const
+{
+ return m_label->text();
+}
+
+void LabeledToolButton::setText(const QString& text)
+{
+ m_label->setText(text);
+}
+
+void LabeledToolButton::setIcon(QIcon icon)
+{
+ m_icon = icon;
+ resetIcon();
+}
+
+/*!
+ \reimp
+*/
+QSize LabeledToolButton::sizeHint() const
+{
+ /*
+ Q_D(const QToolButton);
+ if (d->sizeHint.isValid())
+ return d->sizeHint;
+ */
+ ensurePolished();
+
+ int w = 0, h = 0;
+ QStyleOptionToolButton opt;
+ initStyleOption(&opt);
+ QSize sz = m_label->sizeHint();
+ w = sz.width();
+ h = sz.height();
+
+ opt.rect.setSize(
+ QSize(w, h)); // PM_MenuButtonIndicator depends on the height
+ if (popupMode() == MenuButtonPopup)
+ w += style()->pixelMetric(QStyle::PM_MenuButtonIndicator, &opt, this);
+
+ QSize rawSize = style()->sizeFromContents(QStyle::CT_ToolButton, &opt,
+ QSize(w, h), this);
+ QSize sizeHint = rawSize;
+ return sizeHint;
+}
+
+void LabeledToolButton::resizeEvent(QResizeEvent* event)
+{
+ m_label->setGeometry(QRect(4, 4, width() - 8, height() - 8));
+ if (!m_icon.isNull()) {
+ resetIcon();
+ }
+ QWidget::resizeEvent(event);
+}
+
+void LabeledToolButton::resetIcon()
+{
+ auto iconSz = m_icon.actualSize(QSize(160, 80));
+ float w = iconSz.width();
+ float h = iconSz.height();
+ float ar = w / h;
+ // FIXME: hardcoded max size of 160x80
+ int newW = 80 * ar;
+ if (newW > 160)
+ newW = 160;
+ QSize newSz(newW, 80);
+ auto pixmap = m_icon.pixmap(newSz);
+ m_label->setPixmap(pixmap);
+ m_label->setMinimumHeight(80);
+ m_label->setSizePolicy(QSizePolicy::MinimumExpanding,
+ QSizePolicy::Preferred);
+}
diff --git a/meshmc/launcher/ui/widgets/LabeledToolButton.h b/meshmc/launcher/ui/widgets/LabeledToolButton.h
new file mode 100644
index 0000000000..3cfe2fa13e
--- /dev/null
+++ b/meshmc/launcher/ui/widgets/LabeledToolButton.h
@@ -0,0 +1,64 @@
+/* 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 <QPushButton>
+#include <QToolButton>
+
+class QLabel;
+
+class LabeledToolButton : public QToolButton
+{
+ Q_OBJECT
+
+ QLabel* m_label;
+ QIcon m_icon;
+
+ public:
+ LabeledToolButton(QWidget* parent = 0);
+
+ QString text() const;
+ void setText(const QString& text);
+ void setIcon(QIcon icon);
+ virtual QSize sizeHint() const;
+
+ protected:
+ void resizeEvent(QResizeEvent* event);
+ void resetIcon();
+};
diff --git a/meshmc/launcher/ui/widgets/LanguageSelectionWidget.cpp b/meshmc/launcher/ui/widgets/LanguageSelectionWidget.cpp
new file mode 100644
index 0000000000..658ce638e5
--- /dev/null
+++ b/meshmc/launcher/ui/widgets/LanguageSelectionWidget.cpp
@@ -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/>.
+ */
+
+#include "LanguageSelectionWidget.h"
+
+#include <QVBoxLayout>
+#include <QTreeView>
+#include <QHeaderView>
+#include <QLabel>
+#include "Application.h"
+#include "translations/TranslationsModel.h"
+
+LanguageSelectionWidget::LanguageSelectionWidget(QWidget* parent)
+ : QWidget(parent)
+{
+ verticalLayout = new QVBoxLayout(this);
+ verticalLayout->setObjectName(QStringLiteral("verticalLayout"));
+ languageView = new QTreeView(this);
+ languageView->setObjectName(QStringLiteral("languageView"));
+ languageView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+ languageView->setAlternatingRowColors(true);
+ languageView->setRootIsDecorated(false);
+ languageView->setItemsExpandable(false);
+ languageView->setWordWrap(true);
+ languageView->header()->setCascadingSectionResizes(true);
+ languageView->header()->setStretchLastSection(false);
+ verticalLayout->addWidget(languageView);
+ helpUsLabel = new QLabel(this);
+ helpUsLabel->setObjectName(QStringLiteral("helpUsLabel"));
+ helpUsLabel->setTextInteractionFlags(Qt::LinksAccessibleByMouse);
+ helpUsLabel->setOpenExternalLinks(true);
+ helpUsLabel->setWordWrap(true);
+ verticalLayout->addWidget(helpUsLabel);
+
+ auto translations = APPLICATION->translations();
+ auto index = translations->selectedIndex();
+ languageView->setModel(translations.get());
+ languageView->setCurrentIndex(index);
+ languageView->header()->setSectionResizeMode(QHeaderView::ResizeToContents);
+ languageView->header()->setSectionResizeMode(0, QHeaderView::Stretch);
+ connect(languageView->selectionModel(),
+ &QItemSelectionModel::currentRowChanged, this,
+ &LanguageSelectionWidget::languageRowChanged);
+ verticalLayout->setContentsMargins(0, 0, 0, 0);
+}
+
+QString LanguageSelectionWidget::getSelectedLanguageKey() const
+{
+ auto translations = APPLICATION->translations();
+ return translations->data(languageView->currentIndex(), Qt::UserRole)
+ .toString();
+}
+
+void LanguageSelectionWidget::retranslate()
+{
+ QString text = tr("Don't see your language or the quality is poor?<br/><a "
+ "href=\"%1\">Help us with translations!</a>")
+ .arg("https://github.com/Project-Tick/MeshMC/wiki/"
+ "Translating-MeshMC");
+ helpUsLabel->setText(text);
+}
+
+void LanguageSelectionWidget::languageRowChanged(const QModelIndex& current,
+ const QModelIndex& previous)
+{
+ if (current == previous) {
+ return;
+ }
+ auto translations = APPLICATION->translations();
+ QString key = translations->data(current, Qt::UserRole).toString();
+ translations->selectLanguage(key);
+ translations->updateLanguage(key);
+}
diff --git a/meshmc/launcher/ui/widgets/LanguageSelectionWidget.h b/meshmc/launcher/ui/widgets/LanguageSelectionWidget.h
new file mode 100644
index 0000000000..f279b49cbe
--- /dev/null
+++ b/meshmc/launcher/ui/widgets/LanguageSelectionWidget.h
@@ -0,0 +1,65 @@
+/* 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>
+
+class QVBoxLayout;
+class QTreeView;
+class QLabel;
+
+class LanguageSelectionWidget : public QWidget
+{
+ Q_OBJECT
+ public:
+ explicit LanguageSelectionWidget(QWidget* parent = 0);
+ virtual ~LanguageSelectionWidget() {};
+
+ QString getSelectedLanguageKey() const;
+ void retranslate();
+
+ protected slots:
+ void languageRowChanged(const QModelIndex& current,
+ const QModelIndex& previous);
+
+ private:
+ QVBoxLayout* verticalLayout = nullptr;
+ QTreeView* languageView = nullptr;
+ QLabel* helpUsLabel = nullptr;
+};
diff --git a/meshmc/launcher/ui/widgets/LineSeparator.cpp b/meshmc/launcher/ui/widgets/LineSeparator.cpp
new file mode 100644
index 0000000000..15a7f89a1a
--- /dev/null
+++ b/meshmc/launcher/ui/widgets/LineSeparator.cpp
@@ -0,0 +1,59 @@
+/* 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 "LineSeparator.h"
+
+#include <QStyle>
+#include <QStyleOption>
+#include <QLayout>
+#include <QPainter>
+
+void LineSeparator::initStyleOption(QStyleOption* option) const
+{
+ option->initFrom(this);
+ // in a horizontal layout, the line is vertical (and vice versa)
+ if (m_orientation == Qt::Vertical)
+ option->state |= QStyle::State_Horizontal;
+}
+
+LineSeparator::LineSeparator(QWidget* parent, Qt::Orientation orientation)
+ : QWidget(parent), m_orientation(orientation)
+{
+ setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
+}
+
+QSize LineSeparator::sizeHint() const
+{
+ QStyleOption opt;
+ initStyleOption(&opt);
+ const int extent = style()->pixelMetric(QStyle::PM_ToolBarSeparatorExtent,
+ &opt, parentWidget());
+ return QSize(extent, extent);
+}
+
+void LineSeparator::paintEvent(QPaintEvent*)
+{
+ QPainter p(this);
+ QStyleOption opt;
+ initStyleOption(&opt);
+ style()->drawPrimitive(QStyle::PE_IndicatorToolBarSeparator, &opt, &p,
+ parentWidget());
+}
diff --git a/meshmc/launcher/ui/widgets/LineSeparator.h b/meshmc/launcher/ui/widgets/LineSeparator.h
new file mode 100644
index 0000000000..af70c8fffc
--- /dev/null
+++ b/meshmc/launcher/ui/widgets/LineSeparator.h
@@ -0,0 +1,41 @@
+/* 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>
+
+class QStyleOption;
+
+class LineSeparator : public QWidget
+{
+ Q_OBJECT
+
+ public:
+ /// Create a line separator. orientation is the orientation of the line.
+ explicit LineSeparator(QWidget* parent,
+ Qt::Orientation orientation = Qt::Horizontal);
+ QSize sizeHint() const;
+ void paintEvent(QPaintEvent*);
+ void initStyleOption(QStyleOption* option) const;
+
+ private:
+ Qt::Orientation m_orientation = Qt::Horizontal;
+};
diff --git a/meshmc/launcher/ui/widgets/LogView.cpp b/meshmc/launcher/ui/widgets/LogView.cpp
new file mode 100644
index 0000000000..954b48a93e
--- /dev/null
+++ b/meshmc/launcher/ui/widgets/LogView.cpp
@@ -0,0 +1,161 @@
+/* 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 "LogView.h"
+#include <QTextBlock>
+#include <QScrollBar>
+
+LogView::LogView(QWidget* parent) : QPlainTextEdit(parent)
+{
+ setWordWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere);
+ m_defaultFormat = new QTextCharFormat(currentCharFormat());
+}
+
+LogView::~LogView()
+{
+ delete m_defaultFormat;
+}
+
+void LogView::setWordWrap(bool wrapping)
+{
+ if (wrapping) {
+ setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+ setLineWrapMode(QPlainTextEdit::WidgetWidth);
+ } else {
+ setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded);
+ setLineWrapMode(QPlainTextEdit::NoWrap);
+ }
+}
+
+void LogView::setModel(QAbstractItemModel* model)
+{
+ if (m_model) {
+ disconnect(m_model, &QAbstractItemModel::modelReset, this,
+ &LogView::repopulate);
+ disconnect(m_model, &QAbstractItemModel::rowsInserted, this,
+ &LogView::rowsInserted);
+ disconnect(m_model, &QAbstractItemModel::rowsAboutToBeInserted, this,
+ &LogView::rowsAboutToBeInserted);
+ disconnect(m_model, &QAbstractItemModel::rowsRemoved, this,
+ &LogView::rowsRemoved);
+ }
+ m_model = model;
+ if (m_model) {
+ connect(m_model, &QAbstractItemModel::modelReset, this,
+ &LogView::repopulate);
+ connect(m_model, &QAbstractItemModel::rowsInserted, this,
+ &LogView::rowsInserted);
+ connect(m_model, &QAbstractItemModel::rowsAboutToBeInserted, this,
+ &LogView::rowsAboutToBeInserted);
+ connect(m_model, &QAbstractItemModel::rowsRemoved, this,
+ &LogView::rowsRemoved);
+ connect(m_model, &QAbstractItemModel::destroyed, this,
+ &LogView::modelDestroyed);
+ }
+ repopulate();
+}
+
+QAbstractItemModel* LogView::model() const
+{
+ return m_model;
+}
+
+void LogView::modelDestroyed(QObject* model)
+{
+ if (m_model == model) {
+ setModel(nullptr);
+ }
+}
+
+void LogView::repopulate()
+{
+ auto doc = document();
+ doc->clear();
+ if (!m_model) {
+ return;
+ }
+ rowsInserted(QModelIndex(), 0, m_model->rowCount() - 1);
+}
+
+void LogView::rowsAboutToBeInserted(const QModelIndex& parent, int first,
+ int last)
+{
+ Q_UNUSED(parent)
+ Q_UNUSED(first)
+ Q_UNUSED(last)
+ QScrollBar* bar = verticalScrollBar();
+ int max_bar = bar->maximum();
+ int val_bar = bar->value();
+ if (m_scroll) {
+ m_scroll = (max_bar - val_bar) <= 1;
+ } else {
+ m_scroll = val_bar == max_bar;
+ }
+}
+
+void LogView::rowsInserted(const QModelIndex& parent, int first, int last)
+{
+ for (int i = first; i <= last; i++) {
+ auto idx = m_model->index(i, 0, parent);
+ auto text = m_model->data(idx, Qt::DisplayRole).toString();
+ QTextCharFormat format(*m_defaultFormat);
+ auto font = m_model->data(idx, Qt::FontRole);
+ if (font.isValid()) {
+ format.setFont(font.value<QFont>());
+ }
+ auto fg = m_model->data(idx, Qt::ForegroundRole);
+ if (fg.isValid()) {
+ format.setForeground(fg.value<QColor>());
+ }
+ auto bg = m_model->data(idx, Qt::BackgroundRole);
+ if (bg.isValid()) {
+ format.setBackground(bg.value<QColor>());
+ }
+ auto workCursor = textCursor();
+ workCursor.movePosition(QTextCursor::End);
+ workCursor.insertText(text, format);
+ workCursor.insertBlock();
+ }
+ if (m_scroll && !m_scrolling) {
+ m_scrolling = true;
+ QMetaObject::invokeMethod(this, "scrollToBottom", Qt::QueuedConnection);
+ }
+}
+
+void LogView::rowsRemoved(const QModelIndex& parent, int first, int last)
+{
+ // TODO: some day... maybe
+ Q_UNUSED(parent)
+ Q_UNUSED(first)
+ Q_UNUSED(last)
+}
+
+void LogView::scrollToBottom()
+{
+ m_scrolling = false;
+ verticalScrollBar()->setSliderPosition(verticalScrollBar()->maximum());
+}
+
+void LogView::findNext(const QString& what, bool reverse)
+{
+ find(what, reverse ? QTextDocument::FindFlag::FindBackward
+ : QTextDocument::FindFlag(0));
+}
diff --git a/meshmc/launcher/ui/widgets/LogView.h b/meshmc/launcher/ui/widgets/LogView.h
new file mode 100644
index 0000000000..9762992f5c
--- /dev/null
+++ b/meshmc/launcher/ui/widgets/LogView.h
@@ -0,0 +1,57 @@
+/* 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 <QPlainTextEdit>
+#include <QAbstractItemView>
+
+class QAbstractItemModel;
+
+class LogView : public QPlainTextEdit
+{
+ Q_OBJECT
+ public:
+ explicit LogView(QWidget* parent = nullptr);
+ virtual ~LogView();
+
+ virtual void setModel(QAbstractItemModel* model);
+ QAbstractItemModel* model() const;
+
+ public slots:
+ void setWordWrap(bool wrapping);
+ void findNext(const QString& what, bool reverse);
+ void scrollToBottom();
+
+ protected slots:
+ void repopulate();
+ // note: this supports only appending
+ void rowsInserted(const QModelIndex& parent, int first, int last);
+ void rowsAboutToBeInserted(const QModelIndex& parent, int first, int last);
+ // note: this supports only removing from front
+ void rowsRemoved(const QModelIndex& parent, int first, int last);
+ void modelDestroyed(QObject* model);
+
+ protected:
+ QAbstractItemModel* m_model = nullptr;
+ QTextCharFormat* m_defaultFormat = nullptr;
+ bool m_scroll = false;
+ bool m_scrolling = false;
+};
diff --git a/meshmc/launcher/ui/widgets/MCModInfoFrame.cpp b/meshmc/launcher/ui/widgets/MCModInfoFrame.cpp
new file mode 100644
index 0000000000..f900f848f4
--- /dev/null
+++ b/meshmc/launcher/ui/widgets/MCModInfoFrame.cpp
@@ -0,0 +1,173 @@
+/* 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 <QMessageBox>
+#include <QtGui>
+
+#include "MCModInfoFrame.h"
+#include "ui_MCModInfoFrame.h"
+
+#include "ui/dialogs/CustomMessageBox.h"
+
+void MCModInfoFrame::updateWithMod(Mod& m)
+{
+ if (m.type() == m.MOD_FOLDER) {
+ clear();
+ return;
+ }
+
+ QString text = "";
+ QString name = "";
+ if (m.name().isEmpty())
+ name = m.mmc_id();
+ else
+ name = m.name();
+
+ if (m.homeurl().isEmpty())
+ text = name;
+ else
+ text = "<a href=\"" + m.homeurl() + "\">" + name + "</a>";
+ if (!m.authors().isEmpty())
+ text += " by " + m.authors().join(", ");
+
+ setModText(text);
+
+ if (m.description().isEmpty()) {
+ setModDescription(QString());
+ } else {
+ setModDescription(m.description());
+ }
+}
+
+void MCModInfoFrame::clear()
+{
+ setModText(QString());
+ setModDescription(QString());
+}
+
+MCModInfoFrame::MCModInfoFrame(QWidget* parent)
+ : QFrame(parent), ui(new Ui::MCModInfoFrame)
+{
+ ui->setupUi(this);
+ ui->label_ModDescription->setHidden(true);
+ ui->label_ModText->setHidden(true);
+ updateHiddenState();
+}
+
+MCModInfoFrame::~MCModInfoFrame()
+{
+ delete ui;
+}
+
+void MCModInfoFrame::updateHiddenState()
+{
+ if (ui->label_ModDescription->isHidden() && ui->label_ModText->isHidden()) {
+ setHidden(true);
+ } else {
+ setHidden(false);
+ }
+}
+
+void MCModInfoFrame::setModText(QString text)
+{
+ if (text.isEmpty()) {
+ ui->label_ModText->setHidden(true);
+ } else {
+ ui->label_ModText->setText(text);
+ ui->label_ModText->setHidden(false);
+ }
+ updateHiddenState();
+}
+
+void MCModInfoFrame::setModDescription(QString text)
+{
+ if (text.isEmpty()) {
+ ui->label_ModDescription->setHidden(true);
+ updateHiddenState();
+ return;
+ } else {
+ ui->label_ModDescription->setHidden(false);
+ updateHiddenState();
+ }
+ ui->label_ModDescription->setToolTip("");
+ QString intermediatetext = text.trimmed();
+ bool prev(false);
+ QChar rem('\n');
+ QString finaltext;
+ finaltext.reserve(intermediatetext.size());
+ foreach (const QChar& c, intermediatetext) {
+ if (c == rem && prev) {
+ continue;
+ }
+ prev = c == rem;
+ finaltext += c;
+ }
+ QString labeltext;
+ labeltext.reserve(300);
+ if (finaltext.length() > 290) {
+ ui->label_ModDescription->setOpenExternalLinks(false);
+ ui->label_ModDescription->setTextFormat(Qt::TextFormat::RichText);
+ desc = text;
+ // This allows injecting HTML here.
+ labeltext.append("<html><body>" + finaltext.left(287) +
+ "<a href=\"#mod_desc\">...</a></body></html>");
+ QObject::connect(ui->label_ModDescription, &QLabel::linkActivated, this,
+ &MCModInfoFrame::modDescEllipsisHandler);
+ } else {
+ ui->label_ModDescription->setTextFormat(Qt::TextFormat::PlainText);
+ labeltext.append(finaltext);
+ }
+ ui->label_ModDescription->setText(labeltext);
+}
+
+void MCModInfoFrame::modDescEllipsisHandler(const QString& link)
+{
+ if (!currentBox) {
+ currentBox = CustomMessageBox::selectable(this, QString(), desc);
+ connect(currentBox, &QMessageBox::finished, this,
+ &MCModInfoFrame::boxClosed);
+ currentBox->show();
+ } else {
+ currentBox->setText(desc);
+ }
+}
+
+void MCModInfoFrame::boxClosed(int result)
+{
+ currentBox = nullptr;
+}
diff --git a/meshmc/launcher/ui/widgets/MCModInfoFrame.h b/meshmc/launcher/ui/widgets/MCModInfoFrame.h
new file mode 100644
index 0000000000..baaab6efa5
--- /dev/null
+++ b/meshmc/launcher/ui/widgets/MCModInfoFrame.h
@@ -0,0 +1,74 @@
+/* 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 <QFrame>
+#include "minecraft/mod/Mod.h"
+
+namespace Ui
+{
+ class MCModInfoFrame;
+}
+
+class MCModInfoFrame : public QFrame
+{
+ Q_OBJECT
+
+ public:
+ explicit MCModInfoFrame(QWidget* parent = 0);
+ ~MCModInfoFrame();
+
+ void setModText(QString text);
+ void setModDescription(QString text);
+
+ void updateWithMod(Mod& m);
+ void clear();
+
+ public slots:
+ void modDescEllipsisHandler(const QString& link);
+ void boxClosed(int result);
+
+ private:
+ void updateHiddenState();
+
+ private:
+ Ui::MCModInfoFrame* ui;
+ QString desc;
+ class QMessageBox* currentBox = nullptr;
+};
diff --git a/meshmc/launcher/ui/widgets/MCModInfoFrame.ui b/meshmc/launcher/ui/widgets/MCModInfoFrame.ui
new file mode 100644
index 0000000000..5ef33379da
--- /dev/null
+++ b/meshmc/launcher/ui/widgets/MCModInfoFrame.ui
@@ -0,0 +1,92 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>MCModInfoFrame</class>
+ <widget class="QFrame" name="MCModInfoFrame">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>527</width>
+ <height>113</height>
+ </rect>
+ </property>
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>16777215</width>
+ <height>120</height>
+ </size>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_2">
+ <property name="spacing">
+ <number>6</number>
+ </property>
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QLabel" name="label_ModText">
+ <property name="text">
+ <string notr="true"/>
+ </property>
+ <property name="textFormat">
+ <enum>Qt::RichText</enum>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ <property name="openExternalLinks">
+ <bool>true</bool>
+ </property>
+ <property name="textInteractionFlags">
+ <set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="label_ModDescription">
+ <property name="toolTip">
+ <string notr="true"/>
+ </property>
+ <property name="text">
+ <string notr="true"/>
+ </property>
+ <property name="textFormat">
+ <enum>Qt::RichText</enum>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ <property name="openExternalLinks">
+ <bool>true</bool>
+ </property>
+ <property name="textInteractionFlags">
+ <set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/meshmc/launcher/ui/widgets/ModListView.cpp b/meshmc/launcher/ui/widgets/ModListView.cpp
new file mode 100644
index 0000000000..8da7896bd0
--- /dev/null
+++ b/meshmc/launcher/ui/widgets/ModListView.cpp
@@ -0,0 +1,84 @@
+/* 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 "ModListView.h"
+#include <QHeaderView>
+#include <QMouseEvent>
+#include <QPainter>
+#include <QDrag>
+#include <QRect>
+
+ModListView::ModListView(QWidget* parent) : QTreeView(parent)
+{
+ setAllColumnsShowFocus(true);
+ setExpandsOnDoubleClick(false);
+ setRootIsDecorated(false);
+ setSortingEnabled(true);
+ setAlternatingRowColors(true);
+ setSelectionMode(QAbstractItemView::ExtendedSelection);
+ setHeaderHidden(false);
+ setSelectionBehavior(QAbstractItemView::SelectRows);
+ setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
+ setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded);
+ setDropIndicatorShown(true);
+ setDragEnabled(true);
+ setDragDropMode(QAbstractItemView::DropOnly);
+ viewport()->setAcceptDrops(true);
+}
+
+void ModListView::setModel(QAbstractItemModel* model)
+{
+ QTreeView::setModel(model);
+ auto head = header();
+ head->setStretchLastSection(false);
+ // HACK: this is true for the checkbox column of mod lists
+ auto string = model->headerData(0, head->orientation()).toString();
+ if (head->count() < 1) {
+ return;
+ }
+ if (!string.size()) {
+ head->setSectionResizeMode(0, QHeaderView::ResizeToContents);
+ head->setSectionResizeMode(1, QHeaderView::Stretch);
+ for (int i = 2; i < head->count(); i++)
+ head->setSectionResizeMode(i, QHeaderView::ResizeToContents);
+ } else {
+ head->setSectionResizeMode(0, QHeaderView::Stretch);
+ for (int i = 1; i < head->count(); i++)
+ head->setSectionResizeMode(i, QHeaderView::ResizeToContents);
+ }
+}
diff --git a/meshmc/launcher/ui/widgets/ModListView.h b/meshmc/launcher/ui/widgets/ModListView.h
new file mode 100644
index 0000000000..e8e7c95c6c
--- /dev/null
+++ b/meshmc/launcher/ui/widgets/ModListView.h
@@ -0,0 +1,48 @@
+/* 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 <QTreeView>
+
+class ModListView : public QTreeView
+{
+ Q_OBJECT
+ public:
+ explicit ModListView(QWidget* parent = 0);
+ virtual void setModel(QAbstractItemModel* model);
+};
diff --git a/meshmc/launcher/ui/widgets/PageContainer.cpp b/meshmc/launcher/ui/widgets/PageContainer.cpp
new file mode 100644
index 0000000000..5f50c7d419
--- /dev/null
+++ b/meshmc/launcher/ui/widgets/PageContainer.cpp
@@ -0,0 +1,253 @@
+/* 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 "PageContainer.h"
+#include "PageContainer_p.h"
+
+#include <QStackedLayout>
+#include <QPushButton>
+#include <QSortFilterProxyModel>
+#include <QUrl>
+#include <QStyledItemDelegate>
+#include <QListView>
+#include <QLineEdit>
+#include <QLabel>
+#include <QDialogButtonBox>
+#include <QGridLayout>
+
+#include "settings/SettingsObject.h"
+
+#include "ui/widgets/IconLabel.h"
+
+#include "DesktopServices.h"
+#include "Application.h"
+
+class PageEntryFilterModel : public QSortFilterProxyModel
+{
+ public:
+ explicit PageEntryFilterModel(QObject* parent = 0)
+ : QSortFilterProxyModel(parent)
+ {
+ }
+
+ protected:
+ bool filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const
+ {
+ const QString pattern = filterRegularExpression().pattern();
+ const auto model = static_cast<PageModel*>(sourceModel());
+ const auto page = model->pages().at(sourceRow);
+ if (!page->shouldDisplay())
+ return false;
+ // Regular contents check, then check page-filter.
+ return QSortFilterProxyModel::filterAcceptsRow(sourceRow, sourceParent);
+ }
+};
+
+PageContainer::PageContainer(BasePageProvider* pageProvider, QString defaultId,
+ QWidget* parent)
+ : QWidget(parent)
+{
+ createUI();
+ m_model = new PageModel(this);
+ m_proxyModel = new PageEntryFilterModel(this);
+ int counter = 0;
+ auto pages = pageProvider->getPages();
+ for (auto page : pages) {
+ page->stackIndex = m_pageStack->addWidget(dynamic_cast<QWidget*>(page));
+ page->listIndex = counter;
+ page->setParentContainer(this);
+ counter++;
+ }
+ m_model->setPages(pages);
+
+ m_proxyModel->setSourceModel(m_model);
+ m_proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
+
+ m_pageList->setIconSize(QSize(pageIconSize, pageIconSize));
+ m_pageList->setSelectionMode(QAbstractItemView::SingleSelection);
+ m_pageList->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
+ m_pageList->setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContents);
+ m_pageList->setModel(m_proxyModel);
+ connect(m_pageList->selectionModel(),
+ SIGNAL(currentRowChanged(QModelIndex, QModelIndex)), this,
+ SLOT(currentChanged(QModelIndex)));
+ m_pageStack->setStackingMode(QStackedLayout::StackOne);
+ m_pageList->setFocus();
+ selectPage(defaultId);
+}
+
+bool PageContainer::selectPage(QString pageId)
+{
+ // now find what we want to have selected...
+ auto page = m_model->findPageEntryById(pageId);
+ QModelIndex index;
+ if (page) {
+ index = m_proxyModel->mapFromSource(m_model->index(page->listIndex));
+ }
+ if (!index.isValid()) {
+ index = m_proxyModel->index(0, 0);
+ }
+ if (index.isValid()) {
+ m_pageList->setCurrentIndex(index);
+ return true;
+ }
+ return false;
+}
+
+void PageContainer::refreshContainer()
+{
+ m_proxyModel->invalidate();
+ if (!m_currentPage->shouldDisplay()) {
+ auto index = m_proxyModel->index(0, 0);
+ if (index.isValid()) {
+ m_pageList->setCurrentIndex(index);
+ } else {
+ // FIXME: unhandled corner case: what to do when there's no page to
+ // select?
+ }
+ }
+}
+
+void PageContainer::createUI()
+{
+ m_pageStack = new QStackedLayout;
+ m_pageList = new PageView;
+ m_header = new QLabel();
+ m_iconHeader = new IconLabel(this, QIcon(), QSize(24, 24));
+
+ QFont headerLabelFont = m_header->font();
+ headerLabelFont.setBold(true);
+ const int pointSize = headerLabelFont.pointSize();
+ if (pointSize > 0)
+ headerLabelFont.setPointSize(pointSize + 2);
+ m_header->setFont(headerLabelFont);
+
+ QHBoxLayout* headerHLayout = new QHBoxLayout;
+ const int leftMargin =
+ APPLICATION->style()->pixelMetric(QStyle::PM_LayoutLeftMargin);
+ headerHLayout->addSpacerItem(new QSpacerItem(
+ leftMargin, 0, QSizePolicy::Fixed, QSizePolicy::Ignored));
+ headerHLayout->addWidget(m_header);
+ headerHLayout->addSpacerItem(
+ new QSpacerItem(0, 0, QSizePolicy::Expanding, QSizePolicy::Ignored));
+ headerHLayout->addWidget(m_iconHeader);
+ const int rightMargin =
+ APPLICATION->style()->pixelMetric(QStyle::PM_LayoutRightMargin);
+ headerHLayout->addSpacerItem(new QSpacerItem(
+ rightMargin, 0, QSizePolicy::Fixed, QSizePolicy::Ignored));
+ headerHLayout->setContentsMargins(0, 6, 0, 0);
+
+ m_pageStack->setContentsMargins(0, 0, 0, 0);
+ m_pageStack->addWidget(new QWidget(this));
+
+ m_layout = new QGridLayout;
+ m_layout->addLayout(headerHLayout, 0, 1, 1, 1);
+ m_layout->addWidget(m_pageList, 0, 0, 2, 1);
+ m_layout->addLayout(m_pageStack, 1, 1, 1, 1);
+ m_layout->setColumnStretch(1, 4);
+ m_layout->setContentsMargins(0, 0, 0, 6);
+ setLayout(m_layout);
+}
+
+void PageContainer::addButtons(QWidget* buttons)
+{
+ m_layout->addWidget(buttons, 2, 0, 1, 2);
+}
+
+void PageContainer::addButtons(QLayout* buttons)
+{
+ m_layout->addLayout(buttons, 2, 0, 1, 2);
+}
+
+void PageContainer::showPage(int row)
+{
+ if (m_currentPage) {
+ m_currentPage->closed();
+ }
+ if (row != -1) {
+ m_currentPage = m_model->pages().at(row);
+ } else {
+ m_currentPage = nullptr;
+ }
+ if (m_currentPage) {
+ m_pageStack->setCurrentIndex(m_currentPage->stackIndex);
+ m_header->setText(m_currentPage->displayName());
+ m_iconHeader->setIcon(m_currentPage->icon());
+ m_currentPage->opened();
+ } else {
+ m_pageStack->setCurrentIndex(0);
+ m_header->setText(QString());
+ m_iconHeader->setIcon(APPLICATION->getThemedIcon("bug"));
+ }
+}
+
+void PageContainer::help()
+{
+ if (m_currentPage) {
+ QString pageId = m_currentPage->helpPage();
+ if (pageId.isEmpty())
+ return;
+ DesktopServices::openUrl(
+ QUrl("https://github.com/Project-Tick/MeshMC/wiki/" + pageId));
+ }
+}
+
+void PageContainer::currentChanged(const QModelIndex& current)
+{
+ showPage(current.isValid() ? m_proxyModel->mapToSource(current).row() : -1);
+}
+
+bool PageContainer::prepareToClose()
+{
+ if (!saveAll()) {
+ return false;
+ }
+ if (m_currentPage) {
+ m_currentPage->closed();
+ }
+ return true;
+}
+
+bool PageContainer::saveAll()
+{
+ for (auto page : m_model->pages()) {
+ if (!page->apply())
+ return false;
+ }
+ return true;
+}
diff --git a/meshmc/launcher/ui/widgets/PageContainer.h b/meshmc/launcher/ui/widgets/PageContainer.h
new file mode 100644
index 0000000000..53eaf4f563
--- /dev/null
+++ b/meshmc/launcher/ui/widgets/PageContainer.h
@@ -0,0 +1,112 @@
+/* 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 <QModelIndex>
+
+#include "ui/pages/BasePageProvider.h"
+#include "ui/pages/BasePageContainer.h"
+
+class QLayout;
+class IconLabel;
+class QSortFilterProxyModel;
+class PageModel;
+class QLabel;
+class QListView;
+class QLineEdit;
+class QStackedLayout;
+class QGridLayout;
+
+class PageContainer : public QWidget, public BasePageContainer
+{
+ Q_OBJECT
+ public:
+ explicit PageContainer(BasePageProvider* pageProvider,
+ QString defaultId = QString(), QWidget* parent = 0);
+ virtual ~PageContainer() {}
+
+ void addButtons(QWidget* buttons);
+ void addButtons(QLayout* buttons);
+ /*
+ * Save any unsaved state and prepare to be closed.
+ * @return true if everything can be saved, false if there is something that
+ * requires attention
+ */
+ bool prepareToClose();
+ bool saveAll();
+
+ /* request close - used by individual pages */
+ bool requestClose() override
+ {
+ if (m_container) {
+ return m_container->requestClose();
+ }
+ return false;
+ }
+
+ virtual bool selectPage(QString pageId) override;
+
+ void refreshContainer() override;
+ virtual void setParentContainer(BasePageContainer* container)
+ {
+ m_container = container;
+ };
+
+ private:
+ void createUI();
+
+ public slots:
+ void help();
+
+ private slots:
+ void currentChanged(const QModelIndex& current);
+ void showPage(int row);
+
+ private:
+ BasePageContainer* m_container = nullptr;
+ BasePage* m_currentPage = 0;
+ QSortFilterProxyModel* m_proxyModel;
+ PageModel* m_model;
+ QStackedLayout* m_pageStack;
+ QListView* m_pageList;
+ QLabel* m_header;
+ IconLabel* m_iconHeader;
+ QGridLayout* m_layout;
+};
diff --git a/meshmc/launcher/ui/widgets/PageContainer_p.h b/meshmc/launcher/ui/widgets/PageContainer_p.h
new file mode 100644
index 0000000000..0d041ec57b
--- /dev/null
+++ b/meshmc/launcher/ui/widgets/PageContainer_p.h
@@ -0,0 +1,143 @@
+/* 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 <QListView>
+#include <QStyledItemDelegate>
+#include <QEvent>
+#include <QScrollBar>
+
+class BasePage;
+const int pageIconSize = 24;
+
+class PageViewDelegate : public QStyledItemDelegate
+{
+ public:
+ PageViewDelegate(QObject* parent) : QStyledItemDelegate(parent) {}
+ QSize sizeHint(const QStyleOptionViewItem& option,
+ const QModelIndex& index) const
+ {
+ QSize size = QStyledItemDelegate::sizeHint(option, index);
+ size.setHeight(qMax(size.height(), 32));
+ return size;
+ }
+};
+
+class PageModel : public QAbstractListModel
+{
+ public:
+ PageModel(QObject* parent = 0) : QAbstractListModel(parent)
+ {
+ QPixmap empty(pageIconSize, pageIconSize);
+ empty.fill(Qt::transparent);
+ m_emptyIcon = QIcon(empty);
+ }
+ virtual ~PageModel() {}
+
+ int rowCount(const QModelIndex& parent = QModelIndex()) const
+ {
+ return parent.isValid() ? 0 : m_pages.size();
+ }
+ QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const
+ {
+ switch (role) {
+ case Qt::DisplayRole:
+ return m_pages.at(index.row())->displayName();
+ case Qt::DecorationRole: {
+ QIcon icon = m_pages.at(index.row())->icon();
+ if (icon.isNull())
+ icon = m_emptyIcon;
+ // HACK: fixes icon stretching on windows. TODO: report Qt bug
+ // for this
+ return QIcon(icon.pixmap(QSize(48, 48)));
+ }
+ }
+ return QVariant();
+ }
+
+ void setPages(const QList<BasePage*>& pages)
+ {
+ beginResetModel();
+ m_pages = pages;
+ endResetModel();
+ }
+ const QList<BasePage*>& pages() const
+ {
+ return m_pages;
+ }
+
+ BasePage* findPageEntryById(QString id)
+ {
+ for (auto page : m_pages) {
+ if (page->id() == id)
+ return page;
+ }
+ return nullptr;
+ }
+
+ QList<BasePage*> m_pages;
+ QIcon m_emptyIcon;
+};
+
+class PageView : public QListView
+{
+ public:
+ PageView(QWidget* parent = 0) : QListView(parent)
+ {
+ setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Expanding);
+ setItemDelegate(new PageViewDelegate(this));
+ setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+ }
+
+ virtual QSize sizeHint() const
+ {
+ int width = sizeHintForColumn(0) + frameWidth() * 2 + 5;
+ if (verticalScrollBar()->isVisible())
+ width += verticalScrollBar()->width();
+ return QSize(width, 100);
+ }
+
+ virtual bool eventFilter(QObject* obj, QEvent* event)
+ {
+ if (obj == verticalScrollBar() &&
+ (event->type() == QEvent::Show || event->type() == QEvent::Hide))
+ updateGeometry();
+ return QListView::eventFilter(obj, event);
+ }
+};
diff --git a/meshmc/launcher/ui/widgets/ProgressWidget.cpp b/meshmc/launcher/ui/widgets/ProgressWidget.cpp
new file mode 100644
index 0000000000..dedf6a005f
--- /dev/null
+++ b/meshmc/launcher/ui/widgets/ProgressWidget.cpp
@@ -0,0 +1,95 @@
+/* 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:
+ *
+ */// Licensed under the Apache-2.0 license. See README.md for details.
+
+#include "ProgressWidget.h"
+#include <QProgressBar>
+#include <QLabel>
+#include <QVBoxLayout>
+#include <QEventLoop>
+
+#include "tasks/Task.h"
+
+ProgressWidget::ProgressWidget(QWidget* parent) : QWidget(parent)
+{
+ m_label = new QLabel(this);
+ m_label->setWordWrap(true);
+ m_bar = new QProgressBar(this);
+ m_bar->setMinimum(0);
+ m_bar->setMaximum(100);
+ QVBoxLayout* layout = new QVBoxLayout(this);
+ layout->addWidget(m_label);
+ layout->addWidget(m_bar);
+ layout->addStretch();
+ setLayout(layout);
+}
+
+void ProgressWidget::start(std::shared_ptr<Task> task)
+{
+ if (m_task) {
+ disconnect(m_task.get(), 0, this, 0);
+ }
+ m_task = task;
+ connect(m_task.get(), &Task::finished, this,
+ &ProgressWidget::handleTaskFinish);
+ connect(m_task.get(), &Task::status, this,
+ &ProgressWidget::handleTaskStatus);
+ connect(m_task.get(), &Task::progress, this,
+ &ProgressWidget::handleTaskProgress);
+ connect(m_task.get(), &Task::destroyed, this,
+ &ProgressWidget::taskDestroyed);
+ if (!m_task->isRunning()) {
+ QMetaObject::invokeMethod(m_task.get(), "start", Qt::QueuedConnection);
+ }
+}
+bool ProgressWidget::exec(std::shared_ptr<Task> task)
+{
+ QEventLoop loop;
+ connect(task.get(), &Task::finished, &loop, &QEventLoop::quit);
+ start(task);
+ if (task->isRunning()) {
+ loop.exec();
+ }
+ return task->wasSuccessful();
+}
+
+void ProgressWidget::handleTaskFinish()
+{
+ if (!m_task->wasSuccessful()) {
+ m_label->setText(m_task->failReason());
+ }
+}
+void ProgressWidget::handleTaskStatus(const QString& status)
+{
+ m_label->setText(status);
+}
+void ProgressWidget::handleTaskProgress(qint64 current, qint64 total)
+{
+ m_bar->setMaximum(total);
+ m_bar->setValue(current);
+}
+void ProgressWidget::taskDestroyed()
+{
+ m_task = nullptr;
+}
diff --git a/meshmc/launcher/ui/widgets/ProgressWidget.h b/meshmc/launcher/ui/widgets/ProgressWidget.h
new file mode 100644
index 0000000000..8726c00488
--- /dev/null
+++ b/meshmc/launcher/ui/widgets/ProgressWidget.h
@@ -0,0 +1,55 @@
+/* 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:
+ *
+ */// Licensed under the Apache-2.0 license. See README.md for details.
+
+#pragma once
+
+#include <QWidget>
+#include <memory>
+
+class Task;
+class QProgressBar;
+class QLabel;
+
+class ProgressWidget : public QWidget
+{
+ Q_OBJECT
+ public:
+ explicit ProgressWidget(QWidget* parent = nullptr);
+
+ public slots:
+ void start(std::shared_ptr<Task> task);
+ bool exec(std::shared_ptr<Task> task);
+
+ private slots:
+ void handleTaskFinish();
+ void handleTaskStatus(const QString& status);
+ void handleTaskProgress(qint64 current, qint64 total);
+ void taskDestroyed();
+
+ private:
+ QLabel* m_label;
+ QProgressBar* m_bar;
+ std::shared_ptr<Task> m_task;
+};
diff --git a/meshmc/launcher/ui/widgets/VersionListView.cpp b/meshmc/launcher/ui/widgets/VersionListView.cpp
new file mode 100644
index 0000000000..74ac800108
--- /dev/null
+++ b/meshmc/launcher/ui/widgets/VersionListView.cpp
@@ -0,0 +1,179 @@
+/* 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 <QHeaderView>
+#include <QApplication>
+#include <QMouseEvent>
+#include <QDrag>
+#include <QPainter>
+#include "VersionListView.h"
+
+VersionListView::VersionListView(QWidget* parent) : QTreeView(parent)
+{
+ m_emptyString = tr("No versions are currently available.");
+}
+
+void VersionListView::rowsInserted(const QModelIndex& parent, int start,
+ int end)
+{
+ m_itemCount += end - start + 1;
+ updateEmptyViewPort();
+ QTreeView::rowsInserted(parent, start, end);
+}
+
+void VersionListView::rowsAboutToBeRemoved(const QModelIndex& parent, int start,
+ int end)
+{
+ m_itemCount -= end - start + 1;
+ updateEmptyViewPort();
+ QTreeView::rowsInserted(parent, start, end);
+}
+
+void VersionListView::setModel(QAbstractItemModel* model)
+{
+ m_itemCount = model->rowCount();
+ updateEmptyViewPort();
+ QTreeView::setModel(model);
+}
+
+void VersionListView::reset()
+{
+ if (model()) {
+ m_itemCount = model()->rowCount();
+ } else {
+ m_itemCount = 0;
+ }
+ updateEmptyViewPort();
+ QTreeView::reset();
+}
+
+void VersionListView::setEmptyString(QString emptyString)
+{
+ m_emptyString = emptyString;
+ updateEmptyViewPort();
+}
+
+void VersionListView::setEmptyErrorString(QString emptyErrorString)
+{
+ m_emptyErrorString = emptyErrorString;
+ updateEmptyViewPort();
+}
+
+void VersionListView::setEmptyMode(VersionListView::EmptyMode mode)
+{
+ m_emptyMode = mode;
+ updateEmptyViewPort();
+}
+
+void VersionListView::updateEmptyViewPort()
+{
+#ifndef QT_NO_ACCESSIBILITY
+ setAccessibleDescription(currentEmptyString());
+#endif /* !QT_NO_ACCESSIBILITY */
+
+ if (!m_itemCount) {
+ viewport()->update();
+ }
+}
+
+void VersionListView::paintEvent(QPaintEvent* event)
+{
+ if (m_itemCount) {
+ QTreeView::paintEvent(event);
+ } else {
+ paintInfoLabel(event);
+ }
+}
+
+QString VersionListView::currentEmptyString() const
+{
+ if (m_itemCount) {
+ return QString();
+ }
+ switch (m_emptyMode) {
+ default:
+ case VersionListView::Empty:
+ return QString();
+ case VersionListView::String:
+ return m_emptyString;
+ case VersionListView::ErrorString:
+ return m_emptyErrorString;
+ }
+}
+
+void VersionListView::paintInfoLabel(QPaintEvent* event) const
+{
+ QString emptyString = currentEmptyString();
+
+ // calculate the rect for the overlay
+ QPainter painter(viewport());
+ painter.setRenderHint(QPainter::Antialiasing, true);
+ QFont font("sans", 20);
+ font.setBold(true);
+
+ QRect bounds = viewport()->geometry();
+ bounds.moveTop(0);
+ auto innerBounds = bounds;
+ innerBounds.adjust(10, 10, -10, -10);
+
+ QColor background = QApplication::palette().color(QPalette::Text);
+ QColor foreground = QApplication::palette().color(QPalette::Base);
+ foreground.setAlpha(190);
+ painter.setFont(font);
+ auto fontMetrics = painter.fontMetrics();
+ auto textRect = fontMetrics.boundingRect(
+ innerBounds, Qt::AlignHCenter | Qt::TextWordWrap, emptyString);
+ textRect.moveCenter(bounds.center());
+
+ auto wrapRect = textRect;
+ wrapRect.adjust(-10, -10, 10, 10);
+
+ // check if we are allowed to draw in our area
+ if (!event->rect().intersects(wrapRect)) {
+ return;
+ }
+
+ painter.setBrush(QBrush(background));
+ painter.setPen(foreground);
+ painter.drawRoundedRect(wrapRect, 5.0, 5.0);
+
+ painter.setPen(foreground);
+ painter.setFont(font);
+ painter.drawText(textRect, Qt::AlignHCenter | Qt::TextWordWrap,
+ emptyString);
+}
diff --git a/meshmc/launcher/ui/widgets/VersionListView.h b/meshmc/launcher/ui/widgets/VersionListView.h
new file mode 100644
index 0000000000..5c92a95dc8
--- /dev/null
+++ b/meshmc/launcher/ui/widgets/VersionListView.h
@@ -0,0 +1,75 @@
+/* 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 <QTreeView>
+
+class VersionListView : public QTreeView
+{
+ Q_OBJECT
+ public:
+ explicit VersionListView(QWidget* parent = 0);
+ virtual void paintEvent(QPaintEvent* event) override;
+ virtual void setModel(QAbstractItemModel* model) override;
+
+ enum EmptyMode { Empty, String, ErrorString };
+
+ void setEmptyString(QString emptyString);
+ void setEmptyErrorString(QString emptyErrorString);
+ void setEmptyMode(EmptyMode mode);
+
+ public slots:
+ virtual void reset() override;
+
+ protected slots:
+ virtual void rowsAboutToBeRemoved(const QModelIndex& parent, int start,
+ int end) override;
+ virtual void rowsInserted(const QModelIndex& parent, int start,
+ int end) override;
+
+ private: /* methods */
+ void paintInfoLabel(QPaintEvent* event) const;
+ void updateEmptyViewPort();
+ QString currentEmptyString() const;
+
+ private: /* variables */
+ int m_itemCount = 0;
+ QString m_emptyString;
+ QString m_emptyErrorString;
+ EmptyMode m_emptyMode = Empty;
+};
diff --git a/meshmc/launcher/ui/widgets/VersionSelectWidget.cpp b/meshmc/launcher/ui/widgets/VersionSelectWidget.cpp
new file mode 100644
index 0000000000..74ffb41ae9
--- /dev/null
+++ b/meshmc/launcher/ui/widgets/VersionSelectWidget.cpp
@@ -0,0 +1,232 @@
+/* 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 "VersionSelectWidget.h"
+
+#include <QProgressBar>
+#include <QVBoxLayout>
+#include <QHeaderView>
+
+#include "VersionListView.h"
+#include "VersionProxyModel.h"
+
+#include "ui/dialogs/CustomMessageBox.h"
+
+VersionSelectWidget::VersionSelectWidget(QWidget* parent) : QWidget(parent)
+{
+ setObjectName(QStringLiteral("VersionSelectWidget"));
+ verticalLayout = new QVBoxLayout(this);
+ verticalLayout->setObjectName(QStringLiteral("verticalLayout"));
+ verticalLayout->setContentsMargins(0, 0, 0, 0);
+
+ m_proxyModel = new VersionProxyModel(this);
+
+ listView = new VersionListView(this);
+ listView->setObjectName(QStringLiteral("listView"));
+ listView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+ listView->setAlternatingRowColors(true);
+ listView->setRootIsDecorated(false);
+ listView->setItemsExpandable(false);
+ listView->setWordWrap(true);
+ listView->header()->setCascadingSectionResizes(true);
+ listView->header()->setStretchLastSection(false);
+ listView->setModel(m_proxyModel);
+ verticalLayout->addWidget(listView);
+
+ sneakyProgressBar = new QProgressBar(this);
+ sneakyProgressBar->setObjectName(QStringLiteral("sneakyProgressBar"));
+ sneakyProgressBar->setFormat(QStringLiteral("%p%"));
+ verticalLayout->addWidget(sneakyProgressBar);
+ sneakyProgressBar->setHidden(true);
+ connect(listView->selectionModel(), &QItemSelectionModel::currentRowChanged,
+ this, &VersionSelectWidget::currentRowChanged);
+
+ QMetaObject::connectSlotsByName(this);
+}
+
+void VersionSelectWidget::setCurrentVersion(const QString& version)
+{
+ m_currentVersion = version;
+ m_proxyModel->setCurrentVersion(version);
+}
+
+void VersionSelectWidget::setEmptyString(QString emptyString)
+{
+ listView->setEmptyString(emptyString);
+}
+
+void VersionSelectWidget::setEmptyErrorString(QString emptyErrorString)
+{
+ listView->setEmptyErrorString(emptyErrorString);
+}
+
+VersionSelectWidget::~VersionSelectWidget() {}
+
+void VersionSelectWidget::setResizeOn(int column)
+{
+ listView->header()->setSectionResizeMode(resizeOnColumn,
+ QHeaderView::ResizeToContents);
+ resizeOnColumn = column;
+ listView->header()->setSectionResizeMode(resizeOnColumn,
+ QHeaderView::Stretch);
+}
+
+void VersionSelectWidget::initialize(BaseVersionList* vlist)
+{
+ m_vlist = vlist;
+ m_proxyModel->setSourceModel(vlist);
+ listView->header()->setSectionResizeMode(QHeaderView::ResizeToContents);
+ listView->header()->setSectionResizeMode(resizeOnColumn,
+ QHeaderView::Stretch);
+
+ if (!m_vlist->isLoaded()) {
+ loadList();
+ } else {
+ if (m_proxyModel->rowCount() == 0) {
+ listView->setEmptyMode(VersionListView::String);
+ }
+ preselect();
+ }
+}
+
+void VersionSelectWidget::closeEvent(QCloseEvent* event)
+{
+ QWidget::closeEvent(event);
+}
+
+void VersionSelectWidget::loadList()
+{
+ auto newTask = m_vlist->getLoadTask();
+ if (!newTask) {
+ return;
+ }
+ loadTask = newTask.get();
+ connect(loadTask, &Task::succeeded, this,
+ &VersionSelectWidget::onTaskSucceeded);
+ connect(loadTask, &Task::failed, this, &VersionSelectWidget::onTaskFailed);
+ connect(loadTask, &Task::progress, this,
+ &VersionSelectWidget::changeProgress);
+ if (!loadTask->isRunning()) {
+ loadTask->start();
+ }
+ sneakyProgressBar->setHidden(false);
+}
+
+void VersionSelectWidget::onTaskSucceeded()
+{
+ if (m_proxyModel->rowCount() == 0) {
+ listView->setEmptyMode(VersionListView::String);
+ }
+ sneakyProgressBar->setHidden(true);
+ preselect();
+ loadTask = nullptr;
+}
+
+void VersionSelectWidget::onTaskFailed(const QString& reason)
+{
+ CustomMessageBox::selectable(this, tr("Error"),
+ tr("List update failed:\n%1").arg(reason),
+ QMessageBox::Warning)
+ ->show();
+ onTaskSucceeded();
+}
+
+void VersionSelectWidget::changeProgress(qint64 current, qint64 total)
+{
+ sneakyProgressBar->setMaximum(total);
+ sneakyProgressBar->setValue(current);
+}
+
+void VersionSelectWidget::currentRowChanged(const QModelIndex& current,
+ const QModelIndex&)
+{
+ auto variant =
+ m_proxyModel->data(current, BaseVersionList::VersionPointerRole);
+ emit selectedVersionChanged(variant.value<BaseVersionPtr>());
+}
+
+void VersionSelectWidget::preselect()
+{
+ if (preselectedAlready)
+ return;
+ selectCurrent();
+ if (preselectedAlready)
+ return;
+ selectRecommended();
+}
+
+void VersionSelectWidget::selectCurrent()
+{
+ if (m_currentVersion.isEmpty()) {
+ return;
+ }
+ auto idx = m_proxyModel->getVersion(m_currentVersion);
+ if (idx.isValid()) {
+ preselectedAlready = true;
+ listView->selectionModel()->setCurrentIndex(
+ idx,
+ QItemSelectionModel::SelectCurrent | QItemSelectionModel::Rows);
+ listView->scrollTo(idx, QAbstractItemView::PositionAtCenter);
+ }
+}
+
+void VersionSelectWidget::selectRecommended()
+{
+ auto idx = m_proxyModel->getRecommended();
+ if (idx.isValid()) {
+ preselectedAlready = true;
+ listView->selectionModel()->setCurrentIndex(
+ idx,
+ QItemSelectionModel::SelectCurrent | QItemSelectionModel::Rows);
+ listView->scrollTo(idx, QAbstractItemView::PositionAtCenter);
+ }
+}
+
+bool VersionSelectWidget::hasVersions() const
+{
+ return m_proxyModel->rowCount(QModelIndex()) != 0;
+}
+
+BaseVersionPtr VersionSelectWidget::selectedVersion() const
+{
+ auto currentIndex = listView->selectionModel()->currentIndex();
+ auto variant =
+ m_proxyModel->data(currentIndex, BaseVersionList::VersionPointerRole);
+ return variant.value<BaseVersionPtr>();
+}
+
+void VersionSelectWidget::setExactFilter(BaseVersionList::ModelRoles role,
+ QString filter)
+{
+ m_proxyModel->setFilter(role, new ExactFilter(filter));
+}
+
+void VersionSelectWidget::setFuzzyFilter(BaseVersionList::ModelRoles role,
+ QString filter)
+{
+ m_proxyModel->setFilter(role, new ContainsFilter(filter));
+}
+
+void VersionSelectWidget::setFilter(BaseVersionList::ModelRoles role,
+ Filter* filter)
+{
+ m_proxyModel->setFilter(role, filter);
+}
diff --git a/meshmc/launcher/ui/widgets/VersionSelectWidget.h b/meshmc/launcher/ui/widgets/VersionSelectWidget.h
new file mode 100644
index 0000000000..3adf128d44
--- /dev/null
+++ b/meshmc/launcher/ui/widgets/VersionSelectWidget.h
@@ -0,0 +1,104 @@
+/* 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 <QSortFilterProxyModel>
+#include "BaseVersionList.h"
+
+class VersionProxyModel;
+class VersionListView;
+class QVBoxLayout;
+class QProgressBar;
+class Filter;
+
+class VersionSelectWidget : public QWidget
+{
+ Q_OBJECT
+ public:
+ explicit VersionSelectWidget(QWidget* parent = 0);
+ ~VersionSelectWidget();
+
+ //! loads the list if needed.
+ void initialize(BaseVersionList* vlist);
+
+ //! Starts a task that loads the list.
+ void loadList();
+
+ bool hasVersions() const;
+ BaseVersionPtr selectedVersion() const;
+ void selectRecommended();
+ void selectCurrent();
+
+ void setCurrentVersion(const QString& version);
+ void setFuzzyFilter(BaseVersionList::ModelRoles role, QString filter);
+ void setExactFilter(BaseVersionList::ModelRoles role, QString filter);
+ void setFilter(BaseVersionList::ModelRoles role, Filter* filter);
+ void setEmptyString(QString emptyString);
+ void setEmptyErrorString(QString emptyErrorString);
+ void setResizeOn(int column);
+
+ signals:
+ void selectedVersionChanged(BaseVersionPtr version);
+
+ protected:
+ virtual void closeEvent(QCloseEvent*);
+
+ private slots:
+ void onTaskSucceeded();
+ void onTaskFailed(const QString& reason);
+ void changeProgress(qint64 current, qint64 total);
+ void currentRowChanged(const QModelIndex& current, const QModelIndex&);
+
+ private:
+ void preselect();
+
+ private:
+ QString m_currentVersion;
+ BaseVersionList* m_vlist = nullptr;
+ VersionProxyModel* m_proxyModel = nullptr;
+ int resizeOnColumn = 0;
+ Task* loadTask;
+ bool preselectedAlready = false;
+
+ private:
+ QVBoxLayout* verticalLayout = nullptr;
+ VersionListView* listView = nullptr;
+ QProgressBar* sneakyProgressBar = nullptr;
+};
diff --git a/meshmc/launcher/ui/widgets/WideBar.cpp b/meshmc/launcher/ui/widgets/WideBar.cpp
new file mode 100644
index 0000000000..dfcb9737c8
--- /dev/null
+++ b/meshmc/launcher/ui/widgets/WideBar.cpp
@@ -0,0 +1,135 @@
+/* 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 "WideBar.h"
+#include <QToolButton>
+#include <QMenu>
+
+class ActionButton : public QToolButton
+{
+ Q_OBJECT
+ public:
+ ActionButton(QAction* action, QWidget* parent = 0)
+ : QToolButton(parent), m_action(action)
+ {
+ setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
+ connect(action, &QAction::changed, this, &ActionButton::actionChanged);
+ connect(this, &ActionButton::clicked, action, &QAction::trigger);
+ actionChanged();
+ };
+ private slots:
+ void actionChanged()
+ {
+ setEnabled(m_action->isEnabled());
+ setChecked(m_action->isChecked());
+ setCheckable(m_action->isCheckable());
+ setText(m_action->text());
+ setIcon(m_action->icon());
+ setToolTip(m_action->toolTip());
+ setHidden(!m_action->isVisible());
+ setFocusPolicy(Qt::NoFocus);
+ }
+
+ private:
+ QAction* m_action;
+};
+
+WideBar::WideBar(const QString& title, QWidget* parent)
+ : QToolBar(title, parent)
+{
+ setFloatable(false);
+ setMovable(false);
+}
+
+WideBar::WideBar(QWidget* parent) : QToolBar(parent)
+{
+ setFloatable(false);
+ setMovable(false);
+}
+
+struct WideBar::BarEntry {
+ enum Type { None, Action, Separator, Spacer } type = None;
+ QAction* qAction = nullptr;
+ QAction* wideAction = nullptr;
+};
+
+WideBar::~WideBar()
+{
+ for (auto* iter : m_entries) {
+ delete iter;
+ }
+}
+
+void WideBar::addAction(QAction* action)
+{
+ auto entry = new BarEntry();
+ entry->qAction = addWidget(new ActionButton(action, this));
+ entry->wideAction = action;
+ entry->type = BarEntry::Action;
+ m_entries.push_back(entry);
+}
+
+void WideBar::addSeparator()
+{
+ auto entry = new BarEntry();
+ entry->qAction = QToolBar::addSeparator();
+ entry->type = BarEntry::Separator;
+ m_entries.push_back(entry);
+}
+
+void WideBar::insertSpacer(QAction* action)
+{
+ auto iter = std::find_if(
+ m_entries.begin(), m_entries.end(),
+ [action](BarEntry* entry) { return entry->wideAction == action; });
+ if (iter == m_entries.end()) {
+ return;
+ }
+ QWidget* spacer = new QWidget();
+ spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
+
+ auto entry = new BarEntry();
+ entry->qAction = insertWidget((*iter)->qAction, spacer);
+ entry->type = BarEntry::Spacer;
+ m_entries.insert(iter, entry);
+}
+
+QMenu* WideBar::createContextMenu(QWidget* parent, const QString& title)
+{
+ QMenu* contextMenu = new QMenu(title, parent);
+ for (auto& item : m_entries) {
+ switch (item->type) {
+ default:
+ case BarEntry::None:
+ break;
+ case BarEntry::Separator:
+ case BarEntry::Spacer:
+ contextMenu->addSeparator();
+ break;
+ case BarEntry::Action:
+ contextMenu->addAction(item->wideAction);
+ break;
+ }
+ }
+ return contextMenu;
+}
+
+#include "WideBar.moc"
diff --git a/meshmc/launcher/ui/widgets/WideBar.h b/meshmc/launcher/ui/widgets/WideBar.h
new file mode 100644
index 0000000000..ff2c7a6c01
--- /dev/null
+++ b/meshmc/launcher/ui/widgets/WideBar.h
@@ -0,0 +1,48 @@
+/* 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 <QToolBar>
+#include <QAction>
+#include <QMap>
+
+class QMenu;
+
+class WideBar : public QToolBar
+{
+ Q_OBJECT
+
+ public:
+ explicit WideBar(const QString& title, QWidget* parent = nullptr);
+ explicit WideBar(QWidget* parent = nullptr);
+ virtual ~WideBar();
+
+ void addAction(QAction* action);
+ void addSeparator();
+ void insertSpacer(QAction* action);
+ QMenu* createContextMenu(QWidget* parent = nullptr,
+ const QString& title = QString());
+
+ private:
+ struct BarEntry;
+ QList<BarEntry*> m_entries;
+};