diff options
| author | Mehmet Samet Duman <yongdohyun@projecttick.org> | 2026-04-02 18:51:45 +0300 |
|---|---|---|
| committer | Mehmet Samet Duman <yongdohyun@projecttick.org> | 2026-04-02 18:51:45 +0300 |
| commit | d3261e64152397db2dca4d691a990c6bc2a6f4dd (patch) | |
| tree | fac2f7be638651181a72453d714f0f96675c2b8b /archived/projt-launcher/launcher/ui/widgets | |
| parent | 31b9a8949ed0a288143e23bf739f2eb64fdc63be (diff) | |
| download | Project-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')
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>&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>&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 &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>&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&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>&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><html><head/><body><p>Pre-launch command runs before the instance launches and post-exit command runs after it exits.</p><p>Both will be run in the launcher's working folder with extra environment variables:</p><ul><li>$INST_NAME - Name of the instance</li><li>$INST_ID - ID of the instance (its folder name)</li><li>$INST_DIR - absolute path of the instance</li><li>$INST_MC_DIR - absolute path of Minecraft</li><li>$INST_JAVA - Java binary used for launch</li><li>$INST_JAVA_ARGS - command-line parameters used for launch (warning: will not work correctly if arguments contain spaces)</li></ul><p>Wrapper command allows launching using an extra wrapper program (like 'optirun' on Linux)</p></body></html></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 &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>&Add</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="remove"> + <property name="text"> + <string>&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>&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&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-&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>&Detect</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="javaBrowseBtn"> + <property name="text"> + <string>&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&ettings</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="javaDownloadBtn"> + <property name="text"> + <string>Open Java &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 &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 &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&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>&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&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&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&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 &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 &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><html><head/><body><p><span style=" font-weight:600; color:#f5c211;">Warning</span><span style=" color:#f5c211;">: The maximized option may not be fully supported on all Minecraft versions.</span></p></body></html></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>&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>&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>&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 &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 &playing instances</string> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="recordGameTime"> + <property name="text"> + <string>&Record time spent playing instances</string> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="showGlobalGameTime"> + <property name="text"> + <string>Show the &total time played across instances</string> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="showGameTimeWithoutDays"> + <property name="text"> + <string>Always show durations in &hours</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="instanceAccountGroupBox"> + <property name="title"> + <string>Override &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-&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 &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>&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><html><head/><body><p>Emulates usages of old online services which are no longer operating.</p><p>Current fixes include: skin and online mode support.</p></body></html></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>&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>&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>&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>&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><html><head/><body><p>Enable Project Tick GameMode, to potentially improve gaming performance.</p></body></html></string> + </property> + <property name="text"> + <string>Enable Feral GameMode</string> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="enableMangoHud"> + <property name="toolTip"> + <string><html><head/><body><p>Enable MangoHud's advanced performance overlay.</p></body></html></string> + </property> + <property name="text"> + <string>Enable MangoHud</string> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="useDiscreteGpuCheck"> + <property name="toolTip"> + <string><html><head/><body><p>Use the discrete GPU instead of the primary GPU.</p></body></html></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; +}; |
