summaryrefslogtreecommitdiff
path: root/archived/projt-launcher/launcher/ui/widgets
diff options
context:
space:
mode:
authorMehmet Samet Duman <yongdohyun@projecttick.org>2026-04-02 18:51:45 +0300
committerMehmet Samet Duman <yongdohyun@projecttick.org>2026-04-02 18:51:45 +0300
commitd3261e64152397db2dca4d691a990c6bc2a6f4dd (patch)
treefac2f7be638651181a72453d714f0f96675c2b8b /archived/projt-launcher/launcher/ui/widgets
parent31b9a8949ed0a288143e23bf739f2eb64fdc63be (diff)
downloadProject-Tick-d3261e64152397db2dca4d691a990c6bc2a6f4dd.tar.gz
Project-Tick-d3261e64152397db2dca4d691a990c6bc2a6f4dd.zip
NOISSUE add archived projects
Signed-off-by: Mehmet Samet Duman <yongdohyun@projecttick.org>
Diffstat (limited to 'archived/projt-launcher/launcher/ui/widgets')
-rw-r--r--archived/projt-launcher/launcher/ui/widgets/AppearanceWidget.cpp326
-rw-r--r--archived/projt-launcher/launcher/ui/widgets/AppearanceWidget.h87
-rw-r--r--archived/projt-launcher/launcher/ui/widgets/AppearanceWidget.ui632
-rw-r--r--archived/projt-launcher/launcher/ui/widgets/CefHubView.cpp1334
-rw-r--r--archived/projt-launcher/launcher/ui/widgets/CefHubView.h111
-rw-r--r--archived/projt-launcher/launcher/ui/widgets/CheckComboBox.cpp250
-rw-r--r--archived/projt-launcher/launcher/ui/widgets/CheckComboBox.h89
-rw-r--r--archived/projt-launcher/launcher/ui/widgets/Common.cpp54
-rw-r--r--archived/projt-launcher/launcher/ui/widgets/Common.h29
-rw-r--r--archived/projt-launcher/launcher/ui/widgets/CustomCommands.cpp115
-rw-r--r--archived/projt-launcher/launcher/ui/widgets/CustomCommands.h91
-rw-r--r--archived/projt-launcher/launcher/ui/widgets/CustomCommands.ui158
-rw-r--r--archived/projt-launcher/launcher/ui/widgets/EnvironmentVariables.cpp148
-rw-r--r--archived/projt-launcher/launcher/ui/widgets/EnvironmentVariables.h69
-rw-r--r--archived/projt-launcher/launcher/ui/widgets/EnvironmentVariables.ui122
-rw-r--r--archived/projt-launcher/launcher/ui/widgets/FallbackHubView.cpp100
-rw-r--r--archived/projt-launcher/launcher/ui/widgets/FallbackHubView.h52
-rw-r--r--archived/projt-launcher/launcher/ui/widgets/FastFileIconProvider.cpp71
-rw-r--r--archived/projt-launcher/launcher/ui/widgets/FastFileIconProvider.h49
-rw-r--r--archived/projt-launcher/launcher/ui/widgets/FileIgnoreProxy.cpp362
-rw-r--r--archived/projt-launcher/launcher/ui/widgets/FileIgnoreProxy.h123
-rw-r--r--archived/projt-launcher/launcher/ui/widgets/HubSearchProvider.cpp97
-rw-r--r--archived/projt-launcher/launcher/ui/widgets/HubSearchProvider.h21
-rw-r--r--archived/projt-launcher/launcher/ui/widgets/HubViewBase.h55
-rw-r--r--archived/projt-launcher/launcher/ui/widgets/IconLabel.cpp62
-rw-r--r--archived/projt-launcher/launcher/ui/widgets/IconLabel.h46
-rw-r--r--archived/projt-launcher/launcher/ui/widgets/InfoFrame.cpp524
-rw-r--r--archived/projt-launcher/launcher/ui/widgets/InfoFrame.h111
-rw-r--r--archived/projt-launcher/launcher/ui/widgets/InfoFrame.ui158
-rw-r--r--archived/projt-launcher/launcher/ui/widgets/JavaSettingsWidget.cpp356
-rw-r--r--archived/projt-launcher/launcher/ui/widgets/JavaSettingsWidget.h56
-rw-r--r--archived/projt-launcher/launcher/ui/widgets/JavaSettingsWidget.ui392
-rw-r--r--archived/projt-launcher/launcher/ui/widgets/JavaWizardWidget.cpp645
-rw-r--r--archived/projt-launcher/launcher/ui/widgets/JavaWizardWidget.h137
-rw-r--r--archived/projt-launcher/launcher/ui/widgets/LabeledToolButton.cpp155
-rw-r--r--archived/projt-launcher/launcher/ui/widgets/LabeledToolButton.h63
-rw-r--r--archived/projt-launcher/launcher/ui/widgets/LanguageSelectionWidget.cpp111
-rw-r--r--archived/projt-launcher/launcher/ui/widgets/LanguageSelectionWidget.h67
-rw-r--r--archived/projt-launcher/launcher/ui/widgets/LauncherHubWidget.cpp1297
-rw-r--r--archived/projt-launcher/launcher/ui/widgets/LauncherHubWidget.h116
-rw-r--r--archived/projt-launcher/launcher/ui/widgets/LogView.cpp229
-rw-r--r--archived/projt-launcher/launcher/ui/widgets/LogView.h58
-rw-r--r--archived/projt-launcher/launcher/ui/widgets/MinecraftSettingsWidget.cpp676
-rw-r--r--archived/projt-launcher/launcher/ui/widgets/MinecraftSettingsWidget.h94
-rw-r--r--archived/projt-launcher/launcher/ui/widgets/MinecraftSettingsWidget.ui859
-rw-r--r--archived/projt-launcher/launcher/ui/widgets/ModFilterWidget.cpp479
-rw-r--r--archived/projt-launcher/launcher/ui/widgets/ModFilterWidget.h168
-rw-r--r--archived/projt-launcher/launcher/ui/widgets/ModFilterWidget.ui333
-rw-r--r--archived/projt-launcher/launcher/ui/widgets/ModListView.cpp96
-rw-r--r--archived/projt-launcher/launcher/ui/widgets/ModListView.h49
-rw-r--r--archived/projt-launcher/launcher/ui/widgets/PageContainer.cpp334
-rw-r--r--archived/projt-launcher/launcher/ui/widgets/PageContainer.h150
-rw-r--r--archived/projt-launcher/launcher/ui/widgets/PageContainer_p.h145
-rw-r--r--archived/projt-launcher/launcher/ui/widgets/ProgressWidget.cpp143
-rw-r--r--archived/projt-launcher/launcher/ui/widgets/ProgressWidget.h87
-rw-r--r--archived/projt-launcher/launcher/ui/widgets/ProjectDescriptionPage.cpp45
-rw-r--r--archived/projt-launcher/launcher/ui/widgets/ProjectDescriptionPage.h53
-rw-r--r--archived/projt-launcher/launcher/ui/widgets/ProjectItem.cpp238
-rw-r--r--archived/projt-launcher/launcher/ui/widgets/ProjectItem.h57
-rw-r--r--archived/projt-launcher/launcher/ui/widgets/QtWebEngineHubView.cpp186
-rw-r--r--archived/projt-launcher/launcher/ui/widgets/QtWebEngineHubView.h50
-rw-r--r--archived/projt-launcher/launcher/ui/widgets/SubTaskProgressBar.cpp79
-rw-r--r--archived/projt-launcher/launcher/ui/widgets/SubTaskProgressBar.h69
-rw-r--r--archived/projt-launcher/launcher/ui/widgets/SubTaskProgressBar.ui100
-rw-r--r--archived/projt-launcher/launcher/ui/widgets/VariableSizedImageObject.cpp230
-rw-r--r--archived/projt-launcher/launcher/ui/widgets/VariableSizedImageObject.h107
-rw-r--r--archived/projt-launcher/launcher/ui/widgets/VersionListView.cpp200
-rw-r--r--archived/projt-launcher/launcher/ui/widgets/VersionListView.h77
-rw-r--r--archived/projt-launcher/launcher/ui/widgets/VersionSelectWidget.cpp291
-rw-r--r--archived/projt-launcher/launcher/ui/widgets/VersionSelectWidget.h133
-rw-r--r--archived/projt-launcher/launcher/ui/widgets/WebView2Widget.cpp315
-rw-r--r--archived/projt-launcher/launcher/ui/widgets/WebView2Widget.h68
-rw-r--r--archived/projt-launcher/launcher/ui/widgets/WideBar.cpp362
-rw-r--r--archived/projt-launcher/launcher/ui/widgets/WideBar.h98
74 files changed, 15499 insertions, 0 deletions
diff --git a/archived/projt-launcher/launcher/ui/widgets/AppearanceWidget.cpp b/archived/projt-launcher/launcher/ui/widgets/AppearanceWidget.cpp
new file mode 100644
index 0000000000..05fc1ab59b
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/widgets/AppearanceWidget.cpp
@@ -0,0 +1,326 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2025 TheKodeToad <TheKodeToad@proton.me>
+ * Copyright (C) 2022 Tayou <git@tayou.org>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * 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 "AppearanceWidget.h"
+#include "ui_AppearanceWidget.h"
+
+#include <DesktopServices.h>
+#include <QGraphicsOpacityEffect>
+#include "BuildConfig.h"
+#include "ui/themes/Theme.h"
+#include "ui/themes/ThemeManager.h"
+
+AppearanceWidget::AppearanceWidget(bool themesOnly, QWidget* parent)
+ : QWidget(parent),
+ m_ui(new Ui::AppearanceWidget),
+ m_themesOnly(themesOnly)
+{
+ m_ui->setupUi(this);
+
+ m_ui->catPreview->setGraphicsEffect(new QGraphicsOpacityEffect(this));
+
+ m_defaultFormat = QTextCharFormat(m_ui->consolePreview->currentCharFormat());
+
+ if (themesOnly)
+ {
+ m_ui->catPackLabel->hide();
+ m_ui->catPackComboBox->hide();
+ m_ui->catPackFolder->hide();
+ m_ui->settingsBox->hide();
+ m_ui->consolePreview->hide();
+ m_ui->catPreview->hide();
+ loadThemeSettings();
+ }
+ else
+ {
+ loadSettings();
+ loadThemeSettings();
+
+ updateConsolePreview();
+ updateCatPreview();
+ }
+
+ connect(m_ui->fontSizeBox, &QSpinBox::valueChanged, this, &AppearanceWidget::updateConsolePreview);
+ connect(m_ui->consoleFont, &QFontComboBox::currentFontChanged, this, &AppearanceWidget::updateConsolePreview);
+
+ connect(m_ui->iconsComboBox, &QComboBox::currentIndexChanged, this, &AppearanceWidget::applyIconTheme);
+ connect(m_ui->widgetStyleComboBox, &QComboBox::currentIndexChanged, this, &AppearanceWidget::applyWidgetTheme);
+ connect(m_ui->catPackComboBox, &QComboBox::currentIndexChanged, this, &AppearanceWidget::applyCatTheme);
+ connect(m_ui->catOpacitySlider, &QAbstractSlider::valueChanged, this, &AppearanceWidget::updateCatPreview);
+
+ connect(m_ui->iconsFolder,
+ &QPushButton::clicked,
+ this,
+ [] { DesktopServices::openPath(APPLICATION->themeManager()->getIconThemesFolder().path()); });
+ connect(m_ui->widgetStyleFolder,
+ &QPushButton::clicked,
+ this,
+ [] { DesktopServices::openPath(APPLICATION->themeManager()->getApplicationThemesFolder().path()); });
+ connect(m_ui->catPackFolder,
+ &QPushButton::clicked,
+ this,
+ [] { DesktopServices::openPath(APPLICATION->themeManager()->getCatPacksFolder().path()); });
+ connect(m_ui->reloadThemesButton, &QPushButton::pressed, this, &AppearanceWidget::loadThemeSettings);
+}
+
+AppearanceWidget::~AppearanceWidget()
+{
+ delete m_ui;
+}
+
+void AppearanceWidget::applySettings()
+{
+ SettingsObjectPtr settings = APPLICATION->settings();
+ QString consoleFontFamily = m_ui->consoleFont->currentFont().family();
+ settings->set("ConsoleFont", consoleFontFamily);
+ settings->set("ConsoleFontSize", m_ui->fontSizeBox->value());
+ settings->set("CatOpacity", m_ui->catOpacitySlider->value());
+ auto catFit = m_ui->catFitComboBox->currentIndex();
+ settings->set("CatFit", catFit == 0 ? "fit" : catFit == 1 ? "fill" : "strech");
+}
+
+void AppearanceWidget::loadSettings()
+{
+ SettingsObjectPtr settings = APPLICATION->settings();
+ QString fontFamily = settings->get("ConsoleFont").toString();
+ QFont consoleFont(fontFamily);
+ m_ui->consoleFont->setCurrentFont(consoleFont);
+
+ bool conversionOk = true;
+ int fontSize = settings->get("ConsoleFontSize").toInt(&conversionOk);
+ if (!conversionOk)
+ {
+ fontSize = 11;
+ }
+ m_ui->fontSizeBox->setValue(fontSize);
+
+ m_ui->catOpacitySlider->setValue(settings->get("CatOpacity").toInt());
+
+ auto catFit = settings->get("CatFit").toString();
+ m_ui->catFitComboBox->setCurrentIndex(catFit == "fit" ? 0 : catFit == "fill" ? 1 : 2);
+}
+
+void AppearanceWidget::retranslateUi()
+{
+ m_ui->retranslateUi(this);
+}
+
+void AppearanceWidget::applyIconTheme(int index)
+{
+ auto settings = APPLICATION->settings();
+ auto originalIconTheme = settings->get("IconTheme").toString();
+ auto newIconTheme = m_ui->iconsComboBox->itemData(index).toString();
+ if (originalIconTheme != newIconTheme)
+ {
+ settings->set("IconTheme", newIconTheme);
+ APPLICATION->themeManager()->applyCurrentlySelectedTheme();
+ }
+}
+
+void AppearanceWidget::applyWidgetTheme(int index)
+{
+ auto settings = APPLICATION->settings();
+ auto originalAppTheme = settings->get("ApplicationTheme").toString();
+ auto newAppTheme = m_ui->widgetStyleComboBox->itemData(index).toString();
+ if (originalAppTheme != newAppTheme)
+ {
+ settings->set("ApplicationTheme", newAppTheme);
+ APPLICATION->themeManager()->applyCurrentlySelectedTheme();
+ }
+
+ updateConsolePreview();
+}
+
+void AppearanceWidget::applyCatTheme(int index)
+{
+ auto settings = APPLICATION->settings();
+ auto originalCat = settings->get("BackgroundCat").toString();
+ auto newCat = m_ui->catPackComboBox->itemData(index).toString();
+ if (originalCat != newCat)
+ {
+ settings->set("BackgroundCat", newCat);
+ }
+
+ APPLICATION->currentCatChanged(index);
+ updateCatPreview();
+}
+
+void AppearanceWidget::loadThemeSettings()
+{
+ // Removed refresh() call - themes are already loaded at startup
+ // This was causing a 2+ second delay when opening NewInstanceDialog
+ // qDebug() << "[AppearanceWidget] loadThemeSettings() called - refreshing themes...";
+ // APPLICATION->themeManager()->refresh();
+ // qDebug() << "[AppearanceWidget] Theme refresh complete";
+
+ m_ui->iconsComboBox->blockSignals(true);
+ m_ui->widgetStyleComboBox->blockSignals(true);
+ m_ui->catPackComboBox->blockSignals(true);
+
+ m_ui->iconsComboBox->clear();
+ m_ui->widgetStyleComboBox->clear();
+ m_ui->catPackComboBox->clear();
+
+ const SettingsObjectPtr settings = APPLICATION->settings();
+
+ const QString currentIconTheme = settings->get("IconTheme").toString();
+ const auto iconThemes = APPLICATION->themeManager()->getValidIconThemes();
+
+ for (int i = 0; i < iconThemes.count(); ++i)
+ {
+ const IconTheme* theme = iconThemes[i];
+
+ QIcon iconForComboBox = QIcon(theme->path() + "/scalable/settings");
+ m_ui->iconsComboBox->addItem(iconForComboBox, theme->name(), theme->id());
+
+ if (currentIconTheme == theme->id())
+ m_ui->iconsComboBox->setCurrentIndex(i);
+ }
+
+ const QString currentTheme = settings->get("ApplicationTheme").toString();
+ auto themes = APPLICATION->themeManager()->getValidApplicationThemes();
+ for (int i = 0; i < themes.count(); ++i)
+ {
+ Theme* theme = themes[i];
+
+ m_ui->widgetStyleComboBox->addItem(theme->name(), theme->id());
+
+ if (!theme->tooltip().isEmpty())
+ m_ui->widgetStyleComboBox->setItemData(i, theme->tooltip(), Qt::ToolTipRole);
+
+ if (currentTheme == theme->id())
+ m_ui->widgetStyleComboBox->setCurrentIndex(i);
+ }
+
+ if (!m_themesOnly)
+ {
+ const QString currentCat = settings->get("BackgroundCat").toString();
+ const auto cats = APPLICATION->themeManager()->getValidCatPacks();
+ for (int i = 0; i < cats.count(); ++i)
+ {
+ const CatPack* cat = cats[i];
+
+ QIcon catIcon = QIcon(QString("%1").arg(cat->path()));
+ m_ui->catPackComboBox->addItem(catIcon, cat->name(), cat->id());
+
+ if (currentCat == cat->id())
+ m_ui->catPackComboBox->setCurrentIndex(i);
+ }
+ }
+
+ m_ui->iconsComboBox->blockSignals(false);
+ m_ui->widgetStyleComboBox->blockSignals(false);
+ m_ui->catPackComboBox->blockSignals(false);
+}
+
+void AppearanceWidget::updateConsolePreview()
+{
+ const LogColors& colors = APPLICATION->themeManager()->getLogColors();
+
+ int fontSize = m_ui->fontSizeBox->value();
+ QString fontFamily = m_ui->consoleFont->currentFont().family();
+ m_ui->consolePreview->clear();
+ m_defaultFormat.setFont(QFont(fontFamily, fontSize));
+
+ auto print = [this, colors](const QString& message, MessageLevel::Enum level)
+ {
+ QTextCharFormat format(m_defaultFormat);
+
+ QColor bg = colors.background.value(level);
+ QColor fg = colors.foreground.value(level);
+
+ if (bg.isValid())
+ format.setBackground(bg);
+
+ if (fg.isValid())
+ format.setForeground(fg);
+
+ // append a paragraph/line
+ auto workCursor = m_ui->consolePreview->textCursor();
+ workCursor.movePosition(QTextCursor::End);
+ workCursor.insertText(message, format);
+ workCursor.insertBlock();
+ };
+
+ print(QString("%1 version: %2\n").arg(BuildConfig.LAUNCHER_DISPLAYNAME, BuildConfig.printableVersionString()),
+ MessageLevel::Launcher);
+
+ QDate today = QDate::currentDate();
+
+ if (today.month() == 10 && today.day() == 31)
+ print(tr("[ERROR] OOoooOOOoooo! A spooky error!"), MessageLevel::Error);
+ else
+ print(tr("[ERROR] A spooky error!"), MessageLevel::Error);
+
+ print(tr("[INFO] A harmless message..."), MessageLevel::Info);
+ print(tr("[WARN] A not so spooky warning."), MessageLevel::Warning);
+ print(tr("[DEBUG] A secret debugging message..."), MessageLevel::Debug);
+ print(tr("[FATAL] A terrifying fatal error!"), MessageLevel::Fatal);
+}
+
+void AppearanceWidget::updateCatPreview()
+{
+ QIcon catPackIcon(APPLICATION->themeManager()->getCatPack());
+ m_ui->catPreview->setIcon(catPackIcon);
+
+ auto effect = dynamic_cast<QGraphicsOpacityEffect*>(m_ui->catPreview->graphicsEffect());
+ if (effect)
+ effect->setOpacity(m_ui->catOpacitySlider->value() / 100.0);
+}
diff --git a/archived/projt-launcher/launcher/ui/widgets/AppearanceWidget.h b/archived/projt-launcher/launcher/ui/widgets/AppearanceWidget.h
new file mode 100644
index 0000000000..b08f52d2dc
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/widgets/AppearanceWidget.h
@@ -0,0 +1,87 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2025 TheKodeToad <TheKodeToad@proton.me>
+ * Copyright (C) 2022 Tayou <git@tayou.org>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * ======================================================================== */
+
+#pragma once
+
+#include <QDialog>
+#include <memory>
+
+#include <Application.h>
+#include <translations/TranslationsModel.h>
+#include <QTextCursor>
+#include "ui/pages/BasePage.h"
+
+class QTextCharFormat;
+class SettingsObject;
+
+namespace Ui
+{
+ class AppearanceWidget;
+}
+
+class AppearanceWidget : public QWidget
+{
+ Q_OBJECT
+
+ public:
+ explicit AppearanceWidget(bool simple, QWidget* parent = 0);
+ virtual ~AppearanceWidget();
+
+ public:
+ void applySettings();
+ void loadSettings();
+ void retranslateUi();
+
+ private:
+ void applyIconTheme(int index);
+ void applyWidgetTheme(int index);
+ void applyCatTheme(int index);
+ void loadThemeSettings();
+
+ void updateConsolePreview();
+ void updateCatPreview();
+
+ Ui::AppearanceWidget* m_ui;
+ QTextCharFormat m_defaultFormat;
+ bool m_themesOnly;
+};
diff --git a/archived/projt-launcher/launcher/ui/widgets/AppearanceWidget.ui b/archived/projt-launcher/launcher/ui/widgets/AppearanceWidget.ui
new file mode 100644
index 0000000000..cfe464dd67
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/widgets/AppearanceWidget.ui
@@ -0,0 +1,632 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>AppearanceWidget</class>
+ <widget class="QWidget" name="AppearanceWidget">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>600</width>
+ <height>711</height>
+ </rect>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>300</width>
+ <height>0</height>
+ </size>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout" stretch="0,0,0">
+ <item>
+ <widget class="QGroupBox" name="themingBox">
+ <property name="title">
+ <string/>
+ </property>
+ <property name="flat">
+ <bool>false</bool>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_2">
+ <item>
+ <layout class="QGridLayout" name="gridLayout">
+ <item row="2" column="3">
+ <widget class="QPushButton" name="catPackFolder">
+ <property name="toolTip">
+ <string>View cat packs folder.</string>
+ </property>
+ <property name="text">
+ <string>Open Folder</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="3">
+ <widget class="QPushButton" name="widgetStyleFolder">
+ <property name="toolTip">
+ <string>View widget themes folder.</string>
+ </property>
+ <property name="text">
+ <string>Open Folder</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="3">
+ <widget class="QPushButton" name="iconsFolder">
+ <property name="toolTip">
+ <string>View icon themes folder.</string>
+ </property>
+ <property name="text">
+ <string>Open Folder</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="0">
+ <widget class="QLabel" name="catPackLabel">
+ <property name="text">
+ <string>&amp;Cat Pack:</string>
+ </property>
+ <property name="buddy">
+ <cstring>catPackComboBox</cstring>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="2">
+ <widget class="QComboBox" name="catPackComboBox"/>
+ </item>
+ <item row="1" column="2">
+ <widget class="QComboBox" name="iconsComboBox">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="focusPolicy">
+ <enum>Qt::StrongFocus</enum>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="2">
+ <widget class="QComboBox" name="widgetStyleComboBox">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="focusPolicy">
+ <enum>Qt::StrongFocus</enum>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="2">
+ <widget class="QPushButton" name="reloadThemesButton">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Maximum" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Reload All</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="0">
+ <widget class="QLabel" name="widgetStyleLabel">
+ <property name="text">
+ <string>Theme:</string>
+ </property>
+ <property name="buddy">
+ <cstring>widgetStyleComboBox</cstring>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0">
+ <widget class="QLabel" name="iconsLabel">
+ <property name="text">
+ <string>&amp;Icons:</string>
+ </property>
+ <property name="buddy">
+ <cstring>iconsComboBox</cstring>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="settingsBox">
+ <property name="title">
+ <string/>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_5">
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_5">
+ <item>
+ <widget class="QLabel" name="label">
+ <property name="text">
+ <string>Console Font:</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QFontComboBox" name="consoleFont"/>
+ </item>
+ <item>
+ <widget class="QSpinBox" name="fontSizeBox">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Maximum" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimum">
+ <number>5</number>
+ </property>
+ <property name="maximum">
+ <number>16</number>
+ </property>
+ <property name="value">
+ <number>11</number>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_2">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>0</width>
+ <height>0</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <spacer name="verticalSpacer_2">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Fixed</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>0</width>
+ <height>6</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QLabel" name="catOpacityLabel">
+ <property name="text">
+ <string>Cat Opacity</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QWidget" name="widget_2" native="true">
+ <property name="maximumSize">
+ <size>
+ <width>300</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <layout class="QHBoxLayout" name="horizontalLayout_2">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QWidget" name="widget" native="true">
+ <property name="maximumSize">
+ <size>
+ <width>300</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <layout class="QGridLayout" name="gridLayout_4">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item row="3" column="3">
+ <widget class="QLabel" name="label_5">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ <property name="text">
+ <string>Opaque</string>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="2">
+ <spacer name="horizontalSpacer_4">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>0</width>
+ <height>0</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item row="3" column="1">
+ <widget class="QLabel" name="label_4">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ <property name="text">
+ <string>Transparent</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="0" colspan="4">
+ <widget class="QSlider" name="catOpacitySlider">
+ <property name="minimumSize">
+ <size>
+ <width>0</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="maximum">
+ <number>100</number>
+ </property>
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <spacer name="verticalSpacer">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Fixed</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>0</width>
+ <height>6</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QLabel" name="catFitLabel">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Maximum" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Cat Scaling</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QComboBox" name="catFitComboBox">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Maximum" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>77</width>
+ <height>30</height>
+ </size>
+ </property>
+ <property name="currentIndex">
+ <number>0</number>
+ </property>
+ <item>
+ <property name="text">
+ <string>Fit</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Fill</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Stretch</string>
+ </property>
+ </item>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="previewBox">
+ <property name="title">
+ <string>Preview</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_3">
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_3">
+ <item>
+ <widget class="QPushButton" name="catPreview">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Minimum">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="focusPolicy">
+ <enum>Qt::NoFocus</enum>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ <property name="iconSize">
+ <size>
+ <width>128</width>
+ <height>256</height>
+ </size>
+ </property>
+ <property name="flat">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <layout class="QVBoxLayout" name="verticalLayout_4">
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <item>
+ <widget class="QPushButton" name="icon1">
+ <property name="focusPolicy">
+ <enum>Qt::NoFocus</enum>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ <property name="icon">
+ <iconset theme="new"/>
+ </property>
+ <property name="flat">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="icon2">
+ <property name="focusPolicy">
+ <enum>Qt::NoFocus</enum>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ <property name="icon">
+ <iconset theme="centralmods"/>
+ </property>
+ <property name="flat">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="icon3">
+ <property name="focusPolicy">
+ <enum>Qt::NoFocus</enum>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ <property name="icon">
+ <iconset theme="viewfolder"/>
+ </property>
+ <property name="flat">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="icon4">
+ <property name="focusPolicy">
+ <enum>Qt::NoFocus</enum>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ <property name="icon">
+ <iconset theme="launch"/>
+ </property>
+ <property name="flat">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="icon5">
+ <property name="focusPolicy">
+ <enum>Qt::NoFocus</enum>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ <property name="icon">
+ <iconset theme="copy"/>
+ </property>
+ <property name="flat">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="icon6">
+ <property name="focusPolicy">
+ <enum>Qt::NoFocus</enum>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ <property name="icon">
+ <iconset theme="export"/>
+ </property>
+ <property name="flat">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="icon7">
+ <property name="focusPolicy">
+ <enum>Qt::NoFocus</enum>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ <property name="icon">
+ <iconset theme="delete"/>
+ </property>
+ <property name="flat">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="icon8">
+ <property name="focusPolicy">
+ <enum>Qt::NoFocus</enum>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ <property name="icon">
+ <iconset theme="about"/>
+ </property>
+ <property name="flat">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="icon9">
+ <property name="focusPolicy">
+ <enum>Qt::NoFocus</enum>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ <property name="icon">
+ <iconset theme="settings"/>
+ </property>
+ <property name="flat">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="icon10">
+ <property name="focusPolicy">
+ <enum>Qt::NoFocus</enum>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ <property name="icon">
+ <iconset theme="cat"/>
+ </property>
+ <property name="flat">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>0</width>
+ <height>0</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QTextEdit" name="consolePreview">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="MinimumExpanding">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="horizontalScrollBarPolicy">
+ <enum>Qt::ScrollBarAsNeeded</enum>
+ </property>
+ <property name="undoRedoEnabled">
+ <bool>false</bool>
+ </property>
+ <property name="textInteractionFlags">
+ <set>Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <tabstops>
+ <tabstop>widgetStyleComboBox</tabstop>
+ <tabstop>widgetStyleFolder</tabstop>
+ <tabstop>iconsComboBox</tabstop>
+ <tabstop>iconsFolder</tabstop>
+ <tabstop>catPackComboBox</tabstop>
+ <tabstop>catPackFolder</tabstop>
+ <tabstop>reloadThemesButton</tabstop>
+ <tabstop>consoleFont</tabstop>
+ <tabstop>fontSizeBox</tabstop>
+ <tabstop>catFitComboBox</tabstop>
+ <tabstop>catOpacitySlider</tabstop>
+ <tabstop>consolePreview</tabstop>
+ </tabstops>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/archived/projt-launcher/launcher/ui/widgets/CefHubView.cpp b/archived/projt-launcher/launcher/ui/widgets/CefHubView.cpp
new file mode 100644
index 0000000000..421589216d
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/widgets/CefHubView.cpp
@@ -0,0 +1,1334 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+#include "CefHubView.h"
+
+#if defined(PROJT_USE_CEF)
+
+#include <algorithm>
+#include <cmath>
+#include <QApplication>
+#include <QColor>
+#include <QFocusEvent>
+#include <QHideEvent>
+#include <QKeyEvent>
+#include <QMetaObject>
+#include <QMouseEvent>
+#include <QPainter>
+#include <QPaintEvent>
+#include <QResizeEvent>
+#include <QScreen>
+#include <QShowEvent>
+#include <QTimer>
+#include <QWheelEvent>
+
+#include "CefRuntime.h"
+
+#include "include/cef_browser.h"
+#include "include/cef_client.h"
+#include "include/cef_dialog_handler.h"
+#include "include/cef_display_handler.h"
+#include "include/cef_jsdialog_handler.h"
+#include "include/cef_life_span_handler.h"
+#include "include/cef_load_handler.h"
+#include "include/cef_permission_handler.h"
+#include "include/cef_request_context.h"
+#include "include/cef_render_handler.h"
+#include "include/cef_request_handler.h"
+#include "include/internal/cef_types_wrappers.h"
+#include "include/wrapper/cef_helpers.h"
+
+namespace
+{
+ uint32_t toCefModifiers(Qt::KeyboardModifiers keyboardModifiers, Qt::MouseButtons mouseButtons)
+ {
+ uint32_t modifiers = EVENTFLAG_NONE;
+ if (keyboardModifiers.testFlag(Qt::ShiftModifier))
+ {
+ modifiers |= EVENTFLAG_SHIFT_DOWN;
+ }
+ if (keyboardModifiers.testFlag(Qt::ControlModifier))
+ {
+ modifiers |= EVENTFLAG_CONTROL_DOWN;
+ }
+ if (keyboardModifiers.testFlag(Qt::AltModifier))
+ {
+ modifiers |= EVENTFLAG_ALT_DOWN;
+ }
+ if (keyboardModifiers.testFlag(Qt::MetaModifier))
+ {
+ modifiers |= EVENTFLAG_COMMAND_DOWN;
+ }
+ if (mouseButtons.testFlag(Qt::LeftButton))
+ {
+ modifiers |= EVENTFLAG_LEFT_MOUSE_BUTTON;
+ }
+ if (mouseButtons.testFlag(Qt::MiddleButton))
+ {
+ modifiers |= EVENTFLAG_MIDDLE_MOUSE_BUTTON;
+ }
+ if (mouseButtons.testFlag(Qt::RightButton))
+ {
+ modifiers |= EVENTFLAG_RIGHT_MOUSE_BUTTON;
+ }
+ return modifiers;
+ }
+
+ CefMouseEvent toCefMouseEvent(const QPointF& localPosition,
+ Qt::KeyboardModifiers keyboardModifiers,
+ Qt::MouseButtons mouseButtons)
+ {
+ CefMouseEvent event;
+ event.x = static_cast<int>(std::lround(localPosition.x()));
+ event.y = static_cast<int>(std::lround(localPosition.y()));
+ event.modifiers = toCefModifiers(keyboardModifiers, mouseButtons);
+ return event;
+ }
+
+ cef_mouse_button_type_t toCefMouseButton(Qt::MouseButton button)
+ {
+ switch (button)
+ {
+ case Qt::LeftButton: return MBT_LEFT;
+ case Qt::MiddleButton: return MBT_MIDDLE;
+ case Qt::RightButton: return MBT_RIGHT;
+ default: return MBT_LEFT;
+ }
+ }
+
+ int toWindowsKeyCode(const QKeyEvent* event)
+ {
+ switch (event->key())
+ {
+ case Qt::Key_Backspace: return 0x08;
+ case Qt::Key_Tab: return 0x09;
+ case Qt::Key_Return:
+ case Qt::Key_Enter: return 0x0D;
+ case Qt::Key_Shift: return 0x10;
+ case Qt::Key_Control: return 0x11;
+ case Qt::Key_Alt: return 0x12;
+ case Qt::Key_Pause: return 0x13;
+ case Qt::Key_CapsLock: return 0x14;
+ case Qt::Key_Escape: return 0x1B;
+ case Qt::Key_Space: return 0x20;
+ case Qt::Key_PageUp: return 0x21;
+ case Qt::Key_PageDown: return 0x22;
+ case Qt::Key_End: return 0x23;
+ case Qt::Key_Home: return 0x24;
+ case Qt::Key_Left: return 0x25;
+ case Qt::Key_Up: return 0x26;
+ case Qt::Key_Right: return 0x27;
+ case Qt::Key_Down: return 0x28;
+ case Qt::Key_Insert: return 0x2D;
+ case Qt::Key_Delete: return 0x2E;
+ case Qt::Key_0: return 0x30;
+ case Qt::Key_1: return 0x31;
+ case Qt::Key_2: return 0x32;
+ case Qt::Key_3: return 0x33;
+ case Qt::Key_4: return 0x34;
+ case Qt::Key_5: return 0x35;
+ case Qt::Key_6: return 0x36;
+ case Qt::Key_7: return 0x37;
+ case Qt::Key_8: return 0x38;
+ case Qt::Key_9: return 0x39;
+ case Qt::Key_A: return 0x41;
+ case Qt::Key_B: return 0x42;
+ case Qt::Key_C: return 0x43;
+ case Qt::Key_D: return 0x44;
+ case Qt::Key_E: return 0x45;
+ case Qt::Key_F: return 0x46;
+ case Qt::Key_G: return 0x47;
+ case Qt::Key_H: return 0x48;
+ case Qt::Key_I: return 0x49;
+ case Qt::Key_J: return 0x4A;
+ case Qt::Key_K: return 0x4B;
+ case Qt::Key_L: return 0x4C;
+ case Qt::Key_M: return 0x4D;
+ case Qt::Key_N: return 0x4E;
+ case Qt::Key_O: return 0x4F;
+ case Qt::Key_P: return 0x50;
+ case Qt::Key_Q: return 0x51;
+ case Qt::Key_R: return 0x52;
+ case Qt::Key_S: return 0x53;
+ case Qt::Key_T: return 0x54;
+ case Qt::Key_U: return 0x55;
+ case Qt::Key_V: return 0x56;
+ case Qt::Key_W: return 0x57;
+ case Qt::Key_X: return 0x58;
+ case Qt::Key_Y: return 0x59;
+ case Qt::Key_Z: return 0x5A;
+ case Qt::Key_F1: return 0x70;
+ case Qt::Key_F2: return 0x71;
+ case Qt::Key_F3: return 0x72;
+ case Qt::Key_F4: return 0x73;
+ case Qt::Key_F5: return 0x74;
+ case Qt::Key_F6: return 0x75;
+ case Qt::Key_F7: return 0x76;
+ case Qt::Key_F8: return 0x77;
+ case Qt::Key_F9: return 0x78;
+ case Qt::Key_F10: return 0x79;
+ case Qt::Key_F11: return 0x7A;
+ case Qt::Key_F12: return 0x7B;
+ default: break;
+ }
+
+ if (event->nativeVirtualKey() != 0)
+ {
+ return static_cast<int>(event->nativeVirtualKey());
+ }
+
+ return event->key();
+ }
+
+ bool isDarkPalette(const QPalette& palette)
+ {
+ const QColor baseColor = palette.color(QPalette::Base);
+ const QColor textColor = palette.color(QPalette::Text);
+ return baseColor.lightnessF() < textColor.lightnessF();
+ }
+
+ QString cssColor(const QColor& color)
+ {
+ return color.name(QColor::HexRgb);
+ }
+
+ cef_color_t toCefColor(const QColor& color)
+ {
+ return CefColorSetARGB(static_cast<uint8_t>(color.alpha()),
+ static_cast<uint8_t>(color.red()),
+ static_cast<uint8_t>(color.green()),
+ static_cast<uint8_t>(color.blue()));
+ }
+
+ QString launcherThemeBridgeScript(const QPalette& palette)
+ {
+ const bool prefersDark = isDarkPalette(palette);
+ const QString scheme = prefersDark ? QStringLiteral("dark") : QStringLiteral("light");
+ const QString baseColor = cssColor(palette.color(QPalette::Base));
+ const QString textColor = cssColor(palette.color(QPalette::Text));
+ const QString accentColor = cssColor(palette.color(QPalette::Highlight));
+ const QString surfaceColor = cssColor(palette.color(QPalette::AlternateBase));
+
+ return QStringLiteral(R"JS(
+(() => {
+ const theme = {
+ prefersDark: %1,
+ scheme: '%2',
+ baseColor: '%3',
+ textColor: '%4',
+ accentColor: '%5',
+ surfaceColor: '%6'
+ };
+ window.__projtLauncherTheme = theme;
+
+ const mountPoint = document.head || document.documentElement || document.body;
+ if (mountPoint) {
+ let style = document.getElementById('projt-launcher-theme-bridge');
+ if (!style) {
+ style = document.createElement('style');
+ style.id = 'projt-launcher-theme-bridge';
+ mountPoint.appendChild(style);
+ }
+ style.textContent = `
+ :root {
+ color-scheme: ${theme.scheme};
+ accent-color: ${theme.accentColor};
+ scrollbar-color: ${theme.accentColor} ${theme.surfaceColor};
+ --projt-launcher-base: ${theme.baseColor};
+ --projt-launcher-text: ${theme.textColor};
+ --projt-launcher-accent: ${theme.accentColor};
+ --projt-launcher-surface: ${theme.surfaceColor};
+ }
+ html, body {
+ background: ${theme.baseColor} !important;
+ color: ${theme.textColor} !important;
+ }
+ input, textarea, select, button {
+ color-scheme: ${theme.scheme} !important;
+ }
+ ::selection {
+ background: ${theme.accentColor};
+ }
+ `;
+ }
+
+ if (document.documentElement) {
+ document.documentElement.style.colorScheme = theme.scheme;
+ document.documentElement.style.backgroundColor = theme.baseColor;
+ document.documentElement.style.color = theme.textColor;
+ document.documentElement.dataset.projtLauncherScheme = theme.scheme;
+ }
+
+ if (document.body) {
+ document.body.style.backgroundColor = theme.baseColor;
+ document.body.style.color = theme.textColor;
+ document.body.style.accentColor = theme.accentColor;
+ }
+
+ let themeColorMeta = document.querySelector('meta[name="theme-color"]');
+ if (!themeColorMeta && document.head) {
+ themeColorMeta = document.createElement('meta');
+ themeColorMeta.name = 'theme-color';
+ document.head.appendChild(themeColorMeta);
+ }
+ if (themeColorMeta) {
+ themeColorMeta.content = theme.baseColor;
+ }
+
+ let colorSchemeMeta = document.querySelector('meta[name="color-scheme"]');
+ if (!colorSchemeMeta && document.head) {
+ colorSchemeMeta = document.createElement('meta');
+ colorSchemeMeta.name = 'color-scheme';
+ document.head.appendChild(colorSchemeMeta);
+ }
+ if (colorSchemeMeta) {
+ colorSchemeMeta.content = theme.prefersDark ? 'dark light' : 'light dark';
+ }
+
+ if (window.matchMedia && !window.__projtLauncherMatchMediaPatched) {
+ const originalMatchMedia = window.matchMedia.bind(window);
+ const createList = (query, matches) => {
+ const listeners = new Set();
+ return {
+ matches,
+ media: query,
+ onchange: null,
+ addListener(listener) {
+ if (typeof listener === 'function') listeners.add(listener);
+ },
+ removeListener(listener) {
+ listeners.delete(listener);
+ },
+ addEventListener(type, listener) {
+ if (type === 'change' && typeof listener === 'function') listeners.add(listener);
+ },
+ removeEventListener(type, listener) {
+ if (type === 'change') listeners.delete(listener);
+ },
+ dispatchEvent(event) {
+ listeners.forEach((listener) => listener(event));
+ if (typeof this.onchange === 'function') this.onchange(event);
+ return true;
+ }
+ };
+ };
+
+ window.matchMedia = (query) => {
+ const normalized = String(query).trim().toLowerCase();
+ if (normalized.includes('prefers-color-scheme')) {
+ const currentTheme = window.__projtLauncherTheme || { prefersDark: false };
+ const matches = (normalized.includes('dark') && currentTheme.prefersDark)
+ || (normalized.includes('light') && !currentTheme.prefersDark);
+ return createList(query, matches);
+ }
+ return originalMatchMedia(query);
+ };
+
+ Object.defineProperty(window, '__projtLauncherMatchMediaPatched', {
+ value: true,
+ configurable: true
+ });
+ }
+})();
+)JS")
+ .arg(prefersDark ? QStringLiteral("true") : QStringLiteral("false"),
+ scheme,
+ baseColor,
+ textColor,
+ accentColor,
+ surfaceColor);
+ }
+
+ class CefHubClient final : public CefClient,
+ public CefDisplayHandler,
+ public CefDialogHandler,
+ public CefJSDialogHandler,
+ public CefLifeSpanHandler,
+ public CefLoadHandler,
+ public CefPermissionHandler,
+ public CefRenderHandler,
+ public CefRequestHandler
+ {
+ public:
+ explicit CefHubClient(CefHubView* owner) : m_owner(owner)
+ {}
+
+ bool isPrimaryBrowser(CefRefPtr<CefBrowser> browser) const
+ {
+ return browser && m_browser && browser->GetIdentifier() == m_browser->GetIdentifier();
+ }
+
+ bool isUnexpectedSecondaryBrowser(CefRefPtr<CefBrowser> browser) const
+ {
+ return browser && m_browser && browser->GetIdentifier() != m_browser->GetIdentifier();
+ }
+
+ void queuePopupRequest(const QUrl& url)
+ {
+ if (!m_owner || !url.isValid() || url.isEmpty() || url == QUrl(QStringLiteral("about:blank")))
+ {
+ return;
+ }
+
+ QMetaObject::invokeMethod(
+ m_owner,
+ [owner = m_owner, url]() { owner->handlePopupRequest(url); },
+ Qt::QueuedConnection);
+ }
+
+ CefRefPtr<CefDisplayHandler> GetDisplayHandler() override
+ {
+ return this;
+ }
+
+ CefRefPtr<CefDialogHandler> GetDialogHandler() override
+ {
+ return this;
+ }
+
+ CefRefPtr<CefJSDialogHandler> GetJSDialogHandler() override
+ {
+ return this;
+ }
+
+ CefRefPtr<CefLifeSpanHandler> GetLifeSpanHandler() override
+ {
+ return this;
+ }
+
+ CefRefPtr<CefLoadHandler> GetLoadHandler() override
+ {
+ return this;
+ }
+
+ CefRefPtr<CefPermissionHandler> GetPermissionHandler() override
+ {
+ return this;
+ }
+
+ CefRefPtr<CefRenderHandler> GetRenderHandler() override
+ {
+ return this;
+ }
+
+ CefRefPtr<CefRequestHandler> GetRequestHandler() override
+ {
+ return this;
+ }
+
+ bool GetRootScreenRect(CefRefPtr<CefBrowser>, CefRect& rect) override
+ {
+ if (!m_owner)
+ {
+ return false;
+ }
+
+ const QRect globalRect(m_owner->mapToGlobal(QPoint(0, 0)), m_owner->size());
+ rect = CefRect(globalRect.x(),
+ globalRect.y(),
+ std::max(1, globalRect.width()),
+ std::max(1, globalRect.height()));
+ return true;
+ }
+
+ void GetViewRect(CefRefPtr<CefBrowser>, CefRect& rect) override
+ {
+ if (!m_owner)
+ {
+ rect = CefRect(0, 0, 1, 1);
+ return;
+ }
+
+ rect = CefRect(0, 0, std::max(1, m_owner->width()), std::max(1, m_owner->height()));
+ }
+
+ bool GetScreenPoint(CefRefPtr<CefBrowser>, int viewX, int viewY, int& screenX, int& screenY) override
+ {
+ if (!m_owner)
+ {
+ return false;
+ }
+
+ const QPoint globalPoint = m_owner->mapToGlobal(QPoint(viewX, viewY));
+ screenX = globalPoint.x();
+ screenY = globalPoint.y();
+ return true;
+ }
+
+ bool GetScreenInfo(CefRefPtr<CefBrowser>, CefScreenInfo& screen_info) override
+ {
+ if (!m_owner)
+ {
+ return false;
+ }
+
+ const QRect globalRect(m_owner->mapToGlobal(QPoint(0, 0)), m_owner->size());
+ const qreal scaleFactor = m_owner->devicePixelRatioF();
+
+ screen_info.device_scale_factor = scaleFactor;
+ screen_info.depth = 32;
+ screen_info.depth_per_component = 8;
+ screen_info.is_monochrome = false;
+ screen_info.rect = CefRect(globalRect.x(),
+ globalRect.y(),
+ std::max(1, globalRect.width()),
+ std::max(1, globalRect.height()));
+ screen_info.available_rect = screen_info.rect;
+ return true;
+ }
+
+ void OnPopupShow(CefRefPtr<CefBrowser>, bool show) override
+ {
+ if (m_owner)
+ {
+ m_owner->handlePopupVisibility(show);
+ }
+ }
+
+ void OnPopupSize(CefRefPtr<CefBrowser>, const CefRect& rect) override
+ {
+ if (m_owner)
+ {
+ m_owner->handlePopupRect(QRect(rect.x, rect.y, rect.width, rect.height));
+ }
+ }
+
+ void OnPaint(CefRefPtr<CefBrowser>,
+ PaintElementType type,
+ const RectList& dirtyRects,
+ const void* buffer,
+ int width,
+ int height) override
+ {
+ if (!m_owner || !buffer || width <= 0 || height <= 0)
+ {
+ return;
+ }
+
+ QImage image(static_cast<const uchar*>(buffer), width, height, QImage::Format_ARGB32);
+ QImage copy = image.copy();
+ copy.setDevicePixelRatio(m_owner->devicePixelRatioF());
+
+ QVector<QRect> qtDirtyRects;
+ qtDirtyRects.reserve(static_cast<qsizetype>(dirtyRects.size()));
+ for (const auto& rect : dirtyRects)
+ {
+ qtDirtyRects.append(QRect(rect.x, rect.y, rect.width, rect.height));
+ }
+
+ m_owner->handlePaint(type == PET_POPUP, copy, qtDirtyRects);
+ }
+
+ void OnAddressChange(CefRefPtr<CefBrowser> cefBrowser, CefRefPtr<CefFrame> frame, const CefString& url) override
+ {
+ if (!m_owner || !frame || !frame->IsMain())
+ {
+ return;
+ }
+ const QUrl qtUrl(QString::fromStdString(url.ToString()));
+ if (cefBrowser && (cefBrowser->IsPopup() || isUnexpectedSecondaryBrowser(cefBrowser)))
+ {
+ queuePopupRequest(qtUrl);
+ cefBrowser->GetHost()->CloseBrowser(true);
+ return;
+ }
+ if (cefBrowser && !isPrimaryBrowser(cefBrowser))
+ {
+ return;
+ }
+ QMetaObject::invokeMethod(
+ m_owner,
+ [owner = m_owner, qtUrl]() { owner->handleAddressChange(qtUrl); },
+ Qt::QueuedConnection);
+ }
+
+ void OnTitleChange(CefRefPtr<CefBrowser> browser, const CefString& title) override
+ {
+ if (browser && (browser->IsPopup() || !isPrimaryBrowser(browser)))
+ {
+ return;
+ }
+ const QString qtTitle = QString::fromStdString(title.ToString());
+ QMetaObject::invokeMethod(
+ m_owner,
+ [owner = m_owner, qtTitle]() { owner->handleTitleChange(qtTitle); },
+ Qt::QueuedConnection);
+ }
+
+ void OnLoadingStateChange(CefRefPtr<CefBrowser> browser,
+ bool isLoading,
+ bool canGoBack,
+ bool canGoForward) override
+ {
+ if (browser && browser->IsPopup())
+ {
+ return;
+ }
+ if (browser && isUnexpectedSecondaryBrowser(browser))
+ {
+ if (!isLoading)
+ {
+ const QUrl qtUrl(QString::fromStdString(browser->GetMainFrame()->GetURL().ToString()));
+ queuePopupRequest(qtUrl);
+ browser->GetHost()->CloseBrowser(true);
+ }
+ return;
+ }
+ if (browser && !isPrimaryBrowser(browser))
+ {
+ return;
+ }
+ QMetaObject::invokeMethod(
+ m_owner,
+ [owner = m_owner, isLoading, canGoBack, canGoForward]()
+ { owner->handleLoadingState(isLoading, canGoBack, canGoForward); },
+ Qt::QueuedConnection);
+ }
+
+ void OnLoadStart(CefRefPtr<CefBrowser> browser, CefRefPtr<CefFrame> frame, TransitionType) override
+ {
+ if (!m_owner || !frame || !frame->IsMain())
+ {
+ return;
+ }
+ if (browser && (browser->IsPopup() || !isPrimaryBrowser(browser)))
+ {
+ return;
+ }
+
+ m_owner->applyLauncherThemeToFrame(frame);
+ }
+
+ void OnLoadEnd(CefRefPtr<CefBrowser> browser, CefRefPtr<CefFrame> frame, int) override
+ {
+ if (!m_owner || !frame || !frame->IsMain())
+ {
+ return;
+ }
+ if (browser && (browser->IsPopup() || !isPrimaryBrowser(browser)))
+ {
+ return;
+ }
+ QMetaObject::invokeMethod(
+ m_owner,
+ [owner = m_owner]() { owner->handleLoadFinished(true); },
+ Qt::QueuedConnection);
+ }
+
+ void OnLoadError(CefRefPtr<CefBrowser> browser,
+ CefRefPtr<CefFrame> frame,
+ ErrorCode,
+ const CefString&,
+ const CefString&) override
+ {
+ if (!m_owner || !frame || !frame->IsMain())
+ {
+ return;
+ }
+ if (browser && (browser->IsPopup() || !isPrimaryBrowser(browser)))
+ {
+ return;
+ }
+ QMetaObject::invokeMethod(
+ m_owner,
+ [owner = m_owner]() { owner->handleLoadFinished(false); },
+ Qt::QueuedConnection);
+ }
+
+ void OnAfterCreated(CefRefPtr<CefBrowser> browser) override
+ {
+ if (!browser)
+ {
+ return;
+ }
+
+ if (!m_browser)
+ {
+ m_browser = browser;
+ return;
+ }
+
+ if (browser->IsPopup() || isUnexpectedSecondaryBrowser(browser))
+ {
+ if (m_pendingPopupUrl.isValid())
+ {
+ const QUrl qtUrl = m_pendingPopupUrl;
+ m_pendingPopupUrl = QUrl();
+ queuePopupRequest(qtUrl);
+ }
+ else
+ {
+ const QUrl qtUrl(QString::fromStdString(browser->GetMainFrame()->GetURL().ToString()));
+ queuePopupRequest(qtUrl);
+ }
+ browser->GetHost()->CloseBrowser(true);
+ }
+ }
+
+ bool OnBeforePopup(CefRefPtr<CefBrowser>,
+ CefRefPtr<CefFrame>,
+ int,
+ const CefString& target_url,
+ const CefString&,
+ cef_window_open_disposition_t,
+ bool,
+ const CefPopupFeatures&,
+ CefWindowInfo&,
+ CefRefPtr<CefClient>&,
+ CefBrowserSettings&,
+ CefRefPtr<CefDictionaryValue>&,
+ bool*) override
+ {
+ if (!m_owner)
+ {
+ return true;
+ }
+
+ const QUrl qtUrl(QString::fromStdString(target_url.ToString()));
+ if (qtUrl.isValid() && !qtUrl.isEmpty())
+ {
+ m_pendingPopupUrl = qtUrl;
+ queuePopupRequest(qtUrl);
+ }
+ return true;
+ }
+
+ bool OnOpenURLFromTab(CefRefPtr<CefBrowser>,
+ CefRefPtr<CefFrame>,
+ const CefString& target_url,
+ cef_window_open_disposition_t target_disposition,
+ bool) override
+ {
+ if (!m_owner)
+ {
+ return false;
+ }
+
+ switch (target_disposition)
+ {
+ case CEF_WOD_SINGLETON_TAB:
+ case CEF_WOD_NEW_FOREGROUND_TAB:
+ case CEF_WOD_NEW_BACKGROUND_TAB:
+ case CEF_WOD_SWITCH_TO_TAB:
+ case CEF_WOD_NEW_POPUP:
+ case CEF_WOD_NEW_WINDOW:
+ case CEF_WOD_OFF_THE_RECORD:
+ case CEF_WOD_UNKNOWN:
+ {
+ const QUrl qtUrl(QString::fromStdString(target_url.ToString()));
+ if (qtUrl.isValid() && !qtUrl.isEmpty())
+ {
+ m_pendingPopupUrl = qtUrl;
+ queuePopupRequest(qtUrl);
+ return true;
+ }
+ break;
+ }
+ default: break;
+ }
+
+ return false;
+ }
+
+ bool OnFileDialog(CefRefPtr<CefBrowser>,
+ FileDialogMode,
+ const CefString& title,
+ const CefString&,
+ const std::vector<CefString>&,
+ const std::vector<CefString>&,
+ const std::vector<CefString>&,
+ CefRefPtr<CefFileDialogCallback> callback) override
+ {
+ qWarning() << "[LauncherHub][CEF] Blocked file dialog:" << QString::fromStdString(title.ToString());
+ if (callback)
+ {
+ callback->Cancel();
+ }
+ return true;
+ }
+
+ bool OnJSDialog(CefRefPtr<CefBrowser>,
+ const CefString& origin_url,
+ JSDialogType,
+ const CefString& message_text,
+ const CefString&,
+ CefRefPtr<CefJSDialogCallback> callback,
+ bool& suppress_message) override
+ {
+ qWarning() << "[LauncherHub][CEF] Blocked JS dialog from" << QString::fromStdString(origin_url.ToString())
+ << ":" << QString::fromStdString(message_text.ToString());
+ suppress_message = true;
+ if (callback)
+ {
+ callback->Continue(false, CefString());
+ }
+ return true;
+ }
+
+ bool OnBeforeUnloadDialog(CefRefPtr<CefBrowser>,
+ const CefString& message_text,
+ bool,
+ CefRefPtr<CefJSDialogCallback> callback) override
+ {
+ qWarning() << "[LauncherHub][CEF] Auto-accepted beforeunload dialog:"
+ << QString::fromStdString(message_text.ToString());
+ if (callback)
+ {
+ callback->Continue(true, CefString());
+ }
+ return true;
+ }
+
+ bool OnRequestMediaAccessPermission(CefRefPtr<CefBrowser>,
+ CefRefPtr<CefFrame>,
+ const CefString& requesting_origin,
+ uint32_t,
+ CefRefPtr<CefMediaAccessCallback> callback) override
+ {
+ qWarning() << "[LauncherHub][CEF] Blocked media permission request from"
+ << QString::fromStdString(requesting_origin.ToString());
+ if (callback)
+ {
+ callback->Cancel();
+ }
+ return true;
+ }
+
+ bool OnShowPermissionPrompt(CefRefPtr<CefBrowser>,
+ uint64_t,
+ const CefString& requesting_origin,
+ uint32_t,
+ CefRefPtr<CefPermissionPromptCallback> callback) override
+ {
+ qWarning() << "[LauncherHub][CEF] Blocked permission prompt from"
+ << QString::fromStdString(requesting_origin.ToString());
+ if (callback)
+ {
+ callback->Continue(CEF_PERMISSION_RESULT_DENY);
+ }
+ return true;
+ }
+
+ void OnBeforeClose(CefRefPtr<CefBrowser> browser) override
+ {
+ if (browser && browser->IsPopup())
+ {
+ return;
+ }
+ if (browser && isUnexpectedSecondaryBrowser(browser))
+ {
+ return;
+ }
+ QMetaObject::invokeMethod(
+ m_owner,
+ [owner = m_owner]() { owner->handleBrowserClosed(); },
+ Qt::QueuedConnection);
+ m_browser = nullptr;
+ }
+
+ CefRefPtr<CefBrowser> browser() const
+ {
+ return m_browser;
+ }
+
+ void detachOwner()
+ {
+ m_owner = nullptr;
+ }
+
+ IMPLEMENT_REFCOUNTING(CefHubClient);
+
+ private:
+ CefHubView* m_owner = nullptr;
+ CefRefPtr<CefBrowser> m_browser;
+ QUrl m_pendingPopupUrl;
+ };
+}
+
+struct CefHubView::Impl
+{
+ CefRefPtr<CefHubClient> client;
+};
+
+CefHubView::CefHubView(QWidget* parent) : HubViewBase(parent), m_impl(new Impl())
+{
+ setAttribute(Qt::WA_NativeWindow);
+ setAttribute(Qt::WA_NoSystemBackground);
+ setAttribute(Qt::WA_OpaquePaintEvent);
+ setAttribute(Qt::WA_InputMethodEnabled);
+ setFocusPolicy(Qt::StrongFocus);
+ setMouseTracking(true);
+}
+
+CefHubView::~CefHubView()
+{
+ if (m_impl && m_impl->client)
+ {
+ m_impl->client->detachOwner();
+ if (m_impl->client->browser() && !m_closing)
+ {
+ m_closing = true;
+ m_impl->client->browser()->GetHost()->CloseBrowser(true);
+ }
+ }
+ delete m_impl;
+}
+
+void CefHubView::setUrl(const QUrl& url)
+{
+ m_url = url;
+ ensureBrowser();
+
+ if (m_impl && m_impl->client && m_impl->client->browser() && m_url.isValid())
+ {
+ m_impl->client->browser()->GetMainFrame()->LoadURL(m_url.toString().toStdString());
+ }
+}
+
+QUrl CefHubView::url() const
+{
+ return m_url;
+}
+
+bool CefHubView::canGoBack() const
+{
+ return m_canGoBack;
+}
+
+bool CefHubView::canGoForward() const
+{
+ return m_canGoForward;
+}
+
+void CefHubView::setActive(bool active)
+{
+ m_active = active;
+ if (m_impl && m_impl->client && m_impl->client->browser())
+ {
+ auto host = m_impl->client->browser()->GetHost();
+ host->WasHidden(!m_active || isHidden());
+ if (m_active && !isHidden())
+ {
+ host->Invalidate(PET_VIEW);
+ }
+ }
+}
+
+void CefHubView::back()
+{
+ if (m_impl && m_impl->client && m_impl->client->browser() && m_canGoBack)
+ {
+ m_impl->client->browser()->GoBack();
+ }
+}
+
+void CefHubView::forward()
+{
+ if (m_impl && m_impl->client && m_impl->client->browser() && m_canGoForward)
+ {
+ m_impl->client->browser()->GoForward();
+ }
+}
+
+void CefHubView::reload()
+{
+ if (m_impl && m_impl->client && m_impl->client->browser())
+ {
+ m_impl->client->browser()->Reload();
+ }
+}
+
+void CefHubView::paintEvent(QPaintEvent* event)
+{
+ Q_UNUSED(event);
+
+ QPainter painter(this);
+ painter.fillRect(rect(), palette().brush(QPalette::Base));
+
+ if (!m_viewImage.isNull())
+ {
+ painter.drawImage(QPoint(0, 0), m_viewImage);
+ }
+
+ if (m_popupVisible && !m_popupImage.isNull())
+ {
+ painter.drawImage(m_popupRect.topLeft(), m_popupImage);
+ }
+}
+
+void CefHubView::changeEvent(QEvent* event)
+{
+ HubViewBase::changeEvent(event);
+ if (!event)
+ {
+ return;
+ }
+
+ if (event->type() == QEvent::PaletteChange || event->type() == QEvent::ApplicationPaletteChange)
+ {
+ applyLauncherTheme();
+ }
+}
+
+void CefHubView::resizeEvent(QResizeEvent* event)
+{
+ HubViewBase::resizeEvent(event);
+ if (m_impl && m_impl->client && m_impl->client->browser())
+ {
+ auto host = m_impl->client->browser()->GetHost();
+ host->NotifyMoveOrResizeStarted();
+ host->NotifyScreenInfoChanged();
+ host->WasResized();
+ }
+ else
+ {
+ QTimer::singleShot(0, this, &CefHubView::ensureBrowser);
+ }
+}
+
+void CefHubView::showEvent(QShowEvent* event)
+{
+ HubViewBase::showEvent(event);
+ QTimer::singleShot(0, this, &CefHubView::ensureBrowser);
+ if (m_impl && m_impl->client && m_impl->client->browser())
+ {
+ m_impl->client->browser()->GetHost()->WasHidden(!m_active);
+ }
+}
+
+void CefHubView::hideEvent(QHideEvent* event)
+{
+ HubViewBase::hideEvent(event);
+ if (m_impl && m_impl->client && m_impl->client->browser())
+ {
+ m_impl->client->browser()->GetHost()->WasHidden(true);
+ }
+}
+
+void CefHubView::focusInEvent(QFocusEvent* event)
+{
+ HubViewBase::focusInEvent(event);
+ if (m_impl && m_impl->client && m_impl->client->browser())
+ {
+ m_impl->client->browser()->GetHost()->SetFocus(true);
+ }
+}
+
+void CefHubView::focusOutEvent(QFocusEvent* event)
+{
+ HubViewBase::focusOutEvent(event);
+ if (m_impl && m_impl->client && m_impl->client->browser())
+ {
+ m_impl->client->browser()->GetHost()->SetFocus(false);
+ }
+}
+
+void CefHubView::mouseMoveEvent(QMouseEvent* event)
+{
+ HubViewBase::mouseMoveEvent(event);
+ if (!(m_impl && m_impl->client && m_impl->client->browser()))
+ {
+ return;
+ }
+
+ m_impl->client->browser()->GetHost()->SendMouseMoveEvent(
+ toCefMouseEvent(event->position(), event->modifiers(), event->buttons()),
+ false);
+}
+
+void CefHubView::mousePressEvent(QMouseEvent* event)
+{
+ HubViewBase::mousePressEvent(event);
+ if (!(m_impl && m_impl->client && m_impl->client->browser()))
+ {
+ return;
+ }
+
+ setFocus(Qt::MouseFocusReason);
+ auto host = m_impl->client->browser()->GetHost();
+ host->SetFocus(true);
+ host->SendMouseClickEvent(toCefMouseEvent(event->position(), event->modifiers(), event->buttons()),
+ toCefMouseButton(event->button()),
+ false,
+ 1);
+}
+
+void CefHubView::mouseReleaseEvent(QMouseEvent* event)
+{
+ HubViewBase::mouseReleaseEvent(event);
+ if (!(m_impl && m_impl->client && m_impl->client->browser()))
+ {
+ return;
+ }
+
+ m_impl->client->browser()->GetHost()->SendMouseClickEvent(
+ toCefMouseEvent(event->position(), event->modifiers(), event->buttons()),
+ toCefMouseButton(event->button()),
+ true,
+ 1);
+}
+
+void CefHubView::wheelEvent(QWheelEvent* event)
+{
+ HubViewBase::wheelEvent(event);
+ if (!(m_impl && m_impl->client && m_impl->client->browser()))
+ {
+ return;
+ }
+
+ const QPoint angleDelta = event->angleDelta();
+ m_impl->client->browser()->GetHost()->SendMouseWheelEvent(
+ toCefMouseEvent(event->position(), event->modifiers(), event->buttons()),
+ angleDelta.x(),
+ angleDelta.y());
+}
+
+void CefHubView::keyPressEvent(QKeyEvent* event)
+{
+ HubViewBase::keyPressEvent(event);
+ if (!(m_impl && m_impl->client && m_impl->client->browser()))
+ {
+ return;
+ }
+
+ CefKeyEvent keyEvent;
+ keyEvent.type = KEYEVENT_RAWKEYDOWN;
+ keyEvent.modifiers = toCefModifiers(event->modifiers(), QApplication::mouseButtons());
+ if (event->isAutoRepeat())
+ {
+ keyEvent.modifiers |= EVENTFLAG_IS_REPEAT;
+ }
+ keyEvent.windows_key_code = toWindowsKeyCode(event);
+ keyEvent.native_key_code = static_cast<int>(event->nativeScanCode());
+ keyEvent.is_system_key = 0;
+ keyEvent.character = event->text().isEmpty() ? 0 : event->text().at(0).unicode();
+ keyEvent.unmodified_character = keyEvent.character;
+ keyEvent.focus_on_editable_field = 0;
+
+ auto host = m_impl->client->browser()->GetHost();
+ host->SendKeyEvent(keyEvent);
+
+ if (!event->text().isEmpty())
+ {
+ CefKeyEvent charEvent = keyEvent;
+ charEvent.type = KEYEVENT_CHAR;
+ host->SendKeyEvent(charEvent);
+ }
+}
+
+void CefHubView::keyReleaseEvent(QKeyEvent* event)
+{
+ HubViewBase::keyReleaseEvent(event);
+ if (!(m_impl && m_impl->client && m_impl->client->browser()))
+ {
+ return;
+ }
+
+ CefKeyEvent keyEvent;
+ keyEvent.type = KEYEVENT_KEYUP;
+ keyEvent.modifiers = toCefModifiers(event->modifiers(), QApplication::mouseButtons());
+ keyEvent.windows_key_code = toWindowsKeyCode(event);
+ keyEvent.native_key_code = static_cast<int>(event->nativeScanCode());
+ keyEvent.is_system_key = 0;
+ keyEvent.character = event->text().isEmpty() ? 0 : event->text().at(0).unicode();
+ keyEvent.unmodified_character = keyEvent.character;
+ keyEvent.focus_on_editable_field = 0;
+
+ m_impl->client->browser()->GetHost()->SendKeyEvent(keyEvent);
+}
+
+void CefHubView::leaveEvent(QEvent* event)
+{
+ HubViewBase::leaveEvent(event);
+ if (!(m_impl && m_impl->client && m_impl->client->browser()))
+ {
+ return;
+ }
+
+ m_impl->client->browser()->GetHost()->SendMouseMoveEvent(
+ toCefMouseEvent(QPointF(-1, -1), Qt::NoModifier, Qt::NoButton),
+ true);
+}
+
+void CefHubView::ensureBrowser()
+{
+ if (m_created || width() <= 0 || height() <= 0 || !projt::cef::Runtime::instance().isInitialized())
+ {
+ return;
+ }
+
+ m_impl->client = new CefHubClient(this);
+
+ CefWindowInfo windowInfo;
+ windowInfo.SetAsWindowless(static_cast<CefWindowHandle>(winId()));
+
+ CefBrowserSettings browserSettings;
+ browserSettings.windowless_frame_rate = 60;
+ const QColor baseColor = palette().color(QPalette::Base);
+ browserSettings.background_color = toCefColor(baseColor);
+ const QString initialUrl = m_url.isValid() ? m_url.toString() : QStringLiteral("about:blank");
+ auto browser = CefBrowserHost::CreateBrowserSync(windowInfo,
+ m_impl->client,
+ initialUrl.toStdString(),
+ browserSettings,
+ nullptr,
+ nullptr);
+ if (!browser)
+ {
+ emit loadFinished(false);
+ return;
+ }
+
+ m_created = true;
+ browser->GetHost()->SetWindowlessFrameRate(60);
+ browser->GetHost()->WasHidden(!m_active || isHidden());
+ browser->GetHost()->NotifyScreenInfoChanged();
+ applyLauncherTheme();
+ browser->GetHost()->Invalidate(PET_VIEW);
+ syncNavigationState();
+}
+
+void CefHubView::syncNavigationState()
+{
+ if (!m_impl || !m_impl->client || !m_impl->client->browser())
+ {
+ return;
+ }
+
+ const bool oldCanGoBack = m_canGoBack;
+ const bool oldCanGoForward = m_canGoForward;
+ m_canGoBack = m_impl->client->browser()->CanGoBack();
+ m_canGoForward = m_impl->client->browser()->CanGoForward();
+ if (oldCanGoBack != m_canGoBack || oldCanGoForward != m_canGoForward)
+ {
+ emit navigationStateChanged();
+ }
+}
+
+void CefHubView::handleAddressChange(const QUrl& url)
+{
+ m_url = url;
+ emit urlChanged(m_url);
+ syncNavigationState();
+}
+
+void CefHubView::handleTitleChange(const QString& title)
+{
+ m_title = title;
+ emit titleChanged(m_title);
+}
+
+void CefHubView::handleLoadingState(bool isLoading, bool canGoBack, bool canGoForward)
+{
+ Q_UNUSED(isLoading);
+ m_canGoBack = canGoBack;
+ m_canGoForward = canGoForward;
+ emit navigationStateChanged();
+}
+
+void CefHubView::handleLoadFinished(bool ok)
+{
+ applyLauncherTheme();
+ syncNavigationState();
+ emit loadFinished(ok);
+}
+
+void CefHubView::handleBrowserClosed()
+{
+ m_created = false;
+ m_closing = true;
+ emit navigationStateChanged();
+}
+
+void CefHubView::handlePopupRequest(const QUrl& url)
+{
+ if (!url.isValid())
+ {
+ return;
+ }
+
+ emit newTabRequested(url);
+}
+
+void CefHubView::handlePopupVisibility(bool visible)
+{
+ m_popupVisible = visible;
+ if (!m_popupVisible)
+ {
+ m_popupImage = QImage();
+ m_popupRect = QRect();
+ }
+ update();
+}
+
+void CefHubView::handlePopupRect(const QRect& rect)
+{
+ m_popupRect = rect;
+ update();
+}
+
+void CefHubView::handlePaint(bool popup, const QImage& image, const QVector<QRect>& dirtyRects)
+{
+ Q_UNUSED(dirtyRects);
+
+ if (popup)
+ {
+ m_popupImage = image;
+ }
+ else
+ {
+ m_viewImage = image;
+ }
+
+ update();
+}
+
+void CefHubView::applyLauncherTheme()
+{
+ if (!(m_impl && m_impl->client && m_impl->client->browser()))
+ {
+ return;
+ }
+
+ applyLauncherThemeToFrame(m_impl->client->browser()->GetMainFrame());
+
+ auto requestContext = m_impl->client->browser()->GetHost()->GetRequestContext();
+ if (!requestContext)
+ {
+ return;
+ }
+
+ const cef_color_variant_t variant =
+ isDarkPalette(palette()) ? CEF_COLOR_VARIANT_DARK : CEF_COLOR_VARIANT_LIGHT;
+ requestContext->SetChromeColorScheme(variant, toCefColor(palette().color(QPalette::Highlight)));
+}
+
+void CefHubView::applyLauncherThemeToFrame(CefRefPtr<CefFrame> frame)
+{
+ if (!frame)
+ {
+ return;
+ }
+
+ frame->ExecuteJavaScript(launcherThemeBridgeScript(palette()).toStdString(), frame->GetURL(), 0);
+}
+
+#endif
diff --git a/archived/projt-launcher/launcher/ui/widgets/CefHubView.h b/archived/projt-launcher/launcher/ui/widgets/CefHubView.h
new file mode 100644
index 0000000000..70c0f8b44f
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/widgets/CefHubView.h
@@ -0,0 +1,111 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+#pragma once
+
+#include "ui/widgets/HubViewBase.h"
+
+#include <QImage>
+#include <QRect>
+#include <QVector>
+
+#if defined(PROJT_USE_CEF)
+
+#include "include/cef_frame.h"
+
+class QEvent;
+class QChangeEvent;
+class QFocusEvent;
+class QHideEvent;
+class QKeyEvent;
+class QMouseEvent;
+class QPaintEvent;
+class QResizeEvent;
+class QShowEvent;
+class QWheelEvent;
+
+class CefHubView : public HubViewBase
+{
+ Q_OBJECT
+
+ public:
+ explicit CefHubView(QWidget* parent = nullptr);
+ ~CefHubView() override;
+
+ void setUrl(const QUrl& url) override;
+ QUrl url() const override;
+ bool canGoBack() const override;
+ bool canGoForward() const override;
+ void setActive(bool active) override;
+
+ public slots:
+ void back() override;
+ void forward() override;
+ void reload() override;
+
+ protected:
+ void paintEvent(QPaintEvent* event) override;
+ void changeEvent(QEvent* event) override;
+ void resizeEvent(QResizeEvent* event) override;
+ void showEvent(QShowEvent* event) override;
+ void hideEvent(QHideEvent* event) override;
+ void focusInEvent(QFocusEvent* event) override;
+ void focusOutEvent(QFocusEvent* event) override;
+ void mouseMoveEvent(QMouseEvent* event) override;
+ void mousePressEvent(QMouseEvent* event) override;
+ void mouseReleaseEvent(QMouseEvent* event) override;
+ void wheelEvent(QWheelEvent* event) override;
+ void keyPressEvent(QKeyEvent* event) override;
+ void keyReleaseEvent(QKeyEvent* event) override;
+ void leaveEvent(QEvent* event) override;
+
+ public:
+ void ensureBrowser();
+ void syncNavigationState();
+ void handleAddressChange(const QUrl& url);
+ void handleTitleChange(const QString& title);
+ void handleLoadingState(bool isLoading, bool canGoBack, bool canGoForward);
+ void handleLoadFinished(bool ok);
+ void handleBrowserClosed();
+ void handlePopupRequest(const QUrl& url);
+ void handlePopupVisibility(bool visible);
+ void handlePopupRect(const QRect& rect);
+ void handlePaint(bool popup, const QImage& image, const QVector<QRect>& dirtyRects);
+ void applyLauncherTheme();
+ void applyLauncherThemeToFrame(CefRefPtr<CefFrame> frame);
+
+ private:
+ QUrl m_url;
+ QString m_title;
+ bool m_canGoBack = false;
+ bool m_canGoForward = false;
+ bool m_created = false;
+ bool m_closing = false;
+ bool m_active = true;
+ QImage m_viewImage;
+ QImage m_popupImage;
+ QRect m_popupRect;
+ bool m_popupVisible = false;
+
+ struct Impl;
+ Impl* m_impl = nullptr;
+};
+
+#endif
diff --git a/archived/projt-launcher/launcher/ui/widgets/CheckComboBox.cpp b/archived/projt-launcher/launcher/ui/widgets/CheckComboBox.cpp
new file mode 100644
index 0000000000..d3267efb6b
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/widgets/CheckComboBox.cpp
@@ -0,0 +1,250 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * ======================================================================== */
+
+#include "CheckComboBox.h"
+
+#include <QAbstractItemView>
+#include <QBoxLayout>
+#include <QEvent>
+#include <QIdentityProxyModel>
+#include <QKeyEvent>
+#include <QLineEdit>
+#include <QListView>
+#include <QMouseEvent>
+#include <QStringList>
+#include <QStylePainter>
+
+class CheckComboModel : public QIdentityProxyModel
+{
+ Q_OBJECT
+
+ public:
+ explicit CheckComboModel(QObject* parent = nullptr) : QIdentityProxyModel(parent)
+ {}
+
+ virtual Qt::ItemFlags flags(const QModelIndex& index) const
+ {
+ return QIdentityProxyModel::flags(index) | Qt::ItemIsUserCheckable;
+ }
+ virtual QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const
+ {
+ if (role == Qt::CheckStateRole)
+ {
+ auto txt = QIdentityProxyModel::data(index, Qt::DisplayRole).toString();
+ return m_checked.contains(txt) ? Qt::Checked : Qt::Unchecked;
+ }
+ if (role == Qt::DisplayRole)
+ return QIdentityProxyModel::data(index, Qt::DisplayRole);
+ return {};
+ }
+ virtual bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole)
+ {
+ if (role == Qt::CheckStateRole)
+ {
+ auto txt = QIdentityProxyModel::data(index, Qt::DisplayRole).toString();
+ if (m_checked.contains(txt))
+ {
+ m_checked.removeOne(txt);
+ }
+ else
+ {
+ m_checked.push_back(txt);
+ }
+ emit dataChanged(index, index);
+ emit checkStateChanged();
+ return true;
+ }
+ return QIdentityProxyModel::setData(index, value, role);
+ }
+ QStringList getChecked()
+ {
+ return m_checked;
+ }
+
+ signals:
+ void checkStateChanged();
+
+ private:
+ QStringList m_checked;
+};
+
+CheckComboBox::CheckComboBox(QWidget* parent) : QComboBox(parent), m_separator(", ")
+{
+ view()->installEventFilter(this);
+ view()->window()->installEventFilter(this);
+ view()->viewport()->installEventFilter(this);
+ this->installEventFilter(this);
+}
+
+void CheckComboBox::setSourceModel(QAbstractItemModel* new_model)
+{
+ auto proxy = new CheckComboModel(this);
+ proxy->setSourceModel(new_model);
+ model()->disconnect(this);
+ QComboBox::setModel(proxy);
+ connect(this, &QComboBox::activated, this, &CheckComboBox::toggleCheckState);
+ connect(proxy, &CheckComboModel::checkStateChanged, this, &CheckComboBox::emitCheckedItemsChanged);
+ connect(model(), &CheckComboModel::rowsInserted, this, &CheckComboBox::emitCheckedItemsChanged);
+ connect(model(), &CheckComboModel::rowsRemoved, this, &CheckComboBox::emitCheckedItemsChanged);
+}
+
+void CheckComboBox::hidePopup()
+{
+ if (!m_containerMousePress)
+ QComboBox::hidePopup();
+}
+
+void CheckComboBox::emitCheckedItemsChanged()
+{
+ emit checkedItemsChanged(checkedItems());
+}
+
+QString CheckComboBox::defaultText() const
+{
+ return m_default_text;
+}
+
+void CheckComboBox::setDefaultText(const QString& text)
+{
+ m_default_text = text;
+}
+
+QString CheckComboBox::separator() const
+{
+ return m_separator;
+}
+
+void CheckComboBox::setSeparator(const QString& separator)
+{
+ m_separator = separator;
+}
+
+bool CheckComboBox::eventFilter(QObject* receiver, QEvent* event)
+{
+ switch (event->type())
+ {
+ case QEvent::KeyPress:
+ case QEvent::KeyRelease:
+ {
+ QKeyEvent* keyEvent = static_cast<QKeyEvent*>(event);
+ if (receiver == this && (keyEvent->key() == Qt::Key_Up || keyEvent->key() == Qt::Key_Down))
+ {
+ showPopup();
+ return true;
+ }
+ else if (keyEvent->key() == Qt::Key_Enter || keyEvent->key() == Qt::Key_Return
+ || keyEvent->key() == Qt::Key_Escape)
+ {
+ QComboBox::hidePopup();
+ return (keyEvent->key() != Qt::Key_Escape);
+ }
+ break;
+ }
+ case QEvent::MouseButtonPress:
+ {
+ auto ev = static_cast<QMouseEvent*>(event);
+ m_containerMousePress = ev && view()->indexAt(ev->pos()).isValid();
+ break;
+ }
+ case QEvent::Wheel: return receiver == this;
+ default: break;
+ }
+ return false;
+}
+
+void CheckComboBox::toggleCheckState(int index)
+{
+ QVariant value = itemData(index, Qt::CheckStateRole);
+ if (value.isValid())
+ {
+ Qt::CheckState state = static_cast<Qt::CheckState>(value.toInt());
+ setItemData(index, (state == Qt::Unchecked ? Qt::Checked : Qt::Unchecked), Qt::CheckStateRole);
+ }
+ emitCheckedItemsChanged();
+}
+
+Qt::CheckState CheckComboBox::itemCheckState(int index) const
+{
+ return static_cast<Qt::CheckState>(itemData(index, Qt::CheckStateRole).toInt());
+}
+
+void CheckComboBox::setItemCheckState(int index, Qt::CheckState state)
+{
+ setItemData(index, state, Qt::CheckStateRole);
+}
+
+QStringList CheckComboBox::checkedItems() const
+{
+ if (auto* checkModel = dynamic_cast<CheckComboModel*>(model()))
+ return checkModel->getChecked();
+ return {};
+}
+
+void CheckComboBox::setCheckedItems(const QStringList& items)
+{
+ for (auto text : items)
+ {
+ auto index = findText(text);
+ setItemCheckState(index, index != -1 ? Qt::Checked : Qt::Unchecked);
+ }
+}
+
+void CheckComboBox::paintEvent(QPaintEvent*)
+{
+ QStylePainter painter(this);
+ painter.setPen(palette().color(QPalette::Text));
+
+ // draw the combobox frame, focusrect and selected etc.
+ QStyleOptionComboBox opt;
+ initStyleOption(&opt);
+ QStringList items = checkedItems();
+ if (items.isEmpty())
+ opt.currentText = defaultText();
+ else
+ opt.currentText = items.join(separator());
+ painter.drawComplexControl(QStyle::CC_ComboBox, opt);
+
+ // draw the icon and text
+ painter.drawControl(QStyle::CE_ComboBoxLabel, opt);
+}
+
+#include "CheckComboBox.moc"
diff --git a/archived/projt-launcher/launcher/ui/widgets/CheckComboBox.h b/archived/projt-launcher/launcher/ui/widgets/CheckComboBox.h
new file mode 100644
index 0000000000..bde8a7ecd0
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/widgets/CheckComboBox.h
@@ -0,0 +1,89 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * ======================================================================== */
+
+#pragma once
+
+#include <QComboBox>
+#include <QLineEdit>
+
+class CheckComboBox : public QComboBox
+{
+ Q_OBJECT
+
+ public:
+ explicit CheckComboBox(QWidget* parent = nullptr);
+ virtual ~CheckComboBox() = default;
+
+ void hidePopup() override;
+
+ QString defaultText() const;
+ void setDefaultText(const QString& text);
+
+ Qt::CheckState itemCheckState(int index) const;
+ void setItemCheckState(int index, Qt::CheckState state);
+
+ QString separator() const;
+ void setSeparator(const QString& separator);
+
+ QStringList checkedItems() const;
+
+ void setSourceModel(QAbstractItemModel* model);
+
+ public slots:
+ void setCheckedItems(const QStringList& items);
+
+ signals:
+ void checkedItemsChanged(const QStringList& items);
+
+ protected:
+ void paintEvent(QPaintEvent*) override;
+
+ private:
+ void emitCheckedItemsChanged();
+ bool eventFilter(QObject* receiver, QEvent* event) override;
+ void toggleCheckState(int index);
+
+ private:
+ QString m_default_text;
+ QString m_separator;
+ bool m_containerMousePress = false;
+}; \ No newline at end of file
diff --git a/archived/projt-launcher/launcher/ui/widgets/Common.cpp b/archived/projt-launcher/launcher/ui/widgets/Common.cpp
new file mode 100644
index 0000000000..7285fb7d1f
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/widgets/Common.cpp
@@ -0,0 +1,54 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+#include "Common.h"
+
+// Origin: Qt
+// More specifically, this is a trimmed down version on the algorithm in:
+// https://code.woboq.org/qt5/qtbase/src/widgets/styles/qcommonstyle.cpp.html#846
+QList<std::pair<qreal, QString>> viewItemTextLayout(QTextLayout& textLayout, int lineWidth, qreal& height)
+{
+ QList<std::pair<qreal, QString>> lines;
+ height = 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(std::make_pair(line.naturalTextWidth(), str.mid(line.textStart(), line.textLength())));
+ }
+
+ textLayout.endLayout();
+
+ return lines;
+}
diff --git a/archived/projt-launcher/launcher/ui/widgets/Common.h b/archived/projt-launcher/launcher/ui/widgets/Common.h
new file mode 100644
index 0000000000..b123e3685a
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/widgets/Common.h
@@ -0,0 +1,29 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+#pragma once
+
+#include <QTextLayout>
+
+/** Cuts out the text in textLayout into smaller pieces, according to the lineWidth.
+ * Returns a list of pairs, each containing the width of that line and that line's string, respectively.
+ * The total height of those lines is set in the last argument, 'height'.
+ */
+QList<std::pair<qreal, QString>> viewItemTextLayout(QTextLayout& textLayout, int lineWidth, qreal& height);
diff --git a/archived/projt-launcher/launcher/ui/widgets/CustomCommands.cpp b/archived/projt-launcher/launcher/ui/widgets/CustomCommands.cpp
new file mode 100644
index 0000000000..b8fa173ca2
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/widgets/CustomCommands.cpp
@@ -0,0 +1,115 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2025 TheKodeToad <TheKodeToad@proton.me>
+ * Copyright (C) 2022 Tayou <git@tayou.org>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * 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 "CustomCommands.h"
+#include "ui_CustomCommands.h"
+
+CustomCommands::~CustomCommands()
+{
+ delete ui;
+}
+
+CustomCommands::CustomCommands(QWidget* parent) : QWidget(parent), ui(new Ui::CustomCommands)
+{
+ ui->setupUi(this);
+ connect(ui->overrideCheckBox, &QCheckBox::toggled, ui->customCommandsWidget, &QWidget::setEnabled);
+}
+
+void CustomCommands::initialize(bool checkable,
+ bool checked,
+ const QString& prelaunch,
+ const QString& wrapper,
+ const QString& postexit)
+{
+ ui->overrideCheckBox->setVisible(checkable);
+ if (checkable)
+ {
+ ui->overrideCheckBox->setChecked(checked);
+ }
+ ui->preLaunchCmdTextBox->setText(prelaunch);
+ ui->wrapperCmdTextBox->setText(wrapper);
+ ui->postExitCmdTextBox->setText(postexit);
+}
+
+void CustomCommands::retranslate()
+{
+ ui->retranslateUi(this);
+}
+
+bool CustomCommands::checked() const
+{
+ if (!ui->overrideCheckBox->isVisible())
+ return true;
+ return ui->overrideCheckBox->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/archived/projt-launcher/launcher/ui/widgets/CustomCommands.h b/archived/projt-launcher/launcher/ui/widgets/CustomCommands.h
new file mode 100644
index 0000000000..1ef5af2fd9
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/widgets/CustomCommands.h
@@ -0,0 +1,91 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2025 TheKodeToad <TheKodeToad@proton.me>
+ * Copyright (C) 2022 Tayou <git@tayou.org>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * 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>
+
+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);
+
+ void retranslate();
+ bool checked() const;
+ QString prelaunchCommand() const;
+ QString wrapperCommand() const;
+ QString postexitCommand() const;
+
+ private:
+ Ui::CustomCommands* ui;
+};
diff --git a/archived/projt-launcher/launcher/ui/widgets/CustomCommands.ui b/archived/projt-launcher/launcher/ui/widgets/CustomCommands.ui
new file mode 100644
index 0000000000..6c1366c064
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/widgets/CustomCommands.ui
@@ -0,0 +1,158 @@
+<?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="QCheckBox" name="overrideCheckBox">
+ <property name="text">
+ <string>Override &amp;Global Settings</string>
+ </property>
+ <property name="checked">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QWidget" name="customCommandsWidget" native="true">
+ <property name="enabled">
+ <bool>true</bool>
+ </property>
+ <layout class="QGridLayout" name="gridLayout_4">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <item row="0" column="0">
+ <widget class="QLabel" name="labelPreLaunchCmd">
+ <property name="text">
+ <string>&amp;Pre-launch Command</string>
+ </property>
+ <property name="buddy">
+ <cstring>preLaunchCmdTextBox</cstring>
+ </property>
+ </widget>
+ </item>
+ <item row="5" column="0">
+ <spacer name="verticalSpacer_2">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Fixed</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>0</width>
+ <height>6</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item row="1" column="0">
+ <widget class="QLineEdit" name="preLaunchCmdTextBox"/>
+ </item>
+ <item row="4" column="0">
+ <widget class="QLineEdit" name="wrapperCmdTextBox"/>
+ </item>
+ <item row="6" column="0">
+ <widget class="QLabel" name="labelPostExitCmd">
+ <property name="text">
+ <string>P&amp;ost-exit Command</string>
+ </property>
+ <property name="buddy">
+ <cstring>postExitCmdTextBox</cstring>
+ </property>
+ </widget>
+ </item>
+ <item row="7" column="0">
+ <widget class="QLineEdit" name="postExitCmdTextBox"/>
+ </item>
+ <item row="3" column="0">
+ <widget class="QLabel" name="labelWrapperCmd">
+ <property name="text">
+ <string>&amp;Wrapper Command</string>
+ </property>
+ <property name="buddy">
+ <cstring>wrapperCmdTextBox</cstring>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="0">
+ <spacer name="verticalSpacer_3">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Fixed</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>0</width>
+ <height>6</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="labelCustomCmdsDescription">
+ <property name="text">
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Pre-launch command runs before the instance launches and post-exit command runs after it exits.&lt;/p&gt;&lt;p&gt;Both will be run in the launcher's working folder with extra environment variables:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;$INST_NAME - Name of the instance&lt;/li&gt;&lt;li&gt;$INST_ID - ID of the instance (its folder name)&lt;/li&gt;&lt;li&gt;$INST_DIR - absolute path of the instance&lt;/li&gt;&lt;li&gt;$INST_MC_DIR - absolute path of Minecraft&lt;/li&gt;&lt;li&gt;$INST_JAVA - Java binary used for launch&lt;/li&gt;&lt;li&gt;$INST_JAVA_ARGS - command-line parameters used for launch (warning: will not work correctly if arguments contain spaces)&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/archived/projt-launcher/launcher/ui/widgets/EnvironmentVariables.cpp b/archived/projt-launcher/launcher/ui/widgets/EnvironmentVariables.cpp
new file mode 100644
index 0000000000..19a66332e4
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/widgets/EnvironmentVariables.cpp
@@ -0,0 +1,148 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * ======================================================================== */
+
+#include <QKeyEvent>
+
+#include "Application.h"
+#include "EnvironmentVariables.h"
+#include "ui/dialogs/CustomMessageBox.h"
+#include "ui_EnvironmentVariables.h"
+
+EnvironmentVariables::EnvironmentVariables(QWidget* parent) : QWidget(parent), ui(new Ui::EnvironmentVariables)
+{
+ ui->setupUi(this);
+ ui->list->installEventFilter(this);
+
+ ui->list->sortItems(0, Qt::AscendingOrder);
+ ui->list->setSortingEnabled(true);
+ ui->list->header()->resizeSections(QHeaderView::Interactive);
+ ui->list->header()->resizeSection(0, 200);
+
+ connect(ui->add,
+ &QPushButton::clicked,
+ this,
+ [this]
+ {
+ auto item = new QTreeWidgetItem(ui->list);
+ item->setText(0, "ENV_VAR");
+ item->setText(1, "value");
+ item->setFlags(item->flags() | Qt::ItemIsEditable);
+ ui->list->addTopLevelItem(item);
+ ui->list->selectionModel()->select(ui->list->model()->index(ui->list->indexOfTopLevelItem(item), 0),
+ QItemSelectionModel::ClearAndSelect
+ | QItemSelectionModel::SelectionFlag::Rows);
+ ui->list->editItem(item);
+ });
+
+ connect(ui->remove,
+ &QPushButton::clicked,
+ this,
+ [this]
+ {
+ for (QTreeWidgetItem* item : ui->list->selectedItems())
+ ui->list->takeTopLevelItem(ui->list->indexOfTopLevelItem(item));
+ });
+
+ connect(ui->clear, &QPushButton::clicked, this, [this] { ui->list->clear(); });
+
+ connect(ui->overrideCheckBox, &QCheckBox::toggled, ui->settingsWidget, &QWidget::setEnabled);
+}
+
+EnvironmentVariables::~EnvironmentVariables()
+{
+ delete ui;
+}
+
+void EnvironmentVariables::initialize(bool instance, bool override, const QMap<QString, QVariant>& value)
+{
+ // update widgets to settings
+ ui->overrideCheckBox->setVisible(instance);
+ ui->overrideCheckBox->setChecked(override);
+
+ // populate
+ ui->list->clear();
+ for (auto iter = value.begin(); iter != value.end(); iter++)
+ {
+ auto item = new QTreeWidgetItem(ui->list);
+ item->setText(0, iter.key());
+ item->setText(1, iter.value().toString());
+ item->setFlags(item->flags() | Qt::ItemIsEditable);
+ ui->list->addTopLevelItem(item);
+ }
+}
+
+bool EnvironmentVariables::eventFilter(QObject* watched, QEvent* event)
+{
+ if (watched == ui->list && event->type() == QEvent::KeyPress)
+ {
+ const QKeyEvent* keyEvent = (QKeyEvent*)event;
+ if (keyEvent->key() == Qt::Key_Delete)
+ {
+ emit ui->remove->clicked();
+ return true;
+ }
+ }
+
+ return QObject::eventFilter(watched, event);
+}
+
+void EnvironmentVariables::retranslate()
+{
+ ui->retranslateUi(this);
+}
+
+bool EnvironmentVariables::override() const
+{
+ if (!ui->overrideCheckBox->isVisible())
+ return false;
+ return ui->overrideCheckBox->isChecked();
+}
+
+QMap<QString, QVariant> EnvironmentVariables::value() const
+{
+ QMap<QString, QVariant> result;
+ QTreeWidgetItem* item = ui->list->topLevelItem(0);
+ for (int i = 1; item != nullptr; item = ui->list->topLevelItem(i++))
+ result[item->text(0)] = item->text(1);
+
+ return result;
+}
diff --git a/archived/projt-launcher/launcher/ui/widgets/EnvironmentVariables.h b/archived/projt-launcher/launcher/ui/widgets/EnvironmentVariables.h
new file mode 100644
index 0000000000..09ab495744
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/widgets/EnvironmentVariables.h
@@ -0,0 +1,69 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * ======================================================================== */
+
+#pragma once
+
+#include <QMap>
+#include <QWidget>
+
+namespace Ui
+{
+ class EnvironmentVariables;
+}
+
+class EnvironmentVariables : public QWidget
+{
+ Q_OBJECT
+
+ public:
+ explicit EnvironmentVariables(QWidget* state = nullptr);
+ ~EnvironmentVariables() override;
+ void initialize(bool instance, bool override, const QMap<QString, QVariant>& value);
+ bool eventFilter(QObject* watched, QEvent* event) override;
+
+ void retranslate();
+ bool override() const;
+ QMap<QString, QVariant> value() const;
+
+ private:
+ Ui::EnvironmentVariables* ui;
+};
diff --git a/archived/projt-launcher/launcher/ui/widgets/EnvironmentVariables.ui b/archived/projt-launcher/launcher/ui/widgets/EnvironmentVariables.ui
new file mode 100644
index 0000000000..cc52b5d10d
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/widgets/EnvironmentVariables.ui
@@ -0,0 +1,122 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>EnvironmentVariables</class>
+ <widget class="QWidget" name="EnvironmentVariables">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>565</width>
+ <height>410</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Form</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <widget class="QCheckBox" name="overrideCheckBox">
+ <property name="text">
+ <string>Override &amp;Global Settings</string>
+ </property>
+ <property name="checked">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QWidget" name="settingsWidget" native="true">
+ <property name="enabled">
+ <bool>true</bool>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_2">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <layout class="QHBoxLayout" name="buttons">
+ <item>
+ <widget class="QPushButton" name="add">
+ <property name="text">
+ <string>&amp;Add</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="remove">
+ <property name="text">
+ <string>&amp;Remove</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QPushButton" name="clear">
+ <property name="text">
+ <string>&amp;Clear</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QTreeWidget" name="list">
+ <property name="alternatingRowColors">
+ <bool>true</bool>
+ </property>
+ <property name="selectionMode">
+ <enum>QAbstractItemView::ExtendedSelection</enum>
+ </property>
+ <property name="rootIsDecorated">
+ <bool>false</bool>
+ </property>
+ <property name="itemsExpandable">
+ <bool>false</bool>
+ </property>
+ <property name="animated">
+ <bool>true</bool>
+ </property>
+ <property name="expandsOnDoubleClick">
+ <bool>false</bool>
+ </property>
+ <column>
+ <property name="text">
+ <string>Name</string>
+ </property>
+ </column>
+ <column>
+ <property name="text">
+ <string>Value</string>
+ </property>
+ </column>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/archived/projt-launcher/launcher/ui/widgets/FallbackHubView.cpp b/archived/projt-launcher/launcher/ui/widgets/FallbackHubView.cpp
new file mode 100644
index 0000000000..c556a4c960
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/widgets/FallbackHubView.cpp
@@ -0,0 +1,100 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+#include "FallbackHubView.h"
+
+#include <QDesktopServices>
+#include <QLabel>
+#include <QPushButton>
+#include <QVBoxLayout>
+
+FallbackHubView::FallbackHubView(const QString& title, QWidget* parent) : HubViewBase(parent)
+{
+ auto* layout = new QVBoxLayout(this);
+ layout->setContentsMargins(24, 24, 24, 24);
+ layout->setSpacing(12);
+
+ auto* titleLabel = new QLabel(title, this);
+ titleLabel->setWordWrap(true);
+ titleLabel->setStyleSheet(QStringLiteral("color: #ffffff; font-size: 18px; font-weight: 700;"));
+
+ m_urlLabel = new QLabel(this);
+ m_urlLabel->setWordWrap(true);
+ m_urlLabel->setStyleSheet(QStringLiteral("color: #9bb0cc;"));
+
+ m_openButton = new QPushButton(tr("Open in browser"), this);
+ connect(m_openButton, &QPushButton::clicked, this, &FallbackHubView::reload);
+
+ layout->addWidget(titleLabel);
+ layout->addWidget(m_urlLabel);
+ layout->addWidget(m_openButton, 0, Qt::AlignLeft);
+ layout->addStretch(1);
+}
+
+void FallbackHubView::setUrl(const QUrl& url)
+{
+ m_url = url;
+ m_urlLabel->setText(url.toString());
+ m_openButton->setEnabled(url.isValid());
+ emit urlChanged(m_url);
+ emit titleChanged(m_url.isValid() ? m_url.host() : tr("Open in Browser"));
+ emit navigationStateChanged();
+ if (m_url.isValid())
+ {
+ openCurrentUrl();
+ }
+ emit loadFinished(m_url.isValid());
+}
+
+QUrl FallbackHubView::url() const
+{
+ return m_url;
+}
+
+bool FallbackHubView::canGoBack() const
+{
+ return false;
+}
+
+bool FallbackHubView::canGoForward() const
+{
+ return false;
+}
+
+void FallbackHubView::back()
+{}
+
+void FallbackHubView::forward()
+{}
+
+void FallbackHubView::reload()
+{
+ openCurrentUrl();
+ emit loadFinished(m_url.isValid());
+}
+
+void FallbackHubView::openCurrentUrl() const
+{
+ if (m_url.isValid())
+ {
+ QDesktopServices::openUrl(m_url);
+ }
+}
diff --git a/archived/projt-launcher/launcher/ui/widgets/FallbackHubView.h b/archived/projt-launcher/launcher/ui/widgets/FallbackHubView.h
new file mode 100644
index 0000000000..e81529e3c1
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/widgets/FallbackHubView.h
@@ -0,0 +1,52 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+#pragma once
+
+#include "ui/widgets/HubViewBase.h"
+
+class QLabel;
+class QPushButton;
+
+class FallbackHubView : public HubViewBase
+{
+ Q_OBJECT
+
+ public:
+ explicit FallbackHubView(const QString& title, QWidget* parent = nullptr);
+ ~FallbackHubView() override = default;
+
+ void setUrl(const QUrl& url) override;
+ QUrl url() const override;
+ bool canGoBack() const override;
+ bool canGoForward() const override;
+
+ public slots:
+ void back() override;
+ void forward() override;
+ void reload() override;
+
+ private:
+ void openCurrentUrl() const;
+
+ QUrl m_url;
+ QLabel* m_urlLabel = nullptr;
+ QPushButton* m_openButton = nullptr;
+};
diff --git a/archived/projt-launcher/launcher/ui/widgets/FastFileIconProvider.cpp b/archived/projt-launcher/launcher/ui/widgets/FastFileIconProvider.cpp
new file mode 100644
index 0000000000..eef3cdb58a
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/widgets/FastFileIconProvider.cpp
@@ -0,0 +1,71 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ ======================================================================== */
+#include "FastFileIconProvider.h"
+
+#include <QApplication>
+#include <QStyle>
+
+QIcon FastFileIconProvider::icon(const QFileInfo& info) const
+{
+#if QT_VERSION >= QT_VERSION_CHECK(6, 4, 0)
+ bool link = info.isSymbolicLink() || info.isAlias() || info.isShortcut();
+#else
+ // in versions prior to 6.4 we don't have access to isAlias
+ bool link = info.isSymLink();
+#endif
+ QStyle::StandardPixmap icon;
+
+ if (info.isDir())
+ {
+ if (link)
+ icon = QStyle::SP_DirLinkIcon;
+ else
+ icon = QStyle::SP_DirIcon;
+ }
+ else
+ {
+ if (link)
+ icon = QStyle::SP_FileLinkIcon;
+ else
+ icon = QStyle::SP_FileIcon;
+ }
+
+ return QApplication::style()->standardIcon(icon);
+} \ No newline at end of file
diff --git a/archived/projt-launcher/launcher/ui/widgets/FastFileIconProvider.h b/archived/projt-launcher/launcher/ui/widgets/FastFileIconProvider.h
new file mode 100644
index 0000000000..3b9d4ddf64
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/widgets/FastFileIconProvider.h
@@ -0,0 +1,49 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ ======================================================================== */
+
+#pragma once
+
+#include <QFileIconProvider>
+
+class FastFileIconProvider : public QFileIconProvider
+{
+ public:
+ QIcon icon(const QFileInfo& info) const override;
+}; \ No newline at end of file
diff --git a/archived/projt-launcher/launcher/ui/widgets/FileIgnoreProxy.cpp b/archived/projt-launcher/launcher/ui/widgets/FileIgnoreProxy.cpp
new file mode 100644
index 0000000000..b50a9714c4
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/widgets/FileIgnoreProxy.cpp
@@ -0,0 +1,362 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ === Upstream License Block (Do Not Modify) ==============================
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
+ * Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * 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 "FileIgnoreProxy.h"
+
+#include <QDebug>
+#include <QFileSystemModel>
+#include <QSortFilterProxyModel>
+#include <QStack>
+#include "FileSystem.h"
+#include "SeparatorPrefixTree.h"
+#include "StringUtils.h"
+
+FileIgnoreProxy::FileIgnoreProxy(QString root, QObject* parent) : QSortFilterProxyModel(parent), m_root(root)
+{}
+// NOTE: Sadly, we have to do sorting ourselves.
+bool FileIgnoreProxy::lessThan(const QModelIndex& left, const QModelIndex& right) const
+{
+ QFileSystemModel* fsm = qobject_cast<QFileSystemModel*>(sourceModel());
+ if (!fsm)
+ {
+ return QSortFilterProxyModel::lessThan(left, right);
+ }
+ bool asc = sortOrder() == Qt::AscendingOrder ? true : false;
+
+ QFileInfo leftFileInfo = fsm->fileInfo(left);
+ QFileInfo rightFileInfo = fsm->fileInfo(right);
+
+ if (!leftFileInfo.isDir() && rightFileInfo.isDir())
+ {
+ return !asc;
+ }
+ if (leftFileInfo.isDir() && !rightFileInfo.isDir())
+ {
+ return asc;
+ }
+
+ // sort and proxy model breaks the original model...
+ if (sortColumn() == 0)
+ {
+ return StringUtils::naturalCompare(leftFileInfo.fileName(), rightFileInfo.fileName(), Qt::CaseInsensitive) < 0;
+ }
+ if (sortColumn() == 1)
+ {
+ auto leftSize = leftFileInfo.size();
+ auto rightSize = rightFileInfo.size();
+ if ((leftSize == rightSize) || (leftFileInfo.isDir() && rightFileInfo.isDir()))
+ {
+ return StringUtils::naturalCompare(leftFileInfo.fileName(), rightFileInfo.fileName(), Qt::CaseInsensitive)
+ < 0
+ ? asc
+ : !asc;
+ }
+ return leftSize < rightSize;
+ }
+ return QSortFilterProxyModel::lessThan(left, right);
+}
+
+Qt::ItemFlags FileIgnoreProxy::flags(const QModelIndex& index) const
+{
+ if (!index.isValid())
+ return Qt::NoItemFlags;
+
+ auto sourceIndex = mapToSource(index);
+ Qt::ItemFlags flags = sourceIndex.flags();
+ if (index.column() == 0)
+ {
+ flags |= Qt::ItemIsUserCheckable;
+ if (sourceIndex.model()->hasChildren(sourceIndex))
+ {
+ flags |= Qt::ItemIsAutoTristate;
+ }
+ }
+
+ return flags;
+}
+
+QVariant FileIgnoreProxy::data(const QModelIndex& index, int role) const
+{
+ QModelIndex sourceIndex = mapToSource(index);
+
+ if (index.column() == 0 && role == Qt::CheckStateRole)
+ {
+ QFileSystemModel* fsm = qobject_cast<QFileSystemModel*>(sourceModel());
+ auto blockedPath = relPath(fsm->filePath(sourceIndex));
+ auto cover = m_blocked.cover(blockedPath);
+ if (!cover.isNull())
+ {
+ return QVariant(Qt::Unchecked);
+ }
+ else if (m_blocked.exists(blockedPath))
+ {
+ return QVariant(Qt::PartiallyChecked);
+ }
+ else
+ {
+ return QVariant(Qt::Checked);
+ }
+ }
+
+ return sourceIndex.data(role);
+}
+
+bool FileIgnoreProxy::setData(const QModelIndex& index, const QVariant& value, int role)
+{
+ if (index.column() == 0 && role == Qt::CheckStateRole)
+ {
+ Qt::CheckState state = static_cast<Qt::CheckState>(value.toInt());
+ return setFilterState(index, state);
+ }
+
+ QModelIndex sourceIndex = mapToSource(index);
+ return QSortFilterProxyModel::sourceModel()->setData(sourceIndex, value, role);
+}
+
+QString FileIgnoreProxy::relPath(const QString& path) const
+{
+ return QDir(m_root).relativeFilePath(path);
+}
+
+bool FileIgnoreProxy::setFilterState(QModelIndex index, Qt::CheckState state)
+{
+ QFileSystemModel* fsm = qobject_cast<QFileSystemModel*>(sourceModel());
+
+ if (!fsm)
+ {
+ return false;
+ }
+
+ QModelIndex sourceIndex = mapToSource(index);
+ auto blockedPath = relPath(fsm->filePath(sourceIndex));
+ bool changed = false;
+ if (state == Qt::Unchecked)
+ {
+ // blocking a path
+ auto& node = m_blocked.insert(blockedPath);
+ // get rid of all blocked nodes below
+ node.clear();
+ changed = true;
+ }
+ else if (state == Qt::Checked || state == Qt::PartiallyChecked)
+ {
+ if (!m_blocked.remove(blockedPath))
+ {
+ auto cover = m_blocked.cover(blockedPath);
+ qDebug() << "Blocked by cover" << cover;
+ // uncover
+ m_blocked.remove(cover);
+ // block all contents, except for any cover
+ QModelIndex rootIndex = fsm->index(FS::PathCombine(m_root, cover));
+ QModelIndex doing = rootIndex;
+ int row = 0;
+ QStack<QModelIndex> todo;
+ while (1)
+ {
+ auto node = fsm->index(row, 0, doing);
+ if (!node.isValid())
+ {
+ if (!todo.size())
+ {
+ break;
+ }
+ else
+ {
+ doing = todo.pop();
+ row = 0;
+ continue;
+ }
+ }
+ auto relpath = relPath(fsm->filePath(node));
+ if (blockedPath.startsWith(relpath)) // cover found?
+ {
+ // continue processing cover later
+ todo.push(node);
+ }
+ else
+ {
+ // or just block this one.
+ m_blocked.insert(relpath);
+ }
+ row++;
+ }
+ }
+ changed = true;
+ }
+ if (changed)
+ {
+ // update the thing
+ emit dataChanged(index, index, { Qt::CheckStateRole });
+ // update everything above index
+ QModelIndex up = index.parent();
+ while (1)
+ {
+ if (!up.isValid())
+ break;
+ emit dataChanged(up, up, { Qt::CheckStateRole });
+ up = up.parent();
+ }
+ // and everything below the index
+ QModelIndex doing = index;
+ int row = 0;
+ QStack<QModelIndex> todo;
+ while (1)
+ {
+ auto node = this->index(row, 0, doing);
+ if (!node.isValid())
+ {
+ if (!todo.size())
+ {
+ break;
+ }
+ else
+ {
+ doing = todo.pop();
+ row = 0;
+ continue;
+ }
+ }
+ emit dataChanged(node, node, { Qt::CheckStateRole });
+ todo.push(node);
+ row++;
+ }
+ // siblings and unrelated nodes are ignored
+ }
+ return true;
+}
+
+bool FileIgnoreProxy::shouldExpand(QModelIndex index)
+{
+ QModelIndex sourceIndex = mapToSource(index);
+ QFileSystemModel* fsm = qobject_cast<QFileSystemModel*>(sourceModel());
+ if (!fsm)
+ {
+ return false;
+ }
+ auto blockedPath = relPath(fsm->filePath(sourceIndex));
+ auto found = m_blocked.find(blockedPath);
+ if (found)
+ {
+ return !found->leaf();
+ }
+ return false;
+}
+
+void FileIgnoreProxy::setBlockedPaths(QStringList paths)
+{
+ beginResetModel();
+ m_blocked.clear();
+ m_blocked.insert(paths);
+ endResetModel();
+}
+
+bool FileIgnoreProxy::filterAcceptsColumn(int source_column, const QModelIndex& source_parent) const
+{
+ Q_UNUSED(source_parent)
+
+ // adjust the columns you want to filter out here
+ // return false for those that will be hidden
+ if (source_column == 2 || source_column == 3)
+ return false;
+
+ return true;
+}
+
+bool FileIgnoreProxy::filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const
+{
+ QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent);
+ QFileSystemModel* fsm = qobject_cast<QFileSystemModel*>(sourceModel());
+
+ auto fileInfo = fsm->fileInfo(index);
+ return !ignoreFile(fileInfo);
+}
+
+bool FileIgnoreProxy::ignoreFile(QFileInfo fileInfo) const
+{
+ return m_ignoreFiles.contains(fileInfo.fileName())
+ || m_ignoreFilePaths.covers(relPath(fileInfo.absoluteFilePath()));
+}
+
+bool FileIgnoreProxy::filterFile(const QFileInfo& file) const
+{
+ return m_blocked.covers(relPath(file.absoluteFilePath())) || ignoreFile(file);
+}
+
+void FileIgnoreProxy::loadBlockedPathsFromFile(const QString& fileName)
+{
+ QFile ignoreFile(fileName);
+ if (!ignoreFile.open(QIODevice::ReadOnly))
+ {
+ return;
+ }
+ auto ignoreData = ignoreFile.readAll();
+ auto string = QString::fromUtf8(ignoreData);
+ setBlockedPaths(string.split('\n', Qt::SkipEmptyParts));
+}
+
+void FileIgnoreProxy::saveBlockedPathsToFile(const QString& fileName)
+{
+ auto ignoreData = blockedPaths().toStringList().join('\n').toUtf8();
+ try
+ {
+ FS::write(fileName, ignoreData);
+ }
+ catch (const Exception& e)
+ {
+ qWarning() << e.cause();
+ }
+}
diff --git a/archived/projt-launcher/launcher/ui/widgets/FileIgnoreProxy.h b/archived/projt-launcher/launcher/ui/widgets/FileIgnoreProxy.h
new file mode 100644
index 0000000000..aa3e064800
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/widgets/FileIgnoreProxy.h
@@ -0,0 +1,123 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ === Upstream License Block (Do Not Modify) ==============================
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
+ * Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * 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 <QFileInfo>
+#include <QSortFilterProxyModel>
+#include "SeparatorPrefixTree.h"
+
+class FileIgnoreProxy : public QSortFilterProxyModel
+{
+ Q_OBJECT
+
+ public:
+ FileIgnoreProxy(QString root, QObject* parent);
+ // NOTE: Sadly, we have to do sorting ourselves.
+ bool lessThan(const QModelIndex& left, const QModelIndex& right) const;
+
+ virtual Qt::ItemFlags flags(const QModelIndex& index) const;
+
+ virtual QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const;
+ virtual bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole);
+
+ QString relPath(const QString& path) const;
+
+ bool setFilterState(QModelIndex index, Qt::CheckState state);
+
+ bool shouldExpand(QModelIndex index);
+
+ void setBlockedPaths(QStringList paths);
+
+ inline const SeparatorPrefixTree<'/'>& blockedPaths() const
+ {
+ return m_blocked;
+ }
+ inline SeparatorPrefixTree<'/'>& blockedPaths()
+ {
+ return m_blocked;
+ }
+
+ // list of file names that need to be removed completely from model
+ inline QStringList& ignoreFilesWithName()
+ {
+ return m_ignoreFiles;
+ }
+ // list of relative paths that need to be removed completely from model
+ inline SeparatorPrefixTree<'/'>& ignoreFilesWithPath()
+ {
+ return m_ignoreFilePaths;
+ }
+
+ bool filterFile(const QFileInfo& fileName) const;
+
+ void loadBlockedPathsFromFile(const QString& fileName);
+
+ void saveBlockedPathsToFile(const QString& fileName);
+
+ protected:
+ bool filterAcceptsColumn(int source_column, const QModelIndex& source_parent) const;
+ bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const;
+
+ bool ignoreFile(QFileInfo file) const;
+
+ private:
+ const QString m_root;
+ SeparatorPrefixTree<'/'> m_blocked;
+ QStringList m_ignoreFiles;
+ SeparatorPrefixTree<'/'> m_ignoreFilePaths;
+};
diff --git a/archived/projt-launcher/launcher/ui/widgets/HubSearchProvider.cpp b/archived/projt-launcher/launcher/ui/widgets/HubSearchProvider.cpp
new file mode 100644
index 0000000000..9e39bd8f69
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/widgets/HubSearchProvider.cpp
@@ -0,0 +1,97 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+
+#include "HubSearchProvider.h"
+
+#include <QRegularExpression>
+
+namespace
+{
+ const QList<HubSearchProvider> kProviders{
+ { QStringLiteral("duckduckgo"), QStringLiteral("DuckDuckGo"), QStringLiteral("https://duckduckgo.com/?q=%1") },
+ { QStringLiteral("google"), QStringLiteral("Google"), QStringLiteral("https://www.google.com/search?q=%1") },
+ { QStringLiteral("brave"), QStringLiteral("Brave Search"), QStringLiteral("https://search.brave.com/search?q=%1") },
+ { QStringLiteral("bing"), QStringLiteral("Bing"), QStringLiteral("https://www.bing.com/search?q=%1") },
+ { QStringLiteral("startpage"), QStringLiteral("Startpage"), QStringLiteral("https://www.startpage.com/do/search?query=%1") },
+ };
+
+ bool looksLikeExplicitUrl(const QString& input)
+ {
+ static const QRegularExpression kSchemePattern(QStringLiteral(R"(^[a-zA-Z][a-zA-Z0-9+\-.]*:)"));
+
+ if (input.startsWith(QLatin1Char('/')) || input.startsWith(QStringLiteral("./"))
+ || input.startsWith(QStringLiteral("../")) || input.startsWith(QLatin1Char('~')))
+ {
+ return true;
+ }
+
+ if (kSchemePattern.match(input).hasMatch())
+ {
+ return true;
+ }
+
+ if (input.compare(QStringLiteral("localhost"), Qt::CaseInsensitive) == 0)
+ {
+ return true;
+ }
+
+ return input.contains(QLatin1Char('.')) || input.contains(QLatin1Char(':'));
+ }
+}
+
+const QList<HubSearchProvider>& hubSearchProviders()
+{
+ return kProviders;
+}
+
+QString defaultHubSearchProviderId()
+{
+ return QStringLiteral("duckduckgo");
+}
+
+QString normalizedHubSearchProviderId(const QString& id)
+{
+ for (const auto& provider : kProviders)
+ {
+ if (provider.id == id)
+ {
+ return provider.id;
+ }
+ }
+ return defaultHubSearchProviderId();
+}
+
+QUrl hubSearchUrlForQuery(const QString& query, const QString& providerId)
+{
+ const QString normalizedId = normalizedHubSearchProviderId(providerId);
+ for (const auto& provider : kProviders)
+ {
+ if (provider.id == normalizedId)
+ {
+ return QUrl(provider.templateUrl.arg(QString::fromUtf8(QUrl::toPercentEncoding(query))));
+ }
+ }
+
+ return QUrl(kProviders.constFirst().templateUrl.arg(QString::fromUtf8(QUrl::toPercentEncoding(query))));
+}
+
+QUrl resolveHubInput(const QString& input, const QString& providerId)
+{
+ const QString trimmed = input.trimmed();
+ if (trimmed.isEmpty())
+ {
+ return {};
+ }
+
+ if (looksLikeExplicitUrl(trimmed))
+ {
+ const QUrl url = QUrl::fromUserInput(trimmed);
+ if (url.isValid())
+ {
+ return url;
+ }
+ }
+
+ return hubSearchUrlForQuery(trimmed, providerId);
+}
diff --git a/archived/projt-launcher/launcher/ui/widgets/HubSearchProvider.h b/archived/projt-launcher/launcher/ui/widgets/HubSearchProvider.h
new file mode 100644
index 0000000000..5e05a460df
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/widgets/HubSearchProvider.h
@@ -0,0 +1,21 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+#pragma once
+
+#include <QList>
+#include <QString>
+#include <QUrl>
+
+struct HubSearchProvider
+{
+ QString id;
+ QString displayName;
+ QString templateUrl;
+};
+
+const QList<HubSearchProvider>& hubSearchProviders();
+QString defaultHubSearchProviderId();
+QString normalizedHubSearchProviderId(const QString& id);
+QUrl hubSearchUrlForQuery(const QString& query, const QString& providerId);
+QUrl resolveHubInput(const QString& input, const QString& providerId);
diff --git a/archived/projt-launcher/launcher/ui/widgets/HubViewBase.h b/archived/projt-launcher/launcher/ui/widgets/HubViewBase.h
new file mode 100644
index 0000000000..0a8fb1af01
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/widgets/HubViewBase.h
@@ -0,0 +1,55 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+#pragma once
+
+#include <QUrl>
+#include <QWidget>
+
+class HubViewBase : public QWidget
+{
+ Q_OBJECT
+
+ public:
+ explicit HubViewBase(QWidget* parent = nullptr) : QWidget(parent)
+ {}
+ ~HubViewBase() override = default;
+
+ virtual void setUrl(const QUrl& url) = 0;
+ virtual QUrl url() const = 0;
+ virtual bool canGoBack() const = 0;
+ virtual bool canGoForward() const = 0;
+ virtual void setActive(bool active)
+ {
+ Q_UNUSED(active);
+ }
+
+ public slots:
+ virtual void back() = 0;
+ virtual void forward() = 0;
+ virtual void reload() = 0;
+
+ signals:
+ void titleChanged(const QString& title);
+ void urlChanged(const QUrl& url);
+ void loadFinished(bool ok);
+ void navigationStateChanged();
+ void newTabRequested(const QUrl& url);
+};
diff --git a/archived/projt-launcher/launcher/ui/widgets/IconLabel.cpp b/archived/projt-launcher/launcher/ui/widgets/IconLabel.cpp
new file mode 100644
index 0000000000..118b4e81e7
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/widgets/IconLabel.cpp
@@ -0,0 +1,62 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+#include "IconLabel.h"
+
+#include <QLayout>
+#include <QPainter>
+#include <QRect>
+#include <QStyle>
+#include <QStyleOption>
+
+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/archived/projt-launcher/launcher/ui/widgets/IconLabel.h b/archived/projt-launcher/launcher/ui/widgets/IconLabel.h
new file mode 100644
index 0000000000..f49226dc8b
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/widgets/IconLabel.h
@@ -0,0 +1,46 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+#pragma once
+#include <QIcon>
+#include <QWidget>
+
+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/archived/projt-launcher/launcher/ui/widgets/InfoFrame.cpp b/archived/projt-launcher/launcher/ui/widgets/InfoFrame.cpp
new file mode 100644
index 0000000000..38e5787c8e
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/widgets/InfoFrame.cpp
@@ -0,0 +1,524 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
+ * Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * 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 <QMessageBox>
+#include <QRegularExpression>
+#include <QTextCursor>
+#include <QTextDocument>
+#include <QToolTip>
+
+#include "InfoFrame.h"
+#include "ui_InfoFrame.h"
+
+#include "ui/dialogs/CustomMessageBox.h"
+
+void setupLinkToolTip(QLabel* label)
+{
+ QObject::connect(label,
+ &QLabel::linkHovered,
+ [label](const QString& link)
+ {
+ if (auto url = QUrl(link);
+ !url.isValid() || (url.scheme() != "http" && url.scheme() != "https"))
+ return;
+ label->setToolTip(link);
+ });
+}
+
+InfoFrame::InfoFrame(QWidget* parent) : QFrame(parent), ui(new Ui::InfoFrame)
+{
+ ui->setupUi(this);
+ ui->descriptionLabel->setHidden(true);
+ ui->nameLabel->setHidden(true);
+ ui->licenseLabel->setHidden(true);
+ ui->issueTrackerLabel->setHidden(true);
+
+ setupLinkToolTip(ui->iconLabel);
+ setupLinkToolTip(ui->descriptionLabel);
+ setupLinkToolTip(ui->nameLabel);
+ setupLinkToolTip(ui->licenseLabel);
+ setupLinkToolTip(ui->issueTrackerLabel);
+ updateHiddenState();
+}
+
+InfoFrame::~InfoFrame()
+{
+ delete ui;
+}
+
+void InfoFrame::updateWithMod(Mod const& m)
+{
+ if (m.type() == ResourceType::FOLDER)
+ {
+ clear();
+ return;
+ }
+
+ QString text = "";
+ QString name = "";
+ QString link = m.homepage();
+ if (m.name().isEmpty())
+ name = m.internal_id();
+ else
+ name = m.name();
+
+ if (link.isEmpty())
+ text = name;
+ else
+ {
+ text = "<a href=\"" + QUrl(link).toEncoded() + "\">" + name + "</a>";
+ }
+ if (!m.authors().isEmpty())
+ text += " by " + m.authors().join(", ");
+
+ setName(text);
+
+ if (m.description().isEmpty())
+ {
+ setDescription(QString());
+ }
+ else
+ {
+ setDescription(m.description());
+ }
+
+ setImage(m.icon({ 64, 64 }));
+
+ auto licenses = m.licenses();
+ QString licenseText = "";
+ if (!licenses.empty())
+ {
+ for (auto l : licenses)
+ {
+ if (!licenseText.isEmpty())
+ {
+ licenseText += "\n"; // add newline between licenses
+ }
+ if (!l.name.isEmpty())
+ {
+ if (l.url.isEmpty())
+ {
+ licenseText += l.name;
+ }
+ else
+ {
+ licenseText += "<a href=\"" + l.url + "\">" + l.name + "</a>";
+ }
+ }
+ else if (!l.url.isEmpty())
+ {
+ licenseText += "<a href=\"" + l.url + "\">" + l.url + "</a>";
+ }
+ if (!l.description.isEmpty() && l.description != l.name)
+ {
+ licenseText += " " + l.description;
+ }
+ }
+ }
+ if (!licenseText.isEmpty())
+ {
+ setLicense(tr("License: %1").arg(licenseText));
+ }
+ else
+ {
+ setLicense();
+ }
+
+ QString issueTracker = "";
+ if (!m.issueTracker().isEmpty())
+ {
+ issueTracker += tr("Report issues to: ");
+ issueTracker += "<a href=\"" + m.issueTracker() + "\">" + m.issueTracker() + "</a>";
+ }
+ setIssueTracker(issueTracker);
+}
+
+void InfoFrame::updateWithResource(const Resource& resource)
+{
+ const QString homepage = resource.homepage();
+
+ if (!homepage.isEmpty())
+ setName("<a href=\"" + homepage + "\">" + resource.name() + "</a>");
+ else
+ setName(resource.name());
+
+ setImage();
+}
+
+QString InfoFrame::renderColorCodes(QString input)
+{
+ // We have to manually set the colors for use.
+ //
+ // A color is set using §x, with x = a hex number from 0 to f.
+ //
+ // We traverse the description and, when one of those is found, we create
+ // a span element with that color set.
+ //
+
+ // https://minecraft.wiki/w/Formatting_codes#Color_codes
+ const QMap<QChar, QString> color_codes_map = { { '0', "#000000" }, { '1', "#0000AA" }, { '2', "#00AA00" },
+ { '3', "#00AAAA" }, { '4', "#AA0000" }, { '5', "#AA00AA" },
+ { '6', "#FFAA00" }, { '7', "#AAAAAA" }, { '8', "#555555" },
+ { '9', "#5555FF" }, { 'a', "#55FF55" }, { 'b', "#55FFFF" },
+ { 'c', "#FF5555" }, { 'd', "#FF55FF" }, { 'e', "#FFFF55" },
+ { 'f', "#FFFFFF" } };
+ // https://minecraft.wiki/w/Formatting_codes#Formatting_codes
+ const QMap<QChar, QString> formatting_codes_map = { { 'l', "b" }, { 'm', "s" }, { 'n', "u" }, { 'o', "i" } };
+
+ // Linkify plain http/https URLs so they become clickable when we render HTML below.
+ // This is intentionally simple and conservative: we only match basic http(s) URLs
+ // and wrap them in an anchor tag.
+ static const QRegularExpression urlRe(R"((https?://[^\s"'<>]+))", QRegularExpression::CaseInsensitiveOption);
+ input.replace(urlRe, "<a href=\"\\1\">\\1</a>");
+
+ QString html("<html>");
+ QList<QString> tags{};
+
+ auto it = input.constBegin();
+ while (it != input.constEnd())
+ {
+ // is current char § and is there a following char
+ if (*it == u'§' && (it + 1) != input.constEnd())
+ {
+ auto const& code = *(++it); // incrementing here!
+
+ auto const color_entry = color_codes_map.constFind(code);
+ auto const tag_entry = formatting_codes_map.constFind(code);
+
+ if (color_entry != color_codes_map.constEnd())
+ { // color code
+ html += QString("<span style=\"color: %1;\">").arg(color_entry.value());
+ tags << "span";
+ }
+ else if (tag_entry != formatting_codes_map.constEnd())
+ { // formatting code
+ html += QString("<%1>").arg(tag_entry.value());
+ tags << tag_entry.value();
+ }
+ else if (code == 'r')
+ { // reset all formatting
+ while (!tags.isEmpty())
+ {
+ html += QString("</%1>").arg(tags.takeLast());
+ }
+ }
+ else
+ { // pass unknown codes through
+ html += QString("§%1").arg(code);
+ }
+ }
+ else
+ {
+ html += *it;
+ }
+ it++;
+ }
+ while (!tags.isEmpty())
+ {
+ html += QString("</%1>").arg(tags.takeLast());
+ }
+ html += "</html>";
+
+ html.replace("\n", "<br>");
+ return html;
+}
+
+void InfoFrame::updateWithResourcePack(ResourcePack& resource_pack)
+{
+ QString name = renderColorCodes(resource_pack.name());
+
+ const QString homepage = resource_pack.homepage();
+ if (!homepage.isEmpty())
+ {
+ name = "<a href=\"" + homepage + "\">" + name + "</a>";
+ }
+
+ setName(name);
+ setDescription(renderColorCodes(resource_pack.description()));
+ setImage(resource_pack.image({ 64, 64 }));
+}
+
+void InfoFrame::updateWithDataPack(DataPack& data_pack)
+{
+ setName(renderColorCodes(data_pack.name()));
+ setDescription(renderColorCodes(data_pack.description()));
+ setImage(data_pack.image({ 64, 64 }));
+}
+
+void InfoFrame::updateWithTexturePack(TexturePack& texture_pack)
+{
+ QString name = renderColorCodes(texture_pack.name());
+
+ const QString homepage = texture_pack.homepage();
+ if (!homepage.isEmpty())
+ {
+ name = "<a href=\"" + homepage + "\">" + name + "</a>";
+ }
+
+ setName(name);
+ setDescription(renderColorCodes(texture_pack.description()));
+ setImage(texture_pack.image({ 64, 64 }));
+}
+
+void InfoFrame::clear()
+{
+ setName();
+ setDescription();
+ setImage();
+ setLicense();
+ setIssueTracker();
+}
+
+void InfoFrame::updateHiddenState()
+{
+ if (ui->descriptionLabel->isHidden() && ui->nameLabel->isHidden() && ui->licenseLabel->isHidden()
+ && ui->issueTrackerLabel->isHidden())
+ {
+ setHidden(true);
+ }
+ else
+ {
+ setHidden(false);
+ }
+}
+
+void InfoFrame::setName(QString text)
+{
+ if (text.isEmpty())
+ {
+ ui->nameLabel->setHidden(true);
+ }
+ else
+ {
+ ui->nameLabel->setText(text);
+ ui->nameLabel->setHidden(false);
+ }
+ updateHiddenState();
+}
+
+void InfoFrame::setDescription(QString text)
+{
+ if (text.isEmpty())
+ {
+ ui->descriptionLabel->setHidden(true);
+ updateHiddenState();
+ return;
+ }
+ else
+ {
+ ui->descriptionLabel->setHidden(false);
+ updateHiddenState();
+ }
+ ui->descriptionLabel->setToolTip("");
+ QString intermediatetext = text.trimmed();
+ bool prev(false);
+ QChar rem('\n');
+ QString finaltext;
+ finaltext.reserve(intermediatetext.size());
+ for (const QChar& c : intermediatetext)
+ {
+ if (c == rem && prev)
+ {
+ continue;
+ }
+ prev = c == rem;
+ finaltext += c;
+ }
+ QString labeltext;
+ labeltext.reserve(300);
+
+ // elide rich text by getting characters without formatting
+ const int maxCharacterElide = 290;
+ QTextDocument doc;
+ doc.setHtml(text);
+
+ if (doc.characterCount() > maxCharacterElide)
+ {
+ ui->descriptionLabel->setOpenExternalLinks(false);
+ ui->descriptionLabel->setTextFormat(Qt::TextFormat::RichText); // This allows injecting HTML here.
+ m_description = text;
+
+ // move the cursor to the character elide, doesn't see html
+ QTextCursor cursor(&doc);
+ cursor.movePosition(QTextCursor::End);
+ cursor.setPosition(maxCharacterElide, QTextCursor::KeepAnchor);
+ cursor.removeSelectedText();
+
+ // insert the post fix at the cursor
+ cursor.insertHtml("<a href=\"#mod_desc\">...</a>");
+
+ labeltext.append(doc.toHtml());
+ connect(ui->descriptionLabel, &QLabel::linkActivated, this, &InfoFrame::descriptionEllipsisHandler);
+ }
+ else
+ {
+ ui->descriptionLabel->setTextFormat(Qt::TextFormat::AutoText);
+ labeltext.append(finaltext);
+ }
+ ui->descriptionLabel->setText(labeltext);
+}
+
+void InfoFrame::setLicense(QString text)
+{
+ if (text.isEmpty())
+ {
+ ui->licenseLabel->setHidden(true);
+ updateHiddenState();
+ return;
+ }
+ else
+ {
+ ui->licenseLabel->setHidden(false);
+ updateHiddenState();
+ }
+ ui->licenseLabel->setToolTip("");
+ QString intermediatetext = text.trimmed();
+ bool prev(false);
+ QChar rem('\n');
+ QString finaltext;
+ finaltext.reserve(intermediatetext.size());
+ for (const QChar& c : intermediatetext)
+ {
+ if (c == rem && prev)
+ {
+ continue;
+ }
+ prev = c == rem;
+ finaltext += c;
+ }
+ QString labeltext;
+ labeltext.reserve(300);
+ if (finaltext.length() > 290)
+ {
+ ui->licenseLabel->setOpenExternalLinks(false);
+ ui->licenseLabel->setTextFormat(Qt::TextFormat::RichText);
+ m_license = text;
+ // This allows injecting HTML here.
+ labeltext.append("<html><body>" + finaltext.left(287) + "<a href=\"#mod_desc\">...</a></body></html>");
+ connect(ui->licenseLabel, &QLabel::linkActivated, this, &InfoFrame::licenseEllipsisHandler);
+ }
+ else
+ {
+ ui->licenseLabel->setTextFormat(Qt::TextFormat::AutoText);
+ labeltext.append(finaltext);
+ }
+ ui->licenseLabel->setText(labeltext);
+}
+
+void InfoFrame::setIssueTracker(QString text)
+{
+ if (text.isEmpty())
+ {
+ ui->issueTrackerLabel->setHidden(true);
+ }
+ else
+ {
+ ui->issueTrackerLabel->setText(text);
+ ui->issueTrackerLabel->setHidden(false);
+ }
+ updateHiddenState();
+}
+
+void InfoFrame::setImage(QPixmap img)
+{
+ if (img.isNull())
+ {
+ ui->iconLabel->setHidden(true);
+ }
+ else
+ {
+ ui->iconLabel->setHidden(false);
+ ui->iconLabel->setPixmap(img);
+ }
+}
+
+void InfoFrame::descriptionEllipsisHandler([[maybe_unused]] QString link)
+{
+ if (!m_current_box)
+ {
+ m_current_box = CustomMessageBox::selectable(this, "", m_description);
+ connect(m_current_box, &QMessageBox::finished, this, &InfoFrame::boxClosed);
+ m_current_box->show();
+ }
+ else
+ {
+ m_current_box->setText(m_description);
+ }
+}
+
+void InfoFrame::licenseEllipsisHandler([[maybe_unused]] QString link)
+{
+ if (!m_current_box)
+ {
+ m_current_box = CustomMessageBox::selectable(this, "", m_license);
+ connect(m_current_box, &QMessageBox::finished, this, &InfoFrame::boxClosed);
+ m_current_box->show();
+ }
+ else
+ {
+ m_current_box->setText(m_license);
+ }
+}
+
+void InfoFrame::boxClosed([[maybe_unused]] int result)
+{
+ m_current_box = nullptr;
+}
diff --git a/archived/projt-launcher/launcher/ui/widgets/InfoFrame.h b/archived/projt-launcher/launcher/ui/widgets/InfoFrame.h
new file mode 100644
index 0000000000..a8e3ab0e42
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/widgets/InfoFrame.h
@@ -0,0 +1,111 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * 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/DataPack.hpp"
+#include "minecraft/mod/Mod.hpp"
+#include "minecraft/mod/ResourcePack.hpp"
+#include "minecraft/mod/TexturePack.hpp"
+
+namespace Ui
+{
+ class InfoFrame;
+}
+
+class InfoFrame : public QFrame
+{
+ Q_OBJECT
+
+ public:
+ InfoFrame(QWidget* parent = nullptr);
+ ~InfoFrame() override;
+
+ void setName(QString text = {});
+ void setDescription(QString text = {});
+ void setImage(QPixmap img = {});
+ void setLicense(QString text = {});
+ void setIssueTracker(QString text = {});
+
+ void clear();
+
+ void updateWithMod(Mod const& m);
+ void updateWithResource(Resource const& resource);
+ void updateWithResourcePack(ResourcePack& rp);
+ void updateWithDataPack(DataPack& rp);
+ void updateWithTexturePack(TexturePack& tp);
+
+ static QString renderColorCodes(QString input);
+
+ public slots:
+ void descriptionEllipsisHandler(QString link);
+ void licenseEllipsisHandler(QString link);
+ void boxClosed(int result);
+
+ private:
+ void updateHiddenState();
+
+ private:
+ Ui::InfoFrame* ui;
+ QString m_description;
+ QString m_license;
+ class QMessageBox* m_current_box = nullptr;
+};
diff --git a/archived/projt-launcher/launcher/ui/widgets/InfoFrame.ui b/archived/projt-launcher/launcher/ui/widgets/InfoFrame.ui
new file mode 100644
index 0000000000..c4d8c83d3e
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/widgets/InfoFrame.ui
@@ -0,0 +1,158 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>InfoFrame</class>
+ <widget class="QFrame" name="InfoFrame">
+ <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="QGridLayout" name="gridLayout">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item row="0" column="0" rowspan="2">
+ <widget class="QLabel" name="iconLabel">
+ <property name="minimumSize">
+ <size>
+ <width>0</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>64</width>
+ <height>64</height>
+ </size>
+ </property>
+ <property name="text">
+ <string notr="true"/>
+ </property>
+ <property name="scaledContents">
+ <bool>false</bool>
+ </property>
+ <property name="margin">
+ <number>0</number>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QLabel" name="descriptionLabel">
+ <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>
+ <item row="0" column="1">
+ <widget class="QLabel" name="nameLabel">
+ <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 row="2" column="1">
+ <widget class="QLabel" name="licenseLabel">
+ <property name="text">
+ <string/>
+ </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 row="3" column="1">
+ <widget class="QLabel" name="issueTrackerLabel">
+ <property name="text">
+ <string/>
+ </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/archived/projt-launcher/launcher/ui/widgets/JavaSettingsWidget.cpp b/archived/projt-launcher/launcher/ui/widgets/JavaSettingsWidget.cpp
new file mode 100644
index 0000000000..f05b3e24fa
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/widgets/JavaSettingsWidget.cpp
@@ -0,0 +1,356 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+#include "JavaSettingsWidget.h"
+
+#include <QFileDialog>
+#include <QFileInfo>
+#include "Application.h"
+#include "BuildConfig.h"
+#include "FileSystem.h"
+#include "HardwareInfo.h"
+#include "JavaCommon.h"
+#include "java/services/RuntimeCatalog.hpp"
+#include "java/services/RuntimeProbeTask.hpp"
+#include "java/services/RuntimeScanner.hpp"
+#include "settings/Setting.h"
+#include "ui/dialogs/CustomMessageBox.h"
+#include "ui/dialogs/VersionSelectDialog.h"
+#include "ui/java/InstallJavaDialog.h"
+
+#include "ui_JavaSettingsWidget.h"
+
+JavaSettingsWidget::JavaSettingsWidget(InstancePtr instance, QWidget* parent)
+ : QWidget(parent),
+ m_instance(std::move(instance)),
+ m_ui(new Ui::JavaSettingsWidget)
+{
+ m_ui->setupUi(this);
+
+ if (m_instance == nullptr)
+ {
+ m_ui->javaDownloadBtn->hide();
+ if (BuildConfig.JAVA_DOWNLOADER_ENABLED)
+ {
+ connect(m_ui->autodetectJavaCheckBox,
+ &QCheckBox::stateChanged,
+ this,
+ [this](bool state)
+ {
+ m_ui->autodownloadJavaCheckBox->setEnabled(state);
+ if (!state)
+ m_ui->autodownloadJavaCheckBox->setChecked(false);
+ });
+ }
+ else
+ {
+ m_ui->autodownloadJavaCheckBox->hide();
+ }
+ }
+ else
+ {
+ m_ui->javaDownloadBtn->setVisible(BuildConfig.JAVA_DOWNLOADER_ENABLED);
+ m_ui->skipWizardCheckBox->hide();
+ m_ui->autodetectJavaCheckBox->hide();
+ m_ui->autodownloadJavaCheckBox->hide();
+
+ m_ui->javaInstallationGroupBox->setCheckable(true);
+ m_ui->memoryGroupBox->setCheckable(true);
+ m_ui->javaArgumentsGroupBox->setCheckable(true);
+
+ SettingsObjectPtr settings = m_instance->settings();
+
+ connect(settings->getSetting("OverrideJavaLocation").get(),
+ &Setting::SettingChanged,
+ m_ui->javaInstallationGroupBox,
+ [this, settings]
+ { m_ui->javaInstallationGroupBox->setChecked(settings->get("OverrideJavaLocation").toBool()); });
+ connect(settings->getSetting("JavaPath").get(),
+ &Setting::SettingChanged,
+ m_ui->javaInstallationGroupBox,
+ [this, settings] { m_ui->javaPathTextBox->setText(settings->get("JavaPath").toString()); });
+
+ connect(m_ui->javaDownloadBtn,
+ &QPushButton::clicked,
+ this,
+ [this]
+ {
+ auto javaDialog = new Java::InstallDialog({}, m_instance.get(), this);
+ javaDialog->exec();
+ });
+ connect(m_ui->javaPathTextBox,
+ &QLineEdit::textChanged,
+ [this](QString newValue)
+ {
+ if (m_instance->settings()->get("JavaPath").toString() != newValue)
+ {
+ m_instance->settings()->set("AutomaticJava", false);
+ }
+ });
+ }
+
+ connect(m_ui->javaTestBtn, &QPushButton::clicked, this, &JavaSettingsWidget::onJavaTest);
+ connect(m_ui->javaDetectBtn, &QPushButton::clicked, this, &JavaSettingsWidget::onJavaAutodetect);
+ connect(m_ui->javaBrowseBtn, &QPushButton::clicked, this, &JavaSettingsWidget::onJavaBrowse);
+
+ connect(m_ui->maxMemSpinBox, &QSpinBox::valueChanged, this, &JavaSettingsWidget::updateThresholds);
+ connect(m_ui->minMemSpinBox, &QSpinBox::valueChanged, this, &JavaSettingsWidget::updateThresholds);
+
+ loadSettings();
+ updateThresholds();
+}
+
+JavaSettingsWidget::~JavaSettingsWidget()
+{
+ delete m_ui;
+}
+
+void JavaSettingsWidget::loadSettings()
+{
+ SettingsObjectPtr settings;
+
+ if (m_instance != nullptr)
+ settings = m_instance->settings();
+ else
+ settings = APPLICATION->settings();
+
+ // Java Settings
+ m_ui->javaInstallationGroupBox->setChecked(settings->get("OverrideJavaLocation").toBool());
+ m_ui->javaPathTextBox->setText(settings->get("JavaPath").toString());
+
+ m_ui->skipCompatibilityCheckBox->setChecked(settings->get("IgnoreJavaCompatibility").toBool());
+
+ m_ui->javaArgumentsGroupBox->setChecked(m_instance == nullptr || settings->get("OverrideJavaArgs").toBool());
+ m_ui->jvmArgsTextBox->setPlainText(settings->get("JvmArgs").toString());
+
+ if (m_instance == nullptr)
+ {
+ m_ui->skipWizardCheckBox->setChecked(settings->get("IgnoreJavaWizard").toBool());
+ m_ui->autodetectJavaCheckBox->setChecked(settings->get("AutomaticJavaSwitch").toBool());
+ m_ui->autodetectJavaCheckBox->stateChanged(m_ui->autodetectJavaCheckBox->isChecked());
+ m_ui->autodownloadJavaCheckBox->setChecked(settings->get("AutomaticJavaDownload").toBool());
+ }
+
+ // Memory
+ m_ui->memoryGroupBox->setChecked(m_instance == nullptr || settings->get("OverrideMemory").toBool());
+ int min = settings->get("MinMemAlloc").toInt();
+ int max = settings->get("MaxMemAlloc").toInt();
+ if (min < max)
+ {
+ m_ui->minMemSpinBox->setValue(min);
+ m_ui->maxMemSpinBox->setValue(max);
+ }
+ else
+ {
+ m_ui->minMemSpinBox->setValue(max);
+ m_ui->maxMemSpinBox->setValue(min);
+ }
+ m_ui->permGenSpinBox->setValue(settings->get("PermGen").toInt());
+
+ // Java arguments
+ m_ui->javaArgumentsGroupBox->setChecked(m_instance == nullptr || settings->get("OverrideJavaArgs").toBool());
+ m_ui->jvmArgsTextBox->setPlainText(settings->get("JvmArgs").toString());
+}
+
+void JavaSettingsWidget::saveSettings()
+{
+ SettingsObjectPtr settings;
+
+ if (m_instance != nullptr)
+ settings = m_instance->settings();
+ else
+ settings = APPLICATION->settings();
+
+ SettingsObject::Lock lock(settings);
+
+ // Java Install Settings
+ bool javaInstall = m_instance == nullptr || m_ui->javaInstallationGroupBox->isChecked();
+
+ if (m_instance != nullptr)
+ settings->set("OverrideJavaLocation", javaInstall);
+
+ if (javaInstall)
+ {
+ settings->set("JavaPath", m_ui->javaPathTextBox->text());
+ settings->set("IgnoreJavaCompatibility", m_ui->skipCompatibilityCheckBox->isChecked());
+ }
+ else
+ {
+ settings->reset("JavaPath");
+ settings->reset("IgnoreJavaCompatibility");
+ }
+
+ if (m_instance == nullptr)
+ {
+ settings->set("IgnoreJavaWizard", m_ui->skipWizardCheckBox->isChecked());
+ settings->set("AutomaticJavaSwitch", m_ui->autodetectJavaCheckBox->isChecked());
+ settings->set("AutomaticJavaDownload", m_ui->autodownloadJavaCheckBox->isChecked());
+ }
+
+ // Memory
+ bool memory = m_instance == nullptr || m_ui->memoryGroupBox->isChecked();
+
+ if (m_instance != nullptr)
+ settings->set("OverrideMemory", memory);
+
+ if (memory)
+ {
+ int min = m_ui->minMemSpinBox->value();
+ int max = m_ui->maxMemSpinBox->value();
+ if (min < max)
+ {
+ settings->set("MinMemAlloc", min);
+ settings->set("MaxMemAlloc", max);
+ }
+ else
+ {
+ settings->set("MinMemAlloc", max);
+ settings->set("MaxMemAlloc", min);
+ }
+ settings->set("PermGen", m_ui->permGenSpinBox->value());
+ }
+ else
+ {
+ settings->reset("MinMemAlloc");
+ settings->reset("MaxMemAlloc");
+ settings->reset("PermGen");
+ }
+
+ // Java arguments
+ bool javaArgs = m_instance == nullptr || m_ui->javaArgumentsGroupBox->isChecked();
+
+ if (m_instance != nullptr)
+ settings->set("OverrideJavaArgs", javaArgs);
+
+ if (javaArgs)
+ {
+ settings->set("JvmArgs", m_ui->jvmArgsTextBox->toPlainText().replace("\n", " "));
+ }
+ else
+ {
+ settings->reset("JvmArgs");
+ }
+}
+
+void JavaSettingsWidget::onJavaBrowse()
+{
+ QString rawPath = QFileDialog::getOpenFileName(this, tr("Find Java executable"));
+
+ // do not allow current dir - it's dirty. Do not allow dirs that don't exist
+ if (rawPath.isEmpty())
+ {
+ return;
+ }
+
+ QString cookedPath = FS::NormalizePath(rawPath);
+ QFileInfo javaInfo(cookedPath);
+ if (!javaInfo.exists() || !javaInfo.isExecutable())
+ {
+ return;
+ }
+ m_ui->javaPathTextBox->setText(cookedPath);
+}
+
+void JavaSettingsWidget::onJavaTest()
+{
+ if (m_checker != nullptr)
+ return;
+
+ QString jvmArgs;
+
+ if (m_instance == nullptr || m_ui->javaArgumentsGroupBox->isChecked())
+ jvmArgs = m_ui->jvmArgsTextBox->toPlainText().replace("\n", " ");
+ else
+ jvmArgs = APPLICATION->settings()->get("JvmArgs").toString();
+
+ m_checker.reset(new JavaCommon::TestCheck(this,
+ m_ui->javaPathTextBox->text(),
+ jvmArgs,
+ m_ui->minMemSpinBox->value(),
+ m_ui->maxMemSpinBox->value(),
+ m_ui->permGenSpinBox->value()));
+ connect(m_checker.get(), &JavaCommon::TestCheck::finished, this, [this] { m_checker.reset(); });
+ m_checker->run();
+}
+
+void JavaSettingsWidget::onJavaAutodetect()
+{
+ if (projt::java::RuntimeProbeTask::probeJarPath().isEmpty())
+ {
+ JavaCommon::javaCheckNotFound(this);
+ return;
+ }
+
+ VersionSelectDialog versionDialog(APPLICATION->runtimeCatalog().get(), tr("Select a Java version"), this, true);
+ versionDialog.setResizeOn(2);
+ versionDialog.exec();
+
+ if (versionDialog.result() == QDialog::Accepted && versionDialog.selectedVersion())
+ {
+ projt::java::RuntimeInstallPtr java =
+ std::dynamic_pointer_cast<projt::java::RuntimeInstall>(versionDialog.selectedVersion());
+ m_ui->javaPathTextBox->setText(java->path);
+
+ if (!java->is_64bit && m_ui->maxMemSpinBox->value() > 2048)
+ {
+ CustomMessageBox::selectable(this,
+ tr("Confirm Selection"),
+ tr("You selected a 32-bit version of Java.\n"
+ "This installation does not support more than 2048MiB of RAM.\n"
+ "Please make sure that the maximum memory value is lower."),
+ QMessageBox::Warning,
+ QMessageBox::Ok,
+ QMessageBox::Ok)
+ ->exec();
+ }
+ }
+}
+void JavaSettingsWidget::updateThresholds()
+{
+ auto sysMiB = HardwareInfo::totalRamMiB();
+ unsigned int maxMem = m_ui->maxMemSpinBox->value();
+ unsigned int minMem = m_ui->minMemSpinBox->value();
+
+ const QString warningColour(QStringLiteral("<span style='color:#f5c211'>%1</span>"));
+
+ if (maxMem >= sysMiB)
+ {
+ m_ui->labelMaxMemNotice->setText(
+ QString("<span style='color:red'>%1</span>")
+ .arg(tr("Your maximum memory allocation exceeds your system memory capacity.")));
+ m_ui->labelMaxMemNotice->show();
+ }
+ else if (maxMem > (sysMiB * 0.9))
+ {
+ m_ui->labelMaxMemNotice->setText(
+ warningColour.arg(tr("Your maximum memory allocation is close to your system memory capacity.")));
+ m_ui->labelMaxMemNotice->show();
+ }
+ else if (maxMem < minMem)
+ {
+ m_ui->labelMaxMemNotice->setText(
+ warningColour.arg(tr("Your maximum memory allocation is below the minimum memory allocation.")));
+ m_ui->labelMaxMemNotice->show();
+ }
+ else
+ {
+ m_ui->labelMaxMemNotice->hide();
+ }
+}
diff --git a/archived/projt-launcher/launcher/ui/widgets/JavaSettingsWidget.h b/archived/projt-launcher/launcher/ui/widgets/JavaSettingsWidget.h
new file mode 100644
index 0000000000..eecff6827f
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/widgets/JavaSettingsWidget.h
@@ -0,0 +1,56 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+#pragma once
+
+#include <QWidget>
+#include "BaseInstance.h"
+#include "JavaCommon.h"
+
+namespace Ui
+{
+ class JavaSettingsWidget;
+}
+
+class JavaSettingsWidget : public QWidget
+{
+ Q_OBJECT
+
+ public:
+ explicit JavaSettingsWidget([[maybe_unused]] QWidget* parent = nullptr) : JavaSettingsWidget(nullptr, nullptr)
+ {}
+ explicit JavaSettingsWidget(InstancePtr instance, QWidget* parent = nullptr);
+ ~JavaSettingsWidget() override;
+
+ void loadSettings();
+ void saveSettings();
+
+ private slots:
+ void onJavaBrowse();
+ void onJavaAutodetect();
+ void onJavaTest();
+ void updateThresholds();
+
+ private:
+ InstancePtr m_instance;
+ Ui::JavaSettingsWidget* m_ui;
+ unique_qobject_ptr<JavaCommon::TestCheck> m_checker;
+};
diff --git a/archived/projt-launcher/launcher/ui/widgets/JavaSettingsWidget.ui b/archived/projt-launcher/launcher/ui/widgets/JavaSettingsWidget.ui
new file mode 100644
index 0000000000..46f714b76d
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/widgets/JavaSettingsWidget.ui
@@ -0,0 +1,392 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>JavaSettingsWidget</class>
+ <widget class="QWidget" name="JavaSettingsWidget">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>500</width>
+ <height>1000</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Form</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout" stretch="0,0,1">
+ <item>
+ <widget class="QGroupBox" name="javaInstallationGroupBox">
+ <property name="enabled">
+ <bool>true</bool>
+ </property>
+ <property name="title">
+ <string>Java Insta&amp;llation</string>
+ </property>
+ <property name="checkable">
+ <bool>false</bool>
+ </property>
+ <property name="checked">
+ <bool>false</bool>
+ </property>
+ <layout class="QGridLayout" name="gridLayout_2">
+ <item row="7" column="0">
+ <widget class="QCheckBox" name="autodetectJavaCheckBox">
+ <property name="text">
+ <string>Auto-&amp;detect Java version</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="0">
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <item>
+ <widget class="QPushButton" name="javaDetectBtn">
+ <property name="text">
+ <string>&amp;Detect</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="javaBrowseBtn">
+ <property name="text">
+ <string>&amp;Browse</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </item>
+ <item row="10" column="0">
+ <layout class="QHBoxLayout" name="horizontalLayout_4">
+ <item>
+ <widget class="QPushButton" name="javaTestBtn">
+ <property name="text">
+ <string>Test S&amp;ettings</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="javaDownloadBtn">
+ <property name="text">
+ <string>Open Java &amp;Downloader</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_7">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>0</width>
+ <height>0</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </item>
+ <item row="9" column="0">
+ <spacer name="verticalSpacer_4">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Fixed</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>0</width>
+ <height>6</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item row="8" column="0">
+ <widget class="QCheckBox" name="autodownloadJavaCheckBox">
+ <property name="toolTip">
+ <string>Automatically downloads and selects the Java build recommended by Mojang.</string>
+ </property>
+ <property name="text">
+ <string>Auto-download &amp;Mojang Java</string>
+ </property>
+ </widget>
+ </item>
+ <item row="4" column="0">
+ <widget class="QCheckBox" name="skipCompatibilityCheckBox">
+ <property name="toolTip">
+ <string>If enabled, the launcher will not check if an instance is compatible with the selected Java version.</string>
+ </property>
+ <property name="text">
+ <string>Skip Java compatibility checks</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0">
+ <widget class="QLineEdit" name="javaPathTextBox"/>
+ </item>
+ <item row="0" column="0">
+ <widget class="QLabel" name="label_4">
+ <property name="text">
+ <string>Java &amp;Executable</string>
+ </property>
+ <property name="buddy">
+ <cstring>javaPathTextBox</cstring>
+ </property>
+ </widget>
+ </item>
+ <item row="5" column="0">
+ <widget class="QCheckBox" name="skipWizardCheckBox">
+ <property name="toolTip">
+ <string>If enabled, the launcher won't prompt you to choose a Java version if one is not found on startup.</string>
+ </property>
+ <property name="text">
+ <string>Skip Java setup prompt on startup</string>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="0">
+ <spacer name="verticalSpacer_5">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Fixed</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>0</width>
+ <height>6</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="memoryGroupBox">
+ <property name="enabled">
+ <bool>true</bool>
+ </property>
+ <property name="title">
+ <string>Memor&amp;y</string>
+ </property>
+ <property name="checkable">
+ <bool>false</bool>
+ </property>
+ <property name="checked">
+ <bool>false</bool>
+ </property>
+ <layout class="QGridLayout" name="gridLayout">
+ <item row="2" column="2">
+ <widget class="QLabel" name="label_3">
+ <property name="text">
+ <string>(-XX:PermSize)</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="1">
+ <widget class="QSpinBox" name="permGenSpinBox">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Maximum" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="toolTip">
+ <string>The amount of memory available to store loaded Java classes.</string>
+ </property>
+ <property name="suffix">
+ <string notr="true"> MiB</string>
+ </property>
+ <property name="minimum">
+ <number>4</number>
+ </property>
+ <property name="maximum">
+ <number>1048576</number>
+ </property>
+ <property name="singleStep">
+ <number>8</number>
+ </property>
+ <property name="value">
+ <number>64</number>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QSpinBox" name="maxMemSpinBox">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Maximum" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="toolTip">
+ <string>The maximum amount of memory Minecraft is allowed to use.</string>
+ </property>
+ <property name="suffix">
+ <string notr="true"> MiB</string>
+ </property>
+ <property name="minimum">
+ <number>8</number>
+ </property>
+ <property name="maximum">
+ <number>1048576</number>
+ </property>
+ <property name="singleStep">
+ <number>128</number>
+ </property>
+ <property name="value">
+ <number>1024</number>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="2">
+ <widget class="QLabel" name="label_2">
+ <property name="text">
+ <string>(-Xmx)</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QSpinBox" name="minMemSpinBox">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Maximum" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="toolTip">
+ <string>The amount of memory Minecraft is started with.</string>
+ </property>
+ <property name="suffix">
+ <string notr="true"> MiB</string>
+ </property>
+ <property name="minimum">
+ <number>8</number>
+ </property>
+ <property name="maximum">
+ <number>1048576</number>
+ </property>
+ <property name="singleStep">
+ <number>128</number>
+ </property>
+ <property name="value">
+ <number>256</number>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="0">
+ <widget class="QLabel" name="label_11">
+ <property name="text">
+ <string>&amp;PermGen Size:</string>
+ </property>
+ <property name="buddy">
+ <cstring>permGenSpinBox</cstring>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="2">
+ <widget class="QLabel" name="label">
+ <property name="text">
+ <string>(-Xms)</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0">
+ <widget class="QLabel" name="labelMaxMem">
+ <property name="text">
+ <string>Ma&amp;ximum Memory Usage:</string>
+ </property>
+ <property name="buddy">
+ <cstring>maxMemSpinBox</cstring>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="0">
+ <widget class="QLabel" name="labelMinMem">
+ <property name="text">
+ <string>M&amp;inimum Memory Usage:</string>
+ </property>
+ <property name="buddy">
+ <cstring>minMemSpinBox</cstring>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="3">
+ <spacer name="horizontalSpacer_2">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>0</width>
+ <height>0</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item row="3" column="0" colspan="4">
+ <widget class="QLabel" name="labelMaxMemNotice">
+ <property name="text">
+ <string>Memory Notice</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="javaArgumentsGroupBox">
+ <property name="enabled">
+ <bool>true</bool>
+ </property>
+ <property name="title">
+ <string>Java Argumen&amp;ts</string>
+ </property>
+ <property name="checkable">
+ <bool>false</bool>
+ </property>
+ <property name="checked">
+ <bool>false</bool>
+ </property>
+ <layout class="QGridLayout" name="gridLayout_6">
+ <item row="1" column="1">
+ <widget class="QPlainTextEdit" name="jvmArgsTextBox"/>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <tabstops>
+ <tabstop>javaPathTextBox</tabstop>
+ <tabstop>javaDetectBtn</tabstop>
+ <tabstop>javaBrowseBtn</tabstop>
+ <tabstop>skipCompatibilityCheckBox</tabstop>
+ <tabstop>skipWizardCheckBox</tabstop>
+ <tabstop>autodetectJavaCheckBox</tabstop>
+ <tabstop>autodownloadJavaCheckBox</tabstop>
+ <tabstop>javaTestBtn</tabstop>
+ <tabstop>javaDownloadBtn</tabstop>
+ <tabstop>minMemSpinBox</tabstop>
+ <tabstop>maxMemSpinBox</tabstop>
+ <tabstop>permGenSpinBox</tabstop>
+ <tabstop>jvmArgsTextBox</tabstop>
+ </tabstops>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/archived/projt-launcher/launcher/ui/widgets/JavaWizardWidget.cpp b/archived/projt-launcher/launcher/ui/widgets/JavaWizardWidget.cpp
new file mode 100644
index 0000000000..126736cbf2
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/widgets/JavaWizardWidget.cpp
@@ -0,0 +1,645 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+#include "JavaWizardWidget.h"
+
+#include <QFileDialog>
+#include <QGroupBox>
+#include <QLabel>
+#include <QLayoutItem>
+#include <QLineEdit>
+#include <QMessageBox>
+#include <QPushButton>
+#include <QSizePolicy>
+#include <QSpinBox>
+#include <QToolButton>
+#include <QVBoxLayout>
+
+#include "DesktopServices.h"
+#include "FileSystem.h"
+#include "JavaCommon.h"
+#include "java/core/RuntimeInstall.hpp"
+#include "java/services/RuntimeCatalog.hpp"
+#include "java/services/RuntimeProbeTask.hpp"
+#include "java/services/RuntimeScanner.hpp"
+
+#include "ui/dialogs/CustomMessageBox.h"
+#include "ui/java/InstallJavaDialog.h"
+#include "ui/widgets/VersionSelectWidget.h"
+
+#include "Application.h"
+#include "BuildConfig.h"
+#include "HardwareInfo.h"
+
+JavaWizardWidget::JavaWizardWidget(QWidget* parent) : QWidget(parent)
+{
+ m_availableMemory = HardwareInfo::totalRamMiB();
+
+ goodIcon = QIcon::fromTheme("status-good");
+ yellowIcon = QIcon::fromTheme("status-yellow");
+ badIcon = QIcon::fromTheme("status-bad");
+ m_memoryTimer = new QTimer(this);
+ setupUi();
+
+ connect(m_minMemSpinBox, &QSpinBox::valueChanged, this, &JavaWizardWidget::onSpinBoxValueChanged);
+ connect(m_maxMemSpinBox, &QSpinBox::valueChanged, this, &JavaWizardWidget::onSpinBoxValueChanged);
+ connect(m_permGenSpinBox, &QSpinBox::valueChanged, this, &JavaWizardWidget::onSpinBoxValueChanged);
+ connect(m_memoryTimer, &QTimer::timeout, this, &JavaWizardWidget::memoryValueChanged);
+ connect(m_versionWidget,
+ &VersionSelectWidget::selectedVersionChanged,
+ this,
+ &JavaWizardWidget::javaVersionSelected);
+ connect(m_javaBrowseBtn, &QPushButton::clicked, this, &JavaWizardWidget::on_javaBrowseBtn_clicked);
+ connect(m_javaPathTextBox, &QLineEdit::textEdited, this, &JavaWizardWidget::javaPathEdited);
+ connect(m_javaStatusBtn, &QToolButton::clicked, this, &JavaWizardWidget::on_javaStatusBtn_clicked);
+ if (BuildConfig.JAVA_DOWNLOADER_ENABLED)
+ {
+ connect(m_javaDownloadBtn, &QPushButton::clicked, this, &JavaWizardWidget::javaDownloadBtn_clicked);
+ }
+}
+
+void JavaWizardWidget::setupUi()
+{
+ setObjectName(QStringLiteral("javaSettingsWidget"));
+ m_verticalLayout = new QVBoxLayout(this);
+ m_verticalLayout->setObjectName(QStringLiteral("verticalLayout"));
+
+ m_versionWidget = new VersionSelectWidget(this);
+
+ 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_memoryGroupBox = new QGroupBox(this);
+ m_memoryGroupBox->setObjectName(QStringLiteral("memoryGroupBox"));
+ m_gridLayout_2 = new QGridLayout(m_memoryGroupBox);
+ m_gridLayout_2->setObjectName(QStringLiteral("gridLayout_2"));
+ m_gridLayout_2->setColumnStretch(0, 1);
+
+ 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(8);
+ m_minMemSpinBox->setMaximum(1048576);
+ 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(8);
+ m_maxMemSpinBox->setMaximum(1048576);
+ m_maxMemSpinBox->setSingleStep(128);
+ m_labelMaxMem->setBuddy(m_maxMemSpinBox);
+ m_gridLayout_2->addWidget(m_maxMemSpinBox, 1, 1, 1, 1);
+
+ m_labelMaxMemIcon = new QLabel(m_memoryGroupBox);
+ m_labelMaxMemIcon->setObjectName(QStringLiteral("labelMaxMemIcon"));
+ m_gridLayout_2->addWidget(m_labelMaxMemIcon, 1, 2, 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(4);
+ m_permGenSpinBox->setMaximum(1048576);
+ m_permGenSpinBox->setSingleStep(8);
+ m_gridLayout_2->addWidget(m_permGenSpinBox, 2, 1, 1, 1);
+ m_permGenSpinBox->setVisible(false);
+
+ m_verticalLayout->addWidget(m_memoryGroupBox);
+
+ m_horizontalBtnLayout = new QHBoxLayout();
+ m_horizontalBtnLayout->setObjectName(QStringLiteral("horizontalBtnLayout"));
+
+ if (BuildConfig.JAVA_DOWNLOADER_ENABLED)
+ {
+ m_javaDownloadBtn = new QPushButton(tr("Download Java"), this);
+ m_horizontalBtnLayout->addWidget(m_javaDownloadBtn);
+ }
+
+ m_autoJavaGroupBox = new QGroupBox(this);
+ m_autoJavaGroupBox->setObjectName(QStringLiteral("autoJavaGroupBox"));
+ m_veriticalJavaLayout = new QVBoxLayout(m_autoJavaGroupBox);
+ m_veriticalJavaLayout->setObjectName(QStringLiteral("veriticalJavaLayout"));
+
+ m_autodetectJavaCheckBox = new QCheckBox(m_autoJavaGroupBox);
+ m_autodetectJavaCheckBox->setObjectName("autodetectJavaCheckBox");
+ m_autodetectJavaCheckBox->setChecked(true);
+ m_veriticalJavaLayout->addWidget(m_autodetectJavaCheckBox);
+
+ if (BuildConfig.JAVA_DOWNLOADER_ENABLED)
+ {
+ m_autodownloadCheckBox = new QCheckBox(m_autoJavaGroupBox);
+ m_autodownloadCheckBox->setObjectName("autodownloadCheckBox");
+ m_autodownloadCheckBox->setEnabled(m_autodetectJavaCheckBox->isChecked());
+ m_veriticalJavaLayout->addWidget(m_autodownloadCheckBox);
+ connect(m_autodetectJavaCheckBox,
+ &QCheckBox::stateChanged,
+ this,
+ [this]
+ {
+ m_autodownloadCheckBox->setEnabled(m_autodetectJavaCheckBox->isChecked());
+ if (!m_autodetectJavaCheckBox->isChecked())
+ m_autodownloadCheckBox->setChecked(false);
+ });
+
+ connect(m_autodownloadCheckBox,
+ &QCheckBox::stateChanged,
+ this,
+ [this]
+ {
+ auto isChecked = m_autodownloadCheckBox->isChecked();
+ m_versionWidget->setVisible(!isChecked);
+ m_javaStatusBtn->setVisible(!isChecked);
+ m_javaBrowseBtn->setVisible(!isChecked);
+ m_javaPathTextBox->setVisible(!isChecked);
+ m_javaDownloadBtn->setVisible(!isChecked);
+ if (!isChecked)
+ {
+ m_verticalLayout->removeItem(m_verticalSpacer);
+ }
+ else
+ {
+ m_verticalLayout->addSpacerItem(m_verticalSpacer);
+ }
+ });
+ }
+ m_verticalLayout->addWidget(m_autoJavaGroupBox);
+
+ m_verticalLayout->addLayout(m_horizontalBtnLayout);
+
+ m_verticalLayout->addWidget(m_versionWidget);
+ m_verticalLayout->addLayout(m_horizontalLayout);
+ m_verticalSpacer = new QSpacerItem(20, 40, QSizePolicy::Minimum, QSizePolicy::Expanding);
+
+ retranslate();
+}
+
+void JavaWizardWidget::initialize()
+{
+ m_versionWidget->initialize(APPLICATION->runtimeCatalog().get());
+ m_versionWidget->selectSearch();
+ 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);
+ updateThresholds();
+ if (BuildConfig.JAVA_DOWNLOADER_ENABLED)
+ {
+ m_autodownloadCheckBox->setChecked(true);
+ }
+}
+
+void JavaWizardWidget::refresh()
+{
+ if (BuildConfig.JAVA_DOWNLOADER_ENABLED && m_autodownloadCheckBox->isChecked())
+ {
+ return;
+ }
+ if (projt::java::RuntimeProbeTask::probeJarPath().isEmpty())
+ {
+ JavaCommon::javaCheckNotFound(this);
+ return;
+ }
+ m_versionWidget->loadList();
+}
+
+JavaWizardWidget::ValidationStatus JavaWizardWidget::validate()
+{
+ switch (javaStatus)
+ {
+ default:
+ case JavaStatus::NotSet:
+ /* fallthrough */
+ case JavaStatus::DoesNotExist:
+ /* fallthrough */
+ case JavaStatus::DoesNotStart:
+ /* fallthrough */
+ case JavaStatus::ReturnedInvalidData:
+ {
+ if (!(BuildConfig.JAVA_DOWNLOADER_ENABLED && m_autodownloadCheckBox->isChecked()))
+ { // the java will not be autodownloaded
+ int button = QMessageBox::No;
+ if (m_result.platformTag == "32" && maxHeapSize() > 2048)
+ {
+ button = CustomMessageBox::selectable(this,
+ tr("32-bit Java detected"),
+ tr("You selected a 32-bit installation of Java, but "
+ "allocated more than 2048MiB as maximum memory.\n"
+ "%1 will not be able to start Minecraft.\n"
+ "Do you wish to proceed?"
+ "\n\n"
+ "You can change the Java version in the settings later.\n")
+ .arg(BuildConfig.LAUNCHER_DISPLAYNAME),
+ QMessageBox::Warning,
+ QMessageBox::Yes | QMessageBox::No | QMessageBox::Help,
+ QMessageBox::NoButton)
+ ->exec();
+ }
+ else
+ {
+ button = CustomMessageBox::selectable(
+ this,
+ tr("No Java version selected"),
+ tr("You either didn't select a Java version or selected one that does not work.\n"
+ "%1 will not be able to start Minecraft.\n"
+ "Do you wish to proceed without a functional version of Java?"
+ "\n\n"
+ "You can change the Java version in the settings later.\n")
+ .arg(BuildConfig.LAUNCHER_DISPLAYNAME),
+ QMessageBox::Warning,
+ QMessageBox::Yes | QMessageBox::No | QMessageBox::Help,
+ QMessageBox::NoButton)
+ ->exec();
+ }
+ switch (button)
+ {
+ case QMessageBox::Yes: return ValidationStatus::JavaBad;
+ case QMessageBox::Help:
+ DesktopServices::openUrl(QUrl(BuildConfig.HELP_URL.arg("java-wizard")));
+ [[fallthrough]];
+ case QMessageBox::No:
+ /* fallthrough */
+ default: return ValidationStatus::Bad;
+ }
+ }
+ return ValidationStatus::JavaBad;
+ }
+ break;
+ case JavaStatus::Pending:
+ {
+ return ValidationStatus::Bad;
+ }
+ case JavaStatus::Good:
+ {
+ return ValidationStatus::AllOK;
+ }
+ }
+}
+
+QString JavaWizardWidget::javaPath() const
+{
+ return m_javaPathTextBox->text();
+}
+
+int JavaWizardWidget::maxHeapSize() const
+{
+ auto min = m_minMemSpinBox->value();
+ auto max = m_maxMemSpinBox->value();
+ if (max < min)
+ max = min;
+ return max;
+}
+
+int JavaWizardWidget::minHeapSize() const
+{
+ auto min = m_minMemSpinBox->value();
+ auto max = m_maxMemSpinBox->value();
+ if (min > max)
+ min = max;
+ return min;
+}
+
+bool JavaWizardWidget::permGenEnabled() const
+{
+ return m_permGenSpinBox->isVisible();
+}
+
+int JavaWizardWidget::permGenSize() const
+{
+ return m_permGenSpinBox->value();
+}
+
+void JavaWizardWidget::memoryValueChanged()
+{
+ bool actuallyChanged = false;
+ unsigned int min = m_minMemSpinBox->value();
+ unsigned int max = m_maxMemSpinBox->value();
+ unsigned int permgen = m_permGenSpinBox->value();
+ if (min != observedMinMemory)
+ {
+ observedMinMemory = min;
+ actuallyChanged = true;
+ }
+ if (max != observedMaxMemory)
+ {
+ observedMaxMemory = max;
+ actuallyChanged = true;
+ }
+ if (permgen != observedPermGenMemory)
+ {
+ observedPermGenMemory = permgen;
+ actuallyChanged = true;
+ }
+ if (actuallyChanged)
+ {
+ checkJavaPathOnEdit(m_javaPathTextBox->text());
+ updateThresholds();
+ }
+}
+
+void JavaWizardWidget::javaVersionSelected(BaseVersion::Ptr version)
+{
+ auto java = std::dynamic_pointer_cast<projt::java::RuntimeInstall>(version);
+ if (!java)
+ {
+ return;
+ }
+ auto visible = java->version.needsPermGen();
+ m_labelPermGen->setVisible(visible);
+ m_permGenSpinBox->setVisible(visible);
+ m_javaPathTextBox->setText(java->path);
+ checkJavaPath(java->path);
+}
+
+void JavaWizardWidget::on_javaBrowseBtn_clicked()
+{
+ auto filter = QString("Java (%1)").arg(projt::java::RuntimeScanner::executableName());
+ auto raw_path = QFileDialog::getOpenFileName(this, tr("Find Java executable"), QString(), filter);
+ if (raw_path.isEmpty())
+ {
+ return;
+ }
+ auto cooked_path = FS::NormalizePath(raw_path);
+ m_javaPathTextBox->setText(cooked_path);
+ checkJavaPath(cooked_path);
+}
+
+void JavaWizardWidget::javaDownloadBtn_clicked()
+{
+ auto jdialog = new Java::InstallDialog({}, nullptr, this);
+ jdialog->exec();
+}
+
+void JavaWizardWidget::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.stderrLog;
+ 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.stdoutLog;
+ 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.platformArch, m_result.version.toString());
+ break;
+ case JavaStatus::Pending:
+ CustomMessageBox::selectable(this,
+ QObject::tr("Java test pending"),
+ QObject::tr("The Java check is still in progress. Please wait."),
+ QMessageBox::Information)
+ ->show();
+ return;
+ }
+ CustomMessageBox::selectable(this,
+ failed ? QObject::tr("Java test failure") : QObject::tr("Java test success"),
+ text,
+ failed ? QMessageBox::Critical : QMessageBox::Information)
+ ->show();
+}
+
+void JavaWizardWidget::setJavaStatus(JavaWizardWidget::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 JavaWizardWidget::javaPathEdited(const QString& path)
+{
+ checkJavaPathOnEdit(path);
+}
+
+void JavaWizardWidget::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 JavaWizardWidget::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);
+ projt::java::RuntimeProbeTask::ProbeSettings settings;
+ settings.binaryPath = path;
+ settings.minMem = minHeapSize();
+ settings.maxMem = maxHeapSize();
+ settings.permGen = m_permGenSpinBox->isVisible() ? m_permGenSpinBox->value() : 0;
+ m_checker.reset(new projt::java::RuntimeProbeTask(settings));
+ connect(m_checker.get(), &projt::java::RuntimeProbeTask::probeFinished, this, &JavaWizardWidget::checkFinished);
+ m_checker->start();
+}
+
+void JavaWizardWidget::checkFinished(const projt::java::RuntimeProbeTask::ProbeReport& result)
+{
+ m_result = result;
+ switch (result.status)
+ {
+ case projt::java::RuntimeProbeTask::ProbeReport::Status::Valid:
+ {
+ setJavaStatus(JavaStatus::Good);
+ break;
+ }
+ case projt::java::RuntimeProbeTask::ProbeReport::Status::InvalidData:
+ {
+ setJavaStatus(JavaStatus::ReturnedInvalidData);
+ break;
+ }
+ case projt::java::RuntimeProbeTask::ProbeReport::Status::Errored:
+ {
+ setJavaStatus(JavaStatus::DoesNotStart);
+ break;
+ }
+ }
+ updateThresholds();
+ m_checker.reset();
+ if (!queuedCheck.isNull())
+ {
+ checkJavaPath(queuedCheck);
+ queuedCheck.clear();
+ }
+}
+
+void JavaWizardWidget::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"));
+ if (BuildConfig.JAVA_DOWNLOADER_ENABLED)
+ {
+ m_autodownloadCheckBox->setText(tr("Auto-download Mojang Java"));
+ }
+ m_autodetectJavaCheckBox->setText(tr("Auto-detect Java version"));
+ m_autoJavaGroupBox->setTitle(tr("Autodetect Java"));
+}
+
+void JavaWizardWidget::updateThresholds()
+{
+ QString iconName;
+
+ if (observedMaxMemory >= m_availableMemory)
+ {
+ iconName = "status-bad";
+ m_labelMaxMemIcon->setToolTip(tr("Your maximum memory allocation exceeds your system memory capacity."));
+ }
+ else if (observedMaxMemory > (m_availableMemory * 0.9))
+ {
+ iconName = "status-yellow";
+ m_labelMaxMemIcon->setToolTip(tr("Your maximum memory allocation approaches your system memory capacity."));
+ }
+ else if (observedMaxMemory < observedMinMemory)
+ {
+ iconName = "status-yellow";
+ m_labelMaxMemIcon->setToolTip(tr("Your maximum memory allocation is smaller than the minimum value"));
+ }
+ else if (BuildConfig.JAVA_DOWNLOADER_ENABLED && m_autodownloadCheckBox->isChecked())
+ {
+ iconName = "status-good";
+ m_labelMaxMemIcon->setToolTip("");
+ }
+ else if (observedMaxMemory > 2048 && !m_result.is_64bit)
+ {
+ iconName = "status-bad";
+ m_labelMaxMemIcon->setToolTip(
+ tr("You are exceeding the maximum allocation supported by 32-bit installations of Java."));
+ }
+ else
+ {
+ iconName = "status-good";
+ m_labelMaxMemIcon->setToolTip("");
+ }
+
+ {
+ auto height = m_labelMaxMemIcon->fontInfo().pixelSize();
+ QIcon icon = QIcon::fromTheme(iconName);
+ QPixmap pix = icon.pixmap(height, height);
+ m_labelMaxMemIcon->setPixmap(pix);
+ }
+}
+
+bool JavaWizardWidget::autoDownloadJava() const
+{
+ return m_autodownloadCheckBox && m_autodownloadCheckBox->isChecked();
+}
+
+bool JavaWizardWidget::autoDetectJava() const
+{
+ return m_autodetectJavaCheckBox->isChecked();
+}
+
+void JavaWizardWidget::onSpinBoxValueChanged(int)
+{
+ m_memoryTimer->start(500);
+}
+
+JavaWizardWidget::~JavaWizardWidget()
+{
+ delete m_verticalSpacer;
+};
diff --git a/archived/projt-launcher/launcher/ui/widgets/JavaWizardWidget.h b/archived/projt-launcher/launcher/ui/widgets/JavaWizardWidget.h
new file mode 100644
index 0000000000..83f6b41f39
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/widgets/JavaWizardWidget.h
@@ -0,0 +1,137 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+#pragma once
+#include <QWidget>
+
+#include <BaseVersion.h>
+#include <QObjectPtr.h>
+#include <java/services/RuntimeProbeTask.hpp>
+#include <qcheckbox.h>
+#include <QIcon>
+
+class QLineEdit;
+class VersionSelectWidget;
+class QSpinBox;
+class QPushButton;
+class QVBoxLayout;
+class QHBoxLayout;
+class QGroupBox;
+class QGridLayout;
+class QLabel;
+class QToolButton;
+class QSpacerItem;
+
+class JavaWizardWidget : public QWidget
+{
+ Q_OBJECT
+
+ public:
+ explicit JavaWizardWidget(QWidget* parent);
+ virtual ~JavaWizardWidget();
+
+ 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;
+ bool autoDetectJava() const;
+ bool autoDownloadJava() const;
+
+ void updateThresholds();
+
+ protected slots:
+ void onSpinBoxValueChanged(int);
+ void memoryValueChanged();
+ void javaPathEdited(const QString& path);
+ void javaVersionSelected(BaseVersion::Ptr version);
+ void on_javaBrowseBtn_clicked();
+ void on_javaStatusBtn_clicked();
+ void javaDownloadBtn_clicked();
+ void checkFinished(const projt::java::RuntimeProbeTask::ProbeReport& 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;
+ QSpacerItem* m_verticalSpacer = 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;
+ QLabel* m_labelMaxMemIcon = nullptr;
+ QSpinBox* m_minMemSpinBox = nullptr;
+ QLabel* m_labelPermGen = nullptr;
+ QSpinBox* m_permGenSpinBox = nullptr;
+
+ QHBoxLayout* m_horizontalBtnLayout = nullptr;
+ QPushButton* m_javaDownloadBtn = nullptr;
+ QIcon goodIcon;
+ QIcon yellowIcon;
+ QIcon badIcon;
+
+ QGroupBox* m_autoJavaGroupBox = nullptr;
+ QVBoxLayout* m_veriticalJavaLayout = nullptr;
+ QCheckBox* m_autodetectJavaCheckBox = nullptr;
+ QCheckBox* m_autodownloadCheckBox = nullptr;
+
+ unsigned int observedMinMemory = 0;
+ unsigned int observedMaxMemory = 0;
+ unsigned int observedPermGenMemory = 0;
+ QString queuedCheck;
+ uint64_t m_availableMemory = 0ull;
+ shared_qobject_ptr<projt::java::RuntimeProbeTask> m_checker;
+ projt::java::RuntimeProbeTask::ProbeReport m_result;
+ QTimer* m_memoryTimer;
+};
diff --git a/archived/projt-launcher/launcher/ui/widgets/LabeledToolButton.cpp b/archived/projt-launcher/launcher/ui/widgets/LabeledToolButton.cpp
new file mode 100644
index 0000000000..dd9c503b0a
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/widgets/LabeledToolButton.cpp
@@ -0,0 +1,155 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * 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 "LabeledToolButton.h"
+#include <QApplication>
+#include <QDebug>
+#include <QLabel>
+#include <QResizeEvent>
+#include <QStyleOption>
+#include <QVBoxLayout>
+
+/*
+ *
+ * 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);
+
+ return style()->sizeFromContents(QStyle::CT_ToolButton, &opt, QSize(w, h), this);
+}
+
+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()
+{
+ constexpr int MAX_ICON_WIDTH = 160;
+ constexpr int MAX_ICON_HEIGHT = 80;
+
+ auto iconSz = m_icon.actualSize(QSize(MAX_ICON_WIDTH, MAX_ICON_HEIGHT));
+ float w = iconSz.width();
+ float h = iconSz.height();
+ float ar = w / h;
+
+ int newW = MAX_ICON_HEIGHT * ar;
+ if (newW > MAX_ICON_WIDTH)
+ newW = MAX_ICON_WIDTH;
+ QSize newSz(newW, MAX_ICON_HEIGHT);
+ auto pixmap = m_icon.pixmap(newSz);
+ m_label->setPixmap(pixmap);
+ m_label->setMinimumHeight(80);
+ m_label->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred);
+}
diff --git a/archived/projt-launcher/launcher/ui/widgets/LabeledToolButton.h b/archived/projt-launcher/launcher/ui/widgets/LabeledToolButton.h
new file mode 100644
index 0000000000..d7c5b81e09
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/widgets/LabeledToolButton.h
@@ -0,0 +1,63 @@
+// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ * 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/archived/projt-launcher/launcher/ui/widgets/LanguageSelectionWidget.cpp b/archived/projt-launcher/launcher/ui/widgets/LanguageSelectionWidget.cpp
new file mode 100644
index 0000000000..bdbefbe390
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/widgets/LanguageSelectionWidget.cpp
@@ -0,0 +1,111 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+#include "LanguageSelectionWidget.h"
+
+#include <QCheckBox>
+#include <QHeaderView>
+#include <QLabel>
+#include <QTreeView>
+#include <QVBoxLayout>
+#include "Application.h"
+#include "BuildConfig.h"
+#include "settings/Setting.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);
+
+ formatCheckbox = new QCheckBox(this);
+ formatCheckbox->setObjectName(QStringLiteral("formatCheckbox"));
+ formatCheckbox->setCheckState(APPLICATION->settings()->get("UseSystemLocale").toBool() ? Qt::Checked
+ : Qt::Unchecked);
+ connect(formatCheckbox,
+ &QCheckBox::stateChanged,
+ [this]() { APPLICATION->translations()->setUseSystemLocale(formatCheckbox->isChecked()); });
+ verticalLayout->addWidget(formatCheckbox);
+
+ 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);
+
+ auto language_setting = APPLICATION->settings()->getSetting("Language");
+ connect(language_setting.get(), &Setting::SettingChanged, this, &LanguageSelectionWidget::languageSettingChanged);
+}
+
+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(BuildConfig.TRANSLATIONS_URL);
+ helpUsLabel->setText(text);
+ formatCheckbox->setText(tr("Use system locales"));
+}
+
+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);
+}
+
+void LanguageSelectionWidget::languageSettingChanged(const Setting&, const QVariant&)
+{
+ auto translations = APPLICATION->translations();
+ auto index = translations->selectedIndex();
+ languageView->setCurrentIndex(index);
+}
diff --git a/archived/projt-launcher/launcher/ui/widgets/LanguageSelectionWidget.h b/archived/projt-launcher/launcher/ui/widgets/LanguageSelectionWidget.h
new file mode 100644
index 0000000000..1232b75c2d
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/widgets/LanguageSelectionWidget.h
@@ -0,0 +1,67 @@
+// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ * 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 Setting;
+class QCheckBox;
+
+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);
+ void languageSettingChanged(const Setting&, const QVariant&);
+
+ private:
+ QVBoxLayout* verticalLayout = nullptr;
+ QTreeView* languageView = nullptr;
+ QLabel* helpUsLabel = nullptr;
+ QCheckBox* formatCheckbox = nullptr;
+};
diff --git a/archived/projt-launcher/launcher/ui/widgets/LauncherHubWidget.cpp b/archived/projt-launcher/launcher/ui/widgets/LauncherHubWidget.cpp
new file mode 100644
index 0000000000..37101a0b7f
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/widgets/LauncherHubWidget.cpp
@@ -0,0 +1,1297 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+#include "LauncherHubWidget.h"
+
+#include <QApplication>
+#include <QDateTime>
+#include <QDesktopServices>
+#include <QDir>
+#include <QEvent>
+#include <QFrame>
+#include <QGridLayout>
+#include <QHBoxLayout>
+#include <QIcon>
+#include <QLabel>
+#include <QLineEdit>
+#include <QLocale>
+#include <QPainter>
+#include <QPushButton>
+#include <QScrollArea>
+#include <QSignalBlocker>
+#include <QSizePolicy>
+#include <QStackedWidget>
+#include <QTabBar>
+#include <QTextDocumentFragment>
+#include <QToolButton>
+#include <QVBoxLayout>
+
+#include "Application.h"
+#include "BaseInstance.h"
+#include "BuildConfig.h"
+#include "InstanceList.h"
+#include "MMCTime.h"
+#include "icons/IconList.hpp"
+#include "news/NewsChecker.h"
+#include "ui/widgets/CefHubView.h"
+#include "ui/widgets/FallbackHubView.h"
+#include "ui/widgets/HubSearchProvider.h"
+#include "ui/widgets/WebView2Widget.h"
+#include "ui/widgets/QtWebEngineHubView.h"
+
+#if defined(PROJT_DISABLE_LAUNCHER_HUB)
+LauncherHubWidget::LauncherHubWidget(QWidget* parent) : QWidget(parent)
+{
+ auto* layout = new QVBoxLayout(this);
+ layout->setContentsMargins(24, 24, 24, 24);
+
+ auto* label = new QLabel(tr("Launcher Hub is not available in this build."), this);
+ label->setAlignment(Qt::AlignCenter);
+ label->setWordWrap(true);
+ layout->addWidget(label, 1);
+}
+
+LauncherHubWidget::~LauncherHubWidget() = default;
+
+void LauncherHubWidget::ensureLoaded()
+{}
+
+void LauncherHubWidget::loadHome()
+{}
+
+void LauncherHubWidget::openUrl(const QUrl& url)
+{
+ if (url.isValid())
+ QDesktopServices::openUrl(url);
+}
+
+void LauncherHubWidget::newTab(const QUrl& url)
+{
+ openUrl(url);
+}
+
+void LauncherHubWidget::setHomeUrl(const QUrl& url)
+{
+ m_homeUrl = url;
+}
+
+QUrl LauncherHubWidget::homeUrl() const
+{
+ return m_homeUrl;
+}
+
+void LauncherHubWidget::setSelectedInstanceId(const QString&)
+{}
+
+void LauncherHubWidget::refreshCockpit()
+{}
+
+void LauncherHubWidget::changeEvent(QEvent* event)
+{
+ QWidget::changeEvent(event);
+}
+
+#else
+
+namespace
+{
+ QUrl defaultHubUrl()
+ {
+ if (!BuildConfig.HUB_HOME_URL.isEmpty())
+ {
+ return QUrl(BuildConfig.HUB_HOME_URL);
+ }
+ return QUrl(QStringLiteral("https://projecttick.org/p/projt-launcher/"));
+ }
+
+ void clearLayout(QLayout* layout)
+ {
+ if (!layout)
+ {
+ return;
+ }
+
+ while (auto* item = layout->takeAt(0))
+ {
+ if (auto* widget = item->widget())
+ {
+ delete widget;
+ }
+ if (auto* childLayout = item->layout())
+ {
+ clearLayout(childLayout);
+ delete childLayout;
+ }
+ delete item;
+ }
+ }
+
+ QString relativeTimeLabel(qint64 timestamp)
+ {
+ if (timestamp <= 0)
+ {
+ return LauncherHubWidget::tr("Never launched");
+ }
+
+ const QDateTime launchedAt = QDateTime::fromMSecsSinceEpoch(timestamp);
+ const qint64 secondsAgo = launchedAt.secsTo(QDateTime::currentDateTime());
+ if (secondsAgo < 60)
+ {
+ return LauncherHubWidget::tr("Just now");
+ }
+ if (secondsAgo < 3600)
+ {
+ return LauncherHubWidget::tr("%1 min ago").arg(secondsAgo / 60);
+ }
+ if (secondsAgo < 86400)
+ {
+ return LauncherHubWidget::tr("%1 hr ago").arg(secondsAgo / 3600);
+ }
+ if (secondsAgo < 604800)
+ {
+ return LauncherHubWidget::tr("%1 day(s) ago").arg(secondsAgo / 86400);
+ }
+ return QLocale().toString(launchedAt, QLocale::ShortFormat);
+ }
+
+ QString stripHtmlExcerpt(const QString& html, int maxLength = 120)
+ {
+ QString text = QTextDocumentFragment::fromHtml(html).toPlainText().simplified();
+ if (text.size() <= maxLength)
+ {
+ return text;
+ }
+ return text.left(maxLength - 1) + QStringLiteral("...");
+ }
+
+ QString heroBadgeForInstance(const InstancePtr& instance)
+ {
+ if (!instance)
+ {
+ return LauncherHubWidget::tr("Cockpit");
+ }
+ if (instance->isRunning())
+ {
+ return LauncherHubWidget::tr("Now playing");
+ }
+ if (instance->hasCrashed() || instance->hasVersionBroken())
+ {
+ return LauncherHubWidget::tr("Needs attention");
+ }
+ if (instance->hasUpdateAvailable())
+ {
+ return LauncherHubWidget::tr("Update ready");
+ }
+ return LauncherHubWidget::tr("Ready to launch");
+ }
+
+ QList<InstancePtr> sortedInstances()
+ {
+ QList<InstancePtr> instances;
+ if (!APPLICATION->instances())
+ {
+ return instances;
+ }
+
+ for (int i = 0; i < APPLICATION->instances()->count(); ++i)
+ {
+ instances.append(APPLICATION->instances()->at(i));
+ }
+
+ std::sort(instances.begin(),
+ instances.end(),
+ [](const InstancePtr& left, const InstancePtr& right)
+ {
+ if (left->lastLaunch() == right->lastLaunch())
+ {
+ return left->name().localeAwareCompare(right->name()) < 0;
+ }
+ return left->lastLaunch() > right->lastLaunch();
+ });
+ return instances;
+ }
+
+ QIcon tintedIcon(const QString& themeName, const QWidget* widget, const QSize& size = QSize(18, 18))
+ {
+ QIcon source = QIcon::fromTheme(themeName);
+ if (source.isNull())
+ {
+ return source;
+ }
+
+ const qreal devicePixelRatio = widget ? widget->devicePixelRatioF() : qApp->devicePixelRatio();
+ const QSize pixelSize = QSize(qMax(1, qRound(size.width() * devicePixelRatio)),
+ qMax(1, qRound(size.height() * devicePixelRatio)));
+
+ auto colorizedPixmap = [&](QIcon::Mode mode)
+ {
+ QPixmap sourcePixmap = source.pixmap(pixelSize, devicePixelRatio, mode);
+ if (sourcePixmap.isNull())
+ {
+ sourcePixmap = source.pixmap(pixelSize, devicePixelRatio, QIcon::Normal);
+ }
+
+ QPixmap tinted(pixelSize);
+ tinted.fill(Qt::transparent);
+
+ const QColor color = widget ? widget->palette().color(mode == QIcon::Disabled ? QPalette::Disabled
+ : QPalette::Active,
+ QPalette::ButtonText)
+ : QColor(Qt::white);
+
+ QPainter painter(&tinted);
+ painter.drawPixmap(0, 0, sourcePixmap);
+ painter.setCompositionMode(QPainter::CompositionMode_SourceIn);
+ painter.fillRect(tinted.rect(), color);
+ painter.end();
+ tinted.setDevicePixelRatio(devicePixelRatio);
+ return tinted;
+ };
+
+ QIcon icon;
+ icon.addPixmap(colorizedPixmap(QIcon::Normal), QIcon::Normal);
+ icon.addPixmap(colorizedPixmap(QIcon::Disabled), QIcon::Disabled);
+ icon.addPixmap(colorizedPixmap(QIcon::Active), QIcon::Active);
+ icon.addPixmap(colorizedPixmap(QIcon::Selected), QIcon::Selected);
+ return icon;
+ }
+ HubViewBase* createBrowserView(QWidget* parent)
+ {
+#if defined(PROJT_USE_WEBVIEW2)
+ return new WebView2Widget(parent);
+#elif defined(PROJT_USE_WEBENGINE)
+ return new QtWebEngineHubView(parent);
+#elif defined(PROJT_USE_CEF)
+ return new CefHubView(parent);
+#else
+ return new FallbackHubView(QObject::tr("This page opens in your browser on this platform."), parent);
+#endif
+ }
+}
+
+LauncherHubWidget::LauncherHubWidget(QWidget* parent) : QWidget(parent)
+{
+ m_homeUrl = defaultHubUrl();
+
+ auto* layout = new QVBoxLayout(this);
+ layout->setContentsMargins(0, 0, 0, 0);
+
+ m_tabsBarContainer = new QWidget(this);
+ m_tabsBarContainer->setObjectName("hubTabsBar");
+ auto* tabsLayout = new QHBoxLayout(m_tabsBarContainer);
+ tabsLayout->setContentsMargins(10, 10, 10, 6);
+
+ m_tabBar = new QTabBar(this);
+ m_tabBar->setMovable(true);
+ m_tabBar->setExpanding(false);
+ m_tabBar->setDocumentMode(true);
+ m_tabBar->setTabsClosable(true);
+ tabsLayout->addWidget(m_tabBar, 1);
+
+ m_toolbarContainer = new QWidget(this);
+ m_toolbarContainer->setObjectName("hubToolbar");
+ auto* toolbar = new QHBoxLayout(m_toolbarContainer);
+ toolbar->setContentsMargins(10, 8, 10, 10);
+
+ m_backButton = new QToolButton(this);
+ m_backButton->setToolTip(tr("Back"));
+ m_backButton->setEnabled(false);
+
+ m_forwardButton = new QToolButton(this);
+ m_forwardButton->setToolTip(tr("Forward"));
+ m_forwardButton->setEnabled(false);
+
+ m_reloadButton = new QToolButton(this);
+ m_reloadButton->setToolTip(tr("Reload"));
+
+ m_homeButton = new QToolButton(this);
+ m_homeButton->setToolTip(tr("Cockpit"));
+
+ m_newTabButton = new QToolButton(this);
+ m_newTabButton->setToolTip(tr("New Tab"));
+
+ m_addressBar = new QLineEdit(this);
+ m_addressBar->setPlaceholderText(tr("Search or enter address"));
+ m_addressBar->setClearButtonEnabled(true);
+
+ m_goButton = new QToolButton(this);
+ m_goButton->setToolTip(tr("Go"));
+
+ toolbar->addWidget(m_backButton);
+ toolbar->addWidget(m_forwardButton);
+ toolbar->addWidget(m_reloadButton);
+ toolbar->addWidget(m_homeButton);
+ toolbar->addWidget(m_newTabButton);
+ toolbar->addWidget(m_addressBar, 1);
+ toolbar->addWidget(m_goButton);
+
+ m_stack = new QStackedWidget(this);
+
+ layout->addWidget(m_tabsBarContainer);
+ layout->addWidget(m_toolbarContainer);
+ layout->addWidget(m_stack);
+
+ connect(m_backButton,
+ &QToolButton::clicked,
+ this,
+ [this]()
+ {
+ if (auto* view = currentView())
+ {
+ view->back();
+ }
+ });
+ connect(m_forwardButton,
+ &QToolButton::clicked,
+ this,
+ [this]()
+ {
+ if (auto* view = currentView())
+ {
+ view->forward();
+ }
+ });
+ connect(m_reloadButton,
+ &QToolButton::clicked,
+ this,
+ [this]()
+ {
+ if (auto* view = currentView())
+ {
+ view->reload();
+ }
+ else
+ {
+ refreshCockpit();
+ }
+ });
+ connect(m_homeButton, &QToolButton::clicked, this, &LauncherHubWidget::loadHome);
+ connect(m_goButton,
+ &QToolButton::clicked,
+ this,
+ [this]()
+ {
+ const QString providerId =
+ APPLICATION->settings() ? APPLICATION->settings()->get("HubSearchEngine").toString() : QString();
+ openUrl(resolveHubInput(m_addressBar->text(), providerId));
+ });
+ connect(m_addressBar,
+ &QLineEdit::returnPressed,
+ this,
+ [this]()
+ {
+ const QString providerId =
+ APPLICATION->settings() ? APPLICATION->settings()->get("HubSearchEngine").toString() : QString();
+ openUrl(resolveHubInput(m_addressBar->text(), providerId));
+ });
+ connect(m_newTabButton, &QToolButton::clicked, this, [this]() { newTab(m_homeUrl); });
+
+ connect(m_tabBar,
+ &QTabBar::tabMoved,
+ this,
+ [this](int, int)
+ {
+ updateTabPerformanceState();
+ updateNavigationState();
+ });
+ connect(m_tabBar,
+ &QTabBar::currentChanged,
+ this,
+ [this](int index)
+ {
+ if (auto* view = viewForTabIndex(index))
+ {
+ m_stack->setCurrentWidget(view);
+ activatePendingForPage(view);
+ updateTabPerformanceState();
+ updateNavigationState();
+ }
+ });
+ connect(m_tabBar,
+ &QTabBar::tabCloseRequested,
+ this,
+ [this](int index)
+ {
+ auto* view = viewForTabIndex(index);
+ if (!view)
+ {
+ return;
+ }
+
+ m_stack->removeWidget(view);
+ m_tabBar->removeTab(index);
+ view->deleteLater();
+
+ if (m_tabBar->count() > 0)
+ {
+ const int newIndex = qMin(index, m_tabBar->count() - 1);
+ m_tabBar->setCurrentIndex(newIndex);
+ if (auto* nextView = viewForTabIndex(newIndex))
+ {
+ m_stack->setCurrentWidget(nextView);
+ activatePendingForPage(nextView);
+ }
+ }
+ else
+ {
+ switchToPage(m_cockpitPage);
+ }
+
+ syncTabsUi();
+ updateTabPerformanceState();
+ updateNavigationState();
+ });
+
+ m_newsChecker = new NewsChecker(APPLICATION->network(), BuildConfig.NEWS_RSS_URL);
+ m_newsChecker->setParent(this);
+ connect(m_newsChecker, &NewsChecker::newsLoaded, this, &LauncherHubWidget::rebuildNewsFeed);
+ connect(m_newsChecker, &NewsChecker::newsLoadingFailed, this, &LauncherHubWidget::rebuildNewsFeed);
+
+ if (APPLICATION->instances())
+ {
+ connect(APPLICATION->instances().get(),
+ &InstanceList::instancesChanged,
+ this,
+ &LauncherHubWidget::refreshCockpit);
+ connect(APPLICATION->instances().get(),
+ &InstanceList::dataChanged,
+ this,
+ [this](const QModelIndex&, const QModelIndex&, const QList<int>&) { refreshCockpit(); });
+ }
+ if (APPLICATION->icons())
+ {
+ connect(APPLICATION->icons().get(),
+ &projt::icons::IconList::iconUpdated,
+ this,
+ [this](const QString&) { refreshCockpit(); });
+ }
+
+ createCockpitTab();
+ refreshToolbarIcons();
+
+ refreshCockpit();
+ m_newsChecker->reloadNews();
+ syncTabsUi();
+}
+
+LauncherHubWidget::~LauncherHubWidget() = default;
+
+HubViewBase* LauncherHubWidget::currentView() const
+{
+ if (!m_stack)
+ {
+ return nullptr;
+ }
+ return qobject_cast<HubViewBase*>(m_stack->currentWidget());
+}
+
+HubViewBase* LauncherHubWidget::viewForTabIndex(int index) const
+{
+ if (!m_tabBar || index < 0 || index >= m_tabBar->count())
+ {
+ return nullptr;
+ }
+
+ return qobject_cast<HubViewBase*>(m_tabBar->tabData(index).value<QObject*>());
+}
+
+int LauncherHubWidget::tabIndexForView(const HubViewBase* view) const
+{
+ if (!m_tabBar || !view)
+ {
+ return -1;
+ }
+
+ for (int i = 0; i < m_tabBar->count(); ++i)
+ {
+ if (m_tabBar->tabData(i).value<QObject*>() == view)
+ {
+ return i;
+ }
+ }
+ return -1;
+}
+
+void LauncherHubWidget::createCockpitTab()
+{
+ auto* scrollArea = new QScrollArea(m_stack);
+ scrollArea->setWidgetResizable(true);
+ scrollArea->setFrameShape(QFrame::NoFrame);
+ scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+
+ auto* content = new QWidget(scrollArea);
+ content->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
+ auto* pageLayout = new QVBoxLayout(content);
+ pageLayout->setContentsMargins(18, 18, 18, 18);
+ pageLayout->setSpacing(16);
+
+ auto* heroCard = new QFrame(content);
+ heroCard->setObjectName("hubHeroCard");
+ auto* heroLayout = new QVBoxLayout(heroCard);
+ heroLayout->setContentsMargins(20, 20, 20, 20);
+ heroLayout->setSpacing(14);
+
+ auto* heroTop = new QHBoxLayout();
+ heroTop->setSpacing(14);
+ m_cockpitIconLabel = new QLabel(heroCard);
+ m_cockpitIconLabel->setFixedSize(52, 52);
+ m_cockpitIconLabel->setAlignment(Qt::AlignCenter);
+
+ auto* heroText = new QVBoxLayout();
+ heroText->setSpacing(6);
+ m_cockpitBadgeLabel = new QLabel(heroCard);
+ m_cockpitBadgeLabel->setObjectName("hubBadge");
+ m_cockpitBadgeLabel->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Preferred);
+ m_cockpitTitleLabel = new QLabel(heroCard);
+ m_cockpitTitleLabel->setObjectName("hubHeroTitle");
+ m_cockpitTitleLabel->setWordWrap(true);
+ m_cockpitSubtitleLabel = new QLabel(heroCard);
+ m_cockpitSubtitleLabel->setObjectName("hubHeroSubtitle");
+ m_cockpitSubtitleLabel->setWordWrap(true);
+ heroText->addWidget(m_cockpitBadgeLabel, 0, Qt::AlignLeft);
+ heroText->addWidget(m_cockpitTitleLabel);
+ heroText->addWidget(m_cockpitSubtitleLabel);
+
+ heroTop->addWidget(m_cockpitIconLabel, 0, Qt::AlignTop);
+ heroTop->addLayout(heroText, 1);
+ heroLayout->addLayout(heroTop);
+
+ auto* heroActions = new QHBoxLayout();
+ heroActions->setSpacing(10);
+ m_playButton = new QPushButton(tr("Play"), heroCard);
+ m_playButton->setObjectName("hubPrimaryButton");
+ m_editButton = new QPushButton(tr("Edit"), heroCard);
+ m_editButton->setObjectName("hubSecondaryButton");
+ m_backupsButton = new QPushButton(tr("Backups"), heroCard);
+ m_backupsButton->setObjectName("hubSecondaryButton");
+ m_folderButton = new QPushButton(tr("Open Folder"), heroCard);
+ m_folderButton->setObjectName("hubSecondaryButton");
+ heroActions->addWidget(m_playButton);
+ heroActions->addWidget(m_editButton);
+ heroActions->addWidget(m_backupsButton);
+ heroActions->addWidget(m_folderButton);
+ heroActions->addStretch(1);
+ heroLayout->addLayout(heroActions);
+ pageLayout->addWidget(heroCard);
+
+ auto* metricsLayout = new QGridLayout();
+ metricsLayout->setHorizontalSpacing(12);
+ metricsLayout->setVerticalSpacing(12);
+ metricsLayout->setColumnStretch(0, 1);
+ metricsLayout->setColumnStretch(1, 1);
+ metricsLayout->setColumnStretch(2, 1);
+ auto makeMetricCard = [content](const QString& title, QLabel*& valueLabel, QLabel*& detailLabel)
+ {
+ auto* card = new QFrame(content);
+ card->setObjectName("hubMetricCard");
+ auto* cardLayout = new QVBoxLayout(card);
+ cardLayout->setContentsMargins(16, 16, 16, 16);
+ cardLayout->setSpacing(6);
+
+ auto* titleLabel = new QLabel(title, card);
+ titleLabel->setObjectName("hubPanelSubtitle");
+ valueLabel = new QLabel(card);
+ valueLabel->setObjectName("hubMetricValue");
+ detailLabel = new QLabel(card);
+ detailLabel->setObjectName("hubMetricDetail");
+ detailLabel->setWordWrap(true);
+
+ cardLayout->addWidget(titleLabel);
+ cardLayout->addWidget(valueLabel);
+ cardLayout->addWidget(detailLabel);
+ return card;
+ };
+
+ metricsLayout->addWidget(makeMetricCard(tr("Instances"), m_instancesValueLabel, m_instancesDetailLabel), 0, 0);
+ metricsLayout->addWidget(makeMetricCard(tr("Total Playtime"), m_playtimeValueLabel, m_playtimeDetailLabel), 0, 1);
+ metricsLayout->addWidget(makeMetricCard(tr("Needs Attention"), m_attentionValueLabel, m_attentionDetailLabel),
+ 0,
+ 2);
+ pageLayout->addLayout(metricsLayout);
+
+ auto* lowerGrid = new QGridLayout();
+ lowerGrid->setHorizontalSpacing(12);
+ lowerGrid->setVerticalSpacing(12);
+ lowerGrid->setColumnStretch(0, 1);
+ lowerGrid->setColumnStretch(1, 1);
+
+ auto* recentPanel = new QFrame(content);
+ recentPanel->setObjectName("hubPanel");
+ auto* recentPanelLayout = new QVBoxLayout(recentPanel);
+ recentPanelLayout->setContentsMargins(16, 16, 16, 16);
+ recentPanelLayout->setSpacing(10);
+ auto* recentTitle = new QLabel(tr("Continue Playing"), recentPanel);
+ recentTitle->setObjectName("hubPanelTitle");
+ auto* recentSubtitle = new QLabel(tr("Jump back into your most recent worlds or packs."), recentPanel);
+ recentSubtitle->setObjectName("hubPanelSubtitle");
+ recentSubtitle->setWordWrap(true);
+ recentPanelLayout->addWidget(recentTitle);
+ recentPanelLayout->addWidget(recentSubtitle);
+ m_recentInstancesLayout = new QVBoxLayout();
+ m_recentInstancesLayout->setSpacing(8);
+ recentPanelLayout->addLayout(m_recentInstancesLayout);
+ recentPanelLayout->addStretch(1);
+ lowerGrid->addWidget(recentPanel, 0, 0);
+
+ auto* newsPanel = new QFrame(content);
+ newsPanel->setObjectName("hubPanel");
+ auto* newsPanelLayout = new QVBoxLayout(newsPanel);
+ newsPanelLayout->setContentsMargins(16, 16, 16, 16);
+ newsPanelLayout->setSpacing(10);
+ auto* newsTitle = new QLabel(tr("Community Pulse"), newsPanel);
+ newsTitle->setObjectName("hubPanelTitle");
+ auto* newsSubtitle = new QLabel(tr("Latest launcher news without leaving the cockpit."), newsPanel);
+ newsSubtitle->setObjectName("hubPanelSubtitle");
+ newsSubtitle->setWordWrap(true);
+ newsPanelLayout->addWidget(newsTitle);
+ newsPanelLayout->addWidget(newsSubtitle);
+ m_newsLayout = new QVBoxLayout();
+ m_newsLayout->setSpacing(8);
+ newsPanelLayout->addLayout(m_newsLayout);
+ newsPanelLayout->addStretch(1);
+ lowerGrid->addWidget(newsPanel, 0, 1);
+
+ auto* linksPanel = new QFrame(content);
+ linksPanel->setObjectName("hubPanel");
+ auto* linksLayout = new QVBoxLayout(linksPanel);
+ linksLayout->setContentsMargins(16, 16, 16, 16);
+ linksLayout->setSpacing(10);
+ auto* linksTitle = new QLabel(tr("Quick Routes"), linksPanel);
+ linksTitle->setObjectName("hubPanelTitle");
+ auto* linksSubtitle = new QLabel(tr("Open the spaces you reach for most while you play."), linksPanel);
+ linksSubtitle->setObjectName("hubPanelSubtitle");
+ linksSubtitle->setWordWrap(true);
+ linksLayout->addWidget(linksTitle);
+ linksLayout->addWidget(linksSubtitle);
+
+ auto addLinkButton = [this, linksPanel, linksLayout](const QString& label, const QUrl& url)
+ {
+ auto* button = new QPushButton(label, linksPanel);
+ button->setObjectName("hubSecondaryButton");
+ connect(button, &QPushButton::clicked, this, [this, url]() { openUrl(url); });
+ linksLayout->addWidget(button);
+ };
+
+ addLinkButton(tr("Open website"), m_homeUrl);
+ addLinkButton(tr("Read news"), QUrl(BuildConfig.NEWS_OPEN_URL));
+ if (!BuildConfig.HUB_COMMUNITY_URL.isEmpty())
+ {
+ addLinkButton(tr("Open community"), QUrl(BuildConfig.HUB_COMMUNITY_URL));
+ }
+ addLinkButton(tr("Open help"), QUrl(BuildConfig.HELP_URL.arg("")));
+ lowerGrid->addWidget(linksPanel, 1, 0, 1, 2);
+
+ pageLayout->addLayout(lowerGrid);
+ pageLayout->addStretch(1);
+
+ scrollArea->setWidget(content);
+ m_cockpitPage = scrollArea;
+ m_stack->addWidget(m_cockpitPage);
+ m_stack->setCurrentWidget(m_cockpitPage);
+
+ connect(m_playButton,
+ &QPushButton::clicked,
+ this,
+ [this]()
+ {
+ const QString instanceId = activeInstanceId();
+ if (!instanceId.isEmpty())
+ {
+ emit launchInstanceRequested(instanceId);
+ }
+ });
+ connect(m_editButton,
+ &QPushButton::clicked,
+ this,
+ [this]()
+ {
+ const QString instanceId = activeInstanceId();
+ if (!instanceId.isEmpty())
+ {
+ emit editInstanceRequested(instanceId);
+ }
+ });
+ connect(m_backupsButton,
+ &QPushButton::clicked,
+ this,
+ [this]()
+ {
+ const QString instanceId = activeInstanceId();
+ if (!instanceId.isEmpty())
+ {
+ emit backupsRequested(instanceId);
+ }
+ });
+ connect(m_folderButton,
+ &QPushButton::clicked,
+ this,
+ [this]()
+ {
+ const QString instanceId = activeInstanceId();
+ if (!instanceId.isEmpty())
+ {
+ emit openInstanceFolderRequested(instanceId);
+ }
+ });
+}
+
+void LauncherHubWidget::changeEvent(QEvent* event)
+{
+ QWidget::changeEvent(event);
+ if (!event)
+ {
+ return;
+ }
+
+ if (event->type() == QEvent::PaletteChange || event->type() == QEvent::ApplicationPaletteChange)
+ {
+ refreshToolbarIcons();
+ updateHero();
+ }
+}
+
+HubViewBase* LauncherHubWidget::createTab(const QUrl& url, const QString& label, bool switchTo)
+{
+ if (!m_stack || !m_tabBar)
+ {
+ return nullptr;
+ }
+
+ auto* view = createBrowserView(m_stack);
+
+ QWidget* previousPage = m_stack->currentWidget();
+ const int previousTabIndex = m_tabBar->currentIndex();
+
+ m_stack->addWidget(view);
+ const QString initialLabel = label.isEmpty() ? tr("New Tab") : label;
+ int tabIndex = -1;
+ if (switchTo)
+ {
+ tabIndex = m_tabBar->addTab(initialLabel);
+ }
+ else
+ {
+ const QSignalBlocker blocker(m_tabBar);
+ tabIndex = m_tabBar->addTab(initialLabel);
+ m_tabBar->setCurrentIndex(previousTabIndex);
+ }
+ m_tabBar->setTabData(tabIndex, QVariant::fromValue(static_cast<QObject*>(view)));
+
+ auto updateTitle = [this, view](const QString& title)
+ {
+ const int index = tabIndexForView(view);
+ if (index >= 0 && !title.isEmpty())
+ {
+ m_tabBar->setTabText(index, title);
+ }
+ };
+
+ connect(view, &HubViewBase::titleChanged, this, updateTitle);
+ connect(view,
+ &HubViewBase::urlChanged,
+ this,
+ [this, view](const QUrl& urlChanged)
+ {
+ if (view == currentView())
+ {
+ m_addressBar->setText(urlChanged.toString());
+ updateNavigationState();
+ }
+ });
+ connect(view,
+ &HubViewBase::loadFinished,
+ this,
+ [this, view](bool)
+ {
+ if (view == currentView())
+ {
+ updateNavigationState();
+ }
+ });
+ connect(view, &HubViewBase::navigationStateChanged, this, &LauncherHubWidget::updateNavigationState);
+ connect(view,
+ &HubViewBase::newTabRequested,
+ this,
+ [this](const QUrl& requestedUrl)
+ {
+ if (!requestedUrl.isValid())
+ {
+ return;
+ }
+
+ createTab(requestedUrl, QString(), true);
+ });
+
+ if (switchTo)
+ {
+ m_tabBar->setCurrentIndex(tabIndex);
+ m_stack->setCurrentWidget(view);
+ }
+ else if (previousPage)
+ {
+ m_stack->setCurrentWidget(previousPage);
+ }
+
+ if (url.isValid())
+ {
+ const bool shouldLoadNow = switchTo;
+ if (shouldLoadNow)
+ {
+ view->setUrl(url);
+ }
+ else
+ {
+ view->setProperty("hubPendingUrl", url);
+ }
+ }
+
+ syncTabsUi();
+ updateTabPerformanceState();
+ return view;
+}
+
+void LauncherHubWidget::switchToPage(QWidget* page)
+{
+ if (!m_stack || !m_tabBar || !page)
+ {
+ return;
+ }
+
+ m_stack->setCurrentWidget(page);
+ if (page == m_cockpitPage)
+ {
+ updateTabPerformanceState();
+ updateNavigationState();
+ syncTabsUi();
+ return;
+ }
+
+ if (auto* view = qobject_cast<HubViewBase*>(page))
+ {
+ const int index = tabIndexForView(view);
+ if (index >= 0)
+ {
+ m_tabBar->setCurrentIndex(index);
+ }
+ activatePendingForPage(view);
+ }
+
+ updateTabPerformanceState();
+ updateNavigationState();
+ syncTabsUi();
+}
+
+void LauncherHubWidget::activatePendingForPage(QWidget* page)
+{
+ if (!page)
+ {
+ return;
+ }
+ if (auto* view = qobject_cast<HubViewBase*>(page))
+ {
+ const QUrl pendingUrl = view->property("hubPendingUrl").toUrl();
+ if (pendingUrl.isValid())
+ {
+ view->setProperty("hubPendingUrl", QUrl());
+ view->setUrl(pendingUrl);
+ }
+ }
+}
+
+void LauncherHubWidget::updateNavigationState()
+{
+ auto* view = currentView();
+ if (!view)
+ {
+ m_backButton->setEnabled(false);
+ m_forwardButton->setEnabled(false);
+ m_goButton->setEnabled(false);
+ m_addressBar->clear();
+ m_addressBar->setEnabled(false);
+ m_addressBar->setPlaceholderText(tr("Launcher Hub Cockpit"));
+ return;
+ }
+
+ m_goButton->setEnabled(true);
+ m_addressBar->setEnabled(true);
+ m_addressBar->setPlaceholderText(tr("Search or enter address"));
+ m_backButton->setEnabled(view->canGoBack());
+ m_forwardButton->setEnabled(view->canGoForward());
+ m_addressBar->setText(view->url().toString());
+}
+
+void LauncherHubWidget::syncTabsUi()
+{
+ if (m_tabsBarContainer && m_tabBar)
+ {
+ m_tabsBarContainer->setVisible(m_tabBar->count() > 0);
+ }
+}
+
+void LauncherHubWidget::refreshToolbarIcons()
+{
+ if (m_backButton)
+ {
+ m_backButton->setIcon(tintedIcon(QStringLiteral("go-previous"), this));
+ }
+ if (m_forwardButton)
+ {
+ m_forwardButton->setIcon(tintedIcon(QStringLiteral("go-next"), this));
+ }
+ if (m_reloadButton)
+ {
+ m_reloadButton->setIcon(tintedIcon(QStringLiteral("view-refresh"), this));
+ }
+ if (m_homeButton)
+ {
+ m_homeButton->setIcon(tintedIcon(QStringLiteral("go-home"), this));
+ }
+ if (m_newTabButton)
+ {
+ m_newTabButton->setIcon(tintedIcon(QStringLiteral("list-add"), this));
+ }
+ if (m_goButton)
+ {
+ m_goButton->setIcon(tintedIcon(QStringLiteral("system-search"), this));
+ }
+}
+
+void LauncherHubWidget::updateTabPerformanceState()
+{
+#if defined(PROJT_USE_WEBENGINE)
+ if (!m_stack)
+ {
+ return;
+ }
+
+ const int activeIndex = m_stack->currentIndex();
+ for (int i = 0; i < m_stack->count(); ++i)
+ {
+ auto* view = qobject_cast<HubViewBase*>(m_stack->widget(i));
+ if (!view)
+ {
+ continue;
+ }
+ view->setActive(i == activeIndex);
+ }
+#endif
+}
+
+void LauncherHubWidget::ensureLoaded()
+{
+ loadHome();
+ m_loaded = true;
+}
+
+void LauncherHubWidget::loadHome()
+{
+ refreshCockpit();
+ switchToPage(m_cockpitPage);
+}
+
+void LauncherHubWidget::newTab(const QUrl& url)
+{
+ createTab(url.isValid() ? url : m_homeUrl, QString(), true);
+ m_loaded = true;
+}
+
+void LauncherHubWidget::openUrl(const QUrl& url)
+{
+ if (!url.isValid())
+ {
+ return;
+ }
+
+ auto* view = currentView();
+ if (!view)
+ {
+ createTab(url, QString(), true);
+ updateTabPerformanceState();
+ updateNavigationState();
+ m_loaded = true;
+ return;
+ }
+
+ view->setUrl(url);
+ updateTabPerformanceState();
+ m_loaded = true;
+}
+
+void LauncherHubWidget::setHomeUrl(const QUrl& url)
+{
+ m_homeUrl = url;
+ m_loaded = false;
+}
+
+QUrl LauncherHubWidget::homeUrl() const
+{
+ return m_homeUrl;
+}
+
+void LauncherHubWidget::setSelectedInstanceId(const QString& id)
+{
+ m_selectedInstanceId = id;
+ refreshCockpit();
+}
+
+QString LauncherHubWidget::activeInstanceId() const
+{
+ if (!m_selectedInstanceId.isEmpty())
+ {
+ return m_selectedInstanceId;
+ }
+
+ if (APPLICATION->settings())
+ {
+ const QString selected = APPLICATION->settings()->get("SelectedInstance").toString();
+ if (!selected.isEmpty())
+ {
+ return selected;
+ }
+ }
+
+ const QList<InstancePtr> instances = sortedInstances();
+ if (!instances.isEmpty())
+ {
+ return instances.first()->id();
+ }
+ return {};
+}
+
+void LauncherHubWidget::refreshCockpit()
+{
+ if (!m_cockpitPage)
+ {
+ return;
+ }
+
+ if (m_newsChecker && !m_newsChecker->isLoadingNews() && m_newsChecker->getNewsEntries().isEmpty()
+ && m_newsChecker->getLastLoadErrorMsg().isEmpty())
+ {
+ m_newsChecker->reloadNews();
+ }
+
+ const QList<InstancePtr> instances = sortedInstances();
+ int managedCount = 0;
+ int attentionCount = 0;
+ for (const auto& instance : instances)
+ {
+ if (instance->isManagedPack())
+ {
+ managedCount++;
+ }
+ if (instance->hasUpdateAvailable() || instance->hasCrashed() || instance->hasVersionBroken())
+ {
+ attentionCount++;
+ }
+ }
+
+ if (m_instancesValueLabel)
+ {
+ m_instancesValueLabel->setText(QString::number(instances.size()));
+ }
+ if (m_instancesDetailLabel)
+ {
+ m_instancesDetailLabel->setText(instances.isEmpty() ? tr("No instances yet")
+ : tr("%1 managed pack(s) in rotation").arg(managedCount));
+ }
+
+ const int totalPlaytime = APPLICATION->instances() ? APPLICATION->instances()->getTotalPlayTime() : 0;
+ if (m_playtimeValueLabel)
+ {
+ m_playtimeValueLabel->setText(
+ totalPlaytime > 0 ? Time::prettifyDuration(totalPlaytime,
+ APPLICATION->settings()->get("ShowGameTimeWithoutDays").toBool())
+ : tr("0m"));
+ }
+ if (m_playtimeDetailLabel)
+ {
+ m_playtimeDetailLabel->setText(tr("Your full launcher history across every instance."));
+ }
+ if (m_attentionValueLabel)
+ {
+ m_attentionValueLabel->setText(QString::number(attentionCount));
+ }
+ if (m_attentionDetailLabel)
+ {
+ m_attentionDetailLabel->setText(attentionCount > 0 ? tr("Updates, crashes, or broken versions to review.")
+ : tr("Everything looks healthy right now."));
+ }
+
+ updateHero();
+ rebuildRecentInstances();
+ rebuildNewsFeed();
+}
+
+void LauncherHubWidget::updateHero()
+{
+ const QString instanceId = activeInstanceId();
+ const InstancePtr instance =
+ APPLICATION->instances() ? APPLICATION->instances()->getInstanceById(instanceId) : nullptr;
+
+ if (!instance)
+ {
+ m_cockpitBadgeLabel->setText(tr("Cockpit"));
+ m_cockpitTitleLabel->setText(tr("Launcher Hub is ready"));
+ m_cockpitSubtitleLabel->setText(tr("Open news, community pages, and help from one place. Once you create or "
+ "select an instance, it will appear here."));
+ m_cockpitIconLabel->setPixmap(APPLICATION->logo().pixmap(40, 40));
+ m_playButton->setEnabled(false);
+ m_editButton->setEnabled(false);
+ m_backupsButton->setEnabled(false);
+ m_folderButton->setEnabled(false);
+ return;
+ }
+
+ m_cockpitBadgeLabel->setText(heroBadgeForInstance(instance));
+ m_cockpitTitleLabel->setText(instance->name());
+
+ QString subtitle = instance->getStatusbarDescription();
+ const QString lastLaunchText = relativeTimeLabel(instance->lastLaunch());
+ if (!subtitle.isEmpty())
+ {
+ subtitle += tr(" | Last launch: %1").arg(lastLaunchText);
+ }
+ else
+ {
+ subtitle = tr("Last launch: %1").arg(lastLaunchText);
+ }
+ m_cockpitSubtitleLabel->setText(subtitle);
+ m_cockpitIconLabel->setPixmap(APPLICATION->icons()->getIcon(instance->iconKey()).pixmap(40, 40));
+ m_playButton->setEnabled(instance->canLaunch() && !instance->isRunning());
+ m_editButton->setEnabled(instance->canEdit());
+ m_backupsButton->setEnabled(true);
+ m_folderButton->setEnabled(true);
+}
+
+void LauncherHubWidget::rebuildRecentInstances()
+{
+ clearLayout(m_recentInstancesLayout);
+ if (!m_recentInstancesLayout)
+ {
+ return;
+ }
+
+ const QList<InstancePtr> instances = sortedInstances();
+ if (instances.isEmpty())
+ {
+ auto* label =
+ new QLabel(tr("No instances yet. Your recent worlds and packs will show up here."), m_cockpitPage);
+ label->setObjectName("hubPanelSubtitle");
+ label->setWordWrap(true);
+ m_recentInstancesLayout->addWidget(label);
+ return;
+ }
+
+ const QString currentId = activeInstanceId();
+ const int limit = qMin(6, instances.size());
+ for (int i = 0; i < limit; ++i)
+ {
+ const auto& instance = instances.at(i);
+ auto* row = new QWidget(m_cockpitPage);
+ auto* rowLayout = new QHBoxLayout(row);
+ rowLayout->setContentsMargins(0, 0, 0, 0);
+ rowLayout->setSpacing(8);
+
+ auto* button =
+ new QPushButton(QStringLiteral("%1\n%2").arg(
+ instance->name(),
+ tr("%1 | %2").arg(instance->typeName(), relativeTimeLabel(instance->lastLaunch()))),
+ row);
+ button->setObjectName("hubQuickButton");
+ button->setProperty("active", instance->id() == currentId);
+ button->setIcon(APPLICATION->icons()->getIcon(instance->iconKey()));
+ button->setIconSize(QSize(28, 28));
+ button->setMinimumHeight(56);
+ connect(button,
+ &QPushButton::clicked,
+ this,
+ [this, instance]()
+ {
+ m_selectedInstanceId = instance->id();
+ emit selectInstanceRequested(instance->id());
+ refreshCockpit();
+ });
+
+ auto* launchButton = new QPushButton(tr("Play"), row);
+ launchButton->setObjectName("hubInlineAction");
+ launchButton->setEnabled(instance->canLaunch() && !instance->isRunning());
+ connect(launchButton,
+ &QPushButton::clicked,
+ this,
+ [this, instance]()
+ {
+ m_selectedInstanceId = instance->id();
+ emit launchInstanceRequested(instance->id());
+ refreshCockpit();
+ });
+
+ rowLayout->addWidget(button, 1);
+ rowLayout->addWidget(launchButton);
+ m_recentInstancesLayout->addWidget(row);
+ }
+}
+
+void LauncherHubWidget::rebuildNewsFeed()
+{
+ clearLayout(m_newsLayout);
+ if (!m_newsLayout || !m_newsChecker)
+ {
+ return;
+ }
+
+ const QList<NewsEntryPtr> entries = m_newsChecker->getNewsEntries();
+ if (entries.isEmpty())
+ {
+ auto* label = new QLabel(m_newsChecker->isLoadingNews()
+ ? tr("Loading the latest posts...")
+ : tr("News is quiet right now. Use the button below to open the full feed."),
+ m_cockpitPage);
+ label->setObjectName("hubPanelSubtitle");
+ label->setWordWrap(true);
+ m_newsLayout->addWidget(label);
+ }
+ else
+ {
+ const int limit = qMin(3, entries.size());
+ for (int i = 0; i < limit; ++i)
+ {
+ const auto& entry = entries.at(i);
+ auto* button = new QPushButton(QStringLiteral("%1\n%2").arg(entry->title, stripHtmlExcerpt(entry->content)),
+ m_cockpitPage);
+ button->setObjectName("hubNewsButton");
+ button->setMinimumHeight(66);
+ connect(button,
+ &QPushButton::clicked,
+ this,
+ [this, entry]()
+ { openUrl(QUrl(entry->link.isEmpty() ? BuildConfig.NEWS_OPEN_URL : entry->link)); });
+ m_newsLayout->addWidget(button);
+ }
+ }
+
+ auto* openFeedButton = new QPushButton(tr("Open full news feed"), m_cockpitPage);
+ openFeedButton->setObjectName("hubInlineAction");
+ connect(openFeedButton, &QPushButton::clicked, this, [this]() { openUrl(QUrl(BuildConfig.NEWS_OPEN_URL)); });
+ m_newsLayout->addWidget(openFeedButton, 0, Qt::AlignLeft);
+}
+
+#endif // PROJT_DISABLE_LAUNCHER_HUB
diff --git a/archived/projt-launcher/launcher/ui/widgets/LauncherHubWidget.h b/archived/projt-launcher/launcher/ui/widgets/LauncherHubWidget.h
new file mode 100644
index 0000000000..1a0ca8ee82
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/widgets/LauncherHubWidget.h
@@ -0,0 +1,116 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+#pragma once
+
+#include <QString>
+#include <QUrl>
+#include <QWidget>
+
+#include "ui/widgets/HubViewBase.h"
+
+class QLineEdit;
+class QLabel;
+class QVBoxLayout;
+class QStackedWidget;
+class QTabBar;
+class QToolButton;
+class QWidget;
+class QPushButton;
+class NewsChecker;
+class QEvent;
+
+class LauncherHubWidget : public QWidget
+{
+ Q_OBJECT
+
+ public:
+ explicit LauncherHubWidget(QWidget* parent = nullptr);
+ ~LauncherHubWidget() override;
+
+ void ensureLoaded();
+ void loadHome();
+ void openUrl(const QUrl& url);
+ void newTab(const QUrl& url = QUrl());
+ void setHomeUrl(const QUrl& url);
+ QUrl homeUrl() const;
+ void setSelectedInstanceId(const QString& id);
+ void refreshCockpit();
+
+ signals:
+ void selectInstanceRequested(const QString& instanceId);
+ void launchInstanceRequested(const QString& instanceId);
+ void editInstanceRequested(const QString& instanceId);
+ void backupsRequested(const QString& instanceId);
+ void openInstanceFolderRequested(const QString& instanceId);
+
+ private:
+ HubViewBase* currentView() const;
+ HubViewBase* viewForTabIndex(int index) const;
+ int tabIndexForView(const HubViewBase* view) const;
+ HubViewBase* createTab(const QUrl& url, const QString& label = QString(), bool switchTo = true);
+ void createCockpitTab();
+ void switchToPage(QWidget* page);
+ void activatePendingForPage(QWidget* page);
+ void updateNavigationState();
+ void updateTabPerformanceState();
+ void syncTabsUi();
+ void refreshToolbarIcons();
+ void rebuildRecentInstances();
+ void rebuildNewsFeed();
+ void updateHero();
+ QString activeInstanceId() const;
+
+ protected:
+ void changeEvent(QEvent* event) override;
+
+ QTabBar* m_tabBar = nullptr;
+ QWidget* m_tabsBarContainer = nullptr;
+ QWidget* m_toolbarContainer = nullptr;
+ QStackedWidget* m_stack = nullptr;
+ QLineEdit* m_addressBar = nullptr;
+ QToolButton* m_backButton = nullptr;
+ QToolButton* m_forwardButton = nullptr;
+ QToolButton* m_reloadButton = nullptr;
+ QToolButton* m_homeButton = nullptr;
+ QToolButton* m_goButton = nullptr;
+ QToolButton* m_newTabButton = nullptr;
+ QWidget* m_cockpitPage = nullptr;
+ QLabel* m_cockpitBadgeLabel = nullptr;
+ QLabel* m_cockpitTitleLabel = nullptr;
+ QLabel* m_cockpitSubtitleLabel = nullptr;
+ QLabel* m_cockpitIconLabel = nullptr;
+ QPushButton* m_playButton = nullptr;
+ QPushButton* m_editButton = nullptr;
+ QPushButton* m_backupsButton = nullptr;
+ QPushButton* m_folderButton = nullptr;
+ QLabel* m_instancesValueLabel = nullptr;
+ QLabel* m_instancesDetailLabel = nullptr;
+ QLabel* m_playtimeValueLabel = nullptr;
+ QLabel* m_playtimeDetailLabel = nullptr;
+ QLabel* m_attentionValueLabel = nullptr;
+ QLabel* m_attentionDetailLabel = nullptr;
+ QVBoxLayout* m_recentInstancesLayout = nullptr;
+ QVBoxLayout* m_newsLayout = nullptr;
+ NewsChecker* m_newsChecker = nullptr;
+ QString m_selectedInstanceId;
+ QUrl m_homeUrl;
+ bool m_loaded = false;
+};
diff --git a/archived/projt-launcher/launcher/ui/widgets/LogView.cpp b/archived/projt-launcher/launcher/ui/widgets/LogView.cpp
new file mode 100644
index 0000000000..bcf08131c3
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/widgets/LogView.cpp
@@ -0,0 +1,229 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * 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 "LogView.h"
+#include <QScrollBar>
+#include <QTextBlock>
+#include <QTextDocumentFragment>
+
+LogView::LogView(QWidget* parent) : QPlainTextEdit(parent)
+{
+ setWordWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere);
+ m_defaultFormat = new QTextCharFormat(currentCharFormat());
+ setUndoRedoEnabled(false);
+}
+
+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::setColorLines(bool colorLines)
+{
+ if (m_colorLines == colorLines)
+ return;
+ m_colorLines = colorLines;
+ repopulate();
+}
+
+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)
+{
+ QTextDocument document;
+ QTextCursor cursor(&document);
+
+ cursor.movePosition(QTextCursor::End);
+ cursor.beginEditBlock();
+ 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() && m_colorLines)
+ {
+ format.setForeground(fg.value<QColor>());
+ }
+ auto bg = m_model->data(idx, Qt::BackgroundRole);
+ if (bg.isValid() && m_colorLines)
+ {
+ format.setBackground(bg.value<QColor>());
+ }
+ cursor.insertText(text, format);
+ cursor.insertBlock();
+ }
+ cursor.endEditBlock();
+
+ QTextDocumentFragment fragment(&document);
+ QTextCursor workCursor = textCursor();
+ workCursor.movePosition(QTextCursor::End);
+ workCursor.insertFragment(fragment);
+
+ if (m_scroll && !m_scrolling)
+ {
+ m_scrolling = true;
+ QMetaObject::invokeMethod(this, "scrollToBottom", Qt::QueuedConnection);
+ }
+}
+
+void LogView::rowsRemoved(const QModelIndex& parent, int first, int last)
+{
+ Q_UNUSED(parent)
+ Q_UNUSED(first)
+ Q_UNUSED(last)
+
+ // If we were scrolled to the bottom, stay at the bottom after rows are removed
+ // This ensures the user doesn't lose their position when log lines are removed
+ if (m_scrolling)
+ {
+ QMetaObject::invokeMethod(this, "scrollToBottom", Qt::QueuedConnection);
+ }
+}
+
+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/archived/projt-launcher/launcher/ui/widgets/LogView.h b/archived/projt-launcher/launcher/ui/widgets/LogView.h
new file mode 100644
index 0000000000..264450d62e
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/widgets/LogView.h
@@ -0,0 +1,58 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+#pragma once
+#include <QAbstractItemView>
+#include <QPlainTextEdit>
+
+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 setColorLines(bool colorLines);
+ 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;
+ bool m_colorLines = true;
+};
diff --git a/archived/projt-launcher/launcher/ui/widgets/MinecraftSettingsWidget.cpp b/archived/projt-launcher/launcher/ui/widgets/MinecraftSettingsWidget.cpp
new file mode 100644
index 0000000000..9e6de5b8f2
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/widgets/MinecraftSettingsWidget.cpp
@@ -0,0 +1,676 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org>
+ * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
+ * Copyright (C) 2024 TheKodeToad <TheKodeToad@proton.me>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * 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 "MinecraftSettingsWidget.h"
+#include "modplatform/ModIndex.h"
+#include "ui_MinecraftSettingsWidget.h"
+
+#include <QFileDialog>
+#include "Application.h"
+#include "BuildConfig.h"
+#include "Json.h"
+#include "minecraft/PackProfile.h"
+#include "minecraft/WorldList.h"
+#include "minecraft/auth/AccountList.hpp"
+#include "settings/Setting.h"
+
+MinecraftSettingsWidget::MinecraftSettingsWidget(MinecraftInstancePtr instance, QWidget* parent)
+ : QWidget(parent),
+ m_instance(std::move(instance)),
+ m_ui(new Ui::MinecraftSettingsWidget)
+{
+ m_ui->setupUi(this);
+
+ if (m_instance == nullptr)
+ {
+ m_ui->settingsTabs->removeTab(1);
+
+ m_ui->openGlobalSettingsButton->setVisible(false);
+ m_ui->instanceAccountGroupBox->hide();
+ m_ui->serverJoinGroupBox->hide();
+ m_ui->globalDataPacksGroupBox->hide();
+ m_ui->loaderGroup->hide();
+ }
+ else
+ {
+ m_javaSettings = new JavaSettingsWidget(m_instance, this);
+ m_ui->javaScrollArea->setWidget(m_javaSettings);
+
+ m_ui->showGameTime->setText(tr("Show time &playing this instance"));
+ m_ui->recordGameTime->setText(tr("&Record time playing this instance"));
+ m_ui->showGlobalGameTime->hide();
+ m_ui->showGameTimeWithoutDays->hide();
+
+ m_ui->maximizedWarning->setText(tr("<span style=\" font-weight:600; color:#f5c211;\">Warning</span><span "
+ "style=\" color:#f5c211;\">: The maximized option is "
+ "not fully supported on this Minecraft version.</span>"));
+
+ m_ui->consoleSettingsBox->setCheckable(true);
+ m_ui->windowSizeGroupBox->setCheckable(true);
+ m_ui->nativeWorkaroundsGroupBox->setCheckable(true);
+ m_ui->perfomanceGroupBox->setCheckable(true);
+ m_ui->gameTimeGroupBox->setCheckable(true);
+ m_ui->legacySettingsGroupBox->setCheckable(true);
+
+ m_quickPlaySingleplayer = m_instance->traits().contains("feature:is_quick_play_singleplayer");
+ if (m_quickPlaySingleplayer)
+ {
+ auto worlds = m_instance->worldList();
+ worlds->update();
+ for (const auto& world : worlds->allWorlds())
+ {
+ m_ui->worldsCb->addItem(world.folderName());
+ }
+ }
+ else
+ {
+ m_ui->worldsCb->hide();
+ m_ui->worldJoinButton->hide();
+ m_ui->serverJoinAddressButton->setChecked(true);
+ m_ui->serverJoinAddress->setEnabled(true);
+ m_ui->serverJoinAddressButton->setStyleSheet("QRadioButton::indicator { width: 0px; height: 0px; }");
+ }
+
+ connect(m_ui->openGlobalSettingsButton,
+ &QCommandLinkButton::clicked,
+ this,
+ &MinecraftSettingsWidget::openGlobalSettings);
+ connect(m_ui->serverJoinAddressButton,
+ &QAbstractButton::toggled,
+ m_ui->serverJoinAddress,
+ &QWidget::setEnabled);
+ connect(m_ui->worldJoinButton, &QAbstractButton::toggled, m_ui->worldsCb, &QWidget::setEnabled);
+
+ connect(m_ui->globalDataPacksGroupBox,
+ &QGroupBox::toggled,
+ this,
+ [this](bool value)
+ {
+ m_instance->settings()->set("GlobalDataPacksEnabled", value);
+ if (!value)
+ m_instance->settings()->reset("GlobalDataPacksPath");
+ });
+ connect(m_ui->dataPacksPathEdit,
+ &QLineEdit::editingFinished,
+ this,
+ &MinecraftSettingsWidget::saveDataPacksPath);
+ connect(m_ui->dataPacksPathBrowse,
+ &QPushButton::clicked,
+ this,
+ &MinecraftSettingsWidget::selectDataPacksFolder);
+
+ connect(m_ui->loaderGroup,
+ &QGroupBox::toggled,
+ this,
+ [this](bool value)
+ {
+ m_instance->settings()->set("OverrideModDownloadLoaders", value);
+ if (value)
+ saveSelectedLoaders();
+ else
+ m_instance->settings()->reset("ModDownloadLoaders");
+ });
+ connect(m_ui->neoForge, &QCheckBox::stateChanged, this, &MinecraftSettingsWidget::saveSelectedLoaders);
+ connect(m_ui->forge, &QCheckBox::stateChanged, this, &MinecraftSettingsWidget::saveSelectedLoaders);
+ connect(m_ui->fabric, &QCheckBox::stateChanged, this, &MinecraftSettingsWidget::saveSelectedLoaders);
+ connect(m_ui->quilt, &QCheckBox::stateChanged, this, &MinecraftSettingsWidget::saveSelectedLoaders);
+ connect(m_ui->liteLoader, &QCheckBox::stateChanged, this, &MinecraftSettingsWidget::saveSelectedLoaders);
+ }
+
+ m_ui->maximizedWarning->hide();
+
+ connect(m_ui->maximizedCheckBox,
+ &QCheckBox::toggled,
+ this,
+ [this](const bool value)
+ { m_ui->maximizedWarning->setVisible(value && (m_instance == nullptr || !m_instance->isLegacy())); });
+
+#if !defined(Q_OS_LINUX)
+ m_ui->perfomanceGroupBox->hide();
+#endif
+
+ if (!(APPLICATION->capabilities() & Application::SupportsGameMode))
+ {
+ m_ui->enableFeralGamemodeCheck->setDisabled(true);
+ m_ui->enableFeralGamemodeCheck->setToolTip(tr("Project Tick GameMode could not be found on your system."));
+ }
+
+ if (!(APPLICATION->capabilities() & Application::SupportsMangoHud))
+ {
+ m_ui->enableMangoHud->setEnabled(false);
+ m_ui->enableMangoHud->setToolTip(tr("MangoHud could not be found on your system."));
+ }
+
+ connect(m_ui->useNativeOpenALCheck, &QAbstractButton::toggled, m_ui->lineEditOpenALPath, &QWidget::setEnabled);
+ connect(m_ui->useNativeGLFWCheck, &QAbstractButton::toggled, m_ui->lineEditGLFWPath, &QWidget::setEnabled);
+
+ loadSettings();
+}
+
+MinecraftSettingsWidget::~MinecraftSettingsWidget()
+{
+ delete m_ui;
+}
+
+void MinecraftSettingsWidget::loadSettings()
+{
+ SettingsObjectPtr settings;
+
+ if (m_instance != nullptr)
+ settings = m_instance->settings();
+ else
+ settings = APPLICATION->settings();
+
+ // Game Window
+ m_ui->windowSizeGroupBox->setChecked(m_instance == nullptr || settings->get("OverrideWindow").toBool()
+ || settings->get("OverrideMiscellaneous").toBool());
+ m_ui->maximizedCheckBox->setChecked(settings->get("LaunchMaximized").toBool());
+ m_ui->windowWidthSpinBox->setValue(settings->get("MinecraftWinWidth").toInt());
+ m_ui->windowHeightSpinBox->setValue(settings->get("MinecraftWinHeight").toInt());
+ m_ui->closeAfterLaunchCheck->setChecked(settings->get("CloseAfterLaunch").toBool());
+ m_ui->quitAfterGameStopCheck->setChecked(settings->get("QuitAfterGameStop").toBool());
+
+ // Game Time
+ m_ui->gameTimeGroupBox->setChecked(m_instance == nullptr || settings->get("OverrideGameTime").toBool());
+ m_ui->showGameTime->setChecked(settings->get("ShowGameTime").toBool());
+ m_ui->recordGameTime->setChecked(settings->get("RecordGameTime").toBool());
+ m_ui->showGlobalGameTime->setChecked(m_instance == nullptr && settings->get("ShowGlobalGameTime").toBool());
+ m_ui->showGameTimeWithoutDays->setChecked(m_instance == nullptr
+ && settings->get("ShowGameTimeWithoutDays").toBool());
+
+ // Console
+ m_ui->consoleSettingsBox->setChecked(m_instance == nullptr || settings->get("OverrideConsole").toBool());
+ m_ui->showConsoleCheck->setChecked(settings->get("ShowConsole").toBool());
+ m_ui->autoCloseConsoleCheck->setChecked(settings->get("AutoCloseConsole").toBool());
+ m_ui->showConsoleErrorCheck->setChecked(settings->get("ShowConsoleOnError").toBool());
+
+ if (m_javaSettings != nullptr)
+ m_javaSettings->loadSettings();
+
+ // Custom commands
+ m_ui->customCommands->initialize(m_instance != nullptr,
+ m_instance == nullptr || settings->get("OverrideCommands").toBool(),
+ settings->get("PreLaunchCommand").toString(),
+ settings->get("WrapperCommand").toString(),
+ settings->get("PostExitCommand").toString());
+
+ // Environment variables
+ m_ui->environmentVariables->initialize(m_instance != nullptr,
+ m_instance == nullptr || settings->get("OverrideEnv").toBool(),
+ Json::toMap(settings->get("Env").toString()));
+
+ // Legacy Tweaks
+ m_ui->legacySettingsGroupBox->setChecked(m_instance == nullptr || settings->get("OverrideLegacySettings").toBool());
+ m_ui->onlineFixes->setChecked(settings->get("OnlineFixes").toBool());
+
+ // Native Libraries
+ m_ui->nativeWorkaroundsGroupBox->setChecked(m_instance == nullptr
+ || settings->get("OverrideNativeWorkarounds").toBool());
+ m_ui->useNativeGLFWCheck->setChecked(settings->get("UseNativeGLFW").toBool());
+ m_ui->lineEditGLFWPath->setText(settings->get("CustomGLFWPath").toString());
+#ifdef Q_OS_LINUX
+ m_ui->lineEditGLFWPath->setPlaceholderText(APPLICATION->m_detectedGLFWPath);
+#else
+ m_ui->lineEditGLFWPath->setPlaceholderText(tr("Path to %1 library file").arg(BuildConfig.GLFW_LIBRARY_NAME));
+#endif
+ m_ui->useNativeOpenALCheck->setChecked(settings->get("UseNativeOpenAL").toBool());
+ m_ui->lineEditOpenALPath->setText(settings->get("CustomOpenALPath").toString());
+#ifdef Q_OS_LINUX
+ m_ui->lineEditOpenALPath->setPlaceholderText(APPLICATION->m_detectedOpenALPath);
+#else
+ m_ui->lineEditOpenALPath->setPlaceholderText(tr("Path to %1 library file").arg(BuildConfig.OPENAL_LIBRARY_NAME));
+#endif
+
+ // Performance
+ m_ui->perfomanceGroupBox->setChecked(m_instance == nullptr || settings->get("OverridePerformance").toBool());
+ m_ui->enableFeralGamemodeCheck->setChecked(settings->get("EnableFeralGamemode").toBool());
+ m_ui->enableMangoHud->setChecked(settings->get("EnableMangoHud").toBool());
+ m_ui->useDiscreteGpuCheck->setChecked(settings->get("UseDiscreteGpu").toBool());
+ m_ui->useZink->setChecked(settings->get("UseZink").toBool());
+
+ if (m_instance != nullptr)
+ {
+ m_ui->serverJoinGroupBox->setChecked(settings->get("JoinServerOnLaunch").toBool());
+
+ if (auto server = settings->get("JoinServerOnLaunchAddress").toString(); !server.isEmpty())
+ {
+ m_ui->serverJoinAddress->setText(server);
+ m_ui->serverJoinAddressButton->setChecked(true);
+ m_ui->worldJoinButton->setChecked(false);
+ m_ui->serverJoinAddress->setEnabled(true);
+ m_ui->worldsCb->setEnabled(false);
+ }
+ else if (auto world = settings->get("JoinWorldOnLaunch").toString();
+ !world.isEmpty() && m_quickPlaySingleplayer)
+ {
+ m_ui->worldsCb->setCurrentText(world);
+ m_ui->serverJoinAddressButton->setChecked(false);
+ m_ui->worldJoinButton->setChecked(true);
+ m_ui->serverJoinAddress->setEnabled(false);
+ m_ui->worldsCb->setEnabled(true);
+ }
+ else
+ {
+ m_ui->serverJoinAddressButton->setChecked(true);
+ m_ui->worldJoinButton->setChecked(false);
+ m_ui->serverJoinAddress->setEnabled(m_ui->serverJoinGroupBox->isChecked());
+ m_ui->worldsCb->setEnabled(false);
+ }
+
+ m_ui->instanceAccountGroupBox->setChecked(settings->get("UseAccountForInstance").toBool());
+ updateAccountsMenu(*settings);
+
+ m_ui->loaderGroup->blockSignals(true);
+ m_ui->neoForge->blockSignals(true);
+ m_ui->forge->blockSignals(true);
+ m_ui->fabric->blockSignals(true);
+ m_ui->quilt->blockSignals(true);
+ m_ui->liteLoader->blockSignals(true);
+
+ const bool overrideLoaders = settings->get("OverrideModDownloadLoaders").toBool();
+ const QStringList loaders = Json::toStringList(settings->get("ModDownloadLoaders").toString());
+
+ m_ui->loaderGroup->setChecked(overrideLoaders);
+
+ if (overrideLoaders)
+ {
+ m_ui->neoForge->setChecked(loaders.contains(getModLoaderAsString(ModPlatform::NeoForge)));
+ m_ui->forge->setChecked(loaders.contains(getModLoaderAsString(ModPlatform::Forge)));
+ m_ui->fabric->setChecked(loaders.contains(getModLoaderAsString(ModPlatform::Fabric)));
+ m_ui->quilt->setChecked(loaders.contains(getModLoaderAsString(ModPlatform::Quilt)));
+ m_ui->liteLoader->setChecked(loaders.contains(getModLoaderAsString(ModPlatform::LiteLoader)));
+ }
+ else
+ {
+ auto instLoaders =
+ m_instance->getPackProfile()->getSupportedModLoaders().value_or(ModPlatform::ModLoaderTypes(0));
+
+ m_ui->neoForge->setChecked(instLoaders & ModPlatform::NeoForge);
+ m_ui->forge->setChecked(instLoaders & ModPlatform::Forge);
+ m_ui->fabric->setChecked(instLoaders & ModPlatform::Fabric);
+ m_ui->quilt->setChecked(instLoaders & ModPlatform::Quilt);
+ m_ui->liteLoader->setChecked(instLoaders & ModPlatform::LiteLoader);
+ }
+
+ m_ui->loaderGroup->blockSignals(false);
+ m_ui->neoForge->blockSignals(false);
+ m_ui->forge->blockSignals(false);
+ m_ui->fabric->blockSignals(false);
+ m_ui->quilt->blockSignals(false);
+ m_ui->liteLoader->blockSignals(false);
+ }
+
+ m_ui->legacySettingsGroupBox->setChecked(settings->get("OverrideLegacySettings").toBool());
+ m_ui->onlineFixes->setChecked(settings->get("OnlineFixes").toBool());
+
+ m_ui->globalDataPacksGroupBox->blockSignals(true);
+ m_ui->dataPacksPathEdit->blockSignals(true);
+ m_ui->globalDataPacksGroupBox->setChecked(settings->get("GlobalDataPacksEnabled").toBool());
+ m_ui->dataPacksPathEdit->setText(settings->get("GlobalDataPacksPath").toString());
+ m_ui->globalDataPacksGroupBox->blockSignals(false);
+ m_ui->dataPacksPathEdit->blockSignals(false);
+}
+
+void MinecraftSettingsWidget::saveSettings()
+{
+ SettingsObjectPtr settings;
+
+ if (m_instance != nullptr)
+ settings = m_instance->settings();
+ else
+ settings = APPLICATION->settings();
+
+ {
+ SettingsObject::Lock lock(settings);
+
+ // Console
+ bool console = m_instance == nullptr || m_ui->consoleSettingsBox->isChecked();
+
+ if (m_instance != nullptr)
+ settings->set("OverrideConsole", console);
+
+ if (console)
+ {
+ settings->set("ShowConsole", m_ui->showConsoleCheck->isChecked());
+ settings->set("AutoCloseConsole", m_ui->autoCloseConsoleCheck->isChecked());
+ settings->set("ShowConsoleOnError", m_ui->showConsoleErrorCheck->isChecked());
+ }
+ else
+ {
+ settings->reset("ShowConsole");
+ settings->reset("AutoCloseConsole");
+ settings->reset("ShowConsoleOnError");
+ }
+
+ // Game Window
+ bool window = m_instance == nullptr || m_ui->windowSizeGroupBox->isChecked();
+
+ if (m_instance != nullptr)
+ {
+ settings->set("OverrideWindow", window);
+ settings->set("OverrideMiscellaneous", window);
+ }
+
+ if (window)
+ {
+ settings->set("LaunchMaximized", m_ui->maximizedCheckBox->isChecked());
+ settings->set("MinecraftWinWidth", m_ui->windowWidthSpinBox->value());
+ settings->set("MinecraftWinHeight", m_ui->windowHeightSpinBox->value());
+ settings->set("CloseAfterLaunch", m_ui->closeAfterLaunchCheck->isChecked());
+ settings->set("QuitAfterGameStop", m_ui->quitAfterGameStopCheck->isChecked());
+ }
+ else
+ {
+ settings->reset("LaunchMaximized");
+ settings->reset("MinecraftWinWidth");
+ settings->reset("MinecraftWinHeight");
+ settings->reset("CloseAfterLaunch");
+ settings->reset("QuitAfterGameStop");
+ }
+
+ // Custom Commands
+ bool custcmd = m_instance == nullptr || m_ui->customCommands->checked();
+
+ if (m_instance != nullptr)
+ settings->set("OverrideCommands", custcmd);
+
+ if (custcmd)
+ {
+ settings->set("PreLaunchCommand", m_ui->customCommands->prelaunchCommand());
+ settings->set("WrapperCommand", m_ui->customCommands->wrapperCommand());
+ settings->set("PostExitCommand", m_ui->customCommands->postexitCommand());
+ }
+ else
+ {
+ settings->reset("PreLaunchCommand");
+ settings->reset("WrapperCommand");
+ settings->reset("PostExitCommand");
+ }
+
+ // Environment Variables
+ auto env = m_instance == nullptr || m_ui->environmentVariables->override();
+
+ if (m_instance != nullptr)
+ settings->set("OverrideEnv", env);
+
+ if (env)
+ settings->set("Env", Json::fromMap(m_ui->environmentVariables->value()));
+ else
+ settings->reset("Env");
+
+ // Workarounds
+ bool workarounds = m_instance == nullptr || m_ui->nativeWorkaroundsGroupBox->isChecked();
+
+ if (m_instance != nullptr)
+ settings->set("OverrideNativeWorkarounds", workarounds);
+
+ if (workarounds)
+ {
+ settings->set("UseNativeGLFW", m_ui->useNativeGLFWCheck->isChecked());
+ settings->set("CustomGLFWPath", m_ui->lineEditGLFWPath->text());
+ settings->set("UseNativeOpenAL", m_ui->useNativeOpenALCheck->isChecked());
+ settings->set("CustomOpenALPath", m_ui->lineEditOpenALPath->text());
+ }
+ else
+ {
+ settings->reset("UseNativeGLFW");
+ settings->reset("CustomGLFWPath");
+ settings->reset("UseNativeOpenAL");
+ settings->reset("CustomOpenALPath");
+ }
+
+ // Performance
+ bool performance = m_instance == nullptr || m_ui->perfomanceGroupBox->isChecked();
+
+ if (m_instance != nullptr)
+ settings->set("OverridePerformance", performance);
+
+ if (performance)
+ {
+ settings->set("EnableFeralGamemode", m_ui->enableFeralGamemodeCheck->isChecked());
+ settings->set("EnableMangoHud", m_ui->enableMangoHud->isChecked());
+ settings->set("UseDiscreteGpu", m_ui->useDiscreteGpuCheck->isChecked());
+ settings->set("UseZink", m_ui->useZink->isChecked());
+ }
+ else
+ {
+ settings->reset("EnableFeralGamemode");
+ settings->reset("EnableMangoHud");
+ settings->reset("UseDiscreteGpu");
+ settings->reset("UseZink");
+ }
+
+ // Game time
+ bool gameTime = m_instance == nullptr || m_ui->gameTimeGroupBox->isChecked();
+
+ if (m_instance != nullptr)
+ settings->set("OverrideGameTime", gameTime);
+
+ if (gameTime)
+ {
+ settings->set("ShowGameTime", m_ui->showGameTime->isChecked());
+ settings->set("RecordGameTime", m_ui->recordGameTime->isChecked());
+ }
+ else
+ {
+ settings->reset("ShowGameTime");
+ settings->reset("RecordGameTime");
+ }
+
+ if (m_instance == nullptr)
+ {
+ settings->set("ShowGlobalGameTime", m_ui->showGlobalGameTime->isChecked());
+ settings->set("ShowGameTimeWithoutDays", m_ui->showGameTimeWithoutDays->isChecked());
+ }
+
+ if (m_instance != nullptr)
+ {
+ // Join server on launch
+ bool joinServerOnLaunch = m_ui->serverJoinGroupBox->isChecked();
+ settings->set("JoinServerOnLaunch", joinServerOnLaunch);
+ if (joinServerOnLaunch)
+ {
+ if (m_ui->serverJoinAddressButton->isChecked() || !m_quickPlaySingleplayer)
+ {
+ settings->set("JoinServerOnLaunchAddress", m_ui->serverJoinAddress->text());
+ settings->reset("JoinWorldOnLaunch");
+ }
+ else
+ {
+ settings->set("JoinWorldOnLaunch", m_ui->worldsCb->currentText());
+ settings->reset("JoinServerOnLaunchAddress");
+ }
+ }
+ else
+ {
+ settings->reset("JoinServerOnLaunchAddress");
+ settings->reset("JoinWorldOnLaunch");
+ }
+
+ // Use an account for this instance
+ bool useAccountForInstance = m_ui->instanceAccountGroupBox->isChecked();
+ settings->set("UseAccountForInstance", useAccountForInstance);
+ if (useAccountForInstance)
+ {
+ int accountIndex = m_ui->instanceAccountSelector->currentIndex();
+
+ if (accountIndex != -1)
+ {
+ const MinecraftAccountPtr account = APPLICATION->accounts()->at(accountIndex);
+ if (account != nullptr)
+ settings->set("InstanceAccountId", account->profileId());
+ }
+ }
+ else
+ {
+ settings->reset("InstanceAccountId");
+ }
+ }
+
+ bool overrideLegacySettings = m_instance == nullptr || m_ui->legacySettingsGroupBox->isChecked();
+
+ if (m_instance != nullptr)
+ settings->set("OverrideLegacySettings", overrideLegacySettings);
+
+ if (overrideLegacySettings)
+ {
+ settings->set("OnlineFixes", m_ui->onlineFixes->isChecked());
+ }
+ else
+ {
+ settings->reset("OnlineFixes");
+ }
+ }
+
+ if (m_javaSettings != nullptr)
+ m_javaSettings->saveSettings();
+}
+
+void MinecraftSettingsWidget::openGlobalSettings()
+{
+ const QString id = m_ui->settingsTabs->currentWidget()->objectName();
+
+ qDebug() << id;
+
+ if (id == "javaPage")
+ APPLICATION->ShowGlobalSettings(this, "java-settings");
+ else
+ // Default to minecraft-settings for all other tabs (General, Custom Commands, etc.)
+ APPLICATION->ShowGlobalSettings(this, "minecraft-settings");
+}
+
+void MinecraftSettingsWidget::updateAccountsMenu(const SettingsObject& settings)
+{
+ m_ui->instanceAccountSelector->clear();
+ auto accounts = APPLICATION->accounts();
+ int accountIndex = accounts->findAccountByProfileId(settings.get("InstanceAccountId").toString());
+
+ for (int i = 0; i < accounts->count(); i++)
+ {
+ MinecraftAccountPtr account = accounts->at(i);
+
+ QIcon face = account->getFace();
+
+ if (face.isNull())
+ face = QIcon::fromTheme("noaccount");
+
+ m_ui->instanceAccountSelector->addItem(face, account->profileName(), i);
+ if (i == accountIndex)
+ m_ui->instanceAccountSelector->setCurrentIndex(i);
+ }
+}
+
+bool MinecraftSettingsWidget::isQuickPlaySupported()
+{
+ return m_instance->traits().contains("feature:is_quick_play_singleplayer");
+}
+
+void MinecraftSettingsWidget::saveSelectedLoaders()
+{
+ QStringList loaders;
+
+ if (m_ui->neoForge->isChecked())
+ loaders << getModLoaderAsString(ModPlatform::NeoForge);
+
+ if (m_ui->forge->isChecked())
+ loaders << getModLoaderAsString(ModPlatform::Forge);
+
+ if (m_ui->fabric->isChecked())
+ loaders << getModLoaderAsString(ModPlatform::Fabric);
+
+ if (m_ui->quilt->isChecked())
+ loaders << getModLoaderAsString(ModPlatform::Quilt);
+
+ if (m_ui->liteLoader->isChecked())
+ loaders << getModLoaderAsString(ModPlatform::LiteLoader);
+
+ m_instance->settings()->set("ModDownloadLoaders", Json::fromStringList(loaders));
+}
+
+void MinecraftSettingsWidget::saveDataPacksPath()
+{
+ if (QDir::separator() != '/')
+ m_ui->dataPacksPathEdit->setText(m_ui->dataPacksPathEdit->text().replace(QDir::separator(), '/'));
+
+ m_instance->settings()->set("GlobalDataPacksPath", m_ui->dataPacksPathEdit->text());
+}
+
+void MinecraftSettingsWidget::selectDataPacksFolder()
+{
+ QString path =
+ QFileDialog::getExistingDirectory(this, tr("Select Global Data Packs Folder"), m_instance->gameRoot());
+
+ if (path.isEmpty())
+ return;
+
+ // if it's inside the instance dir, set path relative to .minecraft
+ // (so that if it's directly in instance dir it will still lead with .. but more than two levels up are kept
+ // absolute)
+
+ const QUrl instanceRootUrl = QUrl::fromLocalFile(m_instance->instanceRoot());
+ const QUrl pathUrl = QUrl::fromLocalFile(path);
+
+ if (instanceRootUrl.isParentOf(pathUrl))
+ path = QDir(m_instance->gameRoot()).relativeFilePath(path);
+
+ m_ui->dataPacksPathEdit->setText(path);
+ m_instance->settings()->set("GlobalDataPacksPath", path);
+}
diff --git a/archived/projt-launcher/launcher/ui/widgets/MinecraftSettingsWidget.h b/archived/projt-launcher/launcher/ui/widgets/MinecraftSettingsWidget.h
new file mode 100644
index 0000000000..d38a3be44d
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/widgets/MinecraftSettingsWidget.h
@@ -0,0 +1,94 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org>
+ * Copyright (C) 2024 TheKodeToad <TheKodeToad@proton.me>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * 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 "JavaSettingsWidget.h"
+#include "minecraft/MinecraftInstance.h"
+
+namespace Ui
+{
+ class MinecraftSettingsWidget;
+}
+
+class MinecraftSettingsWidget : public QWidget
+{
+ public:
+ MinecraftSettingsWidget(MinecraftInstancePtr instance, QWidget* parent = nullptr);
+ ~MinecraftSettingsWidget() override;
+
+ void loadSettings();
+ void saveSettings();
+
+ private:
+ void openGlobalSettings();
+ void updateAccountsMenu(const SettingsObject& settings);
+ bool isQuickPlaySupported();
+ private slots:
+ void saveSelectedLoaders();
+ void saveDataPacksPath();
+ void selectDataPacksFolder();
+
+ MinecraftInstancePtr m_instance;
+ Ui::MinecraftSettingsWidget* m_ui;
+ JavaSettingsWidget* m_javaSettings = nullptr;
+ bool m_quickPlaySingleplayer = false;
+};
diff --git a/archived/projt-launcher/launcher/ui/widgets/MinecraftSettingsWidget.ui b/archived/projt-launcher/launcher/ui/widgets/MinecraftSettingsWidget.ui
new file mode 100644
index 0000000000..3efdb58346
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/widgets/MinecraftSettingsWidget.ui
@@ -0,0 +1,859 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>MinecraftSettingsWidget</class>
+ <widget class="QWidget" name="MinecraftSettingsWidget">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>648</width>
+ <height>600</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>6</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QCommandLinkButton" name="openGlobalSettingsButton">
+ <property name="text">
+ <string>Open &amp;Global Settings</string>
+ </property>
+ <property name="description">
+ <string>The settings here are overrides for global settings.</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QTabWidget" name="settingsTabs">
+ <property name="currentIndex">
+ <number>0</number>
+ </property>
+ <widget class="QWidget" name="generalPage">
+ <attribute name="title">
+ <string>General</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_3">
+ <item>
+ <widget class="QScrollArea" name="scrollArea">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Minimum" vsizetype="Expanding">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="widgetResizable">
+ <bool>true</bool>
+ </property>
+ <widget class="QWidget" name="scrollAreaWidgetContents">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>603</width>
+ <height>1042</height>
+ </rect>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_5">
+ <item>
+ <widget class="QGroupBox" name="windowSizeGroupBox">
+ <property name="enabled">
+ <bool>true</bool>
+ </property>
+ <property name="title">
+ <string>Game &amp;Window</string>
+ </property>
+ <property name="checkable">
+ <bool>false</bool>
+ </property>
+ <property name="checked">
+ <bool>false</bool>
+ </property>
+ <layout class="QGridLayout" name="gridLayout">
+ <item row="1" column="0" colspan="6">
+ <widget class="QLabel" name="maximizedWarning">
+ <property name="toolTip">
+ <string>The base game only supports resolution. In order to simulate the maximized behaviour the current implementation approximates the maximum display size.</string>
+ </property>
+ <property name="text">
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; color:#f5c211;&quot;&gt;Warning&lt;/span&gt;&lt;span style=&quot; color:#f5c211;&quot;&gt;: The maximized option may not be fully supported on all Minecraft versions.&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ </widget>
+ </item>
+ <item row="5" column="0" colspan="6">
+ <widget class="QCheckBox" name="quitAfterGameStopCheck">
+ <property name="text">
+ <string>When the game window closes, quit the launcher</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="0" colspan="6">
+ <widget class="QCheckBox" name="maximizedCheckBox">
+ <property name="text">
+ <string>Start Minecraft maximized</string>
+ </property>
+ </widget>
+ </item>
+ <item row="4" column="0" colspan="6">
+ <widget class="QCheckBox" name="closeAfterLaunchCheck">
+ <property name="text">
+ <string>When the game window opens, hide the launcher</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="3">
+ <widget class="QSpinBox" name="windowWidthSpinBox">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Maximum" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="suffix">
+ <string/>
+ </property>
+ <property name="minimum">
+ <number>1</number>
+ </property>
+ <property name="maximum">
+ <number>65536</number>
+ </property>
+ <property name="singleStep">
+ <number>1</number>
+ </property>
+ <property name="value">
+ <number>854</number>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="0">
+ <spacer name="verticalSpacer_3">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Fixed</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>0</width>
+ <height>6</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item row="2" column="1">
+ <widget class="QSpinBox" name="windowHeightSpinBox">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Maximum" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="suffix">
+ <string/>
+ </property>
+ <property name="minimum">
+ <number>1</number>
+ </property>
+ <property name="maximum">
+ <number>65536</number>
+ </property>
+ <property name="value">
+ <number>480</number>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="0">
+ <widget class="QLabel" name="labelWindowWidth">
+ <property name="text">
+ <string>&amp;Window Size:</string>
+ </property>
+ <property name="buddy">
+ <cstring>windowWidthSpinBox</cstring>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="2">
+ <widget class="QLabel" name="label">
+ <property name="text">
+ <string>×</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="4">
+ <widget class="QLabel" name="label_2">
+ <property name="text">
+ <string>pixels</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="5">
+ <spacer name="horizontalSpacer">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>0</width>
+ <height>0</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="consoleSettingsBox">
+ <property name="enabled">
+ <bool>true</bool>
+ </property>
+ <property name="title">
+ <string>&amp;Console Window</string>
+ </property>
+ <property name="checkable">
+ <bool>false</bool>
+ </property>
+ <property name="checked">
+ <bool>false</bool>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_2">
+ <item>
+ <widget class="QCheckBox" name="showConsoleCheck">
+ <property name="text">
+ <string>When the game is launched, show the console window</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="showConsoleErrorCheck">
+ <property name="text">
+ <string>When the game crashes, show the console window</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="autoCloseConsoleCheck">
+ <property name="text">
+ <string>When the game quits, hide the console window</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="globalDataPacksGroupBox">
+ <property name="title">
+ <string>&amp;Global Data Packs</string>
+ </property>
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ <property name="checked">
+ <bool>true</bool>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_18">
+ <item>
+ <widget class="QLabel" name="label_3">
+ <property name="text">
+ <string>Allows installing data packs across all worlds if an applicable mod is installed.
+It is most likely you will need to change the path - please refer to the mod's website.</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="verticalSpacer_4">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Fixed</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>0</width>
+ <height>6</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QLabel" name="label_4">
+ <property name="text">
+ <string>Folder Path</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_2">
+ <item>
+ <widget class="QLineEdit" name="dataPacksPathEdit">
+ <property name="placeholderText">
+ <string>datapacks</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="dataPacksPathBrowse">
+ <property name="text">
+ <string>Browse</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="gameTimeGroupBox">
+ <property name="enabled">
+ <bool>true</bool>
+ </property>
+ <property name="title">
+ <string>Game &amp;Time</string>
+ </property>
+ <property name="checkable">
+ <bool>false</bool>
+ </property>
+ <property name="checked">
+ <bool>false</bool>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_10">
+ <item>
+ <widget class="QCheckBox" name="showGameTime">
+ <property name="text">
+ <string>Show time spent &amp;playing instances</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="recordGameTime">
+ <property name="text">
+ <string>&amp;Record time spent playing instances</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="showGlobalGameTime">
+ <property name="text">
+ <string>Show the &amp;total time played across instances</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="showGameTimeWithoutDays">
+ <property name="text">
+ <string>Always show durations in &amp;hours</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="instanceAccountGroupBox">
+ <property name="title">
+ <string>Override &amp;Default Account</string>
+ </property>
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ <property name="checked">
+ <bool>false</bool>
+ </property>
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <item>
+ <widget class="QLabel" name="instanceAccountNameLabel">
+ <property name="text">
+ <string>Account:</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QComboBox" name="instanceAccountSelector">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Maximum" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_2">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>0</width>
+ <height>0</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="serverJoinGroupBox">
+ <property name="title">
+ <string>Enable Auto-&amp;join</string>
+ </property>
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ <property name="checked">
+ <bool>false</bool>
+ </property>
+ <layout class="QGridLayout" name="gridLayout_2">
+ <item row="1" column="1">
+ <widget class="QComboBox" name="worldsCb">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Maximum" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0">
+ <widget class="QRadioButton" name="worldJoinButton">
+ <property name="text">
+ <string>Singleplayer world:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="0">
+ <widget class="QRadioButton" name="serverJoinAddressButton">
+ <property name="text">
+ <string>Server address:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QLineEdit" name="serverJoinAddress">
+ <property name="maximumSize">
+ <size>
+ <width>200</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="2">
+ <spacer name="horizontalSpacer_3">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>0</width>
+ <height>0</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="loaderGroup">
+ <property name="title">
+ <string>Override Mod Download &amp;Loaders</string>
+ </property>
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ <property name="checked">
+ <bool>false</bool>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_4">
+ <item>
+ <widget class="QCheckBox" name="neoForge">
+ <property name="text">
+ <string>NeoForge</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="forge">
+ <property name="text">
+ <string>Forge</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="fabric">
+ <property name="text">
+ <string>Fabric</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="quilt">
+ <property name="text">
+ <string>Quilt</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="liteLoader">
+ <property name="text">
+ <string>LiteLoader</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <spacer name="verticalSpacer_2">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>40</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QWidget" name="javaPage">
+ <attribute name="title">
+ <string>Java</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_11">
+ <item>
+ <widget class="QScrollArea" name="javaScrollArea">
+ <property name="widgetResizable">
+ <bool>true</bool>
+ </property>
+ <widget class="QWidget" name="scrollAreaWidgetContents_3">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>100</width>
+ <height>30</height>
+ </rect>
+ </property>
+ </widget>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QWidget" name="tweaksPage">
+ <attribute name="title">
+ <string>Tweaks</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_8">
+ <item>
+ <widget class="QScrollArea" name="scrollArea_2">
+ <property name="widgetResizable">
+ <bool>true</bool>
+ </property>
+ <widget class="QWidget" name="scrollAreaWidgetContents_2">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>261</width>
+ <height>434</height>
+ </rect>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_7">
+ <item>
+ <widget class="QGroupBox" name="legacySettingsGroupBox">
+ <property name="title">
+ <string>&amp;Legacy Tweaks</string>
+ </property>
+ <property name="checkable">
+ <bool>false</bool>
+ </property>
+ <property name="checked">
+ <bool>false</bool>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_17">
+ <item>
+ <widget class="QCheckBox" name="onlineFixes">
+ <property name="toolTip">
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Emulates usages of old online services which are no longer operating.&lt;/p&gt;&lt;p&gt;Current fixes include: skin and online mode support.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ <property name="text">
+ <string>Enable online fixes (experimental)</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="nativeWorkaroundsGroupBox">
+ <property name="enabled">
+ <bool>true</bool>
+ </property>
+ <property name="title">
+ <string>&amp;Native Libraries</string>
+ </property>
+ <property name="checkable">
+ <bool>false</bool>
+ </property>
+ <property name="checked">
+ <bool>false</bool>
+ </property>
+ <layout class="QFormLayout" name="formLayout_2">
+ <property name="labelAlignment">
+ <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
+ </property>
+ <item row="2" column="0">
+ <widget class="QLabel" name="labelGLFWPath">
+ <property name="text">
+ <string>&amp;GLFW library path:</string>
+ </property>
+ <property name="buddy">
+ <cstring>lineEditGLFWPath</cstring>
+ </property>
+ </widget>
+ </item>
+ <item row="4" column="0">
+ <spacer name="verticalSpacer_6">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Fixed</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>0</width>
+ <height>6</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item row="6" column="0">
+ <widget class="QLabel" name="labelOpenALPath">
+ <property name="text">
+ <string>&amp;OpenAL library path:</string>
+ </property>
+ <property name="buddy">
+ <cstring>lineEditOpenALPath</cstring>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="1">
+ <widget class="QLineEdit" name="lineEditGLFWPath">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0" colspan="2">
+ <widget class="QCheckBox" name="useNativeGLFWCheck">
+ <property name="text">
+ <string>Use system installation of GLFW</string>
+ </property>
+ </widget>
+ </item>
+ <item row="5" column="0" colspan="2">
+ <widget class="QCheckBox" name="useNativeOpenALCheck">
+ <property name="text">
+ <string>Use system installation of OpenAL</string>
+ </property>
+ </widget>
+ </item>
+ <item row="6" column="1">
+ <widget class="QLineEdit" name="lineEditOpenALPath">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="perfomanceGroupBox">
+ <property name="enabled">
+ <bool>true</bool>
+ </property>
+ <property name="title">
+ <string>&amp;Performance</string>
+ </property>
+ <property name="checkable">
+ <bool>false</bool>
+ </property>
+ <property name="checked">
+ <bool>false</bool>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_13">
+ <item>
+ <widget class="QCheckBox" name="enableFeralGamemodeCheck">
+ <property name="toolTip">
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Enable Project Tick GameMode, to potentially improve gaming performance.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ <property name="text">
+ <string>Enable Feral GameMode</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="enableMangoHud">
+ <property name="toolTip">
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Enable MangoHud's advanced performance overlay.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ <property name="text">
+ <string>Enable MangoHud</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="useDiscreteGpuCheck">
+ <property name="toolTip">
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Use the discrete GPU instead of the primary GPU.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ <property name="text">
+ <string>Use discrete GPU</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="useZink">
+ <property name="toolTip">
+ <string>Use Zink, a Mesa OpenGL driver that implements OpenGL on top of Vulkan. Performance may vary depending on the situation. Note: If no suitable Vulkan driver is found, software rendering will be used.</string>
+ </property>
+ <property name="text">
+ <string>Use Zink</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <spacer name="verticalSpacer">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>40</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QWidget" name="customCommandsPage">
+ <attribute name="title">
+ <string>Custom Commands</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_6">
+ <item>
+ <widget class="CustomCommands" name="customCommands" native="true"/>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QWidget" name="environmentVariablesPage">
+ <attribute name="title">
+ <string>Environment Variables</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_16">
+ <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="EnvironmentVariables" name="environmentVariables" native="true"/>
+ </item>
+ </layout>
+ </widget>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <customwidgets>
+ <customwidget>
+ <class>CustomCommands</class>
+ <extends>QWidget</extends>
+ <header>ui/widgets/CustomCommands.h</header>
+ <container>1</container>
+ </customwidget>
+ <customwidget>
+ <class>EnvironmentVariables</class>
+ <extends>QWidget</extends>
+ <header>ui/widgets/EnvironmentVariables.h</header>
+ <container>1</container>
+ </customwidget>
+ </customwidgets>
+ <tabstops>
+ <tabstop>openGlobalSettingsButton</tabstop>
+ <tabstop>settingsTabs</tabstop>
+ <tabstop>scrollArea</tabstop>
+ <tabstop>maximizedCheckBox</tabstop>
+ <tabstop>windowHeightSpinBox</tabstop>
+ <tabstop>windowWidthSpinBox</tabstop>
+ <tabstop>closeAfterLaunchCheck</tabstop>
+ <tabstop>quitAfterGameStopCheck</tabstop>
+ <tabstop>showConsoleCheck</tabstop>
+ <tabstop>showConsoleErrorCheck</tabstop>
+ <tabstop>autoCloseConsoleCheck</tabstop>
+ <tabstop>showGameTime</tabstop>
+ <tabstop>recordGameTime</tabstop>
+ <tabstop>showGlobalGameTime</tabstop>
+ <tabstop>showGameTimeWithoutDays</tabstop>
+ <tabstop>instanceAccountGroupBox</tabstop>
+ <tabstop>instanceAccountSelector</tabstop>
+ <tabstop>serverJoinGroupBox</tabstop>
+ <tabstop>serverJoinAddressButton</tabstop>
+ <tabstop>serverJoinAddress</tabstop>
+ <tabstop>worldJoinButton</tabstop>
+ <tabstop>worldsCb</tabstop>
+ <tabstop>javaScrollArea</tabstop>
+ <tabstop>scrollArea_2</tabstop>
+ <tabstop>onlineFixes</tabstop>
+ <tabstop>useNativeGLFWCheck</tabstop>
+ <tabstop>lineEditGLFWPath</tabstop>
+ <tabstop>useNativeOpenALCheck</tabstop>
+ <tabstop>lineEditOpenALPath</tabstop>
+ <tabstop>enableFeralGamemodeCheck</tabstop>
+ <tabstop>enableMangoHud</tabstop>
+ <tabstop>useDiscreteGpuCheck</tabstop>
+ <tabstop>useZink</tabstop>
+ </tabstops>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/archived/projt-launcher/launcher/ui/widgets/ModFilterWidget.cpp b/archived/projt-launcher/launcher/ui/widgets/ModFilterWidget.cpp
new file mode 100644
index 0000000000..d04103a8b1
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/widgets/ModFilterWidget.cpp
@@ -0,0 +1,479 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * 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 "ModFilterWidget.h"
+#include <QCheckBox>
+#include <QComboBox>
+#include <QListWidget>
+#include <algorithm>
+#include <list>
+#include "BaseVersionList.h"
+#include "Json.h"
+#include "Version.h"
+#include "meta/Index.hpp"
+#include "modplatform/ModIndex.h"
+#include "ui/widgets/CheckComboBox.h"
+#include "ui_ModFilterWidget.h"
+
+#include "Application.h"
+#include "minecraft/PackProfile.h"
+
+std::unique_ptr<ModFilterWidget> ModFilterWidget::create(MinecraftInstance* instance, bool extended)
+{
+ return std::unique_ptr<ModFilterWidget>(new ModFilterWidget(instance, extended));
+}
+
+class VersionBasicModel : public QIdentityProxyModel
+{
+ Q_OBJECT
+
+ public:
+ explicit VersionBasicModel(QObject* parent = nullptr) : QIdentityProxyModel(parent)
+ {}
+
+ virtual QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override
+ {
+ if (role == Qt::DisplayRole)
+ return QIdentityProxyModel::data(index, BaseVersionList::VersionIdRole);
+ if (role == Qt::UserRole)
+ return QIdentityProxyModel::data(index, BaseVersionList::VersionIdRole);
+ return {};
+ }
+};
+
+class AllVersionProxyModel : public QSortFilterProxyModel
+{
+ Q_OBJECT
+
+ public:
+ AllVersionProxyModel(QObject* parent = nullptr) : QSortFilterProxyModel(parent)
+ {}
+
+ int rowCount(const QModelIndex& parent = QModelIndex()) const override
+ {
+ return QSortFilterProxyModel::rowCount(parent) + 1;
+ }
+
+ QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override
+ {
+ if (!index.isValid())
+ {
+ return {};
+ }
+
+ if (index.row() == 0)
+ {
+ if (role == Qt::DisplayRole)
+ {
+ return tr("All Versions");
+ }
+ if (role == Qt::UserRole)
+ {
+ return "all";
+ }
+ return {};
+ }
+
+ QModelIndex newIndex = QSortFilterProxyModel::index(index.row() - 1, index.column());
+ return QSortFilterProxyModel::data(newIndex, role);
+ }
+
+ Qt::ItemFlags flags(const QModelIndex& index) const override
+ {
+ if (index.row() == 0)
+ {
+ return Qt::ItemIsSelectable | Qt::ItemIsEnabled;
+ }
+ return QSortFilterProxyModel::flags(index);
+ }
+};
+
+ModFilterWidget::ModFilterWidget(MinecraftInstance* instance, bool extended)
+ : QTabWidget(),
+ ui(new Ui::ModFilterWidget),
+ m_instance(instance),
+ m_filter(new Filter())
+{
+ ui->setupUi(this);
+
+ m_versions_proxy = new VersionProxyModel(this);
+ m_versions_proxy->setFilter(BaseVersionList::TypeRole, Filters::equals("release"));
+
+ QAbstractProxyModel* proxy = new VersionBasicModel(this);
+ proxy->setSourceModel(m_versions_proxy);
+
+ if (extended)
+ {
+ if (!m_instance)
+ {
+ ui->environmentGroup->hide();
+ }
+ ui->versions->setSourceModel(proxy);
+ ui->versions->setSeparator(", ");
+ ui->versions->setDefaultText(tr("All Versions"));
+ ui->version->hide();
+ }
+ else
+ {
+ auto allVersions = new AllVersionProxyModel(this);
+ allVersions->setSourceModel(proxy);
+ proxy = allVersions;
+ ui->version->setModel(proxy);
+ ui->versions->hide();
+ ui->showAllVersions->hide();
+ ui->environmentGroup->hide();
+ ui->openSource->hide();
+ }
+
+ ui->versions->setStyleSheet("combobox-popup: 0;");
+ ui->version->setStyleSheet("combobox-popup: 0;");
+ connect(ui->showAllVersions, &QCheckBox::stateChanged, this, &ModFilterWidget::onShowAllVersionsChanged);
+ connect(ui->versions, &QComboBox::currentIndexChanged, this, &ModFilterWidget::onVersionFilterChanged);
+ connect(ui->versions, &CheckComboBox::checkedItemsChanged, this, [this] { onVersionFilterChanged(0); });
+ connect(ui->version, &QComboBox::currentTextChanged, this, &ModFilterWidget::onVersionFilterTextChanged);
+
+ connect(ui->neoForge, &QCheckBox::stateChanged, this, &ModFilterWidget::onLoadersFilterChanged);
+ connect(ui->forge, &QCheckBox::stateChanged, this, &ModFilterWidget::onLoadersFilterChanged);
+ connect(ui->fabric, &QCheckBox::stateChanged, this, &ModFilterWidget::onLoadersFilterChanged);
+ connect(ui->quilt, &QCheckBox::stateChanged, this, &ModFilterWidget::onLoadersFilterChanged);
+ connect(ui->liteLoader, &QCheckBox::stateChanged, this, &ModFilterWidget::onLoadersFilterChanged);
+ connect(ui->babric, &QCheckBox::stateChanged, this, &ModFilterWidget::onLoadersFilterChanged);
+ connect(ui->btaBabric, &QCheckBox::stateChanged, this, &ModFilterWidget::onLoadersFilterChanged);
+ connect(ui->legacyFabric, &QCheckBox::stateChanged, this, &ModFilterWidget::onLoadersFilterChanged);
+ connect(ui->ornithe, &QCheckBox::stateChanged, this, &ModFilterWidget::onLoadersFilterChanged);
+ connect(ui->rift, &QCheckBox::stateChanged, this, &ModFilterWidget::onLoadersFilterChanged);
+
+ connect(ui->showMoreButton, &QPushButton::clicked, this, &ModFilterWidget::onShowMoreClicked);
+
+ if (!extended)
+ {
+ ui->showMoreButton->setVisible(false);
+ ui->extendedModLoadersWidget->setVisible(false);
+ }
+
+ if (extended)
+ {
+ connect(ui->clientSide, &QCheckBox::stateChanged, this, &ModFilterWidget::onSideFilterChanged);
+ connect(ui->serverSide, &QCheckBox::stateChanged, this, &ModFilterWidget::onSideFilterChanged);
+ }
+
+ connect(ui->hideInstalled, &QCheckBox::stateChanged, this, &ModFilterWidget::onHideInstalledFilterChanged);
+ connect(ui->openSource, &QCheckBox::stateChanged, this, &ModFilterWidget::onOpenSourceFilterChanged);
+
+ connect(ui->releaseCb, &QCheckBox::stateChanged, this, &ModFilterWidget::onReleaseFilterChanged);
+ connect(ui->betaCb, &QCheckBox::stateChanged, this, &ModFilterWidget::onReleaseFilterChanged);
+ connect(ui->alphaCb, &QCheckBox::stateChanged, this, &ModFilterWidget::onReleaseFilterChanged);
+ connect(ui->unknownCb, &QCheckBox::stateChanged, this, &ModFilterWidget::onReleaseFilterChanged);
+
+ setHidden(true);
+ loadVersionList();
+ prepareBasicFilter();
+}
+
+auto ModFilterWidget::getFilter() -> std::shared_ptr<Filter>
+{
+ m_filter_changed = false;
+ return m_filter;
+}
+
+ModFilterWidget::~ModFilterWidget()
+{
+ delete ui;
+}
+
+void ModFilterWidget::loadVersionList()
+{
+ m_version_list = APPLICATION->metadataIndex()->component("net.minecraft");
+ if (!m_version_list->isLoaded())
+ {
+ QEventLoop load_version_list_loop;
+
+ QTimer time_limit_for_list_load;
+ time_limit_for_list_load.setTimerType(Qt::TimerType::CoarseTimer);
+ time_limit_for_list_load.setSingleShot(true);
+ time_limit_for_list_load.callOnTimeout(&load_version_list_loop, &QEventLoop::quit);
+ time_limit_for_list_load.start(4000);
+
+ auto task = m_version_list->getLoadTask();
+
+ connect(task.get(),
+ &Task::failed,
+ [this]
+ {
+ ui->versions->setEnabled(false);
+ ui->showAllVersions->setEnabled(false);
+ });
+ connect(task.get(), &Task::finished, &load_version_list_loop, &QEventLoop::quit);
+
+ if (!task->isRunning())
+ task->start();
+
+ load_version_list_loop.exec();
+ if (time_limit_for_list_load.isActive())
+ time_limit_for_list_load.stop();
+ }
+ m_versions_proxy->setSourceModel(m_version_list.get());
+}
+
+void ModFilterWidget::prepareBasicFilter()
+{
+ m_filter->openSource = false;
+ if (m_instance)
+ {
+ m_filter->hideInstalled = false;
+ m_filter->side = ModPlatform::Side::NoSide; // or "both"
+ ModPlatform::ModLoaderTypes loaders;
+ if (m_instance->settings()->get("OverrideModDownloadLoaders").toBool())
+ {
+ for (auto loader : Json::toStringList(m_instance->settings()->get("ModDownloadLoaders").toString()))
+ {
+ loaders |= ModPlatform::getModLoaderFromString(loader);
+ }
+ }
+ else
+ {
+ loaders = m_instance->getPackProfile()->getSupportedModLoaders().value();
+ }
+ ui->neoForge->setChecked(loaders & ModPlatform::NeoForge);
+ ui->forge->setChecked(loaders & ModPlatform::Forge);
+ ui->fabric->setChecked(loaders & ModPlatform::Fabric);
+ ui->quilt->setChecked(loaders & ModPlatform::Quilt);
+ ui->liteLoader->setChecked(loaders & ModPlatform::LiteLoader);
+ m_filter->loaders = loaders;
+ auto def = m_instance->getPackProfile()->getComponentVersion("net.minecraft");
+ m_filter->versions.emplace_front(def);
+ ui->versions->setCheckedItems({ def });
+ ui->version->setCurrentIndex(ui->version->findText(def));
+ }
+ else
+ {
+ ui->hideInstalled->hide();
+ }
+}
+
+void ModFilterWidget::onShowAllVersionsChanged()
+{
+ if (ui->showAllVersions->isChecked())
+ m_versions_proxy->clearFilters();
+ else
+ m_versions_proxy->setFilter(BaseVersionList::TypeRole, Filters::equals("release"));
+}
+
+void ModFilterWidget::onVersionFilterChanged(int)
+{
+ auto versions = ui->versions->checkedItems();
+ versions.sort();
+ std::list<Version> current_list;
+
+ for (const QString& version : versions)
+ current_list.emplace_back(version);
+
+ m_filter_changed =
+ m_filter->versions.size() != current_list.size()
+ || !std::equal(m_filter->versions.begin(), m_filter->versions.end(), current_list.begin(), current_list.end());
+ m_filter->versions = current_list;
+ if (m_filter_changed)
+ emit filterChanged();
+}
+
+void ModFilterWidget::onLoadersFilterChanged()
+{
+ ModPlatform::ModLoaderTypes loaders;
+ if (ui->neoForge->isChecked())
+ loaders |= ModPlatform::NeoForge;
+ if (ui->forge->isChecked())
+ loaders |= ModPlatform::Forge;
+ if (ui->fabric->isChecked())
+ loaders |= ModPlatform::Fabric;
+ if (ui->quilt->isChecked())
+ loaders |= ModPlatform::Quilt;
+ if (ui->liteLoader->isChecked())
+ loaders |= ModPlatform::LiteLoader;
+ if (ui->babric->isChecked())
+ loaders |= ModPlatform::Babric;
+ if (ui->btaBabric->isChecked())
+ loaders |= ModPlatform::BTA;
+ if (ui->legacyFabric->isChecked())
+ loaders |= ModPlatform::LegacyFabric;
+ if (ui->ornithe->isChecked())
+ loaders |= ModPlatform::Ornithe;
+ if (ui->rift->isChecked())
+ loaders |= ModPlatform::Rift;
+ m_filter_changed = loaders != m_filter->loaders;
+ m_filter->loaders = loaders;
+ if (m_filter_changed)
+ emit filterChanged();
+}
+
+void ModFilterWidget::onSideFilterChanged()
+{
+ ModPlatform::Side side;
+
+ if (ui->clientSide->isChecked() && !ui->serverSide->isChecked())
+ {
+ side = ModPlatform::Side::ClientSide;
+ }
+ else if (!ui->clientSide->isChecked() && ui->serverSide->isChecked())
+ {
+ side = ModPlatform::Side::ServerSide;
+ }
+ else if (ui->clientSide->isChecked() && ui->serverSide->isChecked())
+ {
+ side = ModPlatform::Side::UniversalSide;
+ }
+ else
+ {
+ side = ModPlatform::Side::NoSide;
+ }
+
+ m_filter_changed = side != m_filter->side;
+ m_filter->side = side;
+ if (m_filter_changed)
+ emit filterChanged();
+}
+
+void ModFilterWidget::onHideInstalledFilterChanged()
+{
+ auto hide = ui->hideInstalled->isChecked();
+ m_filter_changed = hide != m_filter->hideInstalled;
+ m_filter->hideInstalled = hide;
+ if (m_filter_changed)
+ emit filterChanged();
+}
+
+void ModFilterWidget::onVersionFilterTextChanged(const QString& version)
+{
+ m_filter->versions.clear();
+ if (ui->version->currentData(Qt::UserRole) != "all")
+ {
+ m_filter->versions.emplace_back(version);
+ }
+ m_filter_changed = true;
+ emit filterChanged();
+}
+
+void ModFilterWidget::setCategories(const QList<ModPlatform::Category>& categories)
+{
+ m_categories = categories;
+
+ delete ui->categoryGroup->layout();
+ auto layout = new QVBoxLayout(ui->categoryGroup);
+
+ for (const auto& category : categories)
+ {
+ auto name = category.name;
+ name.replace("-", " ");
+ name.replace("&", "&&");
+ auto checkbox = new QCheckBox(name);
+ auto font = checkbox->font();
+ font.setCapitalization(QFont::Capitalize);
+ checkbox->setFont(font);
+
+ layout->addWidget(checkbox);
+
+ const QString id = category.id;
+ connect(checkbox,
+ &QCheckBox::toggled,
+ this,
+ [this, id](bool checked)
+ {
+ if (checked)
+ m_filter->categoryIds.append(id);
+ else
+ m_filter->categoryIds.removeOne(id);
+
+ m_filter_changed = true;
+ emit filterChanged();
+ });
+ }
+}
+
+void ModFilterWidget::onOpenSourceFilterChanged()
+{
+ auto open = ui->openSource->isChecked();
+ m_filter_changed = open != m_filter->openSource;
+ m_filter->openSource = open;
+ if (m_filter_changed)
+ emit filterChanged();
+}
+
+void ModFilterWidget::onReleaseFilterChanged()
+{
+ std::list<ModPlatform::IndexedVersionType> releases;
+ if (ui->releaseCb->isChecked())
+ releases.push_back(ModPlatform::IndexedVersionType(ModPlatform::IndexedVersionType::VersionType::Release));
+ if (ui->betaCb->isChecked())
+ releases.push_back(ModPlatform::IndexedVersionType(ModPlatform::IndexedVersionType::VersionType::Beta));
+ if (ui->alphaCb->isChecked())
+ releases.push_back(ModPlatform::IndexedVersionType(ModPlatform::IndexedVersionType::VersionType::Alpha));
+ if (ui->unknownCb->isChecked())
+ releases.push_back(ModPlatform::IndexedVersionType(ModPlatform::IndexedVersionType::VersionType::Unknown));
+ m_filter_changed = releases != m_filter->releases;
+ m_filter->releases = releases;
+ if (m_filter_changed)
+ emit filterChanged();
+}
+
+void ModFilterWidget::onShowMoreClicked()
+{
+ ui->extendedModLoadersWidget->setVisible(true);
+ ui->showMoreButton->setVisible(false);
+}
+
+#include "ModFilterWidget.moc"
diff --git a/archived/projt-launcher/launcher/ui/widgets/ModFilterWidget.h b/archived/projt-launcher/launcher/ui/widgets/ModFilterWidget.h
new file mode 100644
index 0000000000..ab71b55d21
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/widgets/ModFilterWidget.h
@@ -0,0 +1,168 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * 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 <QButtonGroup>
+#include <QList>
+#include <QListWidgetItem>
+#include <QTabWidget>
+
+#include "Version.h"
+
+#include "VersionProxyModel.h"
+#include "meta/VersionList.hpp"
+
+#include "minecraft/MinecraftInstance.h"
+#include "modplatform/ModIndex.h"
+
+class MinecraftInstance;
+
+namespace Ui
+{
+ class ModFilterWidget;
+}
+
+class ModFilterWidget : public QTabWidget
+{
+ Q_OBJECT
+ public:
+ struct Filter
+ {
+ std::list<Version> versions;
+ std::list<ModPlatform::IndexedVersionType> releases;
+ ModPlatform::ModLoaderTypes loaders;
+ ModPlatform::Side side;
+ bool hideInstalled;
+ QStringList categoryIds;
+ bool openSource;
+
+ bool operator==(const Filter& other) const
+ {
+ return hideInstalled == other.hideInstalled && side == other.side && loaders == other.loaders
+ && versions == other.versions && releases == other.releases && categoryIds == other.categoryIds
+ && openSource == other.openSource;
+ }
+ bool operator!=(const Filter& other) const
+ {
+ return !(*this == other);
+ }
+
+ bool checkMcVersions(QStringList value)
+ {
+ for (auto mcVersion : versions)
+ if (value.contains(mcVersion.toString()))
+ return true;
+
+ return versions.empty();
+ }
+
+ bool checkModpackFilters(const ModPlatform::IndexedVersion& v)
+ {
+ return ((!loaders || !v.loaders || loaders & v.loaders) && // loaders
+ (releases.empty() || // releases
+ std::find(releases.cbegin(), releases.cend(), v.version_type) != releases.cend())
+ && checkMcVersions({ v.mcVersion })); // gameVersion}
+ }
+ };
+
+ static std::unique_ptr<ModFilterWidget> create(MinecraftInstance* instance, bool extended);
+ virtual ~ModFilterWidget();
+
+ auto getFilter() -> std::shared_ptr<Filter>;
+ auto changed() const -> bool
+ {
+ return m_filter_changed;
+ }
+
+ signals:
+ void filterChanged();
+
+ public slots:
+ void setCategories(const QList<ModPlatform::Category>&);
+
+ private:
+ ModFilterWidget(MinecraftInstance* instance, bool extendedSupport);
+
+ void loadVersionList();
+ void prepareBasicFilter();
+
+ private slots:
+ void onVersionFilterChanged(int);
+ void onVersionFilterTextChanged(const QString& version);
+ void onLoadersFilterChanged();
+ void onSideFilterChanged();
+ void onHideInstalledFilterChanged();
+ void onShowAllVersionsChanged();
+ void onOpenSourceFilterChanged();
+ void onReleaseFilterChanged();
+ void onShowMoreClicked();
+
+ private:
+ Ui::ModFilterWidget* ui;
+
+ MinecraftInstance* m_instance = nullptr;
+ std::shared_ptr<Filter> m_filter;
+ bool m_filter_changed = false;
+
+ projt::meta::MetaVersionList::Ptr m_version_list;
+ VersionProxyModel* m_versions_proxy = nullptr;
+
+ QList<ModPlatform::Category> m_categories;
+};
diff --git a/archived/projt-launcher/launcher/ui/widgets/ModFilterWidget.ui b/archived/projt-launcher/launcher/ui/widgets/ModFilterWidget.ui
new file mode 100644
index 0000000000..d29c9752ab
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/widgets/ModFilterWidget.ui
@@ -0,0 +1,333 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>ModFilterWidget</class>
+ <widget class="QWidget" name="ModFilterWidget">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>310</width>
+ <height>600</height>
+ </rect>
+ </property>
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>275</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>310</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="windowTitle">
+ <string>Form</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_2">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QScrollArea" name="scrollArea">
+ <property name="minimumSize">
+ <size>
+ <width>275</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="sizeAdjustPolicy">
+ <enum>QAbstractScrollArea::AdjustToContentsOnFirstShow</enum>
+ </property>
+ <property name="widgetResizable">
+ <bool>true</bool>
+ </property>
+ <widget class="QWidget" name="scrollAreaWidgetContents">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>294</width>
+ <height>781</height>
+ </rect>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <widget class="QGroupBox" name="categoryGroup">
+ <property name="title">
+ <string>Categories</string>
+ </property>
+ <property name="checkable">
+ <bool>false</bool>
+ </property>
+ <property name="checked">
+ <bool>false</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="loaderGroup">
+ <property name="title">
+ <string>Loaders</string>
+ </property>
+ <property name="checkable">
+ <bool>false</bool>
+ </property>
+ <property name="checked">
+ <bool>false</bool>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_4">
+ <item>
+ <widget class="QCheckBox" name="neoForge">
+ <property name="text">
+ <string>NeoForge</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="forge">
+ <property name="text">
+ <string>Forge</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="fabric">
+ <property name="text">
+ <string>Fabric</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="quilt">
+ <property name="text">
+ <string>Quilt</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="showMoreButton">
+ <property name="text">
+ <string>Show More</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QWidget" name="extendedModLoadersWidget" native="true">
+ <property name="visible">
+ <bool>false</bool>
+ </property>
+ <layout class="QVBoxLayout" name="extendedModLoadersLayout">
+ <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="QCheckBox" name="liteLoader">
+ <property name="text">
+ <string>LiteLoader</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="babric">
+ <property name="text">
+ <string>Babric</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="btaBabric">
+ <property name="text">
+ <string>BTA (Babric)</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="legacyFabric">
+ <property name="text">
+ <string>Legacy Fabric</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="ornithe">
+ <property name="text">
+ <string>Ornithe</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="rift">
+ <property name="text">
+ <string>Rift</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="minecraftVersionGroup">
+ <property name="title">
+ <string>Versions</string>
+ </property>
+ <property name="checkable">
+ <bool>false</bool>
+ </property>
+ <property name="checked">
+ <bool>false</bool>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_3">
+ <item>
+ <widget class="QCheckBox" name="showAllVersions">
+ <property name="text">
+ <string>Show all versions</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="CheckComboBox" name="versions"/>
+ </item>
+ <item>
+ <widget class="QComboBox" name="version"/>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="environmentGroup">
+ <property name="title">
+ <string>Environments</string>
+ </property>
+ <property name="checkable">
+ <bool>false</bool>
+ </property>
+ <property name="checked">
+ <bool>false</bool>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_5">
+ <item>
+ <widget class="QCheckBox" name="clientSide">
+ <property name="text">
+ <string>Client</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="serverSide">
+ <property name="text">
+ <string>Server</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="hideInstalled">
+ <property name="text">
+ <string>Hide installed items</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="openSource">
+ <property name="text">
+ <string>Open source only</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="releaseGroup">
+ <property name="title">
+ <string>Release type</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_6">
+ <item>
+ <widget class="QCheckBox" name="releaseCb">
+ <property name="text">
+ <string>Release</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="betaCb">
+ <property name="text">
+ <string>Beta</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="alphaCb">
+ <property name="text">
+ <string>Alpha</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="unknownCb">
+ <property name="text">
+ <string>Unknown</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <spacer name="verticalSpacer">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>40</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <customwidgets>
+ <customwidget>
+ <class>CheckComboBox</class>
+ <extends>QComboBox</extends>
+ <header>ui/widgets/CheckComboBox.h</header>
+ </customwidget>
+ </customwidgets>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/archived/projt-launcher/launcher/ui/widgets/ModListView.cpp b/archived/projt-launcher/launcher/ui/widgets/ModListView.cpp
new file mode 100644
index 0000000000..7ea0cfbb17
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/widgets/ModListView.cpp
@@ -0,0 +1,96 @@
+// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ * 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 <QDrag>
+#include <QHeaderView>
+#include <QMouseEvent>
+#include <QPainter>
+#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);
+ setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded);
+ setDropIndicatorShown(true);
+ setDragEnabled(true);
+ setDragDropMode(QAbstractItemView::DropOnly);
+ viewport()->setAcceptDrops(true);
+ setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
+}
+
+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::Interactive);
+ head->setSectionResizeMode(1, QHeaderView::Stretch);
+ for (int i = 2; i < head->count(); i++)
+ head->setSectionResizeMode(i, QHeaderView::Interactive);
+ }
+ else
+ {
+ head->setSectionResizeMode(0, QHeaderView::Stretch);
+ for (int i = 1; i < head->count(); i++)
+ head->setSectionResizeMode(i, QHeaderView::Interactive);
+ }
+}
+
+void ModListView::setResizeModes(const QList<QHeaderView::ResizeMode>& modes)
+{
+ auto head = header();
+ for (int i = 0; i < modes.count(); i++)
+ {
+ head->setSectionResizeMode(i, modes[i]);
+ }
+}
diff --git a/archived/projt-launcher/launcher/ui/widgets/ModListView.h b/archived/projt-launcher/launcher/ui/widgets/ModListView.h
new file mode 100644
index 0000000000..84896cbade
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/widgets/ModListView.h
@@ -0,0 +1,49 @@
+// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ * 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 <QHeaderView>
+#include <QTreeView>
+
+class ModListView : public QTreeView
+{
+ Q_OBJECT
+ public:
+ explicit ModListView(QWidget* parent = 0);
+ virtual void setModel(QAbstractItemModel* model);
+ virtual void setResizeModes(const QList<QHeaderView::ResizeMode>& modes);
+};
diff --git a/archived/projt-launcher/launcher/ui/widgets/PageContainer.cpp b/archived/projt-launcher/launcher/ui/widgets/PageContainer.cpp
new file mode 100644
index 0000000000..3a11259711
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/widgets/PageContainer.cpp
@@ -0,0 +1,334 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
+ * Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org>
+ * Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * 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 "BuildConfig.h"
+#include "PageContainer_p.h"
+
+#include <QDialogButtonBox>
+#include <QGridLayout>
+#include <QLabel>
+#include <QLineEdit>
+#include <QListView>
+#include <QPushButton>
+#include <QSortFilterProxyModel>
+#include <QStackedLayout>
+#include <QStyledItemDelegate>
+#include <QUrl>
+
+#include "settings/SettingsObject.h"
+
+#include "ui/widgets/IconLabel.h"
+
+#include "Application.h"
+#include "DesktopServices.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)
+ {
+ auto widget = dynamic_cast<QWidget*>(page);
+ widget->setParent(this);
+ page->stackIndex = m_pageStack->addWidget(widget);
+ page->listIndex = counter;
+ page->setParentContainer(this);
+ counter++;
+ page->updateExtraInfo = [this](QString id, QString info)
+ {
+ if (m_currentPage && id == m_currentPage->id())
+ m_header->setText(m_currentPage->displayName() + info);
+ };
+ }
+ 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(),
+ &QItemSelectionModel::currentRowChanged,
+ this,
+ &PageContainer::currentChanged);
+ 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;
+}
+
+BasePage* PageContainer::getPage(QString pageId)
+{
+ return m_model->findPageEntryById(pageId);
+}
+
+BasePage* PageContainer::selectedPage() const
+{
+ return m_currentPage;
+}
+
+const QList<BasePage*>& PageContainer::getPages() const
+{
+ return m_model->pages();
+}
+
+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
+ {
+ // No page to select - show the empty state
+ showPage(-1);
+ }
+ }
+}
+
+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::retranslate()
+{
+ if (m_currentPage)
+ m_header->setText(m_currentPage->displayName());
+
+ for (auto page : m_model->pages())
+ page->retranslate();
+}
+
+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(QIcon::fromTheme("bug"));
+ }
+}
+
+void PageContainer::help()
+{
+ if (m_currentPage)
+ {
+ QString pageId = m_currentPage->helpPage();
+ if (pageId.isEmpty())
+ return;
+ DesktopServices::openUrl(QUrl(BuildConfig.HELP_URL.arg(pageId)));
+ }
+}
+
+void PageContainer::currentChanged(const QModelIndex& current)
+{
+ int selected_index = current.isValid() ? m_proxyModel->mapToSource(current).row() : -1;
+
+ auto* selected = m_model->pages().at(selected_index);
+ auto* previous = m_currentPage;
+
+ emit selectedPageChanged(previous, selected);
+
+ showPage(selected_index);
+}
+
+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;
+}
+
+void PageContainer::changeEvent(QEvent* event)
+{
+ if (event->type() == QEvent::LanguageChange)
+ {
+ retranslate();
+ }
+ QWidget::changeEvent(event);
+}
diff --git a/archived/projt-launcher/launcher/ui/widgets/PageContainer.h b/archived/projt-launcher/launcher/ui/widgets/PageContainer.h
new file mode 100644
index 0000000000..97c9001b2a
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/widgets/PageContainer.h
@@ -0,0 +1,150 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org>
+ * Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * 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 <QModelIndex>
+#include <QWidget>
+
+#include "ui/pages/BasePageContainer.h"
+#include "ui/pages/BasePageProvider.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;
+ }
+
+ bool selectPage(QString pageId) override;
+ BasePage* selectedPage() const override;
+ BasePage* getPage(QString pageId) override;
+ const QList<BasePage*>& getPages() const;
+
+ void refreshContainer() override;
+ virtual void setParentContainer(BasePageContainer* container)
+ {
+ m_container = container;
+ };
+
+ void changeEvent(QEvent*) override;
+
+ void hidePageList()
+ {
+ m_pageList->hide();
+ }
+
+ private:
+ void createUI();
+ void retranslate();
+
+ public slots:
+ void help();
+
+ signals:
+ /** Emitted when the currently selected page is changed */
+ void selectedPageChanged(BasePage* previous, BasePage* selected);
+
+ 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/archived/projt-launcher/launcher/ui/widgets/PageContainer_p.h b/archived/projt-launcher/launcher/ui/widgets/PageContainer_p.h
new file mode 100644
index 0000000000..0e15300ffa
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/widgets/PageContainer_p.h
@@ -0,0 +1,145 @@
+// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ * 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 <QEvent>
+#include <QListView>
+#include <QScrollBar>
+#include <QStyledItemDelegate>
+
+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;
+ // NOTE: Workaround for Qt icon stretching bug on Windows.
+ 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);
+ // Adjust margins when using Breeze theme
+ setProperty("_kde_side_panel_view", true);
+ }
+
+ 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/archived/projt-launcher/launcher/ui/widgets/ProgressWidget.cpp b/archived/projt-launcher/launcher/ui/widgets/ProgressWidget.cpp
new file mode 100644
index 0000000000..c11ff9af8d
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/widgets/ProgressWidget.cpp
@@ -0,0 +1,143 @@
+// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+/* === Upstream License Block (Do Not Modify) ==============================
+
+ Licensed under the Apache-2.0 license.
+ See README.md for details.
+
+========================================================================== */
+
+#include "ProgressWidget.h"
+#include <QEventLoop>
+#include <QLabel>
+#include <QProgressBar>
+#include <QVBoxLayout>
+
+#include "tasks/Task.h"
+
+ProgressWidget::ProgressWidget(QWidget* parent, bool show_label) : QWidget(parent)
+{
+ auto* layout = new QVBoxLayout(this);
+
+ if (show_label)
+ {
+ m_label = new QLabel(this);
+ m_label->setWordWrap(true);
+ layout->addWidget(m_label);
+ }
+
+ m_bar = new QProgressBar(this);
+ m_bar->setMinimum(0);
+ m_bar->setMaximum(100);
+ layout->addWidget(m_bar);
+
+ setLayout(layout);
+}
+
+void ProgressWidget::reset()
+{
+ m_bar->reset();
+}
+
+void ProgressWidget::progressFormat(QString format)
+{
+ if (format.isEmpty())
+ m_bar->setTextVisible(false);
+ else
+ m_bar->setFormat(format);
+}
+
+void ProgressWidget::watch(Task* task)
+{
+ if (!task)
+ return;
+
+ if (m_task)
+ disconnect(m_task, nullptr, this, nullptr);
+
+ m_task = task;
+
+ connect(m_task, &Task::finished, this, &ProgressWidget::handleTaskFinish);
+ connect(m_task, &Task::status, this, &ProgressWidget::handleTaskStatus);
+ connect(m_task, &Task::details, this, &ProgressWidget::handleTaskStatus);
+ connect(m_task, &Task::progress, this, &ProgressWidget::handleTaskProgress);
+ connect(m_task, &Task::destroyed, this, &ProgressWidget::taskDestroyed);
+
+ if (m_task->isRunning())
+ show();
+ else
+ connect(m_task, &Task::started, this, &ProgressWidget::show);
+}
+
+void ProgressWidget::start(Task* task)
+{
+ watch(task);
+ if (!m_task->isRunning())
+ QMetaObject::invokeMethod(m_task, "start", Qt::QueuedConnection);
+}
+
+bool ProgressWidget::exec(std::shared_ptr<Task> task)
+{
+ QEventLoop loop;
+
+ connect(task.get(), &Task::finished, &loop, &QEventLoop::quit);
+
+ start(task.get());
+
+ if (task->isRunning())
+ loop.exec();
+
+ return task->wasSuccessful();
+}
+
+void ProgressWidget::show()
+{
+ setHidden(false);
+}
+void ProgressWidget::hide()
+{
+ setHidden(true);
+}
+
+void ProgressWidget::handleTaskFinish()
+{
+ if (!m_task->wasSuccessful() && m_label)
+ m_label->setText(m_task->failReason());
+
+ if (m_hide_if_inactive)
+ hide();
+}
+void ProgressWidget::handleTaskStatus(const QString& status)
+{
+ if (m_label)
+ 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/archived/projt-launcher/launcher/ui/widgets/ProgressWidget.h b/archived/projt-launcher/launcher/ui/widgets/ProgressWidget.h
new file mode 100644
index 0000000000..337d062906
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/widgets/ProgressWidget.h
@@ -0,0 +1,87 @@
+// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+/* === Upstream License Block (Do Not Modify) ==============================
+
+ 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, bool show_label = true);
+
+ /** Whether to hide the widget automatically if it's watching no running task. */
+ void hideIfInactive(bool hide)
+ {
+ m_hide_if_inactive = hide;
+ }
+
+ /** Reset the displayed progress to 0 */
+ void reset();
+
+ /** The text that shows up in the middle of the progress bar.
+ * By default it's '%p%', with '%p' being the total progress in percentage.
+ */
+ void progressFormat(QString);
+
+ public slots:
+ /** Watch the progress of a task. */
+ void watch(Task* task);
+
+ /** Watch the progress of a task, and start it if needed */
+ void start(Task* task);
+
+ /** Blocking way of waiting for a task to finish. */
+ bool exec(std::shared_ptr<Task> task);
+
+ /** Un-hide the widget if needed. */
+ void show();
+
+ /** Make the widget invisible. */
+ void hide();
+
+ private slots:
+ void handleTaskFinish();
+ void handleTaskStatus(const QString& status);
+ void handleTaskProgress(qint64 current, qint64 total);
+ void taskDestroyed();
+
+ private:
+ QLabel* m_label = nullptr;
+ QProgressBar* m_bar = nullptr;
+ Task* m_task = nullptr;
+
+ bool m_hide_if_inactive = false;
+};
diff --git a/archived/projt-launcher/launcher/ui/widgets/ProjectDescriptionPage.cpp b/archived/projt-launcher/launcher/ui/widgets/ProjectDescriptionPage.cpp
new file mode 100644
index 0000000000..4f48a7585c
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/widgets/ProjectDescriptionPage.cpp
@@ -0,0 +1,45 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+#include "ProjectDescriptionPage.h"
+
+#include "VariableSizedImageObject.h"
+
+#include <QDebug>
+
+ProjectDescriptionPage::ProjectDescriptionPage(QWidget* parent)
+ : QTextBrowser(parent),
+ m_image_text_object(new VariableSizedImageObject)
+{
+ m_image_text_object->setParent(this);
+ document()->documentLayout()->registerHandler(QTextFormat::ImageObject, m_image_text_object.get());
+}
+
+void ProjectDescriptionPage::setMetaEntry(QString entry)
+{
+ if (m_image_text_object)
+ m_image_text_object->setMetaEntry(entry);
+}
+
+void ProjectDescriptionPage::flush()
+{
+ if (m_image_text_object)
+ m_image_text_object->flush();
+}
diff --git a/archived/projt-launcher/launcher/ui/widgets/ProjectDescriptionPage.h b/archived/projt-launcher/launcher/ui/widgets/ProjectDescriptionPage.h
new file mode 100644
index 0000000000..5ec804a215
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/widgets/ProjectDescriptionPage.h
@@ -0,0 +1,53 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+#pragma once
+
+#include <QTextBrowser>
+
+#include "QObjectPtr.h"
+
+QT_BEGIN_NAMESPACE
+class VariableSizedImageObject;
+QT_END_NAMESPACE
+
+/** This subclasses QTextBrowser to provide additional capabilities
+ * to it, like allowing for images to be shown.
+ */
+class ProjectDescriptionPage final : public QTextBrowser
+{
+ Q_OBJECT
+
+ public:
+ ProjectDescriptionPage(QWidget* parent = nullptr);
+
+ void setMetaEntry(QString entry);
+
+ public slots:
+ /** Flushes the current processing happening in the page.
+ *
+ * Should be called when changing the page's content entirely, to
+ * prevent old tasks from changing the new content.
+ */
+ void flush();
+
+ private:
+ shared_qobject_ptr<VariableSizedImageObject> m_image_text_object;
+};
diff --git a/archived/projt-launcher/launcher/ui/widgets/ProjectItem.cpp b/archived/projt-launcher/launcher/ui/widgets/ProjectItem.cpp
new file mode 100644
index 0000000000..073d951533
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/widgets/ProjectItem.cpp
@@ -0,0 +1,238 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+#include "ProjectItem.h"
+
+#include <QApplication>
+
+#include <QDebug>
+#include <QIcon>
+#include <QPainter>
+#include "Common.h"
+
+ProjectItemDelegate::ProjectItemDelegate(QWidget* parent) : QStyledItemDelegate(parent)
+{}
+
+void ProjectItemDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const
+{
+ painter->save();
+
+ QStyleOptionViewItem opt(option);
+ initStyleOption(&opt, index);
+
+ auto isInstalled = index.data(UserDataTypes::INSTALLED).toBool();
+ auto isChecked = opt.checkState == Qt::Checked;
+ auto isSelected = option.state & QStyle::State_Selected;
+
+ const QStyle* style = opt.widget == nullptr ? QApplication::style() : opt.widget->style();
+
+ auto rect = opt.rect;
+
+ style->drawPrimitive(QStyle::PE_PanelItemViewItem, &opt, painter, opt.widget);
+
+ if (isSelected && style->objectName() != "windowsvista")
+ painter->setPen(opt.palette.highlightedText().color());
+
+ if (opt.features & QStyleOptionViewItem::HasCheckIndicator)
+ {
+ QStyleOptionViewItem checkboxOpt = makeCheckboxStyleOption(opt, style);
+ style->drawPrimitive(QStyle::PE_IndicatorItemViewItemCheck, &checkboxOpt, painter, opt.widget);
+
+ rect.setX(checkboxOpt.rect.right());
+ }
+
+ if (!isSelected && !isChecked && isInstalled)
+ {
+ painter->setOpacity(0.4); // Fade out the entire item
+ }
+ // The default icon size will be a square (and height is usually the lower value).
+ auto icon_width = rect.height(), icon_height = rect.height();
+ int icon_x_margin = (rect.height() - icon_width) / 2;
+ int icon_y_margin = (rect.height() - icon_height) / 2;
+
+ if (!opt.icon.isNull())
+ { // Icon painting
+ {
+ auto icon_size = opt.decorationSize;
+ icon_width = icon_size.width();
+ icon_height = icon_size.height();
+
+ icon_y_margin = (rect.height() - icon_height) / 2;
+ icon_x_margin = icon_y_margin; // use same margins for consistency
+ }
+
+ // Centralize icon with a margin to separate from the other elements
+ int x = rect.x() + icon_x_margin;
+ int y = rect.y() + icon_y_margin;
+
+ if (opt.features & QStyleOptionViewItem::HasCheckIndicator)
+ rect.translate(icon_x_margin / 2, 0);
+
+ // Prevent 'scaling null pixmap' warnings
+ if (icon_width > 0 && icon_height > 0)
+ opt.icon.paint(painter, x, y, icon_width, icon_height);
+ }
+
+ // Change the rect so that funther painting is easier
+ auto remaining_width = rect.width() - icon_width - 2 * icon_x_margin;
+ rect.setRect(rect.x() + icon_width + 2 * icon_x_margin, rect.y(), remaining_width, rect.height());
+
+ int title_height = 0;
+
+ { // Title painting
+ auto title = index.data(UserDataTypes::TITLE).toString();
+
+ painter->save();
+
+ auto font = opt.font;
+ if (isChecked)
+ {
+ font.setBold(true);
+ }
+ if (isInstalled)
+ {
+ title = tr("%1 [installed]").arg(title);
+ }
+
+ font.setPointSize(font.pointSize() + 2);
+ painter->setFont(font);
+
+ title_height = QFontMetrics(font).height();
+
+ // On the top, aligned to the left after the icon
+ painter->drawText(rect.x(), rect.y() + title_height, title);
+
+ painter->restore();
+ }
+
+ { // Description painting
+ auto description = index.data(UserDataTypes::DESCRIPTION).toString().simplified();
+
+ QTextLayout text_layout(description, opt.font);
+
+ qreal height = 0;
+ auto cut_text = viewItemTextLayout(text_layout, remaining_width, height);
+
+ // Get first line unconditionally
+ description = cut_text.first().second;
+ auto num_lines = 1;
+
+ // Get second line, elided if needed
+ if (cut_text.size() > 1)
+ {
+ // 2.5x so because there should be some margin left from the 2x so things don't get too squishy.
+ if (rect.height() - title_height <= 2.5 * opt.fontMetrics.height())
+ {
+ // If there's not enough space, show only a single line, elided.
+ description = opt.fontMetrics.elidedText(description, opt.textElideMode, cut_text.at(0).first);
+ }
+ else
+ {
+ if (cut_text.size() > 2)
+ {
+ description +=
+ opt.fontMetrics.elidedText(cut_text.at(1).second, opt.textElideMode, cut_text.at(1).first);
+ }
+ else
+ {
+ description += cut_text.at(1).second;
+ }
+ num_lines += 1;
+ }
+ }
+
+ int description_x = rect.x();
+
+ // Have the y-value be set based on the number of lines in the description, to centralize the
+ // description text with the space between the base and the title.
+ int description_y = rect.y() + title_height + (rect.height() - title_height) / 2;
+ if (num_lines == 1)
+ description_y -= opt.fontMetrics.height() / 2;
+ else
+ description_y -= opt.fontMetrics.height();
+
+ // On the bottom, aligned to the left after the icon, and featuring at most two lines of text (with some margin
+ // space to spare)
+ painter->drawText(description_x,
+ description_y,
+ remaining_width,
+ cut_text.size() * opt.fontMetrics.height(),
+ Qt::TextWordWrap,
+ description);
+ }
+
+ painter->restore();
+}
+
+bool ProjectItemDelegate::editorEvent(QEvent* event,
+ QAbstractItemModel* model,
+ const QStyleOptionViewItem& option,
+ const QModelIndex& index)
+{
+ if (!(event->type() == QEvent::MouseButtonRelease || event->type() == QEvent::MouseButtonPress
+ || event->type() == QEvent::MouseButtonDblClick))
+ return false;
+
+ auto mouseEvent = (QMouseEvent*)event;
+
+ if (mouseEvent->button() != Qt::LeftButton)
+ return false;
+
+ QStyleOptionViewItem opt(option);
+ initStyleOption(&opt, index);
+
+ const QStyle* style = opt.widget == nullptr ? QApplication::style() : opt.widget->style();
+
+ const QStyleOptionViewItem checkboxOpt = makeCheckboxStyleOption(opt, style);
+
+ if (!checkboxOpt.rect.contains(mouseEvent->pos().x(), mouseEvent->pos().y()))
+ return false;
+
+ // swallow other events
+ // (prevents item being selected or double click action triggering)
+ if (event->type() != QEvent::MouseButtonRelease)
+ return true;
+
+ emit checkboxClicked(index);
+ return true;
+}
+
+QStyleOptionViewItem ProjectItemDelegate::makeCheckboxStyleOption(const QStyleOptionViewItem& opt,
+ const QStyle* style) const
+{
+ QStyleOptionViewItem checkboxOpt = opt;
+
+ checkboxOpt.state &= ~QStyle::State_HasFocus;
+
+ if (checkboxOpt.checkState == Qt::Checked)
+ checkboxOpt.state |= QStyle::State_On;
+ else
+ checkboxOpt.state |= QStyle::State_Off;
+
+ QRect checkboxRect = style->subElementRect(QStyle::SE_ItemViewItemCheckIndicator, &checkboxOpt, opt.widget);
+ // 5px is the typical top margin for image
+ // we don't want the checkboxes to be all over the place :)
+ checkboxOpt.rect = QRect(opt.rect.x() + 5,
+ opt.rect.y() + (opt.rect.height() / 2 - checkboxRect.height() / 2),
+ checkboxRect.width(),
+ checkboxRect.height());
+
+ return checkboxOpt;
+}
diff --git a/archived/projt-launcher/launcher/ui/widgets/ProjectItem.h b/archived/projt-launcher/launcher/ui/widgets/ProjectItem.h
new file mode 100644
index 0000000000..13ca15dc01
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/widgets/ProjectItem.h
@@ -0,0 +1,57 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+#pragma once
+
+#include <QStyledItemDelegate>
+
+/* Custom data types for our custom list models :) */
+enum UserDataTypes
+{
+ TITLE = 257, // QString
+ DESCRIPTION = 258, // QString
+ INSTALLED = 259 // bool
+};
+
+/** This is an item delegate composed of:
+ * - An Icon on the left
+ * - A title
+ * - A description
+ * */
+class ProjectItemDelegate final : public QStyledItemDelegate
+{
+ Q_OBJECT
+
+ public:
+ ProjectItemDelegate(QWidget* parent);
+
+ void paint(QPainter*, const QStyleOptionViewItem&, const QModelIndex&) const override;
+
+ bool editorEvent(QEvent* event,
+ QAbstractItemModel* model,
+ const QStyleOptionViewItem& option,
+ const QModelIndex& index) override;
+
+ signals:
+ void checkboxClicked(const QModelIndex& index);
+
+ private:
+ QStyleOptionViewItem makeCheckboxStyleOption(const QStyleOptionViewItem& opt, const QStyle* style) const;
+};
diff --git a/archived/projt-launcher/launcher/ui/widgets/QtWebEngineHubView.cpp b/archived/projt-launcher/launcher/ui/widgets/QtWebEngineHubView.cpp
new file mode 100644
index 0000000000..faba902d26
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/widgets/QtWebEngineHubView.cpp
@@ -0,0 +1,186 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+#include "QtWebEngineHubView.h"
+
+#if defined(PROJT_USE_WEBENGINE)
+
+#include <QApplication>
+#include <QDir>
+#include <QStandardPaths>
+#include <QVBoxLayout>
+#include <QWebChannel>
+#include <QWebEngineHistory>
+#include <QWebEnginePage>
+#include <QWebEngineProfile>
+#include <QWebEngineSettings>
+
+#include "BuildConfig.h"
+
+namespace
+{
+ class LauncherHubBridge final : public QObject
+ {
+ Q_OBJECT
+ Q_PROPERTY(QString launcherVersion READ launcherVersion CONSTANT)
+
+ public:
+ explicit LauncherHubBridge(QObject* parent = nullptr) : QObject(parent)
+ {}
+
+ QString launcherVersion() const
+ {
+ return BuildConfig.printableVersionString();
+ }
+ };
+
+ class LauncherHubPage final : public QWebEnginePage
+ {
+ public:
+ LauncherHubPage(QWebEngineProfile* profile, QObject* parent = nullptr) : QWebEnginePage(profile, parent)
+ {}
+
+ protected:
+ bool acceptNavigationRequest(const QUrl& url, NavigationType type, bool isMainFrame) override
+ {
+ Q_UNUSED(url);
+ Q_UNUSED(type);
+ Q_UNUSED(isMainFrame);
+ return true;
+ }
+ };
+
+ QWebEngineProfile* sharedHubProfile()
+ {
+ static QWebEngineProfile* sharedProfile = nullptr;
+ if (!sharedProfile)
+ {
+ sharedProfile = new QWebEngineProfile(QStringLiteral("LauncherHub"), qApp);
+ sharedProfile->setPersistentCookiesPolicy(QWebEngineProfile::AllowPersistentCookies);
+ sharedProfile->setHttpCacheType(QWebEngineProfile::DiskHttpCache);
+ sharedProfile->setHttpCacheMaximumSize(256 * 1024 * 1024);
+ const QString storageRoot =
+ QDir::cleanPath(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/webengine");
+ QDir().mkpath(storageRoot);
+ sharedProfile->setPersistentStoragePath(storageRoot + "/storage");
+ sharedProfile->setCachePath(storageRoot + "/cache");
+ }
+ return sharedProfile;
+ }
+}
+
+QtWebEngineHubView::QtWebEngineHubView(QWidget* parent) : HubViewBase(parent)
+{
+ auto* layout = new QVBoxLayout(this);
+ layout->setContentsMargins(0, 0, 0, 0);
+
+ m_view = new QWebEngineView(this);
+ layout->addWidget(m_view);
+
+ auto* page = new LauncherHubPage(sharedHubProfile(), m_view);
+ m_view->setPage(page);
+ m_view->setAttribute(Qt::WA_OpaquePaintEvent, true);
+ m_view->setStyleSheet(QStringLiteral("background: #121822;"));
+ page->setBackgroundColor(QColor(QStringLiteral("#121822")));
+ m_view->settings()->setAttribute(QWebEngineSettings::JavascriptEnabled, true);
+ m_view->settings()->setAttribute(QWebEngineSettings::LocalContentCanAccessRemoteUrls, false);
+ m_view->settings()->setAttribute(QWebEngineSettings::LocalContentCanAccessFileUrls, false);
+ m_view->settings()->setAttribute(QWebEngineSettings::PluginsEnabled, false);
+ m_view->settings()->setAttribute(QWebEngineSettings::HyperlinkAuditingEnabled, false);
+ m_view->settings()->setAttribute(QWebEngineSettings::ScrollAnimatorEnabled, false);
+ m_view->settings()->setAttribute(QWebEngineSettings::WebGLEnabled, false);
+ m_view->settings()->setAttribute(QWebEngineSettings::Accelerated2dCanvasEnabled, false);
+
+ auto* channel = new QWebChannel(m_view);
+ auto* bridge = new LauncherHubBridge(channel);
+ channel->registerObject(QStringLiteral("launcher"), bridge);
+ page->setWebChannel(channel);
+
+ connect(m_view, &QWebEngineView::titleChanged, this, &QtWebEngineHubView::titleChanged);
+ connect(m_view, &QWebEngineView::urlChanged, this, &QtWebEngineHubView::urlChanged);
+ connect(m_view, &QWebEngineView::loadFinished, this, &QtWebEngineHubView::loadFinished);
+ connect(m_view, &QWebEngineView::urlChanged, this, [this](const QUrl&) { emit navigationStateChanged(); });
+ connect(m_view, &QWebEngineView::loadFinished, this, [this](bool) { emit navigationStateChanged(); });
+}
+
+QtWebEngineHubView::~QtWebEngineHubView() = default;
+
+void QtWebEngineHubView::setUrl(const QUrl& url)
+{
+ if (m_view)
+ {
+ m_view->setUrl(url);
+ }
+}
+
+QUrl QtWebEngineHubView::url() const
+{
+ return m_view ? m_view->url() : QUrl();
+}
+
+bool QtWebEngineHubView::canGoBack() const
+{
+ return m_view && m_view->history() ? m_view->history()->canGoBack() : false;
+}
+
+bool QtWebEngineHubView::canGoForward() const
+{
+ return m_view && m_view->history() ? m_view->history()->canGoForward() : false;
+}
+
+void QtWebEngineHubView::setActive(bool active)
+{
+ if (!m_view || !m_view->page())
+ {
+ return;
+ }
+
+ m_view->page()->setLifecycleState(active ? QWebEnginePage::LifecycleState::Active
+ : QWebEnginePage::LifecycleState::Frozen);
+}
+
+void QtWebEngineHubView::back()
+{
+ if (m_view)
+ {
+ m_view->back();
+ }
+}
+
+void QtWebEngineHubView::forward()
+{
+ if (m_view)
+ {
+ m_view->forward();
+ }
+}
+
+void QtWebEngineHubView::reload()
+{
+ if (m_view)
+ {
+ m_view->reload();
+ }
+}
+
+#include "QtWebEngineHubView.moc"
+
+#endif
diff --git a/archived/projt-launcher/launcher/ui/widgets/QtWebEngineHubView.h b/archived/projt-launcher/launcher/ui/widgets/QtWebEngineHubView.h
new file mode 100644
index 0000000000..bf13d2082b
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/widgets/QtWebEngineHubView.h
@@ -0,0 +1,50 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+#pragma once
+
+#include "ui/widgets/HubViewBase.h"
+
+#if defined(PROJT_USE_WEBENGINE)
+#include <QWebEngineView>
+
+class QtWebEngineHubView : public HubViewBase
+{
+ Q_OBJECT
+
+ public:
+ explicit QtWebEngineHubView(QWidget* parent = nullptr);
+ ~QtWebEngineHubView() override;
+
+ void setUrl(const QUrl& url) override;
+ QUrl url() const override;
+ bool canGoBack() const override;
+ bool canGoForward() const override;
+ void setActive(bool active) override;
+
+ public slots:
+ void back() override;
+ void forward() override;
+ void reload() override;
+
+ private:
+ QWebEngineView* m_view = nullptr;
+};
+#endif
diff --git a/archived/projt-launcher/launcher/ui/widgets/SubTaskProgressBar.cpp b/archived/projt-launcher/launcher/ui/widgets/SubTaskProgressBar.cpp
new file mode 100644
index 0000000000..800a6b6f16
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/widgets/SubTaskProgressBar.cpp
@@ -0,0 +1,79 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * ======================================================================== */
+
+#include "SubTaskProgressBar.h"
+#include "ui_SubTaskProgressBar.h"
+
+unique_qobject_ptr<SubTaskProgressBar> SubTaskProgressBar::create(QWidget* parent)
+{
+ auto progress_bar = new SubTaskProgressBar(parent);
+ return unique_qobject_ptr<SubTaskProgressBar>(progress_bar);
+}
+
+SubTaskProgressBar::SubTaskProgressBar(QWidget* parent) : QWidget(parent), ui(new Ui::SubTaskProgressBar)
+{
+ ui->setupUi(this);
+}
+SubTaskProgressBar::~SubTaskProgressBar()
+{
+ delete ui;
+}
+
+void SubTaskProgressBar::setRange(int min, int max)
+{
+ ui->progressBar->setRange(min, max);
+}
+
+void SubTaskProgressBar::setValue(int value)
+{
+ ui->progressBar->setValue(value);
+}
+
+void SubTaskProgressBar::setStatus(QString status)
+{
+ ui->statusLabel->setText(status);
+}
+
+void SubTaskProgressBar::setDetails(QString details)
+{
+ ui->statusDetailsLabel->setText(details);
+}
diff --git a/archived/projt-launcher/launcher/ui/widgets/SubTaskProgressBar.h b/archived/projt-launcher/launcher/ui/widgets/SubTaskProgressBar.h
new file mode 100644
index 0000000000..5cad9c0b66
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/widgets/SubTaskProgressBar.h
@@ -0,0 +1,69 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * ======================================================================== */
+#pragma once
+
+#include <QWidget>
+#include "QObjectPtr.h"
+
+namespace Ui
+{
+ class SubTaskProgressBar;
+}
+
+class SubTaskProgressBar : public QWidget
+{
+ Q_OBJECT
+
+ public:
+ static unique_qobject_ptr<SubTaskProgressBar> create(QWidget* parent = nullptr);
+
+ SubTaskProgressBar(QWidget* parent = nullptr);
+ ~SubTaskProgressBar();
+
+ void setRange(int min, int max);
+ void setValue(int value);
+ void setStatus(QString status);
+ void setDetails(QString details);
+
+ private:
+ Ui::SubTaskProgressBar* ui;
+};
diff --git a/archived/projt-launcher/launcher/ui/widgets/SubTaskProgressBar.ui b/archived/projt-launcher/launcher/ui/widgets/SubTaskProgressBar.ui
new file mode 100644
index 0000000000..aabb68329a
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/widgets/SubTaskProgressBar.ui
@@ -0,0 +1,100 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>SubTaskProgressBar</class>
+ <widget class="QWidget" name="SubTaskProgressBar">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>312</width>
+ <height>86</height>
+ </rect>
+ </property>
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="windowTitle">
+ <string>Form</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout" stretch="0,0">
+ <property name="spacing">
+ <number>0</number>
+ </property>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout" stretch="1,0">
+ <property name="spacing">
+ <number>8</number>
+ </property>
+ <item>
+ <widget class="QLabel" name="statusLabel">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="font">
+ <font>
+ <pointsize>8</pointsize>
+ </font>
+ </property>
+ <property name="text">
+ <string>Sub Task Status...</string>
+ </property>
+ <property name="wordWrap">
+ <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="statusDetailsLabel">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="font">
+ <font>
+ <pointsize>8</pointsize>
+ </font>
+ </property>
+ <property name="text">
+ <string>Status Details</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ <property name="textInteractionFlags">
+ <set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QProgressBar" name="progressBar">
+ <property name="font">
+ <font>
+ <pointsize>8</pointsize>
+ </font>
+ </property>
+ <property name="value">
+ <number>24</number>
+ </property>
+ <property name="textVisible">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/archived/projt-launcher/launcher/ui/widgets/VariableSizedImageObject.cpp b/archived/projt-launcher/launcher/ui/widgets/VariableSizedImageObject.cpp
new file mode 100644
index 0000000000..ed67e95965
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/widgets/VariableSizedImageObject.cpp
@@ -0,0 +1,230 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * ======================================================================== */
+
+#include "VariableSizedImageObject.h"
+
+#include <QAbstractTextDocumentLayout>
+#include <QDebug>
+#include <QPainter>
+#include <QTextObject>
+#include <memory>
+
+#include "Application.h"
+
+#include "net/ApiDownload.h"
+#include "net/NetJob.h"
+
+enum FormatProperties
+{
+ ImageData = QTextFormat::UserProperty + 1
+};
+
+QSizeF VariableSizedImageObject::intrinsicSize(QTextDocument* doc, int posInDocument, const QTextFormat& format)
+{
+ Q_UNUSED(posInDocument);
+
+ auto image = qvariant_cast<QImage>(format.property(ImageData));
+ auto size = image.size();
+ if (size.isEmpty()) // can't resize an empty image
+ return { size };
+
+ // calculate the new image size based on the properties
+ int width = 0;
+ int height = 0;
+ auto widthVar = format.property(QTextFormat::ImageWidth);
+ if (widthVar.isValid())
+ {
+ width = widthVar.toInt();
+ }
+ auto heigthVar = format.property(QTextFormat::ImageHeight);
+ if (heigthVar.isValid())
+ {
+ height = heigthVar.toInt();
+ }
+ if (width != 0 && height != 0)
+ {
+ size.setWidth(width);
+ size.setHeight(height);
+ }
+ else if (width != 0)
+ {
+ size.setHeight((width * size.height()) / size.width());
+ size.setWidth(width);
+ }
+ else if (height != 0)
+ {
+ size.setWidth((height * size.width()) / size.height());
+ size.setHeight(height);
+ }
+
+ // Get the width of the text content to make the image similar sized.
+ // doc->textWidth() includes the margin, so we need to remove it.
+ auto doc_width = doc->textWidth() - 2 * doc->documentMargin();
+
+ if (size.width() > doc_width)
+ size *= doc_width / (double)size.width();
+
+ return { size };
+}
+
+void VariableSizedImageObject::drawObject(QPainter* painter,
+ const QRectF& rect,
+ QTextDocument* doc,
+ int posInDocument,
+ const QTextFormat& format)
+{
+ if (!format.hasProperty(ImageData))
+ {
+ QUrl image_url{ qvariant_cast<QString>(format.property(QTextFormat::ImageName)) };
+ if (m_fetching_images.contains(image_url) || image_url.isEmpty())
+ return;
+
+ auto meta = std::make_shared<ImageMetadata>();
+ meta->posInDocument = posInDocument;
+ meta->url = image_url;
+
+ auto widthVar = format.property(QTextFormat::ImageWidth);
+ if (widthVar.isValid())
+ {
+ meta->width = widthVar.toInt();
+ }
+ auto heigthVar = format.property(QTextFormat::ImageHeight);
+ if (heigthVar.isValid())
+ {
+ meta->height = heigthVar.toInt();
+ }
+
+ loadImage(doc, meta);
+ return;
+ }
+
+ auto image = qvariant_cast<QImage>(format.property(ImageData));
+
+ painter->setRenderHint(QPainter::RenderHint::SmoothPixmapTransform);
+ painter->drawImage(rect, image);
+}
+
+void VariableSizedImageObject::flush()
+{
+ m_fetching_images.clear();
+}
+
+void VariableSizedImageObject::parseImage(QTextDocument* doc, std::shared_ptr<ImageMetadata> meta)
+{
+ QTextCursor cursor(doc);
+ cursor.setPosition(meta->posInDocument);
+ cursor.setKeepPositionOnInsert(true);
+
+ auto image_char_format = cursor.charFormat();
+
+ image_char_format.setObjectType(QTextFormat::ImageObject);
+ image_char_format.setProperty(ImageData, meta->image);
+ image_char_format.setProperty(QTextFormat::ImageName, meta->url.toDisplayString());
+ image_char_format.setProperty(QTextFormat::ImageWidth, meta->width);
+ image_char_format.setProperty(QTextFormat::ImageHeight, meta->height);
+
+ // Qt doesn't allow us to modify the properties of an existing object in the document.
+ // So we remove the old one and add the new one with the ImageData property set.
+ cursor.deleteChar();
+ cursor.insertText(QString(QChar::ObjectReplacementCharacter), image_char_format);
+}
+
+void VariableSizedImageObject::loadImage(QTextDocument* doc, std::shared_ptr<ImageMetadata> meta)
+{
+ m_fetching_images.insert(meta->url);
+
+ MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry(
+ m_meta_entry,
+ QString("images/%1")
+ .arg(
+ QString(QCryptographicHash::hash(meta->url.toEncoded(), QCryptographicHash::Algorithm::Sha1).toHex())));
+
+ auto job = new NetJob(QString("Load Image: %1").arg(meta->url.fileName()), APPLICATION->network());
+ job->setAskRetry(false);
+ job->addNetAction(Net::ApiDownload::makeCached(meta->url, entry));
+
+ auto full_entry_path = entry->getFullPath();
+ auto source_url = meta->url;
+ auto loadImage = [this, doc, full_entry_path, source_url, meta](const QImage& image)
+ {
+ doc->addResource(QTextDocument::ImageResource, source_url, image);
+
+ meta->image = image;
+ parseImage(doc, meta);
+
+ // This size hack is needed to prevent the content from being laid out in an area smaller
+ // than the total width available (weird).
+ auto size = doc->pageSize();
+ doc->adjustSize();
+ doc->setPageSize(size);
+
+ m_fetching_images.remove(source_url);
+ };
+ connect(job,
+ &NetJob::succeeded,
+ this,
+ [this, full_entry_path, source_url, loadImage]
+ {
+ qDebug() << "Loaded resource at:" << full_entry_path;
+ // If we flushed, don't proceed.
+ if (!m_fetching_images.contains(source_url))
+ return;
+
+ QImage image(full_entry_path);
+ loadImage(image);
+ });
+ connect(job,
+ &NetJob::failed,
+ this,
+ [this, full_entry_path, source_url, loadImage](QString reason)
+ {
+ qWarning() << "Failed resource at:" << full_entry_path << " because:" << reason;
+ // If we flushed, don't proceed.
+ if (!m_fetching_images.contains(source_url))
+ return;
+
+ loadImage(QImage());
+ });
+ connect(job, &NetJob::finished, job, &NetJob::deleteLater);
+
+ job->start();
+}
diff --git a/archived/projt-launcher/launcher/ui/widgets/VariableSizedImageObject.h b/archived/projt-launcher/launcher/ui/widgets/VariableSizedImageObject.h
new file mode 100644
index 0000000000..8b38e74b8e
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/widgets/VariableSizedImageObject.h
@@ -0,0 +1,107 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * ======================================================================== */
+
+#pragma once
+
+#include <QObject>
+#include <QString>
+#include <QTextObjectInterface>
+#include <QAbstractTextDocumentLayout>
+#include <QUrl>
+#include <memory>
+
+/** Custom image text object to be used instead of the normal one in ProjectDescriptionPage.
+ *
+ * Why? Because we want to re-scale images dynamically based on the document's size, in order to
+ * not have images being weirdly cropped out in different resolutions.
+ */
+class VariableSizedImageObject final : public QObject, public QTextObjectInterface
+{
+ Q_OBJECT
+ Q_INTERFACES(QTextObjectInterface)
+
+ struct ImageMetadata
+ {
+ int posInDocument;
+ QUrl url;
+ QImage image;
+ int width;
+ int height;
+ };
+
+ public:
+ QSizeF intrinsicSize(QTextDocument* doc, int posInDocument, const QTextFormat& format) override;
+ void drawObject(QPainter* painter,
+ const QRectF& rect,
+ QTextDocument* doc,
+ int posInDocument,
+ const QTextFormat& format) override;
+
+ void setMetaEntry(QString meta_entry)
+ {
+ m_meta_entry = meta_entry;
+ }
+
+ public slots:
+ /** Stops all currently loading images from modifying the document.
+ *
+ * This does not stop the ongoing network tasks, it only prevents their result
+ * from impacting the document any further.
+ */
+ void flush();
+
+ private:
+ /** Adds the image to the document, in the given position.
+ */
+ void parseImage(QTextDocument* doc, std::shared_ptr<ImageMetadata> meta);
+
+ /** Loads an image from an external source, and adds it to the document.
+ *
+ * This uses m_meta_entry to cache the image.
+ */
+ void loadImage(QTextDocument* doc, std::shared_ptr<ImageMetadata> meta);
+
+ private:
+ QString m_meta_entry;
+
+ QSet<QUrl> m_fetching_images;
+};
diff --git a/archived/projt-launcher/launcher/ui/widgets/VersionListView.cpp b/archived/projt-launcher/launcher/ui/widgets/VersionListView.cpp
new file mode 100644
index 0000000000..b2154774bf
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/widgets/VersionListView.cpp
@@ -0,0 +1,200 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
+ * Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * 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 "VersionListView.h"
+#include <QApplication>
+#include <QDrag>
+#include <QHeaderView>
+#include <QMouseEvent>
+#include <QPainter>
+
+VersionListView::VersionListView(QWidget* parent) : QTreeView(parent)
+{
+ m_emptyString = tr("No versions are currently available.");
+ setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
+}
+
+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
+{
+ switch (m_emptyMode)
+ {
+ default:
+ 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::WindowText);
+ 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/archived/projt-launcher/launcher/ui/widgets/VersionListView.h b/archived/projt-launcher/launcher/ui/widgets/VersionListView.h
new file mode 100644
index 0000000000..510c413d01
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/widgets/VersionListView.h
@@ -0,0 +1,77 @@
+// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ * 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/archived/projt-launcher/launcher/ui/widgets/VersionSelectWidget.cpp b/archived/projt-launcher/launcher/ui/widgets/VersionSelectWidget.cpp
new file mode 100644
index 0000000000..86c0f74ba0
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/widgets/VersionSelectWidget.cpp
@@ -0,0 +1,291 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+#include "VersionSelectWidget.h"
+
+#include <QApplication>
+#include <QEvent>
+#include <QHeaderView>
+#include <QKeyEvent>
+#include <QProgressBar>
+#include <QVBoxLayout>
+
+#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);
+
+ search = new QLineEdit(this);
+ search->setPlaceholderText(tr("Search"));
+ search->setClearButtonEnabled(true);
+ verticalLayout->addWidget(search);
+ connect(search,
+ &QLineEdit::textEdited,
+ [this](const QString& value)
+ {
+ m_proxyModel->setSearch(value);
+ if (!value.isEmpty() || !listView->selectionModel()->hasSelection())
+ {
+ const QModelIndex first = listView->model()->index(0, 0);
+ listView->selectionModel()->setCurrentIndex(first,
+ QItemSelectionModel::ClearAndSelect
+ | QItemSelectionModel::Rows);
+ listView->scrollToTop();
+ }
+ else
+ listView->scrollTo(listView->selectionModel()->currentIndex(), QAbstractItemView::PositionAtCenter);
+ });
+ search->installEventFilter(this);
+
+ 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);
+}
+
+void VersionSelectWidget::setEmptyMode(VersionListView::EmptyMode mode)
+{
+ listView->setEmptyMode(mode);
+}
+
+VersionSelectWidget::~VersionSelectWidget()
+{}
+
+void VersionSelectWidget::setResizeOn(int column)
+{
+ listView->header()->setSectionResizeMode(resizeOnColumn, QHeaderView::ResizeToContents);
+ resizeOnColumn = column;
+ listView->header()->setSectionResizeMode(resizeOnColumn, QHeaderView::Stretch);
+}
+
+bool VersionSelectWidget::eventFilter(QObject* watched, QEvent* event)
+{
+ if (watched == search && event->type() == QEvent::KeyPress)
+ {
+ const QKeyEvent* keyEvent = (QKeyEvent*)event;
+ const bool up = keyEvent->key() == Qt::Key_Up;
+ const bool down = keyEvent->key() == Qt::Key_Down;
+ if (up || down)
+ {
+ const QModelIndex index = listView->model()->index(listView->currentIndex().row() + (up ? -1 : 1), 0);
+ if (index.row() >= 0 && index.row() < listView->model()->rowCount())
+ {
+ listView->selectionModel()->setCurrentIndex(index,
+ QItemSelectionModel::ClearAndSelect
+ | QItemSelectionModel::Rows);
+ return true;
+ }
+ }
+ }
+
+ return QObject::eventFilter(watched, event);
+}
+
+void VersionSelectWidget::initialize(BaseVersionList* vlist, bool forceLoad)
+{
+ m_vlist = vlist;
+ m_proxyModel->setSourceModel(vlist);
+ listView->header()->setSectionResizeMode(QHeaderView::ResizeToContents);
+ listView->header()->setSectionResizeMode(resizeOnColumn, QHeaderView::Stretch);
+
+ if (!m_vlist->isLoaded() || forceLoad)
+ {
+ loadList();
+ }
+ else
+ {
+ if (m_proxyModel->rowCount() == 0)
+ {
+ listView->setEmptyMode(VersionListView::String);
+ }
+ preselect();
+ }
+}
+
+void VersionSelectWidget::closeEvent(QCloseEvent* event)
+{
+ QWidget::closeEvent(event);
+}
+
+void VersionSelectWidget::loadList()
+{
+ m_load_task = m_vlist->getLoadTask();
+ connect(m_load_task.get(), &Task::succeeded, this, &VersionSelectWidget::onTaskSucceeded);
+ connect(m_load_task.get(), &Task::failed, this, &VersionSelectWidget::onTaskFailed);
+ connect(m_load_task.get(), &Task::progress, this, &VersionSelectWidget::changeProgress);
+ if (!m_load_task->isRunning())
+ {
+ m_load_task->start();
+ }
+ sneakyProgressBar->setHidden(false);
+}
+
+void VersionSelectWidget::onTaskSucceeded()
+{
+ if (m_proxyModel->rowCount() == 0)
+ {
+ listView->setEmptyMode(VersionListView::String);
+ }
+ sneakyProgressBar->setHidden(true);
+ preselect();
+ m_load_task.reset();
+}
+
+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<BaseVersion::Ptr>());
+}
+
+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::selectSearch()
+{
+ search->setFocus();
+}
+
+VersionListView* VersionSelectWidget::view()
+{
+ return listView;
+}
+
+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;
+}
+
+BaseVersion::Ptr VersionSelectWidget::selectedVersion() const
+{
+ auto currentIndex = listView->selectionModel()->currentIndex();
+ auto variant = m_proxyModel->data(currentIndex, BaseVersionList::VersionPointerRole);
+ return variant.value<BaseVersion::Ptr>();
+}
+
+void VersionSelectWidget::setFuzzyFilter(BaseVersionList::ModelRoles role, QString filter)
+{
+ m_proxyModel->setFilter(role, Filters::contains(filter));
+}
+
+void VersionSelectWidget::setExactFilter(BaseVersionList::ModelRoles role, QString filter)
+{
+ m_proxyModel->setFilter(role, Filters::equals(filter));
+}
+
+void VersionSelectWidget::setExactIfPresentFilter(BaseVersionList::ModelRoles role, QString filter)
+{
+ m_proxyModel->setFilter(role, Filters::equalsOrEmpty(filter));
+}
+
+void VersionSelectWidget::setFilter(BaseVersionList::ModelRoles role, Filter filter)
+{
+ m_proxyModel->setFilter(role, filter);
+}
diff --git a/archived/projt-launcher/launcher/ui/widgets/VersionSelectWidget.h b/archived/projt-launcher/launcher/ui/widgets/VersionSelectWidget.h
new file mode 100644
index 0000000000..f5b01e8377
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/widgets/VersionSelectWidget.h
@@ -0,0 +1,133 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * 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 <QLineEdit>
+#include <QSortFilterProxyModel>
+#include <QWidget>
+#include "BaseVersionList.h"
+#include "Filter.h"
+#include "VersionListView.h"
+
+class VersionProxyModel;
+class VersionListView;
+class QVBoxLayout;
+class QProgressBar;
+
+class VersionSelectWidget : public QWidget
+{
+ Q_OBJECT
+ public:
+ explicit VersionSelectWidget(QWidget* parent);
+ ~VersionSelectWidget();
+
+ //! loads the list if needed.
+ void initialize(BaseVersionList* vlist, bool forceLoad = false);
+
+ //! Starts a task that loads the list.
+ void loadList();
+
+ bool hasVersions() const;
+ BaseVersion::Ptr selectedVersion() const;
+ void selectRecommended();
+ void selectCurrent();
+ void selectSearch();
+ VersionListView* view();
+
+ void setCurrentVersion(const QString& version);
+ void setFuzzyFilter(BaseVersionList::ModelRoles role, QString filter);
+ void setExactFilter(BaseVersionList::ModelRoles role, QString filter);
+ void setExactIfPresentFilter(BaseVersionList::ModelRoles role, QString filter);
+ void setFilter(BaseVersionList::ModelRoles role, Filter filter);
+ void setEmptyString(QString emptyString);
+ void setEmptyErrorString(QString emptyErrorString);
+ void setEmptyMode(VersionListView::EmptyMode mode);
+ void setResizeOn(int column);
+
+ bool eventFilter(QObject* watched, QEvent* event) override;
+
+ signals:
+ void selectedVersionChanged(BaseVersion::Ptr version);
+
+ protected:
+ virtual void closeEvent(QCloseEvent*) override;
+
+ 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::Ptr m_load_task;
+ bool preselectedAlready = false;
+
+ QVBoxLayout* verticalLayout = nullptr;
+ VersionListView* listView = nullptr;
+ QLineEdit* search;
+ QProgressBar* sneakyProgressBar = nullptr;
+};
diff --git a/archived/projt-launcher/launcher/ui/widgets/WebView2Widget.cpp b/archived/projt-launcher/launcher/ui/widgets/WebView2Widget.cpp
new file mode 100644
index 0000000000..65b2b72589
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/widgets/WebView2Widget.cpp
@@ -0,0 +1,315 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+#include "WebView2Widget.h"
+
+#include <QDir>
+#include <QResizeEvent>
+#include <QShowEvent>
+#include <QStandardPaths>
+#include <QTimer>
+
+#if defined(PROJT_USE_WEBVIEW2) && defined(_WIN32)
+#include <windows.h>
+#include <wrl.h>
+#include <WebView2.h>
+#if __has_include(<WebView2Loader.h>)
+#include <WebView2Loader.h>
+#define PROJT_HAVE_WEBVIEW2_LOADER 1
+#else
+#define PROJT_HAVE_WEBVIEW2_LOADER 0
+extern "C" HRESULT STDAPICALLTYPE CreateCoreWebView2EnvironmentWithOptions(
+ PCWSTR browserExecutableFolder,
+ PCWSTR userDataFolder,
+ ICoreWebView2EnvironmentOptions* environmentOptions,
+ ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler* environmentCreatedHandler);
+#endif
+
+using Microsoft::WRL::Callback;
+using Microsoft::WRL::ComPtr;
+
+struct WebView2Widget::Impl
+{
+ ComPtr<ICoreWebView2Environment> env;
+ ComPtr<ICoreWebView2Controller> controller;
+ ComPtr<ICoreWebView2> webview;
+};
+#endif
+
+WebView2Widget::WebView2Widget(QWidget* parent) : HubViewBase(parent)
+{
+ setAttribute(Qt::WA_NativeWindow);
+ setAttribute(Qt::WA_NoSystemBackground);
+
+#if defined(PROJT_USE_WEBVIEW2) && defined(_WIN32)
+ m_impl = new Impl();
+ QTimer::singleShot(0, this, &WebView2Widget::initialize);
+#endif
+}
+
+WebView2Widget::~WebView2Widget()
+{
+#if defined(PROJT_USE_WEBVIEW2) && defined(_WIN32)
+ delete m_impl;
+#endif
+}
+
+void WebView2Widget::setUrl(const QUrl& url)
+{
+ m_url = url;
+#if defined(PROJT_USE_WEBVIEW2) && defined(_WIN32)
+ if (m_impl && m_impl->webview)
+ {
+ const auto wide = url.toString().toStdWString();
+ m_impl->webview->Navigate(wide.c_str());
+ return;
+ }
+#endif
+}
+
+QUrl WebView2Widget::url() const
+{
+ return m_url;
+}
+
+bool WebView2Widget::canGoBack() const
+{
+ return m_canGoBack;
+}
+
+bool WebView2Widget::canGoForward() const
+{
+ return m_canGoForward;
+}
+
+void WebView2Widget::back()
+{
+#if defined(PROJT_USE_WEBVIEW2) && defined(_WIN32)
+ if (m_impl && m_impl->webview && m_canGoBack)
+ {
+ m_impl->webview->GoBack();
+ }
+#endif
+}
+
+void WebView2Widget::forward()
+{
+#if defined(PROJT_USE_WEBVIEW2) && defined(_WIN32)
+ if (m_impl && m_impl->webview && m_canGoForward)
+ {
+ m_impl->webview->GoForward();
+ }
+#endif
+}
+
+void WebView2Widget::reload()
+{
+#if defined(PROJT_USE_WEBVIEW2) && defined(_WIN32)
+ if (m_impl && m_impl->webview)
+ {
+ m_impl->webview->Reload();
+ }
+#endif
+}
+
+void WebView2Widget::resizeEvent(QResizeEvent* event)
+{
+ QWidget::resizeEvent(event);
+ updateBounds();
+}
+
+void WebView2Widget::showEvent(QShowEvent* event)
+{
+ QWidget::showEvent(event);
+ updateBounds();
+#if defined(PROJT_USE_WEBVIEW2) && defined(_WIN32)
+ if (m_impl && m_impl->webview && m_url.isValid())
+ {
+ const auto wide = m_url.toString().toStdWString();
+ m_impl->webview->Navigate(wide.c_str());
+ }
+#endif
+}
+
+void WebView2Widget::initialize()
+{
+#if defined(PROJT_USE_WEBVIEW2) && defined(_WIN32)
+ if (m_initialized || !m_impl)
+ return;
+
+ m_initialized = true;
+ const QString dataPath =
+ QDir::cleanPath(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/webview2");
+ QDir().mkpath(dataPath);
+
+ const auto dataPathWide = dataPath.toStdWString();
+ const auto hwnd = reinterpret_cast<HWND>(winId());
+ HRESULT comInitResult = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
+ Q_UNUSED(comInitResult);
+
+ CreateCoreWebView2EnvironmentWithOptions(
+ nullptr,
+ dataPathWide.c_str(),
+ nullptr,
+ Callback<ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler>(
+ [this, hwnd](HRESULT result, ICoreWebView2Environment* env) -> HRESULT
+ {
+ if (FAILED(result) || !env)
+ {
+ emit loadFinished(false);
+ return S_OK;
+ }
+
+ m_impl->env = env;
+ env->CreateCoreWebView2Controller(
+ hwnd,
+ Callback<ICoreWebView2CreateCoreWebView2ControllerCompletedHandler>(
+ [this](HRESULT controllerResult, ICoreWebView2Controller* controller) -> HRESULT
+ {
+ if (FAILED(controllerResult) || !controller)
+ {
+ emit loadFinished(false);
+ return S_OK;
+ }
+
+ m_impl->controller = controller;
+ m_impl->controller->get_CoreWebView2(&m_impl->webview);
+
+ if (!m_impl->webview)
+ {
+ emit loadFinished(false);
+ return S_OK;
+ }
+
+ updateBounds();
+
+ EventRegistrationToken token{};
+ m_impl->webview->add_DocumentTitleChanged(
+ Callback<ICoreWebView2DocumentTitleChangedEventHandler>(
+ [this](ICoreWebView2*, IUnknown*) -> HRESULT
+ {
+ LPWSTR title = nullptr;
+ if (SUCCEEDED(m_impl->webview->get_DocumentTitle(&title)) && title)
+ {
+ emit titleChanged(QString::fromWCharArray(title));
+ CoTaskMemFree(title);
+ }
+ return S_OK;
+ })
+ .Get(),
+ &token);
+
+ m_impl->webview->add_SourceChanged(
+ Callback<ICoreWebView2SourceChangedEventHandler>(
+ [this](ICoreWebView2*, ICoreWebView2SourceChangedEventArgs*) -> HRESULT
+ {
+ LPWSTR uri = nullptr;
+ if (SUCCEEDED(m_impl->webview->get_Source(&uri)) && uri)
+ {
+ m_url = QUrl(QString::fromWCharArray(uri));
+ emit urlChanged(m_url);
+ CoTaskMemFree(uri);
+ }
+ updateNavigationState();
+ return S_OK;
+ })
+ .Get(),
+ &token);
+
+ m_impl->webview->add_NavigationCompleted(
+ Callback<ICoreWebView2NavigationCompletedEventHandler>(
+ [this](ICoreWebView2*, ICoreWebView2NavigationCompletedEventArgs* args) -> HRESULT
+ {
+ BOOL success = FALSE;
+ if (args)
+ {
+ args->get_IsSuccess(&success);
+ }
+ updateNavigationState();
+ emit loadFinished(success == TRUE);
+ return S_OK;
+ })
+ .Get(),
+ &token);
+
+ m_impl->webview->add_HistoryChanged(Callback<ICoreWebView2HistoryChangedEventHandler>(
+ [this](ICoreWebView2*, IUnknown*) -> HRESULT
+ {
+ updateNavigationState();
+ return S_OK;
+ })
+ .Get(),
+ &token);
+
+ if (m_url.isValid())
+ {
+ const auto wideUrl = m_url.toString().toStdWString();
+ m_impl->webview->Navigate(wideUrl.c_str());
+ }
+
+ return S_OK;
+ })
+ .Get());
+
+ return S_OK;
+ })
+ .Get());
+#else
+ emit loadFinished(false);
+#endif
+}
+
+void WebView2Widget::updateBounds()
+{
+#if defined(PROJT_USE_WEBVIEW2) && defined(_WIN32)
+ if (!m_impl || !m_impl->controller)
+ return;
+
+ RECT bounds{};
+ bounds.left = 0;
+ bounds.top = 0;
+ bounds.right = width();
+ bounds.bottom = height();
+ m_impl->controller->put_Bounds(bounds);
+#endif
+}
+
+void WebView2Widget::updateNavigationState()
+{
+#if defined(PROJT_USE_WEBVIEW2) && defined(_WIN32)
+ if (!m_impl || !m_impl->webview)
+ return;
+
+ BOOL canBack = FALSE;
+ BOOL canForward = FALSE;
+ m_impl->webview->get_CanGoBack(&canBack);
+ m_impl->webview->get_CanGoForward(&canForward);
+
+ const bool newCanBack = (canBack == TRUE);
+ const bool newCanForward = (canForward == TRUE);
+ if (newCanBack != m_canGoBack || newCanForward != m_canGoForward)
+ {
+ m_canGoBack = newCanBack;
+ m_canGoForward = newCanForward;
+ emit navigationStateChanged();
+ }
+#endif
+}
diff --git a/archived/projt-launcher/launcher/ui/widgets/WebView2Widget.h b/archived/projt-launcher/launcher/ui/widgets/WebView2Widget.h
new file mode 100644
index 0000000000..a7948ea945
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/widgets/WebView2Widget.h
@@ -0,0 +1,68 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+#pragma once
+
+#include "ui/widgets/HubViewBase.h"
+
+class WebView2Widget : public HubViewBase
+{
+ Q_OBJECT
+
+ public:
+ explicit WebView2Widget(QWidget* parent = nullptr);
+ ~WebView2Widget() override;
+
+ void setUrl(const QUrl& url);
+ QUrl url() const;
+
+ bool canGoBack() const;
+ bool canGoForward() const;
+
+ public slots:
+ void back();
+ void forward();
+ void reload();
+
+ signals:
+ void titleChanged(const QString& title);
+ void urlChanged(const QUrl& url);
+ void loadFinished(bool ok);
+ void navigationStateChanged();
+
+ protected:
+ void resizeEvent(QResizeEvent* event) override;
+ void showEvent(QShowEvent* event) override;
+
+ private:
+ void initialize();
+ void updateBounds();
+ void updateNavigationState();
+
+ QUrl m_url;
+ bool m_initialized = false;
+ bool m_canGoBack = false;
+ bool m_canGoForward = false;
+
+#if defined(PROJT_USE_WEBVIEW2) && defined(_WIN32)
+ struct Impl;
+ Impl* m_impl = nullptr;
+#endif
+};
diff --git a/archived/projt-launcher/launcher/ui/widgets/WideBar.cpp b/archived/projt-launcher/launcher/ui/widgets/WideBar.cpp
new file mode 100644
index 0000000000..19fc28655e
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/widgets/WideBar.cpp
@@ -0,0 +1,362 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+#include "WideBar.h"
+
+#include <QContextMenuEvent>
+#include <QCryptographicHash>
+#include <QToolButton>
+
+class ActionButton : public QToolButton
+{
+ Q_OBJECT
+ public:
+ ActionButton(QAction* action, QWidget* parent = nullptr, bool use_default_action = false)
+ : QToolButton(parent),
+ m_action(action),
+ m_use_default_action(use_default_action)
+ {
+ setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
+ setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
+ // workaround for breeze and breeze forks
+ setProperty("_kde_toolButton_alignment", Qt::AlignLeft);
+
+ if (m_use_default_action)
+ {
+ setDefaultAction(action);
+ }
+ else
+ {
+ connect(this, &ActionButton::clicked, action, &QAction::trigger);
+ }
+ connect(action, &QAction::changed, this, &ActionButton::actionChanged);
+
+ actionChanged();
+ };
+ public slots:
+ void actionChanged()
+ {
+ setEnabled(m_action->isEnabled());
+ // better pop up mode
+ if (m_action->menu())
+ {
+ setPopupMode(QToolButton::MenuButtonPopup);
+ }
+ if (!m_use_default_action)
+ {
+ 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;
+ bool m_use_default_action;
+};
+
+WideBar::WideBar(const QString& title, QWidget* parent) : QToolBar(title, parent)
+{
+ setFloatable(false);
+ setMovable(false);
+
+ setContextMenuPolicy(Qt::ContextMenuPolicy::CustomContextMenu);
+ connect(this, &QToolBar::customContextMenuRequested, this, &WideBar::showVisibilityMenu);
+}
+
+WideBar::WideBar(QWidget* parent) : QToolBar(parent)
+{
+ setFloatable(false);
+ setMovable(false);
+
+ setContextMenuPolicy(Qt::ContextMenuPolicy::CustomContextMenu);
+ connect(this, &QToolBar::customContextMenuRequested, this, &WideBar::showVisibilityMenu);
+}
+
+void WideBar::addAction(QAction* action)
+{
+ BarEntry entry;
+ entry.bar_action = addWidget(new ActionButton(action, this, m_use_default_action));
+ entry.menu_action = action;
+ entry.type = BarEntry::Type::Action;
+
+ m_entries.push_back(entry);
+
+ m_menu_state = MenuState::Dirty;
+}
+
+void WideBar::addSeparator()
+{
+ BarEntry entry;
+ entry.bar_action = QToolBar::addSeparator();
+ entry.type = BarEntry::Type::Separator;
+
+ m_entries.push_back(entry);
+}
+
+auto WideBar::getMatching(QAction* act) -> QList<BarEntry>::iterator
+{
+ auto iter = std::find_if(m_entries.begin(),
+ m_entries.end(),
+ [act](BarEntry const& entry) { return entry.menu_action == act; });
+
+ return iter;
+}
+
+void WideBar::insertActionBefore(QAction* before, QAction* action)
+{
+ auto iter = getMatching(before);
+ if (iter == m_entries.end())
+ return;
+
+ BarEntry entry;
+ entry.bar_action = insertWidget(iter->bar_action, new ActionButton(action, this, m_use_default_action));
+ entry.menu_action = action;
+ entry.type = BarEntry::Type::Action;
+
+ m_entries.insert(iter, entry);
+
+ m_menu_state = MenuState::Dirty;
+}
+
+void WideBar::insertActionAfter(QAction* after, QAction* action)
+{
+ auto iter = getMatching(after);
+ if (iter == m_entries.end())
+ return;
+
+ iter++;
+ // the action to insert after is present
+ // however, the element after it isn't valid
+ if (iter == m_entries.end())
+ {
+ // append the action instead of inserting it
+ addAction(action);
+ return;
+ }
+
+ BarEntry entry;
+ entry.bar_action = insertWidget(iter->bar_action, new ActionButton(action, this, m_use_default_action));
+ entry.menu_action = action;
+ entry.type = BarEntry::Type::Action;
+
+ m_entries.insert(iter, entry);
+
+ m_menu_state = MenuState::Dirty;
+}
+
+void WideBar::insertWidgetBefore(QAction* before, QWidget* widget)
+{
+ auto iter = getMatching(before);
+ if (iter == m_entries.end())
+ return;
+
+ insertWidget(iter->bar_action, widget);
+}
+
+void WideBar::insertSpacer(QAction* action)
+{
+ auto iter = getMatching(action);
+ if (iter == m_entries.end())
+ return;
+
+ auto* spacer = new QWidget();
+ spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
+
+ BarEntry entry;
+ entry.bar_action = insertWidget(iter->bar_action, spacer);
+ entry.type = BarEntry::Type::Spacer;
+ m_entries.insert(iter, entry);
+}
+
+void WideBar::insertSeparator(QAction* before)
+{
+ auto iter = getMatching(before);
+ if (iter == m_entries.end())
+ return;
+
+ BarEntry entry;
+ entry.bar_action = QToolBar::insertSeparator(iter->bar_action);
+ entry.type = BarEntry::Type::Separator;
+
+ m_entries.insert(iter, entry);
+}
+
+QMenu* WideBar::createContextMenu(QWidget* parent, const QString& title)
+{
+ auto* contextMenu = new QMenu(title, parent);
+ for (auto& item : m_entries)
+ {
+ switch (item.type)
+ {
+ default:
+ case BarEntry::Type::None: break;
+ case BarEntry::Type::Separator:
+ case BarEntry::Type::Spacer: contextMenu->addSeparator(); break;
+ case BarEntry::Type::Action: contextMenu->addAction(item.menu_action); break;
+ }
+ }
+ return contextMenu;
+}
+
+static void copyAction(QAction* from, QAction* to)
+{
+ Q_ASSERT(from);
+ Q_ASSERT(to);
+
+ to->setText(from->text());
+ to->setIcon(from->icon());
+ to->setToolTip(from->toolTip());
+}
+
+void WideBar::showVisibilityMenu(QPoint const& position)
+{
+ if (!m_bar_menu)
+ {
+ m_bar_menu = std::make_unique<QMenu>(this);
+ m_bar_menu->setTearOffEnabled(true);
+ }
+
+ if (m_menu_state == MenuState::Dirty)
+ {
+ for (auto* old_action : m_bar_menu->actions())
+ old_action->deleteLater();
+
+ m_bar_menu->clear();
+
+ m_bar_menu->addActions(m_context_menu_actions);
+
+ m_bar_menu->addSeparator()->setText(tr("Customize toolbar actions"));
+
+ for (auto& entry : m_entries)
+ {
+ if (entry.type != BarEntry::Type::Action)
+ continue;
+
+ auto act = new QAction();
+ copyAction(entry.menu_action, act);
+
+ act->setCheckable(true);
+ act->setChecked(entry.bar_action->isVisible());
+
+ connect(act,
+ &QAction::toggled,
+ entry.bar_action,
+ [this, &entry](bool toggled)
+ {
+ entry.bar_action->setVisible(toggled);
+
+ // NOTE: This is needed so that disabled actions get reflected on the button when it is made
+ // visible.
+ static_cast<ActionButton*>(widgetForAction(entry.bar_action))->actionChanged();
+ });
+
+ m_bar_menu->addAction(act);
+ }
+
+ m_menu_state = MenuState::Fresh;
+ }
+
+ m_bar_menu->popup(mapToGlobal(position));
+}
+
+void WideBar::addContextMenuAction(QAction* action)
+{
+ m_context_menu_actions.append(action);
+}
+
+QByteArray WideBar::getVisibilityState() const
+{
+ QByteArray state;
+
+ for (auto const& entry : m_entries)
+ {
+ if (entry.type != BarEntry::Type::Action)
+ continue;
+
+ state.append(entry.bar_action->isVisible() ? '1' : '0');
+ }
+
+ state.append(',');
+ state.append(getHash());
+
+ return state;
+}
+
+void WideBar::setVisibilityState(QByteArray&& state)
+{
+ auto split = state.split(',');
+
+ auto bits = split.first();
+ auto hash = split.last();
+
+ // If the actions changed, we better not try to load the old one to avoid unwanted hiding
+ if (!checkHash(hash))
+ return;
+
+ qsizetype i = 0;
+ for (auto& entry : m_entries)
+ {
+ if (entry.type != BarEntry::Type::Action)
+ continue;
+ if (i == bits.size())
+ break;
+
+ entry.bar_action->setVisible(bits.at(i++) == '1');
+
+ // NOTE: This is needed so that disabled actions get reflected on the button when it is made visible.
+ static_cast<ActionButton*>(widgetForAction(entry.bar_action))->actionChanged();
+ }
+}
+
+QByteArray WideBar::getHash() const
+{
+ QCryptographicHash hash(QCryptographicHash::Sha1);
+ for (auto const& entry : m_entries)
+ {
+ if (entry.type != BarEntry::Type::Action)
+ continue;
+ hash.addData(entry.menu_action->text().toLatin1());
+ }
+
+ return hash.result().toBase64();
+}
+
+bool WideBar::checkHash(QByteArray const& old_hash) const
+{
+ return old_hash == getHash();
+}
+
+void WideBar::removeAction(QAction* action)
+{
+ auto iter = getMatching(action);
+ if (iter == m_entries.end())
+ return;
+
+ iter->bar_action->setVisible(false);
+ removeAction(iter->bar_action);
+ m_entries.erase(iter);
+}
+
+#include "WideBar.moc"
diff --git a/archived/projt-launcher/launcher/ui/widgets/WideBar.h b/archived/projt-launcher/launcher/ui/widgets/WideBar.h
new file mode 100644
index 0000000000..de639588a7
--- /dev/null
+++ b/archived/projt-launcher/launcher/ui/widgets/WideBar.h
@@ -0,0 +1,98 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+#pragma once
+
+#include <QAction>
+#include <QMap>
+#include <QMenu>
+#include <QToolBar>
+
+#include <memory>
+
+class WideBar : public QToolBar
+{
+ Q_OBJECT
+ // Why: so we can enable / disable alt shortcuts in toolbuttons
+ // with toolbuttons using setDefaultAction, theres no alt shortcuts
+ Q_PROPERTY(bool useDefaultAction MEMBER m_use_default_action)
+
+ public:
+ explicit WideBar(const QString& title, QWidget* parent = nullptr);
+ explicit WideBar(QWidget* parent = nullptr);
+ ~WideBar() override = default;
+
+ void addAction(QAction* action);
+ void addSeparator();
+
+ void insertSpacer(QAction* action);
+ void insertSeparator(QAction* before);
+ void insertActionBefore(QAction* before, QAction* action);
+ void insertActionAfter(QAction* after, QAction* action);
+ void insertWidgetBefore(QAction* before, QWidget* widget);
+
+ QMenu* createContextMenu(QWidget* parent = nullptr, const QString& title = QString());
+ void showVisibilityMenu(const QPoint&);
+
+ void addContextMenuAction(QAction* action);
+
+ // Ideally we would use a QBitArray for this, but it doesn't support string conversion,
+ // so using it in settings is very messy.
+
+ QByteArray getVisibilityState() const;
+ void setVisibilityState(QByteArray&&);
+
+ void removeAction(QAction* action);
+
+ private:
+ struct BarEntry
+ {
+ enum class Type
+ {
+ None,
+ Action,
+ Separator,
+ Spacer
+ } type = Type::None;
+ QAction* bar_action = nullptr;
+ QAction* menu_action = nullptr;
+ };
+
+ auto getMatching(QAction* act) -> QList<BarEntry>::iterator;
+
+ /** Used to distinguish between versions of the WideBar with different actions */
+ QByteArray getHash() const;
+ bool checkHash(QByteArray const&) const;
+
+ private:
+ QList<BarEntry> m_entries;
+
+ QList<QAction*> m_context_menu_actions;
+
+ bool m_use_default_action = false;
+
+ // Menu to toggle visibility from buttons in the bar
+ std::unique_ptr<QMenu> m_bar_menu = nullptr;
+ enum class MenuState
+ {
+ Fresh,
+ Dirty
+ } m_menu_state = MenuState::Dirty;
+};