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