diff options
| author | Mehmet Samet Duman <yongdohyun@projecttick.org> | 2026-04-02 18:45:07 +0300 |
|---|---|---|
| committer | Mehmet Samet Duman <yongdohyun@projecttick.org> | 2026-04-02 18:45:07 +0300 |
| commit | 31b9a8949ed0a288143e23bf739f2eb64fdc63be (patch) | |
| tree | 8a984fa143c38fccad461a77792d6864f3e82cd3 /meshmc/launcher/ui/dialogs | |
| parent | 934382c8a1ce738589dee9ee0f14e1cec812770e (diff) | |
| parent | fad6a1066616b69d7f5fef01178efdf014c59537 (diff) | |
| download | Project-Tick-31b9a8949ed0a288143e23bf739f2eb64fdc63be.tar.gz Project-Tick-31b9a8949ed0a288143e23bf739f2eb64fdc63be.zip | |
Add 'meshmc/' from commit 'fad6a1066616b69d7f5fef01178efdf014c59537'
git-subtree-dir: meshmc
git-subtree-mainline: 934382c8a1ce738589dee9ee0f14e1cec812770e
git-subtree-split: fad6a1066616b69d7f5fef01178efdf014c59537
Diffstat (limited to 'meshmc/launcher/ui/dialogs')
50 files changed, 6307 insertions, 0 deletions
diff --git a/meshmc/launcher/ui/dialogs/AboutDialog.cpp b/meshmc/launcher/ui/dialogs/AboutDialog.cpp new file mode 100644 index 0000000000..45fdcf6bc5 --- /dev/null +++ b/meshmc/launcher/ui/dialogs/AboutDialog.cpp @@ -0,0 +1,146 @@ +/* SPDX-FileCopyrightText: 2026 Project Tick + * SPDX-FileContributor: Project Tick + * SPDX-License-Identifier: GPL-3.0-or-later + * + * MeshMC - A Custom Launcher for Minecraft + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "AboutDialog.h" +#include "ui_AboutDialog.h" +#include <QIcon> +#include "Application.h" +#include "BuildConfig.h" + +#include <net/NetJob.h> + +#include "HoeDown.h" +#include "MMCStrings.h" + +namespace +{ + // Credits + QString getCreditsHtml() + { + QFile dataFile(":/documents/credits.html"); + if (!dataFile.open(QIODevice::ReadOnly)) { + qWarning() << "Failed to open file" << dataFile.fileName() + << "for reading:" << dataFile.errorString(); + return {}; + } + QString fileContent = QString::fromUtf8(dataFile.readAll()); + dataFile.close(); + + return fileContent.arg( + QObject::tr("%1 Developers").arg(BuildConfig.MESHMC_DISPLAYNAME), + QObject::tr("MultiMC Developers")); + } + + QString getLicenseHtml() + { + QFile dataFile(":/documents/COPYING.md"); + if (dataFile.open(QIODevice::ReadOnly)) { + HoeDown hoedown; + QString output = hoedown.process(dataFile.readAll()); + dataFile.close(); + return output; + } else { + qWarning() << "Failed to open file" << dataFile.fileName() + << "for reading:" << dataFile.errorString(); + return QString(); + } + } + +} // namespace + +AboutDialog::AboutDialog(QWidget* parent) + : QDialog(parent), ui(new Ui::AboutDialog) +{ + ui->setupUi(this); + + QString launcherName = BuildConfig.MESHMC_DISPLAYNAME; + + setWindowTitle(tr("About %1").arg(launcherName)); + + QString chtml = getCreditsHtml(); + ui->creditsText->setHtml(Strings::htmlListPatch(chtml)); + + QString lhtml = getLicenseHtml(); + ui->licenseText->setHtml(Strings::htmlListPatch(lhtml)); + + ui->urlLabel->setOpenExternalLinks(true); + + ui->icon->setPixmap(APPLICATION->getThemedIcon("logo").pixmap(64)); + ui->title->setText(launcherName); + + ui->versionLabel->setText(BuildConfig.printableVersionString()); + + if (!BuildConfig.BUILD_PLATFORM.isEmpty()) + ui->platformLabel->setText(tr("Platform") + ": " + + BuildConfig.BUILD_PLATFORM); + else + ui->platformLabel->setVisible(false); + + if (!BuildConfig.GIT_COMMIT.isEmpty()) + ui->commitLabel->setText(tr("Commit: %1").arg(BuildConfig.GIT_COMMIT)); + else + ui->commitLabel->setVisible(false); + + if (!BuildConfig.BUILD_DATE.isEmpty()) + ui->buildDateLabel->setText( + tr("Build date: %1").arg(BuildConfig.BUILD_DATE)); + else + ui->buildDateLabel->setVisible(false); + + if (!BuildConfig.VERSION_CHANNEL.isEmpty()) + ui->channelLabel->setText(tr("Channel") + ": " + + BuildConfig.VERSION_CHANNEL); + else + ui->channelLabel->setVisible(false); + + QString urlText( + "<html><head/><body><p><a href=\"%1\">%1</a></p></body></html>"); + ui->urlLabel->setText(urlText.arg(BuildConfig.MESHMC_GIT)); + + QString copyText("© 2026 %1"); + ui->copyLabel->setText(copyText.arg(BuildConfig.MESHMC_COPYRIGHT)); + + connect(ui->closeButton, &QPushButton::clicked, this, &AboutDialog::close); + + connect(ui->aboutQt, &QPushButton::clicked, &QApplication::aboutQt); +} + +AboutDialog::~AboutDialog() +{ + delete ui; +} diff --git a/meshmc/launcher/ui/dialogs/AboutDialog.h b/meshmc/launcher/ui/dialogs/AboutDialog.h new file mode 100644 index 0000000000..c4a4dcaa7b --- /dev/null +++ b/meshmc/launcher/ui/dialogs/AboutDialog.h @@ -0,0 +1,62 @@ +/* SPDX-FileCopyrightText: 2026 Project Tick + * SPDX-FileContributor: Project Tick + * SPDX-License-Identifier: GPL-3.0-or-later + * + * MeshMC - A Custom Launcher for Minecraft + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <QDialog> +#include <net/NetJob.h> + +namespace Ui +{ + class AboutDialog; +} + +class AboutDialog : public QDialog +{ + Q_OBJECT + + public: + explicit AboutDialog(QWidget* parent = 0); + ~AboutDialog(); + + private: + Ui::AboutDialog* ui; + + NetJob::Ptr netJob; + QByteArray dataSink; +}; diff --git a/meshmc/launcher/ui/dialogs/AboutDialog.ui b/meshmc/launcher/ui/dialogs/AboutDialog.ui new file mode 100644 index 0000000000..b4eb31e982 --- /dev/null +++ b/meshmc/launcher/ui/dialogs/AboutDialog.ui @@ -0,0 +1,335 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>AboutDialog</class> + <widget class="QDialog" name="AboutDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>573</width> + <height>600</height> + </rect> + </property> + <property name="minimumSize"> + <size> + <width>450</width> + <height>400</height> + </size> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QLabel" name="icon"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Fixed" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>64</width> + <height>64</height> + </size> + </property> + <property name="baseSize"> + <size> + <width>64</width> + <height>64</height> + </size> + </property> + <property name="text"> + <string/> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer_2"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + <item> + <widget class="QLabel" name="title"> + <property name="font"> + <font> + <pointsize>15</pointsize> + </font> + </property> + <property name="text"> + <string notr="true">MeshMC</string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + <item alignment="Qt::AlignHCenter"> + <widget class="QLabel" name="versionLabel"> + <property name="cursor"> + <cursorShape>IBeamCursor</cursorShape> + </property> + <property name="textInteractionFlags"> + <set>Qt::TextSelectableByMouse</set> + </property> + </widget> + </item> + <item> + <widget class="QTabWidget" name="tabWidget"> + <property name="currentIndex"> + <number>0</number> + </property> + <widget class="QWidget" name="aboutTab"> + <attribute name="title"> + <string>About</string> + </attribute> + <layout class="QVBoxLayout" name="verticalLayout_5"> + <item> + <widget class="QLabel" name="aboutLabel"> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="text"> + <string><html><head/><body><p>A custom launcher that makes managing Minecraft easier by allowing you to have multiple instances of Minecraft at once.</p></body></html></string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="urlLabel"> + <property name="font"> + <font> + <pointsize>10</pointsize> + </font> + </property> + <property name="text"> + <string notr="true">GIT URL</string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + <property name="textInteractionFlags"> + <set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse</set> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="copyLabel"> + <property name="font"> + <font> + <pointsize>8</pointsize> + <kerning>true</kerning> + </font> + </property> + <property name="text"> + <string notr="true">COPYRIGHT</string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + <item> + <widget class="Line" name="line"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </widget> + </item> + <item alignment="Qt::AlignHCenter"> + <widget class="QLabel" name="platformLabel"> + <property name="cursor"> + <cursorShape>IBeamCursor</cursorShape> + </property> + <property name="text"> + <string>Platform:</string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + <property name="textInteractionFlags"> + <set>Qt::TextSelectableByMouse</set> + </property> + </widget> + </item> + <item alignment="Qt::AlignHCenter"> + <widget class="QLabel" name="buildDateLabel"> + <property name="cursor"> + <cursorShape>IBeamCursor</cursorShape> + </property> + <property name="text"> + <string>Build Date:</string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + <property name="textInteractionFlags"> + <set>Qt::TextSelectableByMouse</set> + </property> + </widget> + </item> + <item alignment="Qt::AlignHCenter"> + <widget class="QLabel" name="commitLabel"> + <property name="cursor"> + <cursorShape>IBeamCursor</cursorShape> + </property> + <property name="text"> + <string>Commit:</string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + <property name="textInteractionFlags"> + <set>Qt::TextSelectableByMouse</set> + </property> + </widget> + </item> + <item alignment="Qt::AlignHCenter"> + <widget class="QLabel" name="channelLabel"> + <property name="cursor"> + <cursorShape>IBeamCursor</cursorShape> + </property> + <property name="text"> + <string>Channel:</string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + <property name="textInteractionFlags"> + <set>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>212</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + <widget class="QWidget" name="creditsTab"> + <attribute name="title"> + <string>Credits</string> + </attribute> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <item> + <widget class="QTextBrowser" name="creditsText"> + <property name="openExternalLinks"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </widget> + <widget class="QWidget" name="licenseTab"> + <attribute name="title"> + <string>License</string> + </attribute> + <layout class="QVBoxLayout" name="verticalLayout_3"> + <item> + <widget class="QTextEdit" name="licenseText"> + <property name="minimumSize"> + <size> + <width>0</width> + <height>0</height> + </size> + </property> + <property name="font"> + <font> + <family>DejaVu Sans Mono</family> + </font> + </property> + <property name="readOnly"> + <bool>true</bool> + </property> + <property name="textInteractionFlags"> + <set>Qt::TextBrowserInteraction</set> + </property> + </widget> + </item> + </layout> + </widget> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_2"> + <item> + <widget class="QPushButton" name="aboutQt"> + <property name="autoFillBackground"> + <bool>false</bool> + </property> + <property name="text"> + <string>About Qt</string> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer_3"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QPushButton" name="closeButton"> + <property name="text"> + <string>Close</string> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + <tabstops> + <tabstop>tabWidget</tabstop> + <tabstop>creditsText</tabstop> + <tabstop>licenseText</tabstop> + <tabstop>aboutQt</tabstop> + <tabstop>closeButton</tabstop> + </tabstops> + <resources/> + <connections/> +</ui> diff --git a/meshmc/launcher/ui/dialogs/BlockedModsDialog.cpp b/meshmc/launcher/ui/dialogs/BlockedModsDialog.cpp new file mode 100644 index 0000000000..884c1d186d --- /dev/null +++ b/meshmc/launcher/ui/dialogs/BlockedModsDialog.cpp @@ -0,0 +1,178 @@ +/* SPDX-FileCopyrightText: 2026 Project Tick + * SPDX-FileContributor: Project Tick + * SPDX-License-Identifier: GPL-3.0-or-later + * + * MeshMC - A Custom Launcher for Minecraft + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include "BlockedModsDialog.h" + +#include <QDesktopServices> +#include <QDir> +#include <QFont> +#include <QGridLayout> +#include <QScrollArea> +#include <QStandardPaths> +#include <QUrl> + +BlockedModsDialog::BlockedModsDialog(QWidget* parent, const QString& title, + const QString& text, + QList<BlockedMod>& mods) + : QDialog(parent), m_mods(mods) +{ + setWindowTitle(title); + setMinimumSize(550, 300); + resize(620, 420); + setWindowModality(Qt::WindowModal); + + auto* mainLayout = new QVBoxLayout(this); + + // Description label at top + auto* descLabel = new QLabel(text, this); + descLabel->setWordWrap(true); + mainLayout->addWidget(descLabel); + + // Scrollable area for mod list + auto* scrollArea = new QScrollArea(this); + scrollArea->setWidgetResizable(true); + + auto* scrollWidget = new QWidget(); + auto* grid = new QGridLayout(scrollWidget); + grid->setColumnStretch(0, 3); // mod name + grid->setColumnStretch(1, 1); // status + grid->setColumnStretch(2, 0); // button + + // Header row + auto* headerName = new QLabel(tr("<b>Mod</b>"), scrollWidget); + auto* headerStatus = new QLabel(tr("<b>Status</b>"), scrollWidget); + grid->addWidget(headerName, 0, 0); + grid->addWidget(headerStatus, 0, 1); + + for (int i = 0; i < m_mods.size(); i++) { + int row = i + 1; + + auto* nameLabel = new QLabel(m_mods[i].fileName, scrollWidget); + nameLabel->setTextInteractionFlags(Qt::TextSelectableByMouse); + + auto* statusLabel = new QLabel(tr("Missing"), scrollWidget); + statusLabel->setStyleSheet("color: #cc3333; font-weight: bold;"); + + auto* downloadBtn = new QPushButton(tr("Download"), scrollWidget); + connect(downloadBtn, &QPushButton::clicked, this, + [this, i]() { openModDownload(i); }); + + grid->addWidget(nameLabel, row, 0); + grid->addWidget(statusLabel, row, 1); + grid->addWidget(downloadBtn, row, 2); + + m_rows.append({nameLabel, statusLabel, downloadBtn}); + } + + // Add stretch at bottom of grid + grid->setRowStretch(m_mods.size() + 1, 1); + + scrollWidget->setLayout(grid); + scrollArea->setWidget(scrollWidget); + mainLayout->addWidget(scrollArea, 1); + + // Button box at bottom + m_buttons = new QDialogButtonBox( + QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); + m_buttons->button(QDialogButtonBox::Ok)->setText(tr("Continue")); + m_buttons->button(QDialogButtonBox::Ok)->setEnabled(false); + connect(m_buttons, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(m_buttons, &QDialogButtonBox::rejected, this, &QDialog::reject); + mainLayout->addWidget(m_buttons); + + setLayout(mainLayout); + + // Set up Downloads folder watching + setupWatch(); + + // Initial scan + scanDownloadsFolder(); +} + +void BlockedModsDialog::setupWatch() +{ + m_downloadDir = + QStandardPaths::writableLocation(QStandardPaths::DownloadLocation); + if (!m_downloadDir.isEmpty() && QDir(m_downloadDir).exists()) { + m_watcher.addPath(m_downloadDir); + connect(&m_watcher, &QFileSystemWatcher::directoryChanged, this, + &BlockedModsDialog::onDownloadDirChanged); + } +} + +void BlockedModsDialog::onDownloadDirChanged(const QString& path) +{ + Q_UNUSED(path); + scanDownloadsFolder(); +} + +void BlockedModsDialog::scanDownloadsFolder() +{ + if (m_downloadDir.isEmpty()) + return; + + QDir dir(m_downloadDir); + QStringList files = dir.entryList(QDir::Files); + + for (int i = 0; i < m_mods.size(); i++) { + if (!m_mods[i].found && files.contains(m_mods[i].fileName)) { + m_mods[i].found = true; + } + } + + updateModStatus(); +} + +void BlockedModsDialog::updateModStatus() +{ + bool allFound = true; + + for (int i = 0; i < m_mods.size(); i++) { + if (m_mods[i].found) { + m_rows[i].statusLabel->setText(QString::fromUtf8("\u2714 ") + + tr("Found")); + m_rows[i].statusLabel->setStyleSheet( + "color: #33aa33; font-weight: bold;"); + m_rows[i].downloadButton->setEnabled(false); + } else { + m_rows[i].statusLabel->setText(tr("Missing")); + m_rows[i].statusLabel->setStyleSheet( + "color: #cc3333; font-weight: bold;"); + m_rows[i].downloadButton->setEnabled(true); + allFound = false; + } + } + + m_buttons->button(QDialogButtonBox::Ok)->setEnabled(allFound); +} + +void BlockedModsDialog::openModDownload(int index) +{ + if (index < 0 || index >= m_mods.size()) + return; + + const auto& mod = m_mods[index]; + QString url = + QString("https://www.curseforge.com/api/v1/mods/%1/files/%2/download") + .arg(mod.projectId) + .arg(mod.fileId); + QDesktopServices::openUrl(QUrl(url)); +} diff --git a/meshmc/launcher/ui/dialogs/BlockedModsDialog.h b/meshmc/launcher/ui/dialogs/BlockedModsDialog.h new file mode 100644 index 0000000000..77db2fc11b --- /dev/null +++ b/meshmc/launcher/ui/dialogs/BlockedModsDialog.h @@ -0,0 +1,74 @@ +/* SPDX-FileCopyrightText: 2026 Project Tick + * SPDX-FileContributor: Project Tick + * SPDX-License-Identifier: GPL-3.0-or-later + * + * MeshMC - A Custom Launcher for Minecraft + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <QDialog> +#include <QFileSystemWatcher> +#include <QLabel> +#include <QPushButton> +#include <QDialogButtonBox> +#include <QVBoxLayout> + +struct BlockedMod { + int projectId; + int fileId; + QString fileName; + QString targetPath; + bool found = false; +}; + +class BlockedModsDialog : public QDialog +{ + Q_OBJECT + + public: + explicit BlockedModsDialog(QWidget* parent, const QString& title, + const QString& text, QList<BlockedMod>& mods); + + /// Returns the list of mods with updated `found` status + QList<BlockedMod>& resultMods() + { + return m_mods; + } + + private slots: + void onDownloadDirChanged(const QString& path); + void openModDownload(int index); + + private: + void scanDownloadsFolder(); + void updateModStatus(); + void setupWatch(); + + QList<BlockedMod>& m_mods; + QString m_downloadDir; + QFileSystemWatcher m_watcher; + + struct ModRow { + QLabel* nameLabel; + QLabel* statusLabel; + QPushButton* downloadButton; + }; + QList<ModRow> m_rows; + + QDialogButtonBox* m_buttons; +}; diff --git a/meshmc/launcher/ui/dialogs/CopyInstanceDialog.cpp b/meshmc/launcher/ui/dialogs/CopyInstanceDialog.cpp new file mode 100644 index 0000000000..926f0b00a1 --- /dev/null +++ b/meshmc/launcher/ui/dialogs/CopyInstanceDialog.cpp @@ -0,0 +1,157 @@ +/* SPDX-FileCopyrightText: 2026 Project Tick + * SPDX-FileContributor: Project Tick + * SPDX-License-Identifier: GPL-3.0-or-later + * + * MeshMC - A Custom Launcher for Minecraft + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <QLayout> +#include <QPushButton> + +#include "Application.h" +#include "CopyInstanceDialog.h" +#include "ui_CopyInstanceDialog.h" + +#include "ui/dialogs/IconPickerDialog.h" + +#include "BaseVersion.h" +#include "icons/IconList.h" +#include "tasks/Task.h" +#include "BaseInstance.h" +#include "InstanceList.h" + +CopyInstanceDialog::CopyInstanceDialog(InstancePtr original, QWidget* parent) + : QDialog(parent), ui(new Ui::CopyInstanceDialog), m_original(original) +{ + ui->setupUi(this); + resize(minimumSizeHint()); + layout()->setSizeConstraint(QLayout::SetFixedSize); + + InstIconKey = original->iconKey(); + ui->iconButton->setIcon(APPLICATION->icons()->getIcon(InstIconKey)); + ui->instNameTextBox->setText(original->name()); + ui->instNameTextBox->setFocus(); + auto groupList = APPLICATION->instances()->getGroups(); + groupList.removeDuplicates(); + groupList.sort(Qt::CaseInsensitive); + groupList.removeOne(""); + groupList.push_front(""); + ui->groupBox->addItems(groupList); + int index = groupList.indexOf( + APPLICATION->instances()->getInstanceGroup(m_original->id())); + if (index == -1) { + index = 0; + } + ui->groupBox->setCurrentIndex(index); + ui->groupBox->lineEdit()->setPlaceholderText(tr("No group")); + ui->copySavesCheckbox->setChecked(m_copySaves); + ui->keepPlaytimeCheckbox->setChecked(m_keepPlaytime); +} + +CopyInstanceDialog::~CopyInstanceDialog() +{ + delete ui; +} + +void CopyInstanceDialog::updateDialogState() +{ + auto allowOK = !instName().isEmpty(); + auto OkButton = ui->buttonBox->button(QDialogButtonBox::Ok); + if (OkButton->isEnabled() != allowOK) { + OkButton->setEnabled(allowOK); + } +} + +QString CopyInstanceDialog::instName() const +{ + auto result = ui->instNameTextBox->text().trimmed(); + if (result.size()) { + return result; + } + return QString(); +} + +QString CopyInstanceDialog::iconKey() const +{ + return InstIconKey; +} + +QString CopyInstanceDialog::instGroup() const +{ + return ui->groupBox->currentText(); +} + +void CopyInstanceDialog::on_iconButton_clicked() +{ + IconPickerDialog dlg(this); + dlg.execWithSelection(InstIconKey); + + if (dlg.result() == QDialog::Accepted) { + InstIconKey = dlg.selectedIconKey; + ui->iconButton->setIcon(APPLICATION->icons()->getIcon(InstIconKey)); + } +} + +void CopyInstanceDialog::on_instNameTextBox_textChanged(const QString& arg1) +{ + updateDialogState(); +} + +bool CopyInstanceDialog::shouldCopySaves() const +{ + return m_copySaves; +} + +void CopyInstanceDialog::on_copySavesCheckbox_stateChanged(int state) +{ + if (state == Qt::Unchecked) { + m_copySaves = false; + } else if (state == Qt::Checked) { + m_copySaves = true; + } +} + +bool CopyInstanceDialog::shouldKeepPlaytime() const +{ + return m_keepPlaytime; +} + +void CopyInstanceDialog::on_keepPlaytimeCheckbox_stateChanged(int state) +{ + if (state == Qt::Unchecked) { + m_keepPlaytime = false; + } else if (state == Qt::Checked) { + m_keepPlaytime = true; + } +} diff --git a/meshmc/launcher/ui/dialogs/CopyInstanceDialog.h b/meshmc/launcher/ui/dialogs/CopyInstanceDialog.h new file mode 100644 index 0000000000..adbbbd36fb --- /dev/null +++ b/meshmc/launcher/ui/dialogs/CopyInstanceDialog.h @@ -0,0 +1,80 @@ +/* SPDX-FileCopyrightText: 2026 Project Tick + * SPDX-FileContributor: Project Tick + * SPDX-License-Identifier: GPL-3.0-or-later + * + * MeshMC - A Custom Launcher for Minecraft + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <QDialog> +#include "BaseVersion.h" +#include <BaseInstance.h> + +class BaseInstance; + +namespace Ui +{ + class CopyInstanceDialog; +} + +class CopyInstanceDialog : public QDialog +{ + Q_OBJECT + + public: + explicit CopyInstanceDialog(InstancePtr original, QWidget* parent = 0); + ~CopyInstanceDialog(); + + void updateDialogState(); + + QString instName() const; + QString instGroup() const; + QString iconKey() const; + bool shouldCopySaves() const; + bool shouldKeepPlaytime() const; + + private slots: + void on_iconButton_clicked(); + void on_instNameTextBox_textChanged(const QString& arg1); + void on_copySavesCheckbox_stateChanged(int state); + void on_keepPlaytimeCheckbox_stateChanged(int state); + + private: + Ui::CopyInstanceDialog* ui; + QString InstIconKey; + InstancePtr m_original; + bool m_copySaves = true; + bool m_keepPlaytime = true; +}; diff --git a/meshmc/launcher/ui/dialogs/CopyInstanceDialog.ui b/meshmc/launcher/ui/dialogs/CopyInstanceDialog.ui new file mode 100644 index 0000000000..f4b191e272 --- /dev/null +++ b/meshmc/launcher/ui/dialogs/CopyInstanceDialog.ui @@ -0,0 +1,182 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>CopyInstanceDialog</class> + <widget class="QDialog" name="CopyInstanceDialog"> + <property name="windowModality"> + <enum>Qt::ApplicationModal</enum> + </property> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>345</width> + <height>323</height> + </rect> + </property> + <property name="windowTitle"> + <string>Copy Instance</string> + </property> + <property name="windowIcon"> + <iconset> + <normaloff>:/icons/toolbar/copy</normaloff>:/icons/toolbar/copy</iconset> + </property> + <property name="modal"> + <bool>true</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <layout class="QHBoxLayout" name="iconBtnLayout"> + <item> + <spacer name="iconBtnLeftSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QToolButton" name="iconButton"> + <property name="icon"> + <iconset> + <normaloff>:/icons/instances/grass</normaloff>:/icons/instances/grass</iconset> + </property> + <property name="iconSize"> + <size> + <width>80</width> + <height>80</height> + </size> + </property> + </widget> + </item> + <item> + <spacer name="iconBtnRightSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + <item> + <widget class="QLineEdit" name="instNameTextBox"> + <property name="placeholderText"> + <string>Name</string> + </property> + </widget> + </item> + <item> + <widget class="Line" name="line"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </widget> + </item> + <item> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="0"> + <widget class="QLabel" name="labelVersion_3"> + <property name="text"> + <string>&Group</string> + </property> + <property name="buddy"> + <cstring>groupBox</cstring> + </property> + </widget> + </item> + <item row="0" column="1" colspan="2"> + <widget class="QComboBox" name="groupBox"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="editable"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="QCheckBox" name="copySavesCheckbox"> + <property name="text"> + <string>Copy saves</string> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="keepPlaytimeCheckbox"> + <property name="text"> + <string>Keep play time</string> + </property> + </widget> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </widget> + <tabstops> + <tabstop>iconButton</tabstop> + <tabstop>instNameTextBox</tabstop> + <tabstop>groupBox</tabstop> + <tabstop>copySavesCheckbox</tabstop> + <tabstop>keepPlaytimeCheckbox</tabstop> + </tabstops> + <resources> + <include location="../../graphics.qrc"/> + </resources> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>CopyInstanceDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>248</x> + <y>254</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>CopyInstanceDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>316</x> + <y>260</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui> diff --git a/meshmc/launcher/ui/dialogs/CustomMessageBox.cpp b/meshmc/launcher/ui/dialogs/CustomMessageBox.cpp new file mode 100644 index 0000000000..80a87d8d0b --- /dev/null +++ b/meshmc/launcher/ui/dialogs/CustomMessageBox.cpp @@ -0,0 +1,59 @@ +/* SPDX-FileCopyrightText: 2026 Project Tick + * SPDX-FileContributor: Project Tick + * SPDX-License-Identifier: GPL-3.0-or-later + * + * MeshMC - A Custom Launcher for Minecraft + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * 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 "CustomMessageBox.h" + +namespace CustomMessageBox +{ + QMessageBox* selectable(QWidget* parent, const QString& title, + const QString& text, QMessageBox::Icon icon, + QMessageBox::StandardButtons buttons, + QMessageBox::StandardButton defaultButton) + { + QMessageBox* messageBox = new QMessageBox(parent); + messageBox->setWindowTitle(title); + messageBox->setText(text); + messageBox->setStandardButtons(buttons); + messageBox->setDefaultButton(defaultButton); + messageBox->setTextInteractionFlags(Qt::TextSelectableByMouse); + messageBox->setIcon(icon); + messageBox->setTextInteractionFlags(Qt::TextBrowserInteraction); + + return messageBox; + } +} // namespace CustomMessageBox diff --git a/meshmc/launcher/ui/dialogs/CustomMessageBox.h b/meshmc/launcher/ui/dialogs/CustomMessageBox.h new file mode 100644 index 0000000000..18656c71db --- /dev/null +++ b/meshmc/launcher/ui/dialogs/CustomMessageBox.h @@ -0,0 +1,50 @@ +/* SPDX-FileCopyrightText: 2026 Project Tick + * SPDX-FileContributor: Project Tick + * SPDX-License-Identifier: GPL-3.0-or-later + * + * MeshMC - A Custom Launcher for Minecraft + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <QMessageBox> + +namespace CustomMessageBox +{ + QMessageBox* selectable( + QWidget* parent, const QString& title, const QString& text, + QMessageBox::Icon icon = QMessageBox::NoIcon, + QMessageBox::StandardButtons buttons = QMessageBox::Ok, + QMessageBox::StandardButton defaultButton = QMessageBox::NoButton); +} diff --git a/meshmc/launcher/ui/dialogs/EditAccountDialog.cpp b/meshmc/launcher/ui/dialogs/EditAccountDialog.cpp new file mode 100644 index 0000000000..30e9f142f9 --- /dev/null +++ b/meshmc/launcher/ui/dialogs/EditAccountDialog.cpp @@ -0,0 +1,85 @@ +/* SPDX-FileCopyrightText: 2026 Project Tick + * SPDX-FileContributor: Project Tick + * SPDX-License-Identifier: GPL-3.0-or-later + * + * MeshMC - A Custom Launcher for Minecraft + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "EditAccountDialog.h" +#include "ui_EditAccountDialog.h" +#include <DesktopServices.h> +#include <QUrl> + +EditAccountDialog::EditAccountDialog(const QString& text, QWidget* parent, + int flags) + : QDialog(parent), ui(new Ui::EditAccountDialog) +{ + ui->setupUi(this); + + ui->label->setText(text); + ui->label->setVisible(!text.isEmpty()); + + ui->userTextBox->setEnabled(flags & UsernameField); + ui->passTextBox->setEnabled(flags & PasswordField); +} + +EditAccountDialog::~EditAccountDialog() +{ + delete ui; +} + +void EditAccountDialog::on_label_linkActivated(const QString& link) +{ + DesktopServices::openUrl(QUrl(link)); +} + +void EditAccountDialog::setUsername(const QString& user) const +{ + ui->userTextBox->setText(user); +} + +QString EditAccountDialog::username() const +{ + return ui->userTextBox->text(); +} + +void EditAccountDialog::setPassword(const QString& pass) const +{ + ui->passTextBox->setText(pass); +} + +QString EditAccountDialog::password() const +{ + return ui->passTextBox->text(); +} diff --git a/meshmc/launcher/ui/dialogs/EditAccountDialog.h b/meshmc/launcher/ui/dialogs/EditAccountDialog.h new file mode 100644 index 0000000000..ac1af0efbf --- /dev/null +++ b/meshmc/launcher/ui/dialogs/EditAccountDialog.h @@ -0,0 +1,78 @@ +/* SPDX-FileCopyrightText: 2026 Project Tick + * SPDX-FileContributor: Project Tick + * SPDX-License-Identifier: GPL-3.0-or-later + * + * MeshMC - A Custom Launcher for Minecraft + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <QDialog> + +namespace Ui +{ + class EditAccountDialog; +} + +class EditAccountDialog : public QDialog +{ + Q_OBJECT + + public: + explicit EditAccountDialog(const QString& text = "", QWidget* parent = 0, + int flags = UsernameField | PasswordField); + ~EditAccountDialog(); + + void setUsername(const QString& user) const; + void setPassword(const QString& pass) const; + + QString username() const; + QString password() const; + + enum Flags { + NoFlags = 0, + + //! Specifies that the dialog should have a username field. + UsernameField, + + //! Specifies that the dialog should have a password field. + PasswordField, + }; + + private slots: + void on_label_linkActivated(const QString& link); + + private: + Ui::EditAccountDialog* ui; +}; diff --git a/meshmc/launcher/ui/dialogs/EditAccountDialog.ui b/meshmc/launcher/ui/dialogs/EditAccountDialog.ui new file mode 100644 index 0000000000..e87509bcbc --- /dev/null +++ b/meshmc/launcher/ui/dialogs/EditAccountDialog.ui @@ -0,0 +1,94 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>EditAccountDialog</class> + <widget class="QDialog" name="EditAccountDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>400</width> + <height>148</height> + </rect> + </property> + <property name="windowTitle"> + <string>Login</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QLabel" name="label"> + <property name="text"> + <string notr="true">Message label placeholder.</string> + </property> + <property name="textFormat"> + <enum>Qt::RichText</enum> + </property> + <property name="textInteractionFlags"> + <set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set> + </property> + </widget> + </item> + <item> + <widget class="QLineEdit" name="userTextBox"> + <property name="placeholderText"> + <string>Email</string> + </property> + </widget> + </item> + <item> + <widget class="QLineEdit" name="passTextBox"> + <property name="echoMode"> + <enum>QLineEdit::Password</enum> + </property> + <property name="placeholderText"> + <string>Password</string> + </property> + </widget> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>EditAccountDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>248</x> + <y>254</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>EditAccountDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>316</x> + <y>260</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui> diff --git a/meshmc/launcher/ui/dialogs/ExportInstanceDialog.cpp b/meshmc/launcher/ui/dialogs/ExportInstanceDialog.cpp new file mode 100644 index 0000000000..059c994dfd --- /dev/null +++ b/meshmc/launcher/ui/dialogs/ExportInstanceDialog.cpp @@ -0,0 +1,462 @@ +/* SPDX-FileCopyrightText: 2026 Project Tick + * SPDX-FileContributor: Project Tick + * SPDX-License-Identifier: GPL-3.0-or-later + * + * MeshMC - A Custom Launcher for Minecraft + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ExportInstanceDialog.h" +#include "ui_ExportInstanceDialog.h" +#include <BaseInstance.h> +#include <MMCZip.h> +#include <QFileDialog> +#include <QMessageBox> +#include <qfilesystemmodel.h> + +#include <QSortFilterProxyModel> +#include <QDebug> +#include <qstack.h> +#include <QSaveFile> +#include "MMCStrings.h" +#include "SeparatorPrefixTree.h" +#include "Application.h" +#include <icons/IconList.h> +#include <FileSystem.h> + +class PackIgnoreProxy : public QSortFilterProxyModel +{ + Q_OBJECT + + public: + PackIgnoreProxy(InstancePtr instance, QObject* parent) + : QSortFilterProxyModel(parent) + { + m_instance = instance; + } + // NOTE: Sadly, we have to do sorting ourselves. + bool lessThan(const QModelIndex& left, const QModelIndex& right) const + { + QFileSystemModel* fsm = qobject_cast<QFileSystemModel*>(sourceModel()); + if (!fsm) { + return QSortFilterProxyModel::lessThan(left, right); + } + bool asc = sortOrder() == Qt::AscendingOrder ? true : false; + + QFileInfo leftFileInfo = fsm->fileInfo(left); + QFileInfo rightFileInfo = fsm->fileInfo(right); + + if (!leftFileInfo.isDir() && rightFileInfo.isDir()) { + return !asc; + } + if (leftFileInfo.isDir() && !rightFileInfo.isDir()) { + return asc; + } + + // sort and proxy model breaks the original model... + if (sortColumn() == 0) { + return Strings::naturalCompare(leftFileInfo.fileName(), + rightFileInfo.fileName(), + Qt::CaseInsensitive) < 0; + } + if (sortColumn() == 1) { + auto leftSize = leftFileInfo.size(); + auto rightSize = rightFileInfo.size(); + if ((leftSize == rightSize) || + (leftFileInfo.isDir() && rightFileInfo.isDir())) { + return Strings::naturalCompare(leftFileInfo.fileName(), + rightFileInfo.fileName(), + Qt::CaseInsensitive) < 0 + ? asc + : !asc; + } + return leftSize < rightSize; + } + return QSortFilterProxyModel::lessThan(left, right); + } + + virtual Qt::ItemFlags flags(const QModelIndex& index) const + { + if (!index.isValid()) + return Qt::NoItemFlags; + + auto sourceIndex = mapToSource(index); + Qt::ItemFlags flags = sourceIndex.flags(); + if (index.column() == 0) { + flags |= Qt::ItemIsUserCheckable; + if (sourceIndex.model()->hasChildren(sourceIndex)) { + flags |= Qt::ItemIsAutoTristate; + } + } + + return flags; + } + + virtual QVariant data(const QModelIndex& index, + int role = Qt::DisplayRole) const + { + QModelIndex sourceIndex = mapToSource(index); + + if (index.column() == 0 && role == Qt::CheckStateRole) { + QFileSystemModel* fsm = + qobject_cast<QFileSystemModel*>(sourceModel()); + auto blockedPath = relPath(fsm->filePath(sourceIndex)); + auto cover = blocked.cover(blockedPath); + if (!cover.isNull()) { + return QVariant(Qt::Unchecked); + } else if (blocked.exists(blockedPath)) { + return QVariant(Qt::PartiallyChecked); + } else { + return QVariant(Qt::Checked); + } + } + + return sourceIndex.data(role); + } + + virtual bool setData(const QModelIndex& index, const QVariant& value, + int role = Qt::EditRole) + { + if (index.column() == 0 && role == Qt::CheckStateRole) { + Qt::CheckState state = static_cast<Qt::CheckState>(value.toInt()); + return setFilterState(index, state); + } + + QModelIndex sourceIndex = mapToSource(index); + return QSortFilterProxyModel::sourceModel()->setData(sourceIndex, value, + role); + } + + QString relPath(const QString& path) const + { + QString prefix = QDir().absoluteFilePath(m_instance->instanceRoot()); + prefix += '/'; + if (!path.startsWith(prefix)) { + return QString(); + } + return path.mid(prefix.size()); + } + + bool setFilterState(QModelIndex index, Qt::CheckState state) + { + QFileSystemModel* fsm = qobject_cast<QFileSystemModel*>(sourceModel()); + + if (!fsm) { + return false; + } + + QModelIndex sourceIndex = mapToSource(index); + auto blockedPath = relPath(fsm->filePath(sourceIndex)); + bool changed = false; + if (state == Qt::Unchecked) { + // blocking a path + auto& node = blocked.insert(blockedPath); + // get rid of all blocked nodes below + node.clear(); + changed = true; + } else if (state == Qt::Checked || state == Qt::PartiallyChecked) { + if (!blocked.remove(blockedPath)) { + auto cover = blocked.cover(blockedPath); + qDebug() << "Blocked by cover" << cover; + // uncover + blocked.remove(cover); + // block all contents, except for any cover + QModelIndex rootIndex = fsm->index( + FS::PathCombine(m_instance->instanceRoot(), cover)); + QModelIndex doing = rootIndex; + int row = 0; + QStack<QModelIndex> todo; + while (1) { + auto node = 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. + blocked.insert(relpath); + } + row++; + } + } + changed = true; + } + if (changed) { + // update the thing + emit dataChanged(index, index, {Qt::CheckStateRole}); + // update everything above index + QModelIndex up = index.parent(); + while (1) { + if (!up.isValid()) + break; + emit dataChanged(up, up, {Qt::CheckStateRole}); + up = up.parent(); + } + // and everything below the index + QModelIndex doing = index; + int row = 0; + QStack<QModelIndex> todo; + while (1) { + auto node = doing.model()->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 shouldExpand(QModelIndex index) + { + QModelIndex sourceIndex = mapToSource(index); + QFileSystemModel* fsm = qobject_cast<QFileSystemModel*>(sourceModel()); + if (!fsm) { + return false; + } + auto blockedPath = relPath(fsm->filePath(sourceIndex)); + auto found = blocked.find(blockedPath); + if (found) { + return !found->leaf(); + } + return false; + } + + void setBlockedPaths(QStringList paths) + { + beginResetModel(); + blocked.clear(); + blocked.insert(paths); + endResetModel(); + } + + const SeparatorPrefixTree<'/'>& blockedPaths() const + { + return blocked; + } + + protected: + bool filterAcceptsColumn(int source_column, + const QModelIndex& source_parent) const + { + Q_UNUSED(source_parent) + + // adjust the columns you want to filter out here + // return false for those that will be hidden + if (source_column == 2 || source_column == 3) + return false; + + return true; + } + + private: + InstancePtr m_instance; + SeparatorPrefixTree<'/'> blocked; +}; + +ExportInstanceDialog::ExportInstanceDialog(InstancePtr instance, + QWidget* parent) + : QDialog(parent), ui(new Ui::ExportInstanceDialog), m_instance(instance) +{ + ui->setupUi(this); + auto model = new QFileSystemModel(this); + proxyModel = new PackIgnoreProxy(m_instance, this); + loadPackIgnore(); + proxyModel->setSourceModel(model); + auto root = instance->instanceRoot(); + ui->treeView->setModel(proxyModel); + ui->treeView->setRootIndex(proxyModel->mapFromSource(model->index(root))); + ui->treeView->sortByColumn(0, Qt::AscendingOrder); + + connect(proxyModel, SIGNAL(rowsInserted(QModelIndex, int, int)), + SLOT(rowsInserted(QModelIndex, int, int))); + + model->setFilter(QDir::AllEntries | QDir::NoDotAndDotDot | QDir::AllDirs | + QDir::Hidden); + model->setRootPath(root); + auto headerView = ui->treeView->header(); + headerView->setSectionResizeMode(QHeaderView::ResizeToContents); + headerView->setSectionResizeMode(0, QHeaderView::Stretch); +} + +ExportInstanceDialog::~ExportInstanceDialog() +{ + delete ui; +} + +/// Save icon to instance's folder is needed +void SaveIcon(InstancePtr m_instance) +{ + auto iconKey = m_instance->iconKey(); + auto iconList = APPLICATION->icons(); + auto mmcIcon = iconList->icon(iconKey); + if (!mmcIcon || mmcIcon->isBuiltIn()) { + return; + } + auto path = mmcIcon->getFilePath(); + if (!path.isNull()) { + QFileInfo inInfo(path); + FS::copy(path, FS::PathCombine(m_instance->instanceRoot(), + inInfo.fileName()))(); + return; + } + auto& image = mmcIcon->m_images[mmcIcon->type()]; + auto& icon = image.icon; + auto sizes = icon.availableSizes(); + if (sizes.size() == 0) { + return; + } + auto areaOf = [](QSize size) { return size.width() * size.height(); }; + QSize largest = sizes[0]; + // find variant with largest area + for (auto size : sizes) { + if (areaOf(largest) < areaOf(size)) { + largest = size; + } + } + auto pixmap = icon.pixmap(largest); + pixmap.save(FS::PathCombine(m_instance->instanceRoot(), iconKey + ".png")); +} + +bool ExportInstanceDialog::doExport() +{ + auto name = FS::RemoveInvalidFilenameChars(m_instance->name()); + + const QString output = QFileDialog::getSaveFileName( + this, tr("Export %1").arg(m_instance->name()), + FS::PathCombine(QDir::homePath(), name + ".zip"), "Zip (*.zip)", + nullptr, QFileDialog::DontConfirmOverwrite); + if (output.isEmpty()) { + return false; + } + if (QFile::exists(output)) { + int ret = QMessageBox::question( + this, tr("Overwrite?"), + tr("This file already exists. Do you want to overwrite it?"), + QMessageBox::No, QMessageBox::Yes); + if (ret == QMessageBox::No) { + return false; + } + } + + SaveIcon(m_instance); + + auto& blocked = proxyModel->blockedPaths(); + using std::placeholders::_1; + if (!MMCZip::compressDir( + output, m_instance->instanceRoot(), + std::bind(&SeparatorPrefixTree<'/'>::covers, blocked, _1))) { + QMessageBox::warning(this, tr("Error"), + tr("Unable to export instance")); + return false; + } + return true; +} + +void ExportInstanceDialog::done(int result) +{ + savePackIgnore(); + if (result == QDialog::Accepted) { + if (doExport()) { + QDialog::done(QDialog::Accepted); + return; + } else { + return; + } + } + QDialog::done(result); +} + +void ExportInstanceDialog::rowsInserted(QModelIndex parent, int top, int bottom) +{ + // WARNING: possible off-by-one? + for (int i = top; i < bottom; i++) { + auto node = parent.model()->index(i, 0, parent); + if (proxyModel->shouldExpand(node)) { + auto expNode = node.parent(); + if (!expNode.isValid()) { + continue; + } + ui->treeView->expand(node); + } + } +} + +QString ExportInstanceDialog::ignoreFileName() +{ + return FS::PathCombine(m_instance->instanceRoot(), ".packignore"); +} + +void ExportInstanceDialog::loadPackIgnore() +{ + auto filename = ignoreFileName(); + QFile ignoreFile(filename); + if (!ignoreFile.open(QIODevice::ReadOnly)) { + return; + } + auto data = ignoreFile.readAll(); + auto string = QString::fromUtf8(data); + proxyModel->setBlockedPaths(string.split('\n', Qt::SkipEmptyParts)); +} + +void ExportInstanceDialog::savePackIgnore() +{ + auto data = proxyModel->blockedPaths().toStringList().join('\n').toUtf8(); + auto filename = ignoreFileName(); + try { + FS::write(filename, data); + } catch (const Exception& e) { + qWarning() << e.cause(); + } +} + +#include "ExportInstanceDialog.moc" diff --git a/meshmc/launcher/ui/dialogs/ExportInstanceDialog.h b/meshmc/launcher/ui/dialogs/ExportInstanceDialog.h new file mode 100644 index 0000000000..e9e9549d4e --- /dev/null +++ b/meshmc/launcher/ui/dialogs/ExportInstanceDialog.h @@ -0,0 +1,77 @@ +/* SPDX-FileCopyrightText: 2026 Project Tick + * SPDX-FileContributor: Project Tick + * SPDX-License-Identifier: GPL-3.0-or-later + * + * MeshMC - A Custom Launcher for Minecraft + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <QDialog> +#include <QModelIndex> +#include <memory> + +class BaseInstance; +class PackIgnoreProxy; +typedef std::shared_ptr<BaseInstance> InstancePtr; + +namespace Ui +{ + class ExportInstanceDialog; +} + +class ExportInstanceDialog : public QDialog +{ + Q_OBJECT + + public: + explicit ExportInstanceDialog(InstancePtr instance, QWidget* parent = 0); + ~ExportInstanceDialog(); + + virtual void done(int result); + + private: + bool doExport(); + void loadPackIgnore(); + void savePackIgnore(); + QString ignoreFileName(); + + private: + Ui::ExportInstanceDialog* ui; + InstancePtr m_instance; + PackIgnoreProxy* proxyModel; + + private slots: + void rowsInserted(QModelIndex parent, int top, int bottom); +}; diff --git a/meshmc/launcher/ui/dialogs/ExportInstanceDialog.ui b/meshmc/launcher/ui/dialogs/ExportInstanceDialog.ui new file mode 100644 index 0000000000..bcd4e84a4d --- /dev/null +++ b/meshmc/launcher/ui/dialogs/ExportInstanceDialog.ui @@ -0,0 +1,83 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>ExportInstanceDialog</class> + <widget class="QDialog" name="ExportInstanceDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>720</width> + <height>625</height> + </rect> + </property> + <property name="windowTitle"> + <string>Export Instance</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QTreeView" name="treeView"> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + <property name="selectionMode"> + <enum>QAbstractItemView::ExtendedSelection</enum> + </property> + <property name="sortingEnabled"> + <bool>true</bool> + </property> + <attribute name="headerStretchLastSection"> + <bool>false</bool> + </attribute> + </widget> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </widget> + <tabstops> + <tabstop>treeView</tabstop> + </tabstops> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>ExportInstanceDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>248</x> + <y>254</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>ExportInstanceDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>316</x> + <y>260</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui> diff --git a/meshmc/launcher/ui/dialogs/IconPickerDialog.cpp b/meshmc/launcher/ui/dialogs/IconPickerDialog.cpp new file mode 100644 index 0000000000..1768b44fd4 --- /dev/null +++ b/meshmc/launcher/ui/dialogs/IconPickerDialog.cpp @@ -0,0 +1,197 @@ +/* SPDX-FileCopyrightText: 2026 Project Tick + * SPDX-FileContributor: Project Tick + * SPDX-License-Identifier: GPL-3.0-or-later + * + * MeshMC - A Custom Launcher for Minecraft + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <QKeyEvent> +#include <QPushButton> +#include <QFileDialog> + +#include "Application.h" + +#include "IconPickerDialog.h" +#include "ui_IconPickerDialog.h" + +#include "ui/instanceview/InstanceDelegate.h" + +#include "icons/IconList.h" +#include "icons/IconUtils.h" +#include <DesktopServices.h> + +IconPickerDialog::IconPickerDialog(QWidget* parent) + : QDialog(parent), ui(new Ui::IconPickerDialog) +{ + ui->setupUi(this); + setWindowModality(Qt::WindowModal); + + auto contentsWidget = ui->iconView; + contentsWidget->setViewMode(QListView::IconMode); + contentsWidget->setFlow(QListView::LeftToRight); + contentsWidget->setIconSize(QSize(48, 48)); + contentsWidget->setMovement(QListView::Static); + contentsWidget->setResizeMode(QListView::Adjust); + contentsWidget->setSelectionMode(QAbstractItemView::SingleSelection); + contentsWidget->setSpacing(5); + contentsWidget->setWordWrap(false); + contentsWidget->setWrapping(true); + contentsWidget->setUniformItemSizes(true); + contentsWidget->setTextElideMode(Qt::ElideRight); + contentsWidget->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); + contentsWidget->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); + contentsWidget->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + contentsWidget->setItemDelegate(new ListViewDelegate()); + + // contentsWidget->setAcceptDrops(true); + contentsWidget->setDropIndicatorShown(true); + contentsWidget->viewport()->setAcceptDrops(true); + contentsWidget->setDragDropMode(QAbstractItemView::DropOnly); + contentsWidget->setDefaultDropAction(Qt::CopyAction); + + contentsWidget->installEventFilter(this); + + contentsWidget->setModel(APPLICATION->icons().get()); + + // NOTE: ResetRole forces the button to be on the left, while the OK/Cancel + // ones are on the right. We win. + auto buttonAdd = + ui->buttonBox->addButton(tr("Add Icon"), QDialogButtonBox::ResetRole); + auto buttonRemove = ui->buttonBox->addButton(tr("Remove Icon"), + QDialogButtonBox::ResetRole); + + connect(buttonAdd, SIGNAL(clicked(bool)), SLOT(addNewIcon())); + connect(buttonRemove, SIGNAL(clicked(bool)), SLOT(removeSelectedIcon())); + + connect(contentsWidget, SIGNAL(doubleClicked(QModelIndex)), + SLOT(activated(QModelIndex))); + + connect(contentsWidget->selectionModel(), + SIGNAL(selectionChanged(QItemSelection, QItemSelection)), + SLOT(selectionChanged(QItemSelection, QItemSelection))); + + auto buttonFolder = ui->buttonBox->addButton(tr("Open Folder"), + QDialogButtonBox::ResetRole); + connect(buttonFolder, &QPushButton::clicked, this, + &IconPickerDialog::openFolder); +} + +bool IconPickerDialog::eventFilter(QObject* obj, QEvent* evt) +{ + if (obj != ui->iconView) + return QDialog::eventFilter(obj, evt); + if (evt->type() != QEvent::KeyPress) { + return QDialog::eventFilter(obj, evt); + } + QKeyEvent* keyEvent = static_cast<QKeyEvent*>(evt); + switch (keyEvent->key()) { + case Qt::Key_Delete: + removeSelectedIcon(); + return true; + case Qt::Key_Plus: + addNewIcon(); + return true; + default: + break; + } + return QDialog::eventFilter(obj, evt); +} + +void IconPickerDialog::addNewIcon() +{ + //: The title of the select icons open file dialog + QString selectIcons = tr("Select Icons"); + //: The type of icon files + auto filter = IconUtils::getIconFilter(); + QStringList fileNames = QFileDialog::getOpenFileNames( + this, selectIcons, QString(), tr("Icons %1").arg(filter)); + APPLICATION->icons()->installIcons(fileNames); +} + +void IconPickerDialog::removeSelectedIcon() +{ + APPLICATION->icons()->deleteIcon(selectedIconKey); +} + +void IconPickerDialog::activated(QModelIndex index) +{ + selectedIconKey = index.data(Qt::UserRole).toString(); + accept(); +} + +void IconPickerDialog::selectionChanged(QItemSelection selected, + QItemSelection deselected) +{ + if (selected.empty()) + return; + + QString key = + selected.first().indexes().first().data(Qt::UserRole).toString(); + if (!key.isEmpty()) { + selectedIconKey = key; + } +} + +int IconPickerDialog::execWithSelection(QString selection) +{ + auto list = APPLICATION->icons(); + auto contentsWidget = ui->iconView; + selectedIconKey = selection; + + int index_nr = list->getIconIndex(selection); + auto model_index = list->index(index_nr); + contentsWidget->selectionModel()->select(model_index, + QItemSelectionModel::Current | + QItemSelectionModel::Select); + + QMetaObject::invokeMethod(this, "delayed_scroll", Qt::QueuedConnection, + Q_ARG(QModelIndex, model_index)); + return QDialog::exec(); +} + +void IconPickerDialog::delayed_scroll(QModelIndex model_index) +{ + auto contentsWidget = ui->iconView; + contentsWidget->scrollTo(model_index); +} + +IconPickerDialog::~IconPickerDialog() +{ + delete ui; +} + +void IconPickerDialog::openFolder() +{ + DesktopServices::openDirectory(APPLICATION->icons()->getDirectory(), true); +} diff --git a/meshmc/launcher/ui/dialogs/IconPickerDialog.h b/meshmc/launcher/ui/dialogs/IconPickerDialog.h new file mode 100644 index 0000000000..0a92be4a53 --- /dev/null +++ b/meshmc/launcher/ui/dialogs/IconPickerDialog.h @@ -0,0 +1,71 @@ +/* SPDX-FileCopyrightText: 2026 Project Tick + * SPDX-FileContributor: Project Tick + * SPDX-License-Identifier: GPL-3.0-or-later + * + * MeshMC - A Custom Launcher for Minecraft + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * 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 <QDialog> +#include <QItemSelection> + +namespace Ui +{ + class IconPickerDialog; +} + +class IconPickerDialog : public QDialog +{ + Q_OBJECT + + public: + explicit IconPickerDialog(QWidget* parent = 0); + ~IconPickerDialog(); + int execWithSelection(QString selection); + QString selectedIconKey; + + protected: + virtual bool eventFilter(QObject*, QEvent*); + + private: + Ui::IconPickerDialog* ui; + + private slots: + void selectionChanged(QItemSelection, QItemSelection); + void activated(QModelIndex); + void delayed_scroll(QModelIndex); + void addNewIcon(); + void removeSelectedIcon(); + void openFolder(); +}; diff --git a/meshmc/launcher/ui/dialogs/IconPickerDialog.ui b/meshmc/launcher/ui/dialogs/IconPickerDialog.ui new file mode 100644 index 0000000000..c548edfb7a --- /dev/null +++ b/meshmc/launcher/ui/dialogs/IconPickerDialog.ui @@ -0,0 +1,67 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>IconPickerDialog</class> + <widget class="QDialog" name="IconPickerDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>676</width> + <height>555</height> + </rect> + </property> + <property name="windowTitle"> + <string>Pick icon</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QListView" name="iconView"/> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>IconPickerDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>248</x> + <y>254</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>IconPickerDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>316</x> + <y>260</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui> diff --git a/meshmc/launcher/ui/dialogs/JavaDownloadDialog.cpp b/meshmc/launcher/ui/dialogs/JavaDownloadDialog.cpp new file mode 100644 index 0000000000..c83542543c --- /dev/null +++ b/meshmc/launcher/ui/dialogs/JavaDownloadDialog.cpp @@ -0,0 +1,399 @@ +/* SPDX-FileCopyrightText: 2026 Project Tick + * SPDX-FileContributor: Project Tick + * SPDX-License-Identifier: GPL-3.0-or-later + * + * MeshMC - A Custom Launcher for Minecraft + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include "JavaDownloadDialog.h" + +#include <QVBoxLayout> +#include <QHBoxLayout> +#include <QGroupBox> +#include <QMessageBox> +#include <QJsonDocument> +#include <QJsonObject> +#include <QJsonArray> +#include <QDebug> +#include <QDir> +#include <QHeaderView> +#include <QSplitter> + +#include "Application.h" +#include "BuildConfig.h" +#include "FileSystem.h" +#include "Json.h" +#include "java/JavaUtils.h" +#include "net/Download.h" + +JavaDownloadDialog::JavaDownloadDialog(QWidget* parent) : QDialog(parent) +{ + setWindowTitle(tr("Download Java")); + setMinimumSize(700, 450); + resize(800, 500); + m_providers = JavaDownload::JavaProviderInfo::availableProviders(); + setupUi(); +} + +void JavaDownloadDialog::setupUi() +{ + auto* mainLayout = new QVBoxLayout(this); + + // --- Three-column selection area --- + auto* columnsLayout = new QHBoxLayout(); + + // Left: Provider list + auto* providerGroup = new QGroupBox(tr("Provider"), this); + auto* providerLayout = new QVBoxLayout(providerGroup); + m_providerList = new QListWidget(this); + m_providerList->setIconSize(QSize(24, 24)); + for (const auto& provider : m_providers) { + auto* item = new QListWidgetItem( + APPLICATION->getThemedIcon(provider.iconName), provider.name); + m_providerList->addItem(item); + } + providerLayout->addWidget(m_providerList); + columnsLayout->addWidget(providerGroup, 1); + + // Center: Major version list + auto* versionGroup = new QGroupBox(tr("Version"), this); + auto* versionLayout = new QVBoxLayout(versionGroup); + m_versionList = new QListWidget(this); + versionLayout->addWidget(m_versionList); + columnsLayout->addWidget(versionGroup, 1); + + // Right: Sub-version / build list + auto* subVersionGroup = new QGroupBox(tr("Build"), this); + auto* subVersionLayout = new QVBoxLayout(subVersionGroup); + m_subVersionList = new QListWidget(this); + subVersionLayout->addWidget(m_subVersionList); + columnsLayout->addWidget(subVersionGroup, 1); + + mainLayout->addLayout(columnsLayout, 1); + + // Info label + m_infoLabel = new QLabel(this); + m_infoLabel->setWordWrap(true); + m_infoLabel->setText( + tr("Select a Java provider, version, and build to download.")); + mainLayout->addWidget(m_infoLabel); + + // Progress bar + m_progressBar = new QProgressBar(this); + m_progressBar->setVisible(false); + m_progressBar->setRange(0, 100); + mainLayout->addWidget(m_progressBar); + + // Status label + m_statusLabel = new QLabel(this); + m_statusLabel->setVisible(false); + mainLayout->addWidget(m_statusLabel); + + // Buttons + auto* buttonLayout = new QHBoxLayout(); + buttonLayout->addStretch(); + + m_downloadBtn = new QPushButton(tr("Download"), this); + m_downloadBtn->setEnabled(false); + buttonLayout->addWidget(m_downloadBtn); + + m_cancelBtn = new QPushButton(tr("Cancel"), this); + buttonLayout->addWidget(m_cancelBtn); + + mainLayout->addLayout(buttonLayout); + + // Connections + connect(m_providerList, &QListWidget::currentRowChanged, this, + &JavaDownloadDialog::providerChanged); + connect(m_versionList, &QListWidget::currentRowChanged, this, + &JavaDownloadDialog::majorVersionChanged); + connect(m_subVersionList, &QListWidget::currentRowChanged, this, + &JavaDownloadDialog::subVersionChanged); + connect(m_downloadBtn, &QPushButton::clicked, this, + &JavaDownloadDialog::onDownloadClicked); + connect(m_cancelBtn, &QPushButton::clicked, this, + &JavaDownloadDialog::onCancelClicked); + + // Select first provider + if (m_providerList->count() > 0) { + m_providerList->setCurrentRow(0); + } +} + +void JavaDownloadDialog::providerChanged(int index) +{ + if (index < 0 || index >= m_providers.size()) + return; + + m_versionList->clear(); + m_subVersionList->clear(); + m_downloadBtn->setEnabled(false); + m_versions.clear(); + m_runtimes.clear(); + + const auto& provider = m_providers[index]; + m_infoLabel->setText(tr("Loading versions for %1...").arg(provider.name)); + + fetchVersionList(provider.uid); +} + +void JavaDownloadDialog::majorVersionChanged(int index) +{ + if (index < 0 || index >= m_versions.size()) + return; + + m_subVersionList->clear(); + m_downloadBtn->setEnabled(false); + m_runtimes.clear(); + + const auto& version = m_versions[index]; + m_infoLabel->setText(tr("Loading builds for %1...").arg(version.versionId)); + + fetchRuntimes(version.uid, version.versionId); +} + +void JavaDownloadDialog::subVersionChanged(int index) +{ + if (index < 0 || index >= m_runtimes.size()) { + m_downloadBtn->setEnabled(false); + return; + } + + const auto& rt = m_runtimes[index]; + m_infoLabel->setText(tr("Ready to download: %1\n" + "Version: %2\n" + "Platform: %3\n" + "Checksum: %4") + .arg(rt.name, rt.version.toString(), rt.runtimeOS, + rt.checksumHash.isEmpty() + ? tr("None") + : tr("Yes (%1)").arg(rt.checksumType))); + m_downloadBtn->setEnabled(true); +} + +void JavaDownloadDialog::fetchVersionList(const QString& uid) +{ + m_fetchJob.reset(); + m_fetchData.clear(); + + QString url = QString("%1%2/index.json").arg(BuildConfig.META_URL, uid); + m_fetchJob = new NetJob(tr("Fetch Java versions"), APPLICATION->network()); + auto dl = Net::Download::makeByteArray(QUrl(url), &m_fetchData); + m_fetchJob->addNetAction(dl); + + connect(m_fetchJob.get(), &NetJob::succeeded, this, [this, uid]() { + m_fetchJob.reset(); + + QJsonDocument doc; + try { + doc = Json::requireDocument(m_fetchData); + } catch (const Exception& e) { + m_infoLabel->setText( + tr("Failed to parse version list: %1").arg(e.cause())); + return; + } + if (!doc.isObject()) { + m_infoLabel->setText(tr("Failed to parse version list.")); + return; + } + + m_versions = JavaDownload::parseVersionIndex(doc.object(), uid); + m_versionList->clear(); + + for (const auto& ver : m_versions) { + QString displayName = ver.versionId; + if (displayName.startsWith("java")) { + displayName = "Java " + displayName.mid(4); + } + m_versionList->addItem(displayName); + } + + if (m_versions.size() > 0) { + m_infoLabel->setText(tr("Select a version.")); + } else { + m_infoLabel->setText( + tr("No versions available for this provider.")); + } + }); + + connect(m_fetchJob.get(), &NetJob::failed, this, [this](QString reason) { + m_fetchJob.reset(); + m_infoLabel->setText(tr("Failed to load versions: %1").arg(reason)); + }); + + m_fetchJob->start(); +} + +void JavaDownloadDialog::fetchRuntimes(const QString& uid, + const QString& versionId) +{ + m_fetchJob.reset(); + m_fetchData.clear(); + + QString url = + QString("%1%2/%3.json").arg(BuildConfig.META_URL, uid, versionId); + m_fetchJob = + new NetJob(tr("Fetch Java runtime details"), APPLICATION->network()); + auto dl = Net::Download::makeByteArray(QUrl(url), &m_fetchData); + m_fetchJob->addNetAction(dl); + + connect(m_fetchJob.get(), &NetJob::succeeded, this, [this]() { + m_fetchJob.reset(); + + QJsonDocument doc; + try { + doc = Json::requireDocument(m_fetchData); + } catch (const Exception& e) { + m_infoLabel->setText( + tr("Failed to parse runtime details: %1").arg(e.cause())); + return; + } + if (!doc.isObject()) { + m_infoLabel->setText(tr("Failed to parse runtime details.")); + return; + } + + auto allRuntimes = JavaDownload::parseRuntimes(doc.object()); + QString myOS = JavaDownload::currentRuntimeOS(); + + m_runtimes.clear(); + m_subVersionList->clear(); + for (const auto& rt : allRuntimes) { + if (rt.runtimeOS == myOS) { + m_runtimes.append(rt); + m_subVersionList->addItem(rt.version.toString()); + } + } + + if (m_runtimes.isEmpty()) { + m_infoLabel->setText( + tr("No builds available for your platform (%1).").arg(myOS)); + m_downloadBtn->setEnabled(false); + } else { + m_infoLabel->setText(tr("Select a build to download.")); + m_subVersionList->setCurrentRow(0); + } + }); + + connect(m_fetchJob.get(), &NetJob::failed, this, [this](QString reason) { + m_fetchJob.reset(); + m_infoLabel->setText( + tr("Failed to load runtime details: %1").arg(reason)); + }); + + m_fetchJob->start(); +} + +QString JavaDownloadDialog::javaInstallDir() const +{ + return JavaUtils::managedJavaRoot(); +} + +void JavaDownloadDialog::onDownloadClicked() +{ + int idx = m_subVersionList->currentRow(); + if (idx < 0 || idx >= m_runtimes.size()) + return; + + const auto& runtime = m_runtimes[idx]; + + // Build target directory path: {dataPath}/java/{vendor}/{name}-{version}/ + QString dirName = + QString("%1-%2").arg(runtime.name, runtime.version.toString()); + QString targetDir = + FS::PathCombine(javaInstallDir(), runtime.vendor, dirName); + + // Check if already installed + if (QDir(targetDir).exists()) { + auto result = QMessageBox::question( + this, tr("Already Installed"), + tr("This Java version appears to be already installed at:\n%1\n\n" + "Do you want to reinstall it?") + .arg(targetDir), + QMessageBox::Yes | QMessageBox::No); + if (result != QMessageBox::Yes) { + return; + } + // Remove existing installation + QDir(targetDir).removeRecursively(); + } + + m_downloadBtn->setEnabled(false); + m_providerList->setEnabled(false); + m_versionList->setEnabled(false); + m_subVersionList->setEnabled(false); + m_progressBar->setVisible(true); + m_progressBar->setValue(0); + m_statusLabel->setVisible(true); + + m_downloadTask = + std::make_unique<JavaDownloadTask>(runtime, targetDir, this); + + connect(m_downloadTask.get(), &Task::progress, this, + [this](qint64 current, qint64 total) { + if (total > 0) { + m_progressBar->setValue( + static_cast<int>(current * 100 / total)); + } + }); + + connect(m_downloadTask.get(), &Task::status, this, + [this](const QString& status) { m_statusLabel->setText(status); }); + + connect(m_downloadTask.get(), &Task::succeeded, this, [this]() { + m_installedJavaPath = m_downloadTask->installedJavaPath(); + m_progressBar->setValue(100); + m_statusLabel->setText(tr("Java installed successfully!")); + + QMessageBox::information( + this, tr("Download Complete"), + tr("Java has been downloaded and installed successfully.\n\n" + "Java binary: %1") + .arg(m_installedJavaPath)); + + accept(); + }); + + connect(m_downloadTask.get(), &Task::failed, this, + [this](const QString& reason) { + m_progressBar->setVisible(false); + m_statusLabel->setText(tr("Download failed: %1").arg(reason)); + m_downloadBtn->setEnabled(true); + m_providerList->setEnabled(true); + m_versionList->setEnabled(true); + m_subVersionList->setEnabled(true); + + QMessageBox::warning( + this, tr("Download Failed"), + tr("Failed to download Java:\n%1").arg(reason)); + }); + + m_downloadTask->start(); +} + +void JavaDownloadDialog::onCancelClicked() +{ + if (m_fetchJob) { + m_fetchJob->abort(); + m_fetchJob.reset(); + } + if (m_downloadTask) { + m_downloadTask->abort(); + m_downloadTask.reset(); + } + reject(); +} diff --git a/meshmc/launcher/ui/dialogs/JavaDownloadDialog.h b/meshmc/launcher/ui/dialogs/JavaDownloadDialog.h new file mode 100644 index 0000000000..48d29a7328 --- /dev/null +++ b/meshmc/launcher/ui/dialogs/JavaDownloadDialog.h @@ -0,0 +1,82 @@ +/* SPDX-FileCopyrightText: 2026 Project Tick + * SPDX-FileContributor: Project Tick + * SPDX-License-Identifier: GPL-3.0-or-later + * + * MeshMC - A Custom Launcher for Minecraft + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <QDialog> +#include <QListWidget> +#include <QPushButton> +#include <QProgressBar> +#include <QLabel> +#include <QSplitter> + +#include "java/download/JavaRuntime.h" +#include "java/download/JavaDownloadTask.h" +#include "net/NetJob.h" + +class JavaDownloadDialog : public QDialog +{ + Q_OBJECT + + public: + explicit JavaDownloadDialog(QWidget* parent = nullptr); + ~JavaDownloadDialog() override = default; + + QString installedJavaPath() const + { + return m_installedJavaPath; + } + + private slots: + void providerChanged(int index); + void majorVersionChanged(int index); + void subVersionChanged(int index); + void onDownloadClicked(); + void onCancelClicked(); + + private: + void setupUi(); + void fetchVersionList(const QString& uid); + void fetchRuntimes(const QString& uid, const QString& versionId); + QString javaInstallDir() const; + + // Left panel: providers + QListWidget* m_providerList = nullptr; + // Center panel: major versions (Java 25, Java 21, ...) + QListWidget* m_versionList = nullptr; + // Right panel: sub-versions / builds + QListWidget* m_subVersionList = nullptr; + + QLabel* m_infoLabel = nullptr; + QLabel* m_statusLabel = nullptr; + QPushButton* m_downloadBtn = nullptr; + QPushButton* m_cancelBtn = nullptr; + QProgressBar* m_progressBar = nullptr; + + QList<JavaDownload::JavaProviderInfo> m_providers; + QList<JavaDownload::JavaVersionInfo> m_versions; + QList<JavaDownload::RuntimeEntry> m_runtimes; + + NetJob::Ptr m_fetchJob; + QByteArray m_fetchData; + std::unique_ptr<JavaDownloadTask> m_downloadTask; + QString m_installedJavaPath; +}; diff --git a/meshmc/launcher/ui/dialogs/MSALoginDialog.cpp b/meshmc/launcher/ui/dialogs/MSALoginDialog.cpp new file mode 100644 index 0000000000..2038ab8bdf --- /dev/null +++ b/meshmc/launcher/ui/dialogs/MSALoginDialog.cpp @@ -0,0 +1,147 @@ +/* SPDX-FileCopyrightText: 2026 Project Tick + * SPDX-FileContributor: Project Tick + * SPDX-License-Identifier: GPL-3.0-or-later + * + * MeshMC - A Custom Launcher for Minecraft + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "MSALoginDialog.h" +#include "ui_MSALoginDialog.h" + +#include "minecraft/auth/AccountTask.h" + +#include <QtWidgets/QPushButton> +#include <QUrl> + +MSALoginDialog::MSALoginDialog(QWidget* parent) + : QDialog(parent), ui(new Ui::MSALoginDialog) +{ + ui->setupUi(this); + ui->progressBar->setVisible(false); + + connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); +} + +int MSALoginDialog::exec() +{ + setUserInputsEnabled(false); + ui->progressBar->setVisible(true); + ui->progressBar->setMaximum(0); // Indeterminate progress + ui->label->setText(tr("Opening your browser for Microsoft login...")); + + // Setup the login task and start it + m_account = MinecraftAccount::createBlankMSA(); + m_loginTask = m_account->loginMSA(); + connect(m_loginTask.get(), &Task::failed, this, + &MSALoginDialog::onTaskFailed); + connect(m_loginTask.get(), &Task::succeeded, this, + &MSALoginDialog::onTaskSucceeded); + connect(m_loginTask.get(), &Task::status, this, + &MSALoginDialog::onTaskStatus); + connect(m_loginTask.get(), &Task::progress, this, + &MSALoginDialog::onTaskProgress); + connect(m_loginTask.get(), &AccountTask::authorizeWithBrowser, this, + &MSALoginDialog::onAuthorizeWithBrowser); + m_loginTask->start(); + + return QDialog::exec(); +} + +MSALoginDialog::~MSALoginDialog() +{ + delete ui; +} + +void MSALoginDialog::onAuthorizeWithBrowser(const QUrl& url) +{ + QString urlString = url.toString(); + QString linkString = + QString("<a href=\"%1\">%2</a>").arg(urlString, tr("here")); + ui->label->setText( + tr("<p>A browser window will open for Microsoft login.</p>" + "<p>If it doesn't open automatically, click %1.</p>") + .arg(linkString)); +} + +void MSALoginDialog::setUserInputsEnabled(bool enable) +{ + ui->buttonBox->setEnabled(enable); +} + +void MSALoginDialog::onTaskFailed(const QString& reason) +{ + // Set message + auto lines = reason.split('\n'); + QString processed; + for (auto line : lines) { + if (line.size()) { + processed += "<font color='red'>" + line + "</font><br />"; + } else { + processed += "<br />"; + } + } + ui->label->setText(processed); + + // Re-enable user-interaction + setUserInputsEnabled(true); + ui->progressBar->setVisible(false); +} + +void MSALoginDialog::onTaskSucceeded() +{ + QDialog::accept(); +} + +void MSALoginDialog::onTaskStatus(const QString& status) +{ + ui->label->setText(status); +} + +void MSALoginDialog::onTaskProgress(qint64 current, qint64 total) +{ + ui->progressBar->setMaximum(total); + ui->progressBar->setValue(current); +} + +// Public interface +MinecraftAccountPtr MSALoginDialog::newAccount(QWidget* parent, QString msg) +{ + MSALoginDialog dlg(parent); + dlg.ui->label->setText(msg); + if (dlg.exec() == QDialog::Accepted) { + return dlg.m_account; + } + return nullptr; +} diff --git a/meshmc/launcher/ui/dialogs/MSALoginDialog.h b/meshmc/launcher/ui/dialogs/MSALoginDialog.h new file mode 100644 index 0000000000..b68074d232 --- /dev/null +++ b/meshmc/launcher/ui/dialogs/MSALoginDialog.h @@ -0,0 +1,77 @@ +/* SPDX-FileCopyrightText: 2026 Project Tick + * SPDX-FileContributor: Project Tick + * SPDX-License-Identifier: GPL-3.0-or-later + * + * MeshMC - A Custom Launcher for Minecraft + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <QtWidgets/QDialog> +#include <QtCore/QEventLoop> + +#include "minecraft/auth/MinecraftAccount.h" + +namespace Ui +{ + class MSALoginDialog; +} + +class MSALoginDialog : public QDialog +{ + Q_OBJECT + + public: + ~MSALoginDialog(); + + static MinecraftAccountPtr newAccount(QWidget* parent, QString message); + int exec() override; + + private: + explicit MSALoginDialog(QWidget* parent = 0); + + void setUserInputsEnabled(bool enable); + + protected slots: + void onTaskFailed(const QString& reason); + void onTaskSucceeded(); + void onTaskStatus(const QString& status); + void onTaskProgress(qint64 current, qint64 total); + void onAuthorizeWithBrowser(const QUrl& url); + + private: + Ui::MSALoginDialog* ui; + MinecraftAccountPtr m_account; + shared_qobject_ptr<AccountTask> m_loginTask; +}; diff --git a/meshmc/launcher/ui/dialogs/MSALoginDialog.ui b/meshmc/launcher/ui/dialogs/MSALoginDialog.ui new file mode 100644 index 0000000000..78cbfb269f --- /dev/null +++ b/meshmc/launcher/ui/dialogs/MSALoginDialog.ui @@ -0,0 +1,65 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>MSALoginDialog</class> + <widget class="QDialog" name="MSALoginDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>491</width> + <height>143</height> + </rect> + </property> + <property name="sizePolicy"> + <sizepolicy hsizetype="Fixed" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="windowTitle"> + <string>Add Microsoft Account</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QLabel" name="label"> + <property name="text"> + <string notr="true">Message label placeholder. + +aaaaa</string> + </property> + <property name="textFormat"> + <enum>Qt::RichText</enum> + </property> + <property name="openExternalLinks"> + <bool>true</bool> + </property> + <property name="textInteractionFlags"> + <set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set> + </property> + </widget> + </item> + <item> + <widget class="QProgressBar" name="progressBar"> + <property name="value"> + <number>24</number> + </property> + <property name="textVisible"> + <bool>false</bool> + </property> + </widget> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel</set> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui> diff --git a/meshmc/launcher/ui/dialogs/NewComponentDialog.cpp b/meshmc/launcher/ui/dialogs/NewComponentDialog.cpp new file mode 100644 index 0000000000..f87e94afb6 --- /dev/null +++ b/meshmc/launcher/ui/dialogs/NewComponentDialog.cpp @@ -0,0 +1,129 @@ +/* SPDX-FileCopyrightText: 2026 Project Tick + * SPDX-FileContributor: Project Tick + * SPDX-License-Identifier: GPL-3.0-or-later + * + * MeshMC - A Custom Launcher for Minecraft + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Application.h" +#include "NewComponentDialog.h" +#include "ui_NewComponentDialog.h" + +#include <BaseVersion.h> +#include <icons/IconList.h> +#include <tasks/Task.h> +#include <InstanceList.h> + +#include "VersionSelectDialog.h" +#include "ProgressDialog.h" +#include "IconPickerDialog.h" + +#include <QLayout> +#include <QPushButton> +#include <QFileDialog> +#include <QValidator> + +#include <meta/Index.h> +#include <meta/VersionList.h> + +NewComponentDialog::NewComponentDialog(const QString& initialName, + const QString& initialUid, + QWidget* parent) + : QDialog(parent), ui(new Ui::NewComponentDialog) +{ + ui->setupUi(this); + resize(minimumSizeHint()); + + ui->nameTextBox->setText(initialName); + ui->uidTextBox->setText(initialUid); + + connect(ui->nameTextBox, &QLineEdit::textChanged, this, + &NewComponentDialog::updateDialogState); + connect(ui->uidTextBox, &QLineEdit::textChanged, this, + &NewComponentDialog::updateDialogState); + + auto groups = APPLICATION->instances()->getGroups(); + groups.removeDuplicates(); + ui->nameTextBox->setFocus(); + + originalPlaceholderText = ui->uidTextBox->placeholderText(); + updateDialogState(); +} + +NewComponentDialog::~NewComponentDialog() +{ + delete ui; +} + +void NewComponentDialog::updateDialogState() +{ + auto protoUid = ui->nameTextBox->text().toLower(); + protoUid.remove(QRegularExpression("[^a-z]")); + if (protoUid.isEmpty()) { + ui->uidTextBox->setPlaceholderText(originalPlaceholderText); + } else { + QString suggestedUid = "org.projecttick.custom." + protoUid; + ui->uidTextBox->setPlaceholderText(suggestedUid); + } + bool allowOK = + !name().isEmpty() && !uid().isEmpty() && !uidBlacklist.contains(uid()); + ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(allowOK); +} + +QString NewComponentDialog::name() const +{ + auto result = ui->nameTextBox->text(); + if (result.size()) { + return result.trimmed(); + } + return QString(); +} + +QString NewComponentDialog::uid() const +{ + auto result = ui->uidTextBox->text(); + if (result.size()) { + return result.trimmed(); + } + result = ui->uidTextBox->placeholderText(); + if (result.size() && result != originalPlaceholderText) { + return result.trimmed(); + } + return QString(); +} + +void NewComponentDialog::setBlacklist(QStringList badUids) +{ + uidBlacklist = badUids; +} diff --git a/meshmc/launcher/ui/dialogs/NewComponentDialog.h b/meshmc/launcher/ui/dialogs/NewComponentDialog.h new file mode 100644 index 0000000000..7ad8b89ee2 --- /dev/null +++ b/meshmc/launcher/ui/dialogs/NewComponentDialog.h @@ -0,0 +1,73 @@ +/* SPDX-FileCopyrightText: 2026 Project Tick + * SPDX-FileContributor: Project Tick + * SPDX-License-Identifier: GPL-3.0-or-later + * + * MeshMC - A Custom Launcher for Minecraft + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <QDialog> + +#include <QString> +#include <QStringList> + +namespace Ui +{ + class NewComponentDialog; +} + +class NewComponentDialog : public QDialog +{ + Q_OBJECT + + public: + explicit NewComponentDialog(const QString& initialName = QString(), + const QString& initialUid = QString(), + QWidget* parent = 0); + virtual ~NewComponentDialog(); + void setBlacklist(QStringList badUids); + + QString name() const; + QString uid() const; + + private slots: + void updateDialogState(); + + private: + Ui::NewComponentDialog* ui; + + QString originalPlaceholderText; + QStringList uidBlacklist; +}; diff --git a/meshmc/launcher/ui/dialogs/NewComponentDialog.ui b/meshmc/launcher/ui/dialogs/NewComponentDialog.ui new file mode 100644 index 0000000000..03b0d22294 --- /dev/null +++ b/meshmc/launcher/ui/dialogs/NewComponentDialog.ui @@ -0,0 +1,101 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>NewComponentDialog</class> + <widget class="QDialog" name="NewComponentDialog"> + <property name="windowModality"> + <enum>Qt::ApplicationModal</enum> + </property> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>345</width> + <height>146</height> + </rect> + </property> + <property name="windowTitle"> + <string>Add Empty Component</string> + </property> + <property name="windowIcon"> + <iconset> + <normaloff>:/icons/toolbar/copy</normaloff>:/icons/toolbar/copy</iconset> + </property> + <property name="modal"> + <bool>true</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QLineEdit" name="nameTextBox"> + <property name="placeholderText"> + <string>Name</string> + </property> + </widget> + </item> + <item> + <widget class="QLineEdit" name="uidTextBox"> + <property name="placeholderText"> + <string>uid</string> + </property> + </widget> + </item> + <item> + <widget class="Line" name="line"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </widget> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </widget> + <tabstops> + <tabstop>nameTextBox</tabstop> + <tabstop>uidTextBox</tabstop> + </tabstops> + <resources> + <include location="../../graphics.qrc"/> + </resources> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>NewComponentDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>248</x> + <y>254</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>NewComponentDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>316</x> + <y>260</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui> diff --git a/meshmc/launcher/ui/dialogs/NewInstanceDialog.cpp b/meshmc/launcher/ui/dialogs/NewInstanceDialog.cpp new file mode 100644 index 0000000000..571bc3717e --- /dev/null +++ b/meshmc/launcher/ui/dialogs/NewInstanceDialog.cpp @@ -0,0 +1,278 @@ +/* SPDX-FileCopyrightText: 2026 Project Tick + * SPDX-FileContributor: Project Tick + * SPDX-License-Identifier: GPL-3.0-or-later + * + * MeshMC - A Custom Launcher for Minecraft + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Application.h" +#include "NewInstanceDialog.h" +#include "ui_NewInstanceDialog.h" + +#include <BaseVersion.h> +#include <icons/IconList.h> +#include <tasks/Task.h> +#include <InstanceList.h> + +#include "VersionSelectDialog.h" +#include "ProgressDialog.h" +#include "IconPickerDialog.h" + +#include <QLayout> +#include <QPushButton> +#include <QFileDialog> +#include <QValidator> +#include <QDialogButtonBox> + +#include "ui/widgets/PageContainer.h" +#include "ui/pages/modplatform/VanillaPage.h" +#include "ui/pages/modplatform/atlauncher/AtlPage.h" +#include "ui/pages/modplatform/ftb/FtbPage.h" +#include "ui/pages/modplatform/legacy_ftb/Page.h" +#include "ui/pages/modplatform/flame/FlamePage.h" +#include "ui/pages/modplatform/modrinth/ModrinthPage.h" +#include "ui/pages/modplatform/ImportPage.h" +#include "ui/pages/modplatform/technic/TechnicPage.h" + +NewInstanceDialog::NewInstanceDialog(const QString& initialGroup, + const QString& url, QWidget* parent) + : QDialog(parent), ui(new Ui::NewInstanceDialog) +{ + ui->setupUi(this); + + setWindowIcon(APPLICATION->getThemedIcon("new")); + + InstIconKey = "default"; + ui->iconButton->setIcon(APPLICATION->icons()->getIcon(InstIconKey)); + + auto groupList = APPLICATION->instances()->getGroups(); + groupList.removeDuplicates(); + groupList.sort(Qt::CaseInsensitive); + groupList.removeOne(""); + groupList.push_front(initialGroup); + groupList.push_front(""); + ui->groupBox->addItems(groupList); + int index = groupList.indexOf(initialGroup); + if (index == -1) { + index = 0; + } + ui->groupBox->setCurrentIndex(index); + ui->groupBox->lineEdit()->setPlaceholderText(tr("No group")); + + // NOTE: m_buttons must be initialized before PageContainer, because it + // indirectly accesses m_buttons through setSuggestedPack! Do not move + // this below. + m_buttons = + new QDialogButtonBox(QDialogButtonBox::Help | QDialogButtonBox::Ok | + QDialogButtonBox::Cancel); + + m_container = new PageContainer(this); + m_container->setSizePolicy(QSizePolicy::Policy::Preferred, + QSizePolicy::Policy::Expanding); + m_container->layout()->setContentsMargins(0, 0, 0, 0); + ui->verticalLayout->insertWidget(2, m_container); + + m_container->addButtons(m_buttons); + + // Bonk Qt over its stupid head and make sure it understands which button is + // the default one... See: + // https://stackoverflow.com/questions/24556831/qbuttonbox-set-default-button + auto OkButton = m_buttons->button(QDialogButtonBox::Ok); + OkButton->setDefault(true); + OkButton->setAutoDefault(true); + connect(OkButton, &QPushButton::clicked, this, &NewInstanceDialog::accept); + + auto CancelButton = m_buttons->button(QDialogButtonBox::Cancel); + CancelButton->setDefault(false); + CancelButton->setAutoDefault(false); + connect(CancelButton, &QPushButton::clicked, this, + &NewInstanceDialog::reject); + + auto HelpButton = m_buttons->button(QDialogButtonBox::Help); + HelpButton->setDefault(false); + HelpButton->setAutoDefault(false); + connect(HelpButton, &QPushButton::clicked, m_container, + &PageContainer::help); + + if (!url.isEmpty()) { + QUrl actualUrl(url); + m_container->selectPage("import"); + importPage->setUrl(url); + } + + updateDialogState(); + + restoreGeometry(QByteArray::fromBase64( + APPLICATION->settings()->get("NewInstanceGeometry").toByteArray())); +} + +void NewInstanceDialog::reject() +{ + APPLICATION->settings()->set("NewInstanceGeometry", + saveGeometry().toBase64()); + QDialog::reject(); +} + +void NewInstanceDialog::accept() +{ + APPLICATION->settings()->set("NewInstanceGeometry", + saveGeometry().toBase64()); + importIconNow(); + QDialog::accept(); +} + +QList<BasePage*> NewInstanceDialog::getPages() +{ + importPage = new ImportPage(this); + flamePage = new FlamePage(this); + auto technicPage = new TechnicPage(this); + return {new VanillaPage(this), importPage, + new AtlPage(this), flamePage, + new ModrinthPage(this), new FtbPage(this), + new LegacyFTB::Page(this), technicPage}; +} + +QString NewInstanceDialog::dialogTitle() +{ + return tr("New Instance"); +} + +NewInstanceDialog::~NewInstanceDialog() +{ + delete ui; +} + +void NewInstanceDialog::setSuggestedPack(const QString& name, + InstanceTask* task) +{ + creationTask.reset(task); + ui->instNameTextBox->setPlaceholderText(name); + + if (!task) { + ui->iconButton->setIcon(APPLICATION->icons()->getIcon("default")); + importIcon = false; + } + + auto allowOK = task && !instName().isEmpty(); + m_buttons->button(QDialogButtonBox::Ok)->setEnabled(allowOK); +} + +void NewInstanceDialog::setSuggestedIconFromFile(const QString& path, + const QString& name) +{ + importIcon = true; + importIconPath = path; + importIconName = name; + + // Hmm, for some reason they can be to small + ui->iconButton->setIcon(QIcon(path)); +} + +void NewInstanceDialog::setSuggestedIcon(const QString& key) +{ + auto icon = APPLICATION->icons()->getIcon(key); + importIcon = false; + + ui->iconButton->setIcon(icon); +} + +InstanceTask* NewInstanceDialog::extractTask() +{ + InstanceTask* extracted = creationTask.get(); + creationTask.release(); + extracted->setName(instName()); + extracted->setGroup(instGroup()); + extracted->setIcon(iconKey()); + return extracted; +} + +void NewInstanceDialog::updateDialogState() +{ + auto allowOK = creationTask && !instName().isEmpty(); + auto OkButton = m_buttons->button(QDialogButtonBox::Ok); + if (OkButton->isEnabled() != allowOK) { + OkButton->setEnabled(allowOK); + } +} + +QString NewInstanceDialog::instName() const +{ + auto result = ui->instNameTextBox->text().trimmed(); + if (result.size()) { + return result; + } + result = ui->instNameTextBox->placeholderText().trimmed(); + if (result.size()) { + return result; + } + return QString(); +} + +QString NewInstanceDialog::instGroup() const +{ + return ui->groupBox->currentText(); +} +QString NewInstanceDialog::iconKey() const +{ + return InstIconKey; +} + +void NewInstanceDialog::on_iconButton_clicked() +{ + importIconNow(); // so the user can switch back + IconPickerDialog dlg(this); + dlg.execWithSelection(InstIconKey); + + if (dlg.result() == QDialog::Accepted) { + InstIconKey = dlg.selectedIconKey; + ui->iconButton->setIcon(APPLICATION->icons()->getIcon(InstIconKey)); + importIcon = false; + } +} + +void NewInstanceDialog::on_instNameTextBox_textChanged(const QString& arg1) +{ + updateDialogState(); +} + +void NewInstanceDialog::importIconNow() +{ + if (importIcon) { + APPLICATION->icons()->installIcon(importIconPath, importIconName); + InstIconKey = importIconName; + importIcon = false; + } + APPLICATION->settings()->set("NewInstanceGeometry", + saveGeometry().toBase64()); +} diff --git a/meshmc/launcher/ui/dialogs/NewInstanceDialog.h b/meshmc/launcher/ui/dialogs/NewInstanceDialog.h new file mode 100644 index 0000000000..ba042eca1a --- /dev/null +++ b/meshmc/launcher/ui/dialogs/NewInstanceDialog.h @@ -0,0 +1,106 @@ +/* SPDX-FileCopyrightText: 2026 Project Tick + * SPDX-FileContributor: Project Tick + * SPDX-License-Identifier: GPL-3.0-or-later + * + * MeshMC - A Custom Launcher for Minecraft + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <QDialog> + +#include "BaseVersion.h" +#include "ui/pages/BasePageProvider.h" +#include "InstanceTask.h" + +namespace Ui +{ + class NewInstanceDialog; +} + +class PageContainer; +class QDialogButtonBox; +class ImportPage; +class FlamePage; + +class NewInstanceDialog : public QDialog, public BasePageProvider +{ + Q_OBJECT + + public: + explicit NewInstanceDialog(const QString& initialGroup, + const QString& url = QString(), + QWidget* parent = 0); + ~NewInstanceDialog(); + + void updateDialogState(); + + void setSuggestedPack(const QString& name = QString(), + InstanceTask* task = nullptr); + void setSuggestedIconFromFile(const QString& path, const QString& name); + void setSuggestedIcon(const QString& key); + + InstanceTask* extractTask(); + + QString dialogTitle() override; + QList<BasePage*> getPages() override; + + QString instName() const; + QString instGroup() const; + QString iconKey() const; + + public slots: + void accept() override; + void reject() override; + + private slots: + void on_iconButton_clicked(); + void on_instNameTextBox_textChanged(const QString& arg1); + + private: + Ui::NewInstanceDialog* ui = nullptr; + PageContainer* m_container = nullptr; + QDialogButtonBox* m_buttons = nullptr; + + QString InstIconKey; + ImportPage* importPage = nullptr; + FlamePage* flamePage = nullptr; + std::unique_ptr<InstanceTask> creationTask; + + bool importIcon = false; + QString importIconPath; + QString importIconName; + + void importIconNow(); +}; diff --git a/meshmc/launcher/ui/dialogs/NewInstanceDialog.ui b/meshmc/launcher/ui/dialogs/NewInstanceDialog.ui new file mode 100644 index 0000000000..7fb19ff5cf --- /dev/null +++ b/meshmc/launcher/ui/dialogs/NewInstanceDialog.ui @@ -0,0 +1,87 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>NewInstanceDialog</class> + <widget class="QDialog" name="NewInstanceDialog"> + <property name="windowModality"> + <enum>Qt::ApplicationModal</enum> + </property> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>730</width> + <height>127</height> + </rect> + </property> + <property name="windowTitle"> + <string>New Instance</string> + </property> + <property name="windowIcon"> + <iconset> + <normaloff>:/icons/toolbar/new</normaloff>:/icons/toolbar/new</iconset> + </property> + <property name="modal"> + <bool>true</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <layout class="QGridLayout" name="gridLayout_2"> + <item row="1" column="2"> + <widget class="QComboBox" name="groupBox"> + <property name="editable"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QLabel" name="groupLabel"> + <property name="text"> + <string>&Group:</string> + </property> + <property name="buddy"> + <cstring>groupBox</cstring> + </property> + </widget> + </item> + <item row="0" column="2"> + <widget class="QLineEdit" name="instNameTextBox"/> + </item> + <item row="0" column="1"> + <widget class="QLabel" name="nameLabel"> + <property name="text"> + <string>&Name:</string> + </property> + <property name="buddy"> + <cstring>instNameTextBox</cstring> + </property> + </widget> + </item> + <item row="0" column="0" rowspan="2"> + <widget class="QToolButton" name="iconButton"> + <property name="iconSize"> + <size> + <width>80</width> + <height>80</height> + </size> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="Line" name="line"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </widget> + </item> + </layout> + </widget> + <tabstops> + <tabstop>iconButton</tabstop> + <tabstop>instNameTextBox</tabstop> + <tabstop>groupBox</tabstop> + </tabstops> + <resources/> + <connections/> +</ui> diff --git a/meshmc/launcher/ui/dialogs/NotificationDialog.cpp b/meshmc/launcher/ui/dialogs/NotificationDialog.cpp new file mode 100644 index 0000000000..6d43d09ebd --- /dev/null +++ b/meshmc/launcher/ui/dialogs/NotificationDialog.cpp @@ -0,0 +1,103 @@ +/* SPDX-FileCopyrightText: 2026 Project Tick + * SPDX-FileContributor: Project Tick + * SPDX-License-Identifier: GPL-3.0-or-later + * + * MeshMC - A Custom Launcher for Minecraft + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include "NotificationDialog.h" +#include "ui_NotificationDialog.h" + +#include <QTimerEvent> +#include <QStyle> + +NotificationDialog::NotificationDialog( + const NotificationChecker::NotificationEntry& entry, QWidget* parent) + : QDialog(parent, Qt::MSWindowsFixedSizeDialogHint | Qt::WindowTitleHint | + Qt::CustomizeWindowHint), + ui(new Ui::NotificationDialog) +{ + ui->setupUi(this); + + QStyle::StandardPixmap icon; + switch (entry.type) { + case NotificationChecker::NotificationEntry::Critical: + icon = QStyle::SP_MessageBoxCritical; + break; + case NotificationChecker::NotificationEntry::Warning: + icon = QStyle::SP_MessageBoxWarning; + break; + default: + case NotificationChecker::NotificationEntry::Information: + icon = QStyle::SP_MessageBoxInformation; + break; + } + ui->iconLabel->setPixmap(style()->standardPixmap(icon, 0, this)); + ui->messageLabel->setText(entry.message); + + m_dontShowAgainText = tr("Don't show again"); + m_closeText = tr("Close"); + + ui->dontShowAgainBtn->setText(m_dontShowAgainText + + QString(" (%1)").arg(m_dontShowAgainTime)); + ui->closeBtn->setText(m_closeText + QString(" (%1)").arg(m_closeTime)); + + startTimer(1000); +} + +NotificationDialog::~NotificationDialog() +{ + delete ui; +} + +void NotificationDialog::timerEvent(QTimerEvent* event) +{ + if (m_dontShowAgainTime > 0) { + m_dontShowAgainTime--; + if (m_dontShowAgainTime == 0) { + ui->dontShowAgainBtn->setText(m_dontShowAgainText); + ui->dontShowAgainBtn->setEnabled(true); + } else { + ui->dontShowAgainBtn->setText( + m_dontShowAgainText + + QString(" (%1)").arg(m_dontShowAgainTime)); + } + } + if (m_closeTime > 0) { + m_closeTime--; + if (m_closeTime == 0) { + ui->closeBtn->setText(m_closeText); + ui->closeBtn->setEnabled(true); + } else { + ui->closeBtn->setText(m_closeText + + QString(" (%1)").arg(m_closeTime)); + } + } + + if (m_closeTime == 0 && m_dontShowAgainTime == 0) { + killTimer(event->timerId()); + } +} + +void NotificationDialog::on_dontShowAgainBtn_clicked() +{ + done(DontShowAgain); +} +void NotificationDialog::on_closeBtn_clicked() +{ + done(Normal); +} diff --git a/meshmc/launcher/ui/dialogs/NotificationDialog.h b/meshmc/launcher/ui/dialogs/NotificationDialog.h new file mode 100644 index 0000000000..82f578f5fb --- /dev/null +++ b/meshmc/launcher/ui/dialogs/NotificationDialog.h @@ -0,0 +1,63 @@ +/* SPDX-FileCopyrightText: 2026 Project Tick + * SPDX-FileContributor: Project Tick + * SPDX-License-Identifier: GPL-3.0-or-later + * + * MeshMC - A Custom Launcher for Minecraft + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#ifndef NOTIFICATIONDIALOG_H +#define NOTIFICATIONDIALOG_H + +#include <QDialog> + +#include "notifications/NotificationChecker.h" + +namespace Ui +{ + class NotificationDialog; +} + +class NotificationDialog : public QDialog +{ + Q_OBJECT + + public: + explicit NotificationDialog( + const NotificationChecker::NotificationEntry& entry, + QWidget* parent = 0); + ~NotificationDialog(); + + enum ExitCode { Normal, DontShowAgain }; + + protected: + void timerEvent(QTimerEvent* event); + + private: + Ui::NotificationDialog* ui; + + int m_dontShowAgainTime = 10; + int m_closeTime = 5; + + QString m_dontShowAgainText; + QString m_closeText; + + private slots: + void on_dontShowAgainBtn_clicked(); + void on_closeBtn_clicked(); +}; + +#endif // NOTIFICATIONDIALOG_H diff --git a/meshmc/launcher/ui/dialogs/NotificationDialog.ui b/meshmc/launcher/ui/dialogs/NotificationDialog.ui new file mode 100644 index 0000000000..3e6c22bc80 --- /dev/null +++ b/meshmc/launcher/ui/dialogs/NotificationDialog.ui @@ -0,0 +1,85 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>NotificationDialog</class> + <widget class="QDialog" name="NotificationDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>320</width> + <height>240</height> + </rect> + </property> + <property name="windowTitle"> + <string>Notification</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout" stretch="1,0"> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_2" stretch="0,1"> + <item> + <widget class="QLabel" name="iconLabel"> + <property name="text"> + <string notr="true">TextLabel</string> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="messageLabel"> + <property name="text"> + <string notr="true">TextLabel</string> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + <property name="openExternalLinks"> + <bool>true</bool> + </property> + <property name="textInteractionFlags"> + <set>Qt::TextBrowserInteraction</set> + </property> + </widget> + </item> + </layout> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QPushButton" name="dontShowAgainBtn"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="text"> + <string>Don't show again</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="closeBtn"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="text"> + <string>Close</string> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui> diff --git a/meshmc/launcher/ui/dialogs/ProfileSelectDialog.cpp b/meshmc/launcher/ui/dialogs/ProfileSelectDialog.cpp new file mode 100644 index 0000000000..77687c699e --- /dev/null +++ b/meshmc/launcher/ui/dialogs/ProfileSelectDialog.cpp @@ -0,0 +1,137 @@ +/* SPDX-FileCopyrightText: 2026 Project Tick + * SPDX-FileContributor: Project Tick + * SPDX-License-Identifier: GPL-3.0-or-later + * + * MeshMC - A Custom Launcher for Minecraft + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ProfileSelectDialog.h" +#include "ui_ProfileSelectDialog.h" + +#include <QItemSelectionModel> +#include <QDebug> + +#include "SkinUtils.h" +#include "Application.h" + +#include "ui/dialogs/ProgressDialog.h" + +ProfileSelectDialog::ProfileSelectDialog(const QString& message, int flags, + QWidget* parent) + : QDialog(parent), ui(new Ui::ProfileSelectDialog) +{ + ui->setupUi(this); + + m_accounts = APPLICATION->accounts(); + auto view = ui->listView; + // view->setModel(m_accounts.get()); + // view->hideColumn(AccountList::ActiveColumn); + view->setColumnCount(1); + view->setRootIsDecorated(false); + // FIXME: use a real model, not this + if (QTreeWidgetItem* header = view->headerItem()) { + header->setText(0, tr("Name")); + } else { + view->setHeaderLabel(tr("Name")); + } + QList<QTreeWidgetItem*> items; + for (int i = 0; i < m_accounts->count(); i++) { + MinecraftAccountPtr account = m_accounts->at(i); + QString profileLabel; + if (account->isInUse()) { + profileLabel = tr("%1 (in use)").arg(account->profileName()); + } else { + profileLabel = account->profileName(); + } + auto item = new QTreeWidgetItem(view); + item->setText(0, profileLabel); + item->setIcon(0, account->getFace()); + item->setData(0, AccountList::PointerRole, + QVariant::fromValue(account)); + items.append(item); + } + view->addTopLevelItems(items); + + // Set the message label. + ui->msgLabel->setVisible(!message.isEmpty()); + ui->msgLabel->setText(message); + + // Flags... + ui->globalDefaultCheck->setVisible(flags & GlobalDefaultCheckbox); + ui->instDefaultCheck->setVisible(flags & InstanceDefaultCheckbox); + qDebug() << flags; + + // Select the first entry in the list. + ui->listView->setCurrentIndex(ui->listView->model()->index(0, 0)); + + connect(ui->listView, SIGNAL(doubleClicked(QModelIndex)), + SLOT(on_buttonBox_accepted())); +} + +ProfileSelectDialog::~ProfileSelectDialog() +{ + delete ui; +} + +MinecraftAccountPtr ProfileSelectDialog::selectedAccount() const +{ + return m_selected; +} + +bool ProfileSelectDialog::useAsGlobalDefault() const +{ + return ui->globalDefaultCheck->isChecked(); +} + +bool ProfileSelectDialog::useAsInstDefaullt() const +{ + return ui->instDefaultCheck->isChecked(); +} + +void ProfileSelectDialog::on_buttonBox_accepted() +{ + QModelIndexList selection = + ui->listView->selectionModel()->selectedIndexes(); + if (selection.size() > 0) { + QModelIndex selected = selection.first(); + m_selected = selected.data(AccountList::PointerRole) + .value<MinecraftAccountPtr>(); + } + close(); +} + +void ProfileSelectDialog::on_buttonBox_rejected() +{ + close(); +} diff --git a/meshmc/launcher/ui/dialogs/ProfileSelectDialog.h b/meshmc/launcher/ui/dialogs/ProfileSelectDialog.h new file mode 100644 index 0000000000..f121dfcc15 --- /dev/null +++ b/meshmc/launcher/ui/dialogs/ProfileSelectDialog.h @@ -0,0 +1,115 @@ +/* SPDX-FileCopyrightText: 2026 Project Tick + * SPDX-FileContributor: Project Tick + * SPDX-License-Identifier: GPL-3.0-or-later + * + * MeshMC - A Custom Launcher for Minecraft + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <QDialog> + +#include <memory> + +#include "minecraft/auth/AccountList.h" + +namespace Ui +{ + class ProfileSelectDialog; +} + +class ProfileSelectDialog : public QDialog +{ + Q_OBJECT + public: + enum Flags { + NoFlags = 0, + + /*! + * Shows a check box on the dialog that allows the user to specify that + * the account they've selected should be used as the global default for + * all instances. + */ + GlobalDefaultCheckbox, + + /*! + * Shows a check box on the dialog that allows the user to specify that + * the account they've selected should be used as the default for the + * instance they are currently launching. This is not currently + * implemented. + */ + InstanceDefaultCheckbox, + }; + + /*! + * Constructs a new account select dialog with the given parent and message. + * The message will be shown at the top of the dialog. It is an empty string + * by default. + */ + explicit ProfileSelectDialog(const QString& message = "", int flags = 0, + QWidget* parent = 0); + ~ProfileSelectDialog(); + + /*! + * Gets a pointer to the account that the user selected. + * This is null if the user clicked cancel or hasn't clicked OK yet. + */ + MinecraftAccountPtr selectedAccount() const; + + /*! + * Returns true if the user checked the "use as global default" checkbox. + * If the checkbox wasn't shown, this function returns false. + */ + bool useAsGlobalDefault() const; + + /*! + * Returns true if the user checked the "use as instance default" checkbox. + * If the checkbox wasn't shown, this function returns false. + */ + bool useAsInstDefaullt() const; + + public slots: + void on_buttonBox_accepted(); + + void on_buttonBox_rejected(); + + protected: + shared_qobject_ptr<AccountList> m_accounts; + + //! The account that was selected when the user clicked OK. + MinecraftAccountPtr m_selected; + + private: + Ui::ProfileSelectDialog* ui; +}; diff --git a/meshmc/launcher/ui/dialogs/ProfileSelectDialog.ui b/meshmc/launcher/ui/dialogs/ProfileSelectDialog.ui new file mode 100644 index 0000000000..e779b51bf1 --- /dev/null +++ b/meshmc/launcher/ui/dialogs/ProfileSelectDialog.ui @@ -0,0 +1,62 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>ProfileSelectDialog</class> + <widget class="QDialog" name="ProfileSelectDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>465</width> + <height>300</height> + </rect> + </property> + <property name="windowTitle"> + <string>Select an Account</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QLabel" name="msgLabel"> + <property name="text"> + <string>Select a profile.</string> + </property> + </widget> + </item> + <item> + <widget class="QTreeWidget" name="listView"> + <column> + <property name="text"> + <string notr="true">1</string> + </property> + </column> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <widget class="QCheckBox" name="globalDefaultCheck"> + <property name="text"> + <string>Use as default?</string> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="instDefaultCheck"> + <property name="text"> + <string>Use as default for this instance only?</string> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui> diff --git a/meshmc/launcher/ui/dialogs/ProfileSetupDialog.cpp b/meshmc/launcher/ui/dialogs/ProfileSetupDialog.cpp new file mode 100644 index 0000000000..9798af8199 --- /dev/null +++ b/meshmc/launcher/ui/dialogs/ProfileSetupDialog.cpp @@ -0,0 +1,298 @@ +/* SPDX-FileCopyrightText: 2026 Project Tick + * SPDX-FileContributor: Project Tick + * SPDX-License-Identifier: GPL-3.0-or-later + * + * MeshMC - A Custom Launcher for Minecraft + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ProfileSetupDialog.h" +#include "ui_ProfileSetupDialog.h" + +#include <QPushButton> +#include <QAction> +#include <QRegularExpressionValidator> +#include <QJsonDocument> +#include <QDebug> + +#include "ui/dialogs/ProgressDialog.h" + +#include <Application.h> +#include "minecraft/auth/AuthRequest.h" +#include "minecraft/auth/Parsers.h" + +ProfileSetupDialog::ProfileSetupDialog(MinecraftAccountPtr accountToSetup, + QWidget* parent) + : QDialog(parent), m_accountToSetup(accountToSetup), + ui(new Ui::ProfileSetupDialog) +{ + ui->setupUi(this); + ui->errorLabel->setVisible(false); + + goodIcon = APPLICATION->getThemedIcon("status-good"); + yellowIcon = APPLICATION->getThemedIcon("status-yellow"); + badIcon = APPLICATION->getThemedIcon("status-bad"); + + QRegularExpression permittedNames("[a-zA-Z0-9_]{3,16}"); + auto nameEdit = ui->nameEdit; + nameEdit->setValidator(new QRegularExpressionValidator(permittedNames)); + nameEdit->setClearButtonEnabled(true); + validityAction = + nameEdit->addAction(yellowIcon, QLineEdit::LeadingPosition); + connect(nameEdit, &QLineEdit::textEdited, this, + &ProfileSetupDialog::nameEdited); + + checkStartTimer.setSingleShot(true); + connect(&checkStartTimer, &QTimer::timeout, this, + &ProfileSetupDialog::startCheck); + + setNameStatus(NameStatus::NotSet, QString()); +} + +ProfileSetupDialog::~ProfileSetupDialog() +{ + delete ui; +} + +void ProfileSetupDialog::on_buttonBox_accepted() +{ + setupProfile(currentCheck); +} + +void ProfileSetupDialog::on_buttonBox_rejected() +{ + reject(); +} + +void ProfileSetupDialog::setNameStatus(ProfileSetupDialog::NameStatus status, + QString errorString = QString()) +{ + nameStatus = status; + auto okButton = ui->buttonBox->button(QDialogButtonBox::Ok); + switch (nameStatus) { + case NameStatus::Available: { + validityAction->setIcon(goodIcon); + okButton->setEnabled(true); + } break; + case NameStatus::NotSet: + case NameStatus::Pending: + validityAction->setIcon(yellowIcon); + okButton->setEnabled(false); + break; + case NameStatus::Exists: + case NameStatus::Error: + validityAction->setIcon(badIcon); + okButton->setEnabled(false); + break; + } + if (!errorString.isEmpty()) { + ui->errorLabel->setText(errorString); + ui->errorLabel->setVisible(true); + } else { + ui->errorLabel->setVisible(false); + } +} + +void ProfileSetupDialog::nameEdited(const QString& name) +{ + if (!ui->nameEdit->hasAcceptableInput()) { + setNameStatus(NameStatus::NotSet, + tr("Name is too short - must be between 3 and 16 " + "characters long.")); + return; + } + scheduleCheck(name); +} + +void ProfileSetupDialog::scheduleCheck(const QString& name) +{ + queuedCheck = name; + setNameStatus(NameStatus::Pending); + checkStartTimer.start(1000); +} + +void ProfileSetupDialog::startCheck() +{ + if (isChecking) { + return; + } + if (queuedCheck.isNull()) { + return; + } + checkName(queuedCheck); +} + +void ProfileSetupDialog::checkName(const QString& name) +{ + if (isChecking) { + return; + } + + currentCheck = name; + isChecking = true; + + auto token = m_accountToSetup->accessToken(); + + auto url = QString("https://api.minecraftservices.com/minecraft/profile/" + "name/%1/available") + .arg(name); + QNetworkRequest request = QNetworkRequest(url); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + request.setRawHeader("Accept", "application/json"); + request.setRawHeader("Authorization", + QString("Bearer %1").arg(token).toUtf8()); + + AuthRequest* requestor = new AuthRequest(this); + connect(requestor, &AuthRequest::finished, this, + &ProfileSetupDialog::checkFinished); + requestor->get(request); +} + +void ProfileSetupDialog::checkFinished( + QNetworkReply::NetworkError error, QByteArray data, + QList<QNetworkReply::RawHeaderPair> headers) +{ + auto requestor = qobject_cast<AuthRequest*>(QObject::sender()); + requestor->deleteLater(); + + if (error == QNetworkReply::NoError) { + auto doc = QJsonDocument::fromJson(data); + auto root = doc.object(); + auto statusValue = root.value("status").toString("INVALID"); + if (statusValue == "AVAILABLE") { + setNameStatus(NameStatus::Available); + } else if (statusValue == "DUPLICATE") { + setNameStatus(NameStatus::Exists, + tr("Minecraft profile with name %1 already exists.") + .arg(currentCheck)); + } else if (statusValue == "NOT_ALLOWED") { + setNameStatus(NameStatus::Exists, + tr("The name %1 is not allowed.").arg(currentCheck)); + } else { + setNameStatus( + NameStatus::Error, + tr("Unhandled profile name status: %1").arg(statusValue)); + } + } else { + setNameStatus(NameStatus::Error, + tr("Failed to check name availability.")); + } + isChecking = false; +} + +void ProfileSetupDialog::setupProfile(const QString& profileName) +{ + if (isWorking) { + return; + } + + auto token = m_accountToSetup->accessToken(); + + auto url = QString("https://api.minecraftservices.com/minecraft/profile"); + QNetworkRequest request = QNetworkRequest(url); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + request.setRawHeader("Accept", "application/json"); + request.setRawHeader("Authorization", + QString("Bearer %1").arg(token).toUtf8()); + + QString payloadTemplate("{\"profileName\":\"%1\"}"); + auto data = payloadTemplate.arg(profileName).toUtf8(); + + AuthRequest* requestor = new AuthRequest(this); + connect(requestor, &AuthRequest::finished, this, + &ProfileSetupDialog::setupProfileFinished); + requestor->post(request, data); + isWorking = true; + + auto button = ui->buttonBox->button(QDialogButtonBox::Cancel); + button->setEnabled(false); +} + +namespace +{ + + struct MojangError { + static MojangError fromJSON(QByteArray data) + { + MojangError out; + out.error = QString::fromUtf8(data); + auto doc = QJsonDocument::fromJson(data, &out.parseError); + auto object = doc.object(); + + out.fullyParsed = true; + out.fullyParsed &= + Parsers::getString(object.value("path"), out.path); + out.fullyParsed &= + Parsers::getString(object.value("error"), out.error); + out.fullyParsed &= Parsers::getString(object.value("errorMessage"), + out.errorMessage); + + return out; + } + + QString rawError; + QJsonParseError parseError; + bool fullyParsed; + + QString path; + QString error; + QString errorMessage; + }; + +} // namespace + +void ProfileSetupDialog::setupProfileFinished( + QNetworkReply::NetworkError error, QByteArray data, + QList<QNetworkReply::RawHeaderPair> headers) +{ + auto requestor = qobject_cast<AuthRequest*>(QObject::sender()); + requestor->deleteLater(); + + isWorking = false; + if (error == QNetworkReply::NoError) { + /* + * data contains the profile in the response + * ... we could parse it and update the account, but let's just return + * back to the normal login flow instead... + */ + accept(); + } else { + auto parsedError = MojangError::fromJSON(data); + ui->errorLabel->setVisible(true); + ui->errorLabel->setText(tr("The server returned the following error:") + + "\n\n" + parsedError.errorMessage); + qDebug() << parsedError.rawError; + auto button = ui->buttonBox->button(QDialogButtonBox::Cancel); + button->setEnabled(true); + } +} diff --git a/meshmc/launcher/ui/dialogs/ProfileSetupDialog.h b/meshmc/launcher/ui/dialogs/ProfileSetupDialog.h new file mode 100644 index 0000000000..44cf3d8930 --- /dev/null +++ b/meshmc/launcher/ui/dialogs/ProfileSetupDialog.h @@ -0,0 +1,105 @@ +/* SPDX-FileCopyrightText: 2026 Project Tick + * SPDX-FileContributor: Project Tick + * SPDX-License-Identifier: GPL-3.0-or-later + * + * MeshMC - A Custom Launcher for Minecraft + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <QDialog> +#include <QIcon> +#include <QTimer> +#include <QNetworkReply> + +#include <memory> +#include <minecraft/auth/MinecraftAccount.h> + +namespace Ui +{ + class ProfileSetupDialog; +} + +class ProfileSetupDialog : public QDialog +{ + Q_OBJECT + public: + explicit ProfileSetupDialog(MinecraftAccountPtr accountToSetup, + QWidget* parent = 0); + ~ProfileSetupDialog(); + + enum class NameStatus { + NotSet, + Pending, + Available, + Exists, + Error + } nameStatus = NameStatus::NotSet; + + private slots: + void on_buttonBox_accepted(); + void on_buttonBox_rejected(); + + void nameEdited(const QString& name); + void checkFinished(QNetworkReply::NetworkError error, QByteArray data, + QList<QNetworkReply::RawHeaderPair> headers); + void startCheck(); + + void setupProfileFinished(QNetworkReply::NetworkError error, + QByteArray data, + QList<QNetworkReply::RawHeaderPair> headers); + + protected: + void scheduleCheck(const QString& name); + void checkName(const QString& name); + void setNameStatus(NameStatus status, QString errorString); + + void setupProfile(const QString& profileName); + + private: + MinecraftAccountPtr m_accountToSetup; + Ui::ProfileSetupDialog* ui; + QIcon goodIcon; + QIcon yellowIcon; + QIcon badIcon; + QAction* validityAction = nullptr; + + QString queuedCheck; + + bool isChecking = false; + bool isWorking = false; + QString currentCheck; + + QTimer checkStartTimer; +}; diff --git a/meshmc/launcher/ui/dialogs/ProfileSetupDialog.ui b/meshmc/launcher/ui/dialogs/ProfileSetupDialog.ui new file mode 100644 index 0000000000..9dbabb4b3e --- /dev/null +++ b/meshmc/launcher/ui/dialogs/ProfileSetupDialog.ui @@ -0,0 +1,74 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>ProfileSetupDialog</class> + <widget class="QDialog" name="ProfileSetupDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>615</width> + <height>208</height> + </rect> + </property> + <property name="windowTitle"> + <string>Choose Minecraft name</string> + </property> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="0" colspan="2"> + <widget class="QLabel" name="descriptionLabel"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Minimum" vsizetype="Minimum"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>You just need to take one more step to be able to play Minecraft on this account. + +Choose your name carefully:</string> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + <property name="buddy"> + <cstring>nameEdit</cstring> + </property> + </widget> + </item> + <item row="2" column="0"> + <widget class="QLineEdit" name="nameEdit"/> + </item> + <item row="4" column="0" colspan="2"> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + <item row="3" column="0"> + <widget class="QLabel" name="errorLabel"> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="text"> + <string notr="true">Errors go here</string> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + <property name="openExternalLinks"> + <bool>true</bool> + </property> + <property name="textInteractionFlags"> + <set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set> + </property> + </widget> + </item> + </layout> + </widget> + <tabstops> + <tabstop>nameEdit</tabstop> + </tabstops> + <resources/> + <connections/> +</ui> diff --git a/meshmc/launcher/ui/dialogs/ProgressDialog.cpp b/meshmc/launcher/ui/dialogs/ProgressDialog.cpp new file mode 100644 index 0000000000..61109946bc --- /dev/null +++ b/meshmc/launcher/ui/dialogs/ProgressDialog.cpp @@ -0,0 +1,201 @@ +/* SPDX-FileCopyrightText: 2026 Project Tick + * SPDX-FileContributor: Project Tick + * SPDX-License-Identifier: GPL-3.0-or-later + * + * MeshMC - A Custom Launcher for Minecraft + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ProgressDialog.h" +#include "ui_ProgressDialog.h" + +#include <QKeyEvent> +#include <QDebug> + +#include "tasks/Task.h" + +ProgressDialog::ProgressDialog(QWidget* parent) + : QDialog(parent), ui(new Ui::ProgressDialog) +{ + ui->setupUi(this); + this->setWindowFlags(this->windowFlags() & + ~Qt::WindowContextHelpButtonHint); + setSkipButton(false); + changeProgress(0, 100); +} + +void ProgressDialog::setSkipButton(bool present, QString label) +{ + ui->skipButton->setAutoDefault(false); + ui->skipButton->setDefault(false); + ui->skipButton->setFocusPolicy(Qt::ClickFocus); + ui->skipButton->setEnabled(present); + ui->skipButton->setVisible(present); + ui->skipButton->setText(label); + updateSize(); +} + +void ProgressDialog::on_skipButton_clicked(bool checked) +{ + Q_UNUSED(checked); + task->abort(); +} + +ProgressDialog::~ProgressDialog() +{ + delete ui; +} + +void ProgressDialog::updateSize() +{ + QSize qSize = QSize(480, minimumSizeHint().height()); + resize(qSize); + setFixedSize(qSize); +} + +int ProgressDialog::execWithTask(Task* task) +{ + this->task = task; + QDialog::DialogCode result; + + if (!task) { + qDebug() << "Programmer error: progress dialog created with null task."; + return Accepted; + } + + if (handleImmediateResult(result)) { + return result; + } + + // Connect signals. + connect(task, SIGNAL(started()), SLOT(onTaskStarted())); + connect(task, SIGNAL(failed(QString)), SLOT(onTaskFailed(QString))); + connect(task, SIGNAL(succeeded()), SLOT(onTaskSucceeded())); + connect(task, SIGNAL(status(QString)), SLOT(changeStatus(const QString&))); + connect(task, SIGNAL(progress(qint64, qint64)), + SLOT(changeProgress(qint64, qint64))); + + // if this didn't connect to an already running task, invoke start + if (!task->isRunning()) { + task->start(); + } + if (task->isRunning()) { + changeProgress(task->getProgress(), task->getTotalProgress()); + changeStatus(task->getStatus()); + return QDialog::exec(); + } else if (handleImmediateResult(result)) { + return result; + } else { + return QDialog::Rejected; + } +} + +// TODO: only provide the unique_ptr overloads +int ProgressDialog::execWithTask(std::unique_ptr<Task>&& task) +{ + connect(this, &ProgressDialog::destroyed, task.get(), &Task::deleteLater); + return execWithTask(task.release()); +} +int ProgressDialog::execWithTask(std::unique_ptr<Task>& task) +{ + connect(this, &ProgressDialog::destroyed, task.get(), &Task::deleteLater); + return execWithTask(task.release()); +} + +bool ProgressDialog::handleImmediateResult(QDialog::DialogCode& result) +{ + if (task->isFinished()) { + if (task->wasSuccessful()) { + result = QDialog::Accepted; + } else { + result = QDialog::Rejected; + } + return true; + } + return false; +} + +Task* ProgressDialog::getTask() +{ + return task; +} + +void ProgressDialog::onTaskStarted() {} + +void ProgressDialog::onTaskFailed(QString failure) +{ + reject(); +} + +void ProgressDialog::onTaskSucceeded() +{ + accept(); +} + +void ProgressDialog::changeStatus(const QString& status) +{ + ui->statusLabel->setText(status); + updateSize(); +} + +void ProgressDialog::changeProgress(qint64 current, qint64 total) +{ + ui->taskProgressBar->setMaximum(total); + ui->taskProgressBar->setValue(current); +} + +void ProgressDialog::keyPressEvent(QKeyEvent* e) +{ + if (ui->skipButton->isVisible()) { + if (e->key() == Qt::Key_Escape) { + on_skipButton_clicked(true); + return; + } else if (e->key() == Qt::Key_Tab) { + ui->skipButton->setFocusPolicy(Qt::StrongFocus); + ui->skipButton->setFocus(); + ui->skipButton->setAutoDefault(true); + ui->skipButton->setDefault(true); + return; + } + } + QDialog::keyPressEvent(e); +} + +void ProgressDialog::closeEvent(QCloseEvent* e) +{ + if (task && task->isRunning()) { + e->ignore(); + } else { + QDialog::closeEvent(e); + } +} diff --git a/meshmc/launcher/ui/dialogs/ProgressDialog.h b/meshmc/launcher/ui/dialogs/ProgressDialog.h new file mode 100644 index 0000000000..38d4454a26 --- /dev/null +++ b/meshmc/launcher/ui/dialogs/ProgressDialog.h @@ -0,0 +1,91 @@ +/* SPDX-FileCopyrightText: 2026 Project Tick + * SPDX-FileContributor: Project Tick + * SPDX-License-Identifier: GPL-3.0-or-later + * + * MeshMC - A Custom Launcher for Minecraft + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <QDialog> +#include <memory> + +class Task; + +namespace Ui +{ + class ProgressDialog; +} + +class ProgressDialog : public QDialog +{ + Q_OBJECT + + public: + explicit ProgressDialog(QWidget* parent = 0); + ~ProgressDialog(); + + void updateSize(); + + int execWithTask(Task* task); + int execWithTask(std::unique_ptr<Task>&& task); + int execWithTask(std::unique_ptr<Task>& task); + + void setSkipButton(bool present, QString label = QString()); + + Task* getTask(); + + public slots: + void onTaskStarted(); + void onTaskFailed(QString failure); + void onTaskSucceeded(); + + void changeStatus(const QString& status); + void changeProgress(qint64 current, qint64 total); + + private slots: + void on_skipButton_clicked(bool checked); + + protected: + virtual void keyPressEvent(QKeyEvent* e); + virtual void closeEvent(QCloseEvent* e); + + private: + bool handleImmediateResult(QDialog::DialogCode& result); + + private: + Ui::ProgressDialog* ui; + + Task* task; +}; diff --git a/meshmc/launcher/ui/dialogs/ProgressDialog.ui b/meshmc/launcher/ui/dialogs/ProgressDialog.ui new file mode 100644 index 0000000000..04b8fef33a --- /dev/null +++ b/meshmc/launcher/ui/dialogs/ProgressDialog.ui @@ -0,0 +1,66 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>ProgressDialog</class> + <widget class="QDialog" name="ProgressDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>400</width> + <height>100</height> + </rect> + </property> + <property name="minimumSize"> + <size> + <width>400</width> + <height>0</height> + </size> + </property> + <property name="maximumSize"> + <size> + <width>600</width> + <height>16777215</height> + </size> + </property> + <property name="windowTitle"> + <string>Please wait...</string> + </property> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="0"> + <widget class="QLabel" name="statusLabel"> + <property name="text"> + <string>Task Status...</string> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QProgressBar" name="taskProgressBar"> + <property name="value"> + <number>24</number> + </property> + <property name="textVisible"> + <bool>false</bool> + </property> + </widget> + </item> + <item row="2" column="0"> + <widget class="QPushButton" name="skipButton"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Expanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Skip</string> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui> diff --git a/meshmc/launcher/ui/dialogs/SkinUploadDialog.cpp b/meshmc/launcher/ui/dialogs/SkinUploadDialog.cpp new file mode 100644 index 0000000000..414e0acaf0 --- /dev/null +++ b/meshmc/launcher/ui/dialogs/SkinUploadDialog.cpp @@ -0,0 +1,163 @@ +/* SPDX-FileCopyrightText: 2026 Project Tick + * SPDX-FileContributor: Project Tick + * SPDX-License-Identifier: GPL-3.0-or-later + * + * MeshMC - A Custom Launcher for Minecraft + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <QFileInfo> +#include <QFileDialog> +#include <QPainter> +#include <QRegularExpression> + +#include <FileSystem.h> + +#include <minecraft/services/SkinUpload.h> +#include <minecraft/services/CapeChange.h> +#include <tasks/SequentialTask.h> + +#include "SkinUploadDialog.h" +#include "ui_SkinUploadDialog.h" +#include "ProgressDialog.h" +#include "CustomMessageBox.h" + +void SkinUploadDialog::on_buttonBox_rejected() +{ + close(); +} + +void SkinUploadDialog::on_buttonBox_accepted() +{ + QString fileName; + QString input = ui->skinPathTextBox->text(); + QRegularExpression urlPrefixMatcher("^([a-z]+)://.+$"); + bool isLocalFile = false; + // it has an URL prefix -> it is an URL + if (urlPrefixMatcher.match(input).hasMatch()) { + QUrl fileURL = input; + if (fileURL.isValid()) { + // local? + if (fileURL.isLocalFile()) { + isLocalFile = true; + fileName = fileURL.toLocalFile(); + } else { + CustomMessageBox::selectable( + this, tr("Skin Upload"), + tr("Using remote URLs for setting skins is not implemented " + "yet."), + QMessageBox::Warning) + ->exec(); + close(); + return; + } + } else { + CustomMessageBox::selectable( + this, tr("Skin Upload"), + tr("You cannot use an invalid URL for uploading skins."), + QMessageBox::Warning) + ->exec(); + close(); + return; + } + } else { + // just assume it's a path then + isLocalFile = true; + fileName = ui->skinPathTextBox->text(); + } + if (isLocalFile && !QFile::exists(fileName)) { + CustomMessageBox::selectable(this, tr("Skin Upload"), + tr("Skin file does not exist!"), + QMessageBox::Warning) + ->exec(); + close(); + return; + } + SkinUpload::Model model = SkinUpload::STEVE; + if (ui->steveBtn->isChecked()) { + model = SkinUpload::STEVE; + } else if (ui->alexBtn->isChecked()) { + model = SkinUpload::ALEX; + } + ProgressDialog prog(this); + SequentialTask skinUpload; + skinUpload.addTask(shared_qobject_ptr<SkinUpload>(new SkinUpload( + this, m_acct->accessToken(), FS::read(fileName), model))); + auto selectedCape = ui->capeCombo->currentData().toString(); + if (selectedCape != m_acct->accountData()->minecraftProfile.currentCape) { + skinUpload.addTask(shared_qobject_ptr<CapeChange>( + new CapeChange(this, m_acct->accessToken(), selectedCape))); + } + if (prog.execWithTask(&skinUpload) != QDialog::Accepted) { + CustomMessageBox::selectable(this, tr("Skin Upload"), + tr("Failed to upload skin!"), + QMessageBox::Warning) + ->exec(); + close(); + return; + } + CustomMessageBox::selectable(this, tr("Skin Upload"), tr("Success"), + QMessageBox::Information) + ->exec(); + close(); +} + +void SkinUploadDialog::on_skinBrowseBtn_clicked() +{ + QString raw_path = QFileDialog::getOpenFileName( + this, tr("Select Skin Texture"), QString(), "*.png"); + if (raw_path.isEmpty() || !QFileInfo::exists(raw_path)) { + return; + } + QString cooked_path = FS::NormalizePath(raw_path); + ui->skinPathTextBox->setText(cooked_path); +} + +SkinUploadDialog::SkinUploadDialog(MinecraftAccountPtr acct, QWidget* parent) + : QDialog(parent), m_acct(acct), ui(new Ui::SkinUploadDialog) +{ + ui->setupUi(this); + + // FIXME: add a model for this, download/refresh the capes on demand + auto& data = *acct->accountData(); + int index = 0; + ui->capeCombo->addItem(tr("No Cape"), QVariant()); + auto currentCape = data.minecraftProfile.currentCape; + if (currentCape.isEmpty()) { + ui->capeCombo->setCurrentIndex(index); + } + + for (auto& cape : data.minecraftProfile.capes) { + index++; + if (cape.data.size()) { + QPixmap capeImage; + if (capeImage.loadFromData(cape.data, "PNG")) { + QPixmap preview = QPixmap(10, 16); + QPainter painter(&preview); + painter.drawPixmap(0, 0, capeImage.copy(1, 1, 10, 16)); + ui->capeCombo->addItem(capeImage, cape.alias, cape.id); + if (currentCape == cape.id) { + ui->capeCombo->setCurrentIndex(index); + } + continue; + } + } + ui->capeCombo->addItem(cape.alias, cape.id); + if (currentCape == cape.id) { + ui->capeCombo->setCurrentIndex(index); + } + } +} diff --git a/meshmc/launcher/ui/dialogs/SkinUploadDialog.h b/meshmc/launcher/ui/dialogs/SkinUploadDialog.h new file mode 100644 index 0000000000..3c9b7aceb2 --- /dev/null +++ b/meshmc/launcher/ui/dialogs/SkinUploadDialog.h @@ -0,0 +1,51 @@ +/* SPDX-FileCopyrightText: 2026 Project Tick + * SPDX-FileContributor: Project Tick + * SPDX-License-Identifier: GPL-3.0-or-later + * + * MeshMC - A Custom Launcher for Minecraft + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <QDialog> +#include <minecraft/auth/MinecraftAccount.h> + +namespace Ui +{ + class SkinUploadDialog; +} + +class SkinUploadDialog : public QDialog +{ + Q_OBJECT + public: + explicit SkinUploadDialog(MinecraftAccountPtr acct, QWidget* parent = 0); + virtual ~SkinUploadDialog() {}; + + public slots: + void on_buttonBox_accepted(); + + void on_buttonBox_rejected(); + + void on_skinBrowseBtn_clicked(); + + protected: + MinecraftAccountPtr m_acct; + + private: + Ui::SkinUploadDialog* ui; +}; diff --git a/meshmc/launcher/ui/dialogs/SkinUploadDialog.ui b/meshmc/launcher/ui/dialogs/SkinUploadDialog.ui new file mode 100644 index 0000000000..f4b0ed0aa7 --- /dev/null +++ b/meshmc/launcher/ui/dialogs/SkinUploadDialog.ui @@ -0,0 +1,97 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>SkinUploadDialog</class> + <widget class="QDialog" name="SkinUploadDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>394</width> + <height>360</height> + </rect> + </property> + <property name="windowTitle"> + <string>Skin Upload</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QGroupBox" name="fileBox"> + <property name="title"> + <string>Skin File</string> + </property> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <widget class="QLineEdit" name="skinPathTextBox"/> + </item> + <item> + <widget class="QPushButton" name="skinBrowseBtn"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Fixed" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="maximumSize"> + <size> + <width>28</width> + <height>16777215</height> + </size> + </property> + <property name="text"> + <string notr="true">...</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="modelBox"> + <property name="title"> + <string>Player Model</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_1"> + <item> + <widget class="QRadioButton" name="steveBtn"> + <property name="text"> + <string>Steve Model</string> + </property> + <property name="checked"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QRadioButton" name="alexBtn"> + <property name="text"> + <string>Alex Model</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="capeBox"> + <property name="title"> + <string>Cape</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <item> + <widget class="QComboBox" name="capeCombo"/> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui> diff --git a/meshmc/launcher/ui/dialogs/UpdateDialog.cpp b/meshmc/launcher/ui/dialogs/UpdateDialog.cpp new file mode 100644 index 0000000000..74838fc8be --- /dev/null +++ b/meshmc/launcher/ui/dialogs/UpdateDialog.cpp @@ -0,0 +1,77 @@ +/* SPDX-FileCopyrightText: 2026 Project Tick + * SPDX-FileContributor: Project Tick + * SPDX-License-Identifier: GPL-3.0-or-later + * + * MeshMC - A Custom Launcher for Minecraft + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include "UpdateDialog.h" +#include "ui_UpdateDialog.h" +#include "Application.h" +#include "BuildConfig.h" + +UpdateDialog::UpdateDialog(bool hasUpdate, const UpdateAvailableStatus& status, + QWidget* parent) + : QDialog(parent), ui(new Ui::UpdateDialog) +{ + ui->setupUi(this); + + if (hasUpdate) { + ui->label->setText( + tr("<b>%1 %2</b> is available!") + .arg(BuildConfig.MESHMC_DISPLAYNAME, status.version)); + + if (!status.releaseNotes.isEmpty()) { + ui->changelogBrowser->setHtml(status.releaseNotes); + } else { + ui->changelogBrowser->setHtml( + tr("<center><p>No release notes available.</p></center>")); + } + } else { + ui->label->setText(tr("You are running the latest version of %1.") + .arg(BuildConfig.MESHMC_DISPLAYNAME)); + ui->changelogBrowser->setHtml( + tr("<center><p>No updates found.</p></center>")); + ui->btnUpdateNow->setHidden(true); + ui->btnUpdateLater->setText(tr("Close")); + } + + restoreGeometry(QByteArray::fromBase64( + APPLICATION->settings()->get("UpdateDialogGeometry").toByteArray())); +} + +UpdateDialog::~UpdateDialog() +{ + delete ui; +} + +void UpdateDialog::on_btnUpdateLater_clicked() +{ + reject(); +} + +void UpdateDialog::on_btnUpdateNow_clicked() +{ + done(UPDATE_NOW); +} + +void UpdateDialog::closeEvent(QCloseEvent* evt) +{ + APPLICATION->settings()->set("UpdateDialogGeometry", + saveGeometry().toBase64()); + QDialog::closeEvent(evt); +} diff --git a/meshmc/launcher/ui/dialogs/UpdateDialog.h b/meshmc/launcher/ui/dialogs/UpdateDialog.h new file mode 100644 index 0000000000..1bc1775710 --- /dev/null +++ b/meshmc/launcher/ui/dialogs/UpdateDialog.h @@ -0,0 +1,80 @@ +/* SPDX-FileCopyrightText: 2026 Project Tick + * SPDX-FileContributor: Project Tick + * SPDX-License-Identifier: GPL-3.0-or-later + * + * MeshMC - A Custom Launcher for Minecraft + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <QDialog> +#include "updater/UpdateChecker.h" + +namespace Ui +{ + class UpdateDialog; +} + +enum UpdateAction { + UPDATE_LATER = QDialog::Rejected, + UPDATE_NOW = QDialog::Accepted, +}; + +class UpdateDialog : public QDialog +{ + Q_OBJECT + + public: + /*! + * Constructs the update dialog. + * \a hasUpdate - true when an update is available (shows "Update now" + * button). + * \a status - update information (version, release notes); ignored + * when hasUpdate is false. + */ + explicit UpdateDialog(bool hasUpdate, + const UpdateAvailableStatus& status = {}, + QWidget* parent = nullptr); + ~UpdateDialog(); + + public slots: + void on_btnUpdateNow_clicked(); + void on_btnUpdateLater_clicked(); + + protected: + void closeEvent(QCloseEvent*) override; + + private: + Ui::UpdateDialog* ui; +}; diff --git a/meshmc/launcher/ui/dialogs/UpdateDialog.ui b/meshmc/launcher/ui/dialogs/UpdateDialog.ui new file mode 100644 index 0000000000..ed895883c9 --- /dev/null +++ b/meshmc/launcher/ui/dialogs/UpdateDialog.ui @@ -0,0 +1,91 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>UpdateDialog</class> + <widget class="QDialog" name="UpdateDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>657</width> + <height>673</height> + </rect> + </property> + <property name="windowTitle"> + <string>MeshMC Update</string> + </property> + <property name="windowIcon"> + <iconset> + <normaloff>:/icons/toolbar/checkupdate</normaloff>:/icons/toolbar/checkupdate</iconset> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_2"> + <item> + <widget class="QLabel" name="label"> + <property name="font"> + <font> + <pointsize>14</pointsize> + </font> + </property> + <property name="text"> + <string/> + </property> + <property name="alignment"> + <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set> + </property> + <property name="buddy"> + <cstring>changelogBrowser</cstring> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="QTextBrowser" name="changelogBrowser"> + <property name="openExternalLinks"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="0"> + <widget class="QPushButton" name="btnUpdateNow"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Update now</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QPushButton" name="btnUpdateLater"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Don't update yet</string> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + <tabstops> + <tabstop>changelogBrowser</tabstop> + <tabstop>btnUpdateNow</tabstop> + <tabstop>btnUpdateLater</tabstop> + </tabstops> + <resources> + <include location="../../resources/multimc/multimc.qrc"/> + </resources> + <connections/> +</ui> diff --git a/meshmc/launcher/ui/dialogs/VersionSelectDialog.cpp b/meshmc/launcher/ui/dialogs/VersionSelectDialog.cpp new file mode 100644 index 0000000000..d8ef9b1245 --- /dev/null +++ b/meshmc/launcher/ui/dialogs/VersionSelectDialog.cpp @@ -0,0 +1,166 @@ +/* SPDX-FileCopyrightText: 2026 Project Tick + * SPDX-FileContributor: Project Tick + * SPDX-License-Identifier: GPL-3.0-or-later + * + * MeshMC - A Custom Launcher for Minecraft + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "VersionSelectDialog.h" + +#include <QtWidgets/QButtonGroup> +#include <QtWidgets/QDialogButtonBox> +#include <QtWidgets/QHBoxLayout> +#include <QtWidgets/QPushButton> +#include <QtWidgets/QVBoxLayout> +#include <QDebug> + +#include "ui/dialogs/ProgressDialog.h" +#include "ui/widgets/VersionSelectWidget.h" +#include "ui/dialogs/CustomMessageBox.h" + +#include "BaseVersion.h" +#include "BaseVersionList.h" +#include "tasks/Task.h" +#include "Application.h" +#include "VersionProxyModel.h" + +VersionSelectDialog::VersionSelectDialog(BaseVersionList* vlist, QString title, + QWidget* parent, bool cancelable) + : QDialog(parent) +{ + setObjectName(QStringLiteral("VersionSelectDialog")); + resize(400, 347); + m_verticalLayout = new QVBoxLayout(this); + m_verticalLayout->setObjectName(QStringLiteral("verticalLayout")); + + m_versionWidget = new VersionSelectWidget(parent); + m_verticalLayout->addWidget(m_versionWidget); + + m_horizontalLayout = new QHBoxLayout(); + m_horizontalLayout->setObjectName(QStringLiteral("horizontalLayout")); + + m_refreshButton = new QPushButton(this); + m_refreshButton->setObjectName(QStringLiteral("refreshButton")); + m_horizontalLayout->addWidget(m_refreshButton); + + m_buttonBox = new QDialogButtonBox(this); + m_buttonBox->setObjectName(QStringLiteral("buttonBox")); + m_buttonBox->setOrientation(Qt::Horizontal); + m_buttonBox->setStandardButtons(QDialogButtonBox::Cancel | + QDialogButtonBox::Ok); + m_horizontalLayout->addWidget(m_buttonBox); + + m_verticalLayout->addLayout(m_horizontalLayout); + + retranslate(); + + QObject::connect(m_buttonBox, SIGNAL(accepted()), this, SLOT(accept())); + QObject::connect(m_buttonBox, SIGNAL(rejected()), this, SLOT(reject())); + + QMetaObject::connectSlotsByName(this); + setWindowModality(Qt::WindowModal); + setWindowTitle(title); + + m_vlist = vlist; + + if (!cancelable) { + m_buttonBox->button(QDialogButtonBox::Cancel)->setEnabled(false); + } +} + +void VersionSelectDialog::retranslate() +{ + // FIXME: overrides custom title given in constructor! + setWindowTitle(tr("Choose Version")); + m_refreshButton->setToolTip(tr("Reloads the version list.")); + m_refreshButton->setText(tr("&Refresh")); +} + +void VersionSelectDialog::setCurrentVersion(const QString& version) +{ + m_currentVersion = version; + m_versionWidget->setCurrentVersion(version); +} + +void VersionSelectDialog::setEmptyString(QString emptyString) +{ + m_versionWidget->setEmptyString(emptyString); +} + +void VersionSelectDialog::setEmptyErrorString(QString emptyErrorString) +{ + m_versionWidget->setEmptyErrorString(emptyErrorString); +} + +void VersionSelectDialog::setResizeOn(int column) +{ + resizeOnColumn = column; +} + +int VersionSelectDialog::exec() +{ + QDialog::open(); + m_versionWidget->initialize(m_vlist); + if (resizeOnColumn != -1) { + m_versionWidget->setResizeOn(resizeOnColumn); + } + return QDialog::exec(); +} + +void VersionSelectDialog::selectRecommended() +{ + m_versionWidget->selectRecommended(); +} + +BaseVersionPtr VersionSelectDialog::selectedVersion() const +{ + return m_versionWidget->selectedVersion(); +} + +void VersionSelectDialog::on_refreshButton_clicked() +{ + m_versionWidget->loadList(); +} + +void VersionSelectDialog::setExactFilter(BaseVersionList::ModelRoles role, + QString filter) +{ + m_versionWidget->setExactFilter(role, filter); +} + +void VersionSelectDialog::setFuzzyFilter(BaseVersionList::ModelRoles role, + QString filter) +{ + m_versionWidget->setFuzzyFilter(role, filter); +} diff --git a/meshmc/launcher/ui/dialogs/VersionSelectDialog.h b/meshmc/launcher/ui/dialogs/VersionSelectDialog.h new file mode 100644 index 0000000000..8db01cc62c --- /dev/null +++ b/meshmc/launcher/ui/dialogs/VersionSelectDialog.h @@ -0,0 +1,101 @@ +/* SPDX-FileCopyrightText: 2026 Project Tick + * SPDX-FileContributor: Project Tick + * SPDX-License-Identifier: GPL-3.0-or-later + * + * MeshMC - A Custom Launcher for Minecraft + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <QDialog> +#include <QSortFilterProxyModel> + +#include "BaseVersionList.h" + +class QVBoxLayout; +class QHBoxLayout; +class QDialogButtonBox; +class VersionSelectWidget; +class QPushButton; + +namespace Ui +{ + class VersionSelectDialog; +} + +class VersionProxyModel; + +class VersionSelectDialog : public QDialog +{ + Q_OBJECT + + public: + explicit VersionSelectDialog(BaseVersionList* vlist, QString title, + QWidget* parent = 0, bool cancelable = true); + virtual ~VersionSelectDialog() {}; + + int exec() override; + + BaseVersionPtr selectedVersion() const; + + void setCurrentVersion(const QString& version); + void setFuzzyFilter(BaseVersionList::ModelRoles role, QString filter); + void setExactFilter(BaseVersionList::ModelRoles role, QString filter); + void setEmptyString(QString emptyString); + void setEmptyErrorString(QString emptyErrorString); + void setResizeOn(int column); + + private slots: + void on_refreshButton_clicked(); + + private: + void retranslate(); + void selectRecommended(); + + private: + QString m_currentVersion; + VersionSelectWidget* m_versionWidget = nullptr; + QVBoxLayout* m_verticalLayout = nullptr; + QHBoxLayout* m_horizontalLayout = nullptr; + QPushButton* m_refreshButton = nullptr; + QDialogButtonBox* m_buttonBox = nullptr; + + BaseVersionList* m_vlist = nullptr; + + VersionProxyModel* m_proxyModel = nullptr; + + int resizeOnColumn = -1; + + Task* loadTask = nullptr; +}; |
