diff options
author | Petr Mrázek <peterix@gmail.com> | 2021-07-25 19:11:59 +0200 |
---|---|---|
committer | Petr Mrázek <peterix@gmail.com> | 2021-07-25 19:50:44 +0200 |
commit | 20b9f2b42a3b58b6081af271774fbcc34025dccb (patch) | |
tree | 064fa59facb3357139b47bd4e60bfc8edb35ca11 /launcher/dialogs | |
parent | dd133680858351e3e07690e286882327a4f42ba5 (diff) | |
download | PrismLauncher-20b9f2b42a3b58b6081af271774fbcc34025dccb.tar.gz PrismLauncher-20b9f2b42a3b58b6081af271774fbcc34025dccb.tar.bz2 PrismLauncher-20b9f2b42a3b58b6081af271774fbcc34025dccb.zip |
NOISSUE Flatten gui and logic libraries into MultiMC
Diffstat (limited to 'launcher/dialogs')
43 files changed, 4586 insertions, 0 deletions
diff --git a/launcher/dialogs/AboutDialog.cpp b/launcher/dialogs/AboutDialog.cpp new file mode 100644 index 00000000..c97c471e --- /dev/null +++ b/launcher/dialogs/AboutDialog.cpp @@ -0,0 +1,138 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "AboutDialog.h" +#include "ui_AboutDialog.h" +#include <QIcon> +#include "MultiMC.h" +#include "BuildConfig.h" + +#include <net/NetJob.h> + +#include "HoeDown.h" + +namespace { +// Credits +// This is a hack, but I can't think of a better way to do this easily without screwing with QTextDocument... +QString getCreditsHtml(QStringList patrons) +{ + QString patronsHeading = QObject::tr("Patrons", "About Credits"); + QString output; + QTextStream stream(&output); + stream << "<center>\n"; + // TODO: possibly retrieve from git history at build time? + stream << "<h3>" << QObject::tr("MultiMC Developers", "About Credits") << "</h3>\n"; + stream << "<p>Andrew Okin <<a href='mailto:forkk@forkk.net'>forkk@forkk.net</a>></p>\n"; + stream << "<p>Petr Mrázek <<a href='mailto:peterix@gmail.com'>peterix@gmail.com</a>></p>\n"; + stream << "<p>Sky Welch <<a href='mailto:multimc@bunnies.io'>multimc@bunnies.io</a>></p>\n"; + stream << "<p>Jan (02JanDal) <<a href='mailto:02jandal@gmail.com'>02jandal@gmail.com</a>></p>\n"; + stream << "<p>RoboSky <<a href='https://twitter.com/RoboSky_'>@RoboSky_</a>></p>\n"; + stream << "<br />\n"; + + stream << "<h3>" << QObject::tr("With thanks to", "About Credits") << "</h3>\n"; + stream << "<p>Orochimarufan <<a href='mailto:orochimarufan.x3@gmail.com'>orochimarufan.x3@gmail.com</a>></p>\n"; + stream << "<p>TakSuyu <<a href='mailto:taksuyu@gmail.com'>taksuyu@gmail.com</a>></p>\n"; + stream << "<p>Kilobyte <<a href='mailto:stiepen22@gmx.de'>stiepen22@gmx.de</a>></p>\n"; + stream << "<p>Rootbear75 <<a href='https://twitter.com/rootbear75'>@rootbear75</a>></p>\n"; + stream << "<p>Zeker Zhayard <<a href='https://twitter.com/zeker_zhayard'>@Zeker_Zhayard</a>></p>\n"; + stream << "<br />\n"; + + if(!patrons.isEmpty()) { + stream << "<h3>" << QObject::tr("Patrons", "About Credits") << "</h3>\n"; + for (QString patron : patrons) + { + stream << "<p>" << patron << "</p>\n"; + } + } + stream << "</center>\n"; + return output; +} + +QString getLicenseHtml() +{ + HoeDown hoedown; + QFile dataFile(":/documents/COPYING.md"); + dataFile.open(QIODevice::ReadOnly); + QString output = hoedown.process(dataFile.readAll()); + return output; +} + +} + +AboutDialog::AboutDialog(QWidget *parent) : QDialog(parent), ui(new Ui::AboutDialog) +{ + ui->setupUi(this); + + QString chtml = getCreditsHtml(QStringList()); + ui->creditsText->setHtml(chtml); + + QString lhtml = getLicenseHtml(); + ui->licenseText->setHtml(lhtml); + + ui->urlLabel->setOpenExternalLinks(true); + + ui->icon->setPixmap(MMC->getThemedIcon("logo").pixmap(64)); + ui->title->setText("MultiMC 5"); + + ui->versionLabel->setText(tr("Version") +": " + BuildConfig.printableVersionString()); + ui->platformLabel->setText(tr("Platform") +": " + BuildConfig.BUILD_PLATFORM); + + if (BuildConfig.VERSION_BUILD >= 0) + ui->buildNumLabel->setText(tr("Build Number") +": " + QString::number(BuildConfig.VERSION_BUILD)); + else + ui->buildNumLabel->setVisible(false); + + if (!BuildConfig.VERSION_CHANNEL.isEmpty()) + ui->channelLabel->setText(tr("Channel") +": " + BuildConfig.VERSION_CHANNEL); + else + ui->channelLabel->setVisible(false); + + ui->redistributionText->setHtml(tr( +"<p>We keep MultiMC open source because we think it's important to be able to see the source code for a project like this, and we do so using the Apache license.</p>\n" +"<p>Part of the reason for using the Apache license is we don't want people using the "MultiMC" name when redistributing the project. " +"This means people must take the time to go through the source code and remove all references to "MultiMC", including but not limited to the project " +"icon and the title of windows, (no <b>MultiMC-fork</b> in the title).</p>\n" +"<p>The Apache license covers reasonable use for the name - a mention of the project's origins in the About dialog and the license is acceptable. " +"However, it should be abundantly clear that the project is a fork <b>without</b> implying that you have our blessing.</p>" + )); + + connect(ui->closeButton, SIGNAL(clicked()), SLOT(close())); + + connect(ui->aboutQt, &QPushButton::clicked, &QApplication::aboutQt); + + loadPatronList(); +} + +AboutDialog::~AboutDialog() +{ + delete ui; +} + +void AboutDialog::loadPatronList() +{ + netJob.reset(new NetJob("Patreon Patron List")); + netJob->addNetAction(Net::Download::makeByteArray(QUrl("https://files.multimc.org/patrons.txt"), &dataSink)); + connect(netJob.get(), &NetJob::succeeded, this, &AboutDialog::patronListLoaded); + netJob->start(); +} + +void AboutDialog::patronListLoaded() +{ + QString patronListStr(dataSink); + dataSink.clear(); + QString html = getCreditsHtml(patronListStr.split("\n", QString::SkipEmptyParts)); + ui->creditsText->setHtml(html); +} + diff --git a/launcher/dialogs/AboutDialog.h b/launcher/dialogs/AboutDialog.h new file mode 100644 index 00000000..c7621c37 --- /dev/null +++ b/launcher/dialogs/AboutDialog.h @@ -0,0 +1,47 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <QDialog> +#include <net/NetJob.h> + +namespace Ui +{ +class AboutDialog; +} + +class AboutDialog : public QDialog +{ + Q_OBJECT + +public: + explicit AboutDialog(QWidget *parent = 0); + ~AboutDialog(); + +public +slots: + /// Starts loading a list of Patreon patrons. + void loadPatronList(); + + /// Slot for when the patron list loads successfully. + void patronListLoaded(); + +private: + Ui::AboutDialog *ui; + + NetJobPtr netJob; + QByteArray dataSink; +}; diff --git a/launcher/dialogs/AboutDialog.ui b/launcher/dialogs/AboutDialog.ui new file mode 100644 index 00000000..c6de9ebb --- /dev/null +++ b/launcher/dialogs/AboutDialog.ui @@ -0,0 +1,312 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>AboutDialog</class> + <widget class="QDialog" name="AboutDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>783</width> + <height>843</height> + </rect> + </property> + <property name="minimumSize"> + <size> + <width>450</width> + <height>400</height> + </size> + </property> + <property name="windowTitle"> + <string>About MultiMC</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QLabel" name="icon"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Fixed" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>64</width> + <height>64</height> + </size> + </property> + <property name="baseSize"> + <size> + <width>64</width> + <height>64</height> + </size> + </property> + <property name="text"> + <string/> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer_2"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + <item> + <widget class="QLabel" name="title"> + <property name="font"> + <font> + <pointsize>15</pointsize> + </font> + </property> + <property name="text"> + <string notr="true">MultiMC 5</string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + <item> + <widget class="QTabWidget" name="tabWidget"> + <property name="currentIndex"> + <number>0</number> + </property> + <widget class="QWidget" name="aboutTab"> + <attribute name="title"> + <string>About</string> + </attribute> + <layout class="QVBoxLayout" name="verticalLayout_5"> + <item> + <widget class="QLabel" name="versionLabel"> + <property name="text"> + <string>Version:</string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="platformLabel"> + <property name="text"> + <string>Platform:</string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="buildNumLabel"> + <property name="text"> + <string>Build Number:</string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="channelLabel"> + <property name="text"> + <string>Channel:</string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="aboutLabel"> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="text"> + <string><html><head/><body><p>MultiMC is a custom launcher that makes managing Minecraft easier by allowing you to have multiple instances of Minecraft at once.</p></body></html></string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="copyLabel"> + <property name="font"> + <font> + <pointsize>8</pointsize> + <kerning>true</kerning> + </font> + </property> + <property name="text"> + <string>© 2012-2021 MultiMC Contributors</string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="urlLabel"> + <property name="font"> + <font> + <pointsize>10</pointsize> + </font> + </property> + <property name="text"> + <string notr="true"><html><head/><body><p><a href="https://github.com/MultiMC/MultiMC5">https://github.com/MultiMC/MultiMC5</a></p></body></html></string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + <item> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>212</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + <widget class="QWidget" name="creditsTab"> + <attribute name="title"> + <string>Credits</string> + </attribute> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <item> + <widget class="QTextEdit" name="creditsText"> + <property name="readOnly"> + <bool>true</bool> + </property> + <property name="textInteractionFlags"> + <set>Qt::TextBrowserInteraction</set> + </property> + </widget> + </item> + </layout> + </widget> + <widget class="QWidget" name="licenseTab"> + <attribute name="title"> + <string>License</string> + </attribute> + <layout class="QVBoxLayout" name="verticalLayout_3"> + <item> + <widget class="QTextEdit" name="licenseText"> + <property name="minimumSize"> + <size> + <width>0</width> + <height>0</height> + </size> + </property> + <property name="font"> + <font> + <family>DejaVu Sans Mono</family> + </font> + </property> + <property name="readOnly"> + <bool>true</bool> + </property> + <property name="textInteractionFlags"> + <set>Qt::TextBrowserInteraction</set> + </property> + </widget> + </item> + </layout> + </widget> + <widget class="QWidget" name="forkingTab"> + <attribute name="title"> + <string>Forking/Redistribution</string> + </attribute> + <layout class="QVBoxLayout" name="verticalLayout_4"> + <item> + <widget class="QTextEdit" name="redistributionText"> + <property name="textInteractionFlags"> + <set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set> + </property> + </widget> + </item> + </layout> + </widget> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_2"> + <item> + <widget class="QPushButton" name="aboutQt"> + <property name="autoFillBackground"> + <bool>false</bool> + </property> + <property name="text"> + <string>About Qt</string> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer_3"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QPushButton" name="closeButton"> + <property name="text"> + <string>Close</string> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + <tabstops> + <tabstop>tabWidget</tabstop> + <tabstop>creditsText</tabstop> + <tabstop>licenseText</tabstop> + <tabstop>redistributionText</tabstop> + <tabstop>aboutQt</tabstop> + <tabstop>closeButton</tabstop> + </tabstops> + <resources/> + <connections/> +</ui> diff --git a/launcher/dialogs/CopyInstanceDialog.cpp b/launcher/dialogs/CopyInstanceDialog.cpp new file mode 100644 index 00000000..5fe90334 --- /dev/null +++ b/launcher/dialogs/CopyInstanceDialog.cpp @@ -0,0 +1,144 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <QLayout> +#include <QPushButton> + +#include "MultiMC.h" +#include "CopyInstanceDialog.h" +#include "ui_CopyInstanceDialog.h" + +#include "dialogs/IconPickerDialog.h" + +#include "BaseVersion.h" +#include "icons/IconList.h" +#include "tasks/Task.h" +#include "BaseInstance.h" +#include "InstanceList.h" + +CopyInstanceDialog::CopyInstanceDialog(InstancePtr original, QWidget *parent) + :QDialog(parent), ui(new Ui::CopyInstanceDialog), m_original(original) +{ + ui->setupUi(this); + resize(minimumSizeHint()); + layout()->setSizeConstraint(QLayout::SetFixedSize); + + InstIconKey = original->iconKey(); + ui->iconButton->setIcon(MMC->icons()->getIcon(InstIconKey)); + ui->instNameTextBox->setText(original->name()); + ui->instNameTextBox->setFocus(); + auto groups = MMC->instances()->getGroups().toSet(); + auto groupList = QStringList(groups.toList()); + groupList.sort(Qt::CaseInsensitive); + groupList.removeOne(""); + groupList.push_front(""); + ui->groupBox->addItems(groupList); + int index = groupList.indexOf(MMC->instances()->getInstanceGroup(m_original->id())); + if(index == -1) + { + index = 0; + } + ui->groupBox->setCurrentIndex(index); + ui->groupBox->lineEdit()->setPlaceholderText(tr("No group")); + ui->copySavesCheckbox->setChecked(m_copySaves); + ui->keepPlaytimeCheckbox->setChecked(m_keepPlaytime); +} + +CopyInstanceDialog::~CopyInstanceDialog() +{ + delete ui; +} + +void CopyInstanceDialog::updateDialogState() +{ + auto allowOK = !instName().isEmpty(); + auto OkButton = ui->buttonBox->button(QDialogButtonBox::Ok); + if(OkButton->isEnabled() != allowOK) + { + OkButton->setEnabled(allowOK); + } +} + +QString CopyInstanceDialog::instName() const +{ + auto result = ui->instNameTextBox->text().trimmed(); + if(result.size()) + { + return result; + } + return QString(); +} + +QString CopyInstanceDialog::iconKey() const +{ + return InstIconKey; +} + +QString CopyInstanceDialog::instGroup() const +{ + return ui->groupBox->currentText(); +} + +void CopyInstanceDialog::on_iconButton_clicked() +{ + IconPickerDialog dlg(this); + dlg.execWithSelection(InstIconKey); + + if (dlg.result() == QDialog::Accepted) + { + InstIconKey = dlg.selectedIconKey; + ui->iconButton->setIcon(MMC->icons()->getIcon(InstIconKey)); + } +} + +void CopyInstanceDialog::on_instNameTextBox_textChanged(const QString &arg1) +{ + updateDialogState(); +} + +bool CopyInstanceDialog::shouldCopySaves() const +{ + return m_copySaves; +} + +void CopyInstanceDialog::on_copySavesCheckbox_stateChanged(int state) +{ + if(state == Qt::Unchecked) + { + m_copySaves = false; + } + else if(state == Qt::Checked) + { + m_copySaves = true; + } +} + +bool CopyInstanceDialog::shouldKeepPlaytime() const +{ + return m_keepPlaytime; +} + + +void CopyInstanceDialog::on_keepPlaytimeCheckbox_stateChanged(int state) +{ + if(state == Qt::Unchecked) + { + m_keepPlaytime = false; + } + else if(state == Qt::Checked) + { + m_keepPlaytime = true; + } +} diff --git a/launcher/dialogs/CopyInstanceDialog.h b/launcher/dialogs/CopyInstanceDialog.h new file mode 100644 index 00000000..bf3cd920 --- /dev/null +++ b/launcher/dialogs/CopyInstanceDialog.h @@ -0,0 +1,58 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <QDialog> +#include "BaseVersion.h" +#include <BaseInstance.h> + +class BaseInstance; + +namespace Ui +{ +class CopyInstanceDialog; +} + +class CopyInstanceDialog : public QDialog +{ + Q_OBJECT + +public: + explicit CopyInstanceDialog(InstancePtr original, QWidget *parent = 0); + ~CopyInstanceDialog(); + + void updateDialogState(); + + QString instName() const; + QString instGroup() const; + QString iconKey() const; + bool shouldCopySaves() const; + bool shouldKeepPlaytime() const; + +private +slots: + void on_iconButton_clicked(); + void on_instNameTextBox_textChanged(const QString &arg1); + void on_copySavesCheckbox_stateChanged(int state); + void on_keepPlaytimeCheckbox_stateChanged(int state); + +private: + Ui::CopyInstanceDialog *ui; + QString InstIconKey; + InstancePtr m_original; + bool m_copySaves = true; + bool m_keepPlaytime = true; +}; diff --git a/launcher/dialogs/CopyInstanceDialog.ui b/launcher/dialogs/CopyInstanceDialog.ui new file mode 100644 index 00000000..fa675455 --- /dev/null +++ b/launcher/dialogs/CopyInstanceDialog.ui @@ -0,0 +1,182 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>CopyInstanceDialog</class> + <widget class="QDialog" name="CopyInstanceDialog"> + <property name="windowModality"> + <enum>Qt::ApplicationModal</enum> + </property> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>345</width> + <height>323</height> + </rect> + </property> + <property name="windowTitle"> + <string>Copy Instance</string> + </property> + <property name="windowIcon"> + <iconset> + <normaloff>:/icons/toolbar/copy</normaloff>:/icons/toolbar/copy</iconset> + </property> + <property name="modal"> + <bool>true</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <layout class="QHBoxLayout" name="iconBtnLayout"> + <item> + <spacer name="iconBtnLeftSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QToolButton" name="iconButton"> + <property name="icon"> + <iconset> + <normaloff>:/icons/instances/infinity</normaloff>:/icons/instances/infinity</iconset> + </property> + <property name="iconSize"> + <size> + <width>80</width> + <height>80</height> + </size> + </property> + </widget> + </item> + <item> + <spacer name="iconBtnRightSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + <item> + <widget class="QLineEdit" name="instNameTextBox"> + <property name="placeholderText"> + <string>Name</string> + </property> + </widget> + </item> + <item> + <widget class="Line" name="line"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </widget> + </item> + <item> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="0"> + <widget class="QLabel" name="labelVersion_3"> + <property name="text"> + <string>&Group</string> + </property> + <property name="buddy"> + <cstring>groupBox</cstring> + </property> + </widget> + </item> + <item row="0" column="1" colspan="2"> + <widget class="QComboBox" name="groupBox"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="editable"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="QCheckBox" name="copySavesCheckbox"> + <property name="text"> + <string>Copy saves</string> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="keepPlaytimeCheckbox"> + <property name="text"> + <string>Keep play time</string> + </property> + </widget> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </widget> + <tabstops> + <tabstop>iconButton</tabstop> + <tabstop>instNameTextBox</tabstop> + <tabstop>groupBox</tabstop> + <tabstop>copySavesCheckbox</tabstop> + <tabstop>keepPlaytimeCheckbox</tabstop> + </tabstops> + <resources> + <include location="../../graphics.qrc"/> + </resources> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>CopyInstanceDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>248</x> + <y>254</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>CopyInstanceDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>316</x> + <y>260</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui> diff --git a/launcher/dialogs/CustomMessageBox.cpp b/launcher/dialogs/CustomMessageBox.cpp new file mode 100644 index 00000000..19029f68 --- /dev/null +++ b/launcher/dialogs/CustomMessageBox.cpp @@ -0,0 +1,35 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "CustomMessageBox.h" + +namespace CustomMessageBox +{ +QMessageBox *selectable(QWidget *parent, const QString &title, const QString &text, + QMessageBox::Icon icon, QMessageBox::StandardButtons buttons, + QMessageBox::StandardButton defaultButton) +{ + QMessageBox *messageBox = new QMessageBox(parent); + messageBox->setWindowTitle(title); + messageBox->setText(text); + messageBox->setStandardButtons(buttons); + messageBox->setDefaultButton(defaultButton); + messageBox->setTextInteractionFlags(Qt::TextSelectableByMouse); + messageBox->setIcon(icon); + messageBox->setTextInteractionFlags(Qt::TextBrowserInteraction); + + return messageBox; +} +} diff --git a/launcher/dialogs/CustomMessageBox.h b/launcher/dialogs/CustomMessageBox.h new file mode 100644 index 00000000..712c6518 --- /dev/null +++ b/launcher/dialogs/CustomMessageBox.h @@ -0,0 +1,26 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <QMessageBox> + +namespace CustomMessageBox +{ +QMessageBox *selectable(QWidget *parent, const QString &title, const QString &text, + QMessageBox::Icon icon = QMessageBox::NoIcon, + QMessageBox::StandardButtons buttons = QMessageBox::Ok, + QMessageBox::StandardButton defaultButton = QMessageBox::NoButton); +} diff --git a/launcher/dialogs/EditAccountDialog.cpp b/launcher/dialogs/EditAccountDialog.cpp new file mode 100644 index 00000000..002c064b --- /dev/null +++ b/launcher/dialogs/EditAccountDialog.cpp @@ -0,0 +1,61 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "EditAccountDialog.h" +#include "ui_EditAccountDialog.h" +#include <DesktopServices.h> +#include <QUrl> + +EditAccountDialog::EditAccountDialog(const QString &text, QWidget *parent, int flags) + : QDialog(parent), ui(new Ui::EditAccountDialog) +{ + ui->setupUi(this); + + ui->label->setText(text); + ui->label->setVisible(!text.isEmpty()); + + ui->userTextBox->setEnabled(flags & UsernameField); + ui->passTextBox->setEnabled(flags & PasswordField); +} + +EditAccountDialog::~EditAccountDialog() +{ + delete ui; +} + +void EditAccountDialog::on_label_linkActivated(const QString &link) +{ + DesktopServices::openUrl(QUrl(link)); +} + +void EditAccountDialog::setUsername(const QString & user) const +{ + ui->userTextBox->setText(user); +} + +QString EditAccountDialog::username() const +{ + return ui->userTextBox->text(); +} + +void EditAccountDialog::setPassword(const QString & pass) const +{ + ui->passTextBox->setText(pass); +} + +QString EditAccountDialog::password() const +{ + return ui->passTextBox->text(); +} diff --git a/launcher/dialogs/EditAccountDialog.h b/launcher/dialogs/EditAccountDialog.h new file mode 100644 index 00000000..6b5eb4aa --- /dev/null +++ b/launcher/dialogs/EditAccountDialog.h @@ -0,0 +1,56 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <QDialog> + +namespace Ui +{ +class EditAccountDialog; +} + +class EditAccountDialog : public QDialog +{ + Q_OBJECT + +public: + explicit EditAccountDialog(const QString &text = "", QWidget *parent = 0, + int flags = UsernameField | PasswordField); + ~EditAccountDialog(); + + void setUsername(const QString & user) const; + void setPassword(const QString & pass) const; + + QString username() const; + QString password() const; + + enum Flags + { + NoFlags = 0, + + //! Specifies that the dialog should have a username field. + UsernameField, + + //! Specifies that the dialog should have a password field. + PasswordField, + }; + +private slots: + void on_label_linkActivated(const QString &link); + +private: + Ui::EditAccountDialog *ui; +}; diff --git a/launcher/dialogs/EditAccountDialog.ui b/launcher/dialogs/EditAccountDialog.ui new file mode 100644 index 00000000..e87509bc --- /dev/null +++ b/launcher/dialogs/EditAccountDialog.ui @@ -0,0 +1,94 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>EditAccountDialog</class> + <widget class="QDialog" name="EditAccountDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>400</width> + <height>148</height> + </rect> + </property> + <property name="windowTitle"> + <string>Login</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QLabel" name="label"> + <property name="text"> + <string notr="true">Message label placeholder.</string> + </property> + <property name="textFormat"> + <enum>Qt::RichText</enum> + </property> + <property name="textInteractionFlags"> + <set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set> + </property> + </widget> + </item> + <item> + <widget class="QLineEdit" name="userTextBox"> + <property name="placeholderText"> + <string>Email</string> + </property> + </widget> + </item> + <item> + <widget class="QLineEdit" name="passTextBox"> + <property name="echoMode"> + <enum>QLineEdit::Password</enum> + </property> + <property name="placeholderText"> + <string>Password</string> + </property> + </widget> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>EditAccountDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>248</x> + <y>254</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>EditAccountDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>316</x> + <y>260</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui> diff --git a/launcher/dialogs/ExportInstanceDialog.cpp b/launcher/dialogs/ExportInstanceDialog.cpp new file mode 100644 index 00000000..a42779d4 --- /dev/null +++ b/launcher/dialogs/ExportInstanceDialog.cpp @@ -0,0 +1,482 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ExportInstanceDialog.h" +#include "ui_ExportInstanceDialog.h" +#include <BaseInstance.h> +#include <MMCZip.h> +#include <QFileDialog> +#include <QMessageBox> +#include <qfilesystemmodel.h> + +#include <QSortFilterProxyModel> +#include <QDebug> +#include <qstack.h> +#include <QSaveFile> +#include "MMCStrings.h" +#include "SeparatorPrefixTree.h" +#include "MultiMC.h" +#include <icons/IconList.h> +#include <FileSystem.h> + +class PackIgnoreProxy : public QSortFilterProxyModel +{ + Q_OBJECT + +public: + PackIgnoreProxy(InstancePtr instance, QObject *parent) : QSortFilterProxyModel(parent) + { + m_instance = instance; + } + // NOTE: Sadly, we have to do sorting ourselves. + bool lessThan(const QModelIndex &left, const QModelIndex &right) const + { + QFileSystemModel *fsm = qobject_cast<QFileSystemModel *>(sourceModel()); + if (!fsm) + { + return QSortFilterProxyModel::lessThan(left, right); + } + bool asc = sortOrder() == Qt::AscendingOrder ? true : false; + + QFileInfo leftFileInfo = fsm->fileInfo(left); + QFileInfo rightFileInfo = fsm->fileInfo(right); + + if (!leftFileInfo.isDir() && rightFileInfo.isDir()) + { + return !asc; + } + if (leftFileInfo.isDir() && !rightFileInfo.isDir()) + { + return asc; + } + + // sort and proxy model breaks the original model... + if (sortColumn() == 0) + { + return Strings::naturalCompare(leftFileInfo.fileName(), rightFileInfo.fileName(), + Qt::CaseInsensitive) < 0; + } + if (sortColumn() == 1) + { + auto leftSize = leftFileInfo.size(); + auto rightSize = rightFileInfo.size(); + if ((leftSize == rightSize) || (leftFileInfo.isDir() && rightFileInfo.isDir())) + { + return Strings::naturalCompare(leftFileInfo.fileName(), + rightFileInfo.fileName(), + Qt::CaseInsensitive) < 0 + ? asc + : !asc; + } + return leftSize < rightSize; + } + return QSortFilterProxyModel::lessThan(left, right); + } + + virtual Qt::ItemFlags flags(const QModelIndex &index) const + { + if (!index.isValid()) + return Qt::NoItemFlags; + + auto sourceIndex = mapToSource(index); + Qt::ItemFlags flags = sourceIndex.flags(); + if (index.column() == 0) + { + flags |= Qt::ItemIsUserCheckable; + if (sourceIndex.model()->hasChildren(sourceIndex)) + { + flags |= Qt::ItemIsTristate; + } + } + + return flags; + } + + virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const + { + QModelIndex sourceIndex = mapToSource(index); + + if (index.column() == 0 && role == Qt::CheckStateRole) + { + QFileSystemModel *fsm = qobject_cast<QFileSystemModel *>(sourceModel()); + auto blockedPath = relPath(fsm->filePath(sourceIndex)); + auto cover = blocked.cover(blockedPath); + if (!cover.isNull()) + { + return QVariant(Qt::Unchecked); + } + else if (blocked.exists(blockedPath)) + { + return QVariant(Qt::PartiallyChecked); + } + else + { + return QVariant(Qt::Checked); + } + } + + return sourceIndex.data(role); + } + + virtual bool setData(const QModelIndex &index, const QVariant &value, + int role = Qt::EditRole) + { + if (index.column() == 0 && role == Qt::CheckStateRole) + { + Qt::CheckState state = static_cast<Qt::CheckState>(value.toInt()); + return setFilterState(index, state); + } + + QModelIndex sourceIndex = mapToSource(index); + return QSortFilterProxyModel::sourceModel()->setData(sourceIndex, value, role); + } + + QString relPath(const QString &path) const + { + QString prefix = QDir().absoluteFilePath(m_instance->instanceRoot()); + prefix += '/'; + if (!path.startsWith(prefix)) + { + return QString(); + } + return path.mid(prefix.size()); + } + + bool setFilterState(QModelIndex index, Qt::CheckState state) + { + QFileSystemModel *fsm = qobject_cast<QFileSystemModel *>(sourceModel()); + + if (!fsm) + { + return false; + } + + QModelIndex sourceIndex = mapToSource(index); + auto blockedPath = relPath(fsm->filePath(sourceIndex)); + bool changed = false; + if (state == Qt::Unchecked) + { + // blocking a path + auto &node = blocked.insert(blockedPath); + // get rid of all blocked nodes below + node.clear(); + changed = true; + } + else if (state == Qt::Checked || state == Qt::PartiallyChecked) + { + if (!blocked.remove(blockedPath)) + { + auto cover = blocked.cover(blockedPath); + qDebug() << "Blocked by cover" << cover; + // uncover + blocked.remove(cover); + // block all contents, except for any cover + QModelIndex rootIndex = + fsm->index(FS::PathCombine(m_instance->instanceRoot(), cover)); + QModelIndex doing = rootIndex; + int row = 0; + QStack<QModelIndex> todo; + while (1) + { + auto node = doing.child(row, 0); + if (!node.isValid()) + { + if (!todo.size()) + { + break; + } + else + { + doing = todo.pop(); + row = 0; + continue; + } + } + auto relpath = relPath(fsm->filePath(node)); + if (blockedPath.startsWith(relpath)) // cover found? + { + // continue processing cover later + todo.push(node); + } + else + { + // or just block this one. + blocked.insert(relpath); + } + row++; + } + } + changed = true; + } + if (changed) + { + // update the thing + emit dataChanged(index, index, {Qt::CheckStateRole}); + // update everything above index + QModelIndex up = index.parent(); + while (1) + { + if (!up.isValid()) + break; + emit dataChanged(up, up, {Qt::CheckStateRole}); + up = up.parent(); + } + // and everything below the index + QModelIndex doing = index; + int row = 0; + QStack<QModelIndex> todo; + while (1) + { + auto node = doing.child(row, 0); + if (!node.isValid()) + { + if (!todo.size()) + { + break; + } + else + { + doing = todo.pop(); + row = 0; + continue; + } + } + emit dataChanged(node, node, {Qt::CheckStateRole}); + todo.push(node); + row++; + } + // siblings and unrelated nodes are ignored + } + return true; + } + + bool shouldExpand(QModelIndex index) + { + QModelIndex sourceIndex = mapToSource(index); + QFileSystemModel *fsm = qobject_cast<QFileSystemModel *>(sourceModel()); + if (!fsm) + { + return false; + } + auto blockedPath = relPath(fsm->filePath(sourceIndex)); + auto found = blocked.find(blockedPath); + if(found) + { + return !found->leaf(); + } + return false; + } + + void setBlockedPaths(QStringList paths) + { + beginResetModel(); + blocked.clear(); + blocked.insert(paths); + endResetModel(); + } + + const SeparatorPrefixTree<'/'> & blockedPaths() const + { + return blocked; + } + +protected: + bool filterAcceptsColumn(int source_column, const QModelIndex &source_parent) const + { + Q_UNUSED(source_parent) + + // adjust the columns you want to filter out here + // return false for those that will be hidden + if (source_column == 2 || source_column == 3) + return false; + + return true; + } + +private: + InstancePtr m_instance; + SeparatorPrefixTree<'/'> blocked; +}; + +ExportInstanceDialog::ExportInstanceDialog(InstancePtr instance, QWidget *parent) + : QDialog(parent), ui(new Ui::ExportInstanceDialog), m_instance(instance) +{ + ui->setupUi(this); + auto model = new QFileSystemModel(this); + proxyModel = new PackIgnoreProxy(m_instance, this); + loadPackIgnore(); + proxyModel->setSourceModel(model); + auto root = instance->instanceRoot(); + ui->treeView->setModel(proxyModel); + ui->treeView->setRootIndex(proxyModel->mapFromSource(model->index(root))); + ui->treeView->sortByColumn(0, Qt::AscendingOrder); + + connect(proxyModel, SIGNAL(rowsInserted(QModelIndex,int,int)), SLOT(rowsInserted(QModelIndex,int,int))); + + model->setFilter(QDir::AllEntries | QDir::NoDotAndDotDot | QDir::AllDirs | QDir::Hidden); + model->setRootPath(root); + auto headerView = ui->treeView->header(); + headerView->setSectionResizeMode(QHeaderView::ResizeToContents); + headerView->setSectionResizeMode(0, QHeaderView::Stretch); +} + +ExportInstanceDialog::~ExportInstanceDialog() +{ + delete ui; +} + +/// Save icon to instance's folder is needed +void SaveIcon(InstancePtr m_instance) +{ + auto iconKey = m_instance->iconKey(); + auto iconList = MMC->icons(); + auto mmcIcon = iconList->icon(iconKey); + if(!mmcIcon || mmcIcon->isBuiltIn()) { + return; + } + auto path = mmcIcon->getFilePath(); + if(!path.isNull()) { + QFileInfo inInfo (path); + FS::copy(path, FS::PathCombine(m_instance->instanceRoot(), inInfo.fileName())) (); + return; + } + auto & image = mmcIcon->m_images[mmcIcon->type()]; + auto & icon = image.icon; + auto sizes = icon.availableSizes(); + if(sizes.size() == 0) + { + return; + } + auto areaOf = [](QSize size) + { + return size.width() * size.height(); + }; + QSize largest = sizes[0]; + // find variant with largest area + for(auto size: sizes) + { + if(areaOf(largest) < areaOf(size)) + { + largest = size; + } + } + auto pixmap = icon.pixmap(largest); + pixmap.save(FS::PathCombine(m_instance->instanceRoot(), iconKey + ".png")); +} + +bool ExportInstanceDialog::doExport() +{ + auto name = FS::RemoveInvalidFilenameChars(m_instance->name()); + + const QString output = QFileDialog::getSaveFileName( + this, tr("Export %1").arg(m_instance->name()), + FS::PathCombine(QDir::homePath(), name + ".zip"), "Zip (*.zip)", nullptr, QFileDialog::DontConfirmOverwrite); + if (output.isEmpty()) + { + return false; + } + if (QFile::exists(output)) + { + int ret = + QMessageBox::question(this, tr("Overwrite?"), + tr("This file already exists. Do you want to overwrite it?"), + QMessageBox::No, QMessageBox::Yes); + if (ret == QMessageBox::No) + { + return false; + } + } + + SaveIcon(m_instance); + + auto & blocked = proxyModel->blockedPaths(); + using std::placeholders::_1; + if (!JlCompress::compressDir(output, m_instance->instanceRoot(), name, std::bind(&SeparatorPrefixTree<'/'>::covers, blocked, _1))) + { + QMessageBox::warning(this, tr("Error"), tr("Unable to export instance")); + return false; + } + return true; +} + +void ExportInstanceDialog::done(int result) +{ + savePackIgnore(); + if (result == QDialog::Accepted) + { + if (doExport()) + { + QDialog::done(QDialog::Accepted); + return; + } + else + { + return; + } + } + QDialog::done(result); +} + +void ExportInstanceDialog::rowsInserted(QModelIndex parent, int top, int bottom) +{ + //WARNING: possible off-by-one? + for(int i = top; i < bottom; i++) + { + auto node = parent.child(i, 0); + if(proxyModel->shouldExpand(node)) + { + auto expNode = node.parent(); + if(!expNode.isValid()) + { + continue; + } + ui->treeView->expand(node); + } + } +} + +QString ExportInstanceDialog::ignoreFileName() +{ + return FS::PathCombine(m_instance->instanceRoot(), ".packignore"); +} + +void ExportInstanceDialog::loadPackIgnore() +{ + auto filename = ignoreFileName(); + QFile ignoreFile(filename); + if(!ignoreFile.open(QIODevice::ReadOnly)) + { + return; + } + auto data = ignoreFile.readAll(); + auto string = QString::fromUtf8(data); + proxyModel->setBlockedPaths(string.split('\n', QString::SkipEmptyParts)); +} + +void ExportInstanceDialog::savePackIgnore() +{ + auto data = proxyModel->blockedPaths().toStringList().join('\n').toUtf8(); + auto filename = ignoreFileName(); + try + { + FS::write(filename, data); + } + catch (const Exception &e) + { + qWarning() << e.cause(); + } +} + +#include "ExportInstanceDialog.moc" diff --git a/launcher/dialogs/ExportInstanceDialog.h b/launcher/dialogs/ExportInstanceDialog.h new file mode 100644 index 00000000..dea02d1b --- /dev/null +++ b/launcher/dialogs/ExportInstanceDialog.h @@ -0,0 +1,54 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <QDialog> +#include <QModelIndex> +#include <memory> + +class BaseInstance; +class PackIgnoreProxy; +typedef std::shared_ptr<BaseInstance> InstancePtr; + +namespace Ui +{ +class ExportInstanceDialog; +} + +class ExportInstanceDialog : public QDialog +{ + Q_OBJECT + +public: + explicit ExportInstanceDialog(InstancePtr instance, QWidget *parent = 0); + ~ExportInstanceDialog(); + + virtual void done(int result); + +private: + bool doExport(); + void loadPackIgnore(); + void savePackIgnore(); + QString ignoreFileName(); + +private: + Ui::ExportInstanceDialog *ui; + InstancePtr m_instance; + PackIgnoreProxy * proxyModel; + +private slots: + void rowsInserted(QModelIndex parent, int top, int bottom); +}; diff --git a/launcher/dialogs/ExportInstanceDialog.ui b/launcher/dialogs/ExportInstanceDialog.ui new file mode 100644 index 00000000..bcd4e84a --- /dev/null +++ b/launcher/dialogs/ExportInstanceDialog.ui @@ -0,0 +1,83 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>ExportInstanceDialog</class> + <widget class="QDialog" name="ExportInstanceDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>720</width> + <height>625</height> + </rect> + </property> + <property name="windowTitle"> + <string>Export Instance</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QTreeView" name="treeView"> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + <property name="selectionMode"> + <enum>QAbstractItemView::ExtendedSelection</enum> + </property> + <property name="sortingEnabled"> + <bool>true</bool> + </property> + <attribute name="headerStretchLastSection"> + <bool>false</bool> + </attribute> + </widget> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </widget> + <tabstops> + <tabstop>treeView</tabstop> + </tabstops> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>ExportInstanceDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>248</x> + <y>254</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>ExportInstanceDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>316</x> + <y>260</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui> diff --git a/launcher/dialogs/IconPickerDialog.cpp b/launcher/dialogs/IconPickerDialog.cpp new file mode 100644 index 00000000..90436554 --- /dev/null +++ b/launcher/dialogs/IconPickerDialog.cpp @@ -0,0 +1,163 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <QKeyEvent> +#include <QPushButton> +#include <QFileDialog> + +#include "MultiMC.h" + +#include "IconPickerDialog.h" +#include "ui_IconPickerDialog.h" + +#include "groupview/InstanceDelegate.h" + +#include "icons/IconList.h" +#include "icons/IconUtils.h" +#include <DesktopServices.h> + +IconPickerDialog::IconPickerDialog(QWidget *parent) + : QDialog(parent), ui(new Ui::IconPickerDialog) +{ + ui->setupUi(this); + setWindowModality(Qt::WindowModal); + + auto contentsWidget = ui->iconView; + contentsWidget->setViewMode(QListView::IconMode); + contentsWidget->setFlow(QListView::LeftToRight); + contentsWidget->setIconSize(QSize(48, 48)); + contentsWidget->setMovement(QListView::Static); + contentsWidget->setResizeMode(QListView::Adjust); + contentsWidget->setSelectionMode(QAbstractItemView::SingleSelection); + contentsWidget->setSpacing(5); + contentsWidget->setWordWrap(false); + contentsWidget->setWrapping(true); + contentsWidget->setUniformItemSizes(true); + contentsWidget->setTextElideMode(Qt::ElideRight); + contentsWidget->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); + contentsWidget->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); + contentsWidget->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + contentsWidget->setItemDelegate(new ListViewDelegate()); + + // contentsWidget->setAcceptDrops(true); + contentsWidget->setDropIndicatorShown(true); + contentsWidget->viewport()->setAcceptDrops(true); + contentsWidget->setDragDropMode(QAbstractItemView::DropOnly); + contentsWidget->setDefaultDropAction(Qt::CopyAction); + + contentsWidget->installEventFilter(this); + + contentsWidget->setModel(MMC->icons().get()); + + // NOTE: ResetRole forces the button to be on the left, while the OK/Cancel ones are on the right. We win. + auto buttonAdd = ui->buttonBox->addButton(tr("Add Icon"), QDialogButtonBox::ResetRole); + auto buttonRemove = ui->buttonBox->addButton(tr("Remove Icon"), QDialogButtonBox::ResetRole); + + connect(buttonAdd, SIGNAL(clicked(bool)), SLOT(addNewIcon())); + connect(buttonRemove, SIGNAL(clicked(bool)), SLOT(removeSelectedIcon())); + + connect(contentsWidget, SIGNAL(doubleClicked(QModelIndex)), SLOT(activated(QModelIndex))); + + connect(contentsWidget->selectionModel(), SIGNAL(selectionChanged(QItemSelection, QItemSelection)), SLOT(selectionChanged(QItemSelection, QItemSelection))); + + auto buttonFolder = ui->buttonBox->addButton(tr("Open Folder"), QDialogButtonBox::ResetRole); + connect(buttonFolder, &QPushButton::clicked, this, &IconPickerDialog::openFolder); +} + +bool IconPickerDialog::eventFilter(QObject *obj, QEvent *evt) +{ + if (obj != ui->iconView) + return QDialog::eventFilter(obj, evt); + if (evt->type() != QEvent::KeyPress) + { + return QDialog::eventFilter(obj, evt); + } + QKeyEvent *keyEvent = static_cast<QKeyEvent *>(evt); + switch (keyEvent->key()) + { + case Qt::Key_Delete: + removeSelectedIcon(); + return true; + case Qt::Key_Plus: + addNewIcon(); + return true; + default: + break; + } + return QDialog::eventFilter(obj, evt); +} + +void IconPickerDialog::addNewIcon() +{ + //: The title of the select icons open file dialog + QString selectIcons = tr("Select Icons"); + //: The type of icon files + auto filter = IconUtils::getIconFilter(); + QStringList fileNames = QFileDialog::getOpenFileNames(this, selectIcons, QString(), tr("Icons %1").arg(filter)); + MMC->icons()->installIcons(fileNames); +} + +void IconPickerDialog::removeSelectedIcon() +{ + MMC->icons()->deleteIcon(selectedIconKey); +} + +void IconPickerDialog::activated(QModelIndex index) +{ + selectedIconKey = index.data(Qt::UserRole).toString(); + accept(); +} + +void IconPickerDialog::selectionChanged(QItemSelection selected, QItemSelection deselected) +{ + if (selected.empty()) + return; + + QString key = selected.first().indexes().first().data(Qt::UserRole).toString(); + if (!key.isEmpty()) + selectedIconKey = key; +} + +int IconPickerDialog::execWithSelection(QString selection) +{ + auto list = MMC->icons(); + auto contentsWidget = ui->iconView; + selectedIconKey = selection; + + int index_nr = list->getIconIndex(selection); + auto model_index = list->index(index_nr); + contentsWidget->selectionModel()->select( + model_index, QItemSelectionModel::Current | QItemSelectionModel::Select); + + QMetaObject::invokeMethod(this, "delayed_scroll", Qt::QueuedConnection, + Q_ARG(QModelIndex, model_index)); + return QDialog::exec(); +} + +void IconPickerDialog::delayed_scroll(QModelIndex model_index) +{ + auto contentsWidget = ui->iconView; + contentsWidget->scrollTo(model_index); +} + +IconPickerDialog::~IconPickerDialog() +{ + delete ui; +} + +void IconPickerDialog::openFolder() +{ + DesktopServices::openDirectory(MMC->icons()->getDirectory(), true); +} diff --git a/launcher/dialogs/IconPickerDialog.h b/launcher/dialogs/IconPickerDialog.h new file mode 100644 index 00000000..9af6a678 --- /dev/null +++ b/launcher/dialogs/IconPickerDialog.h @@ -0,0 +1,49 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once +#include <QDialog> +#include <QItemSelection> + +namespace Ui +{ +class IconPickerDialog; +} + +class IconPickerDialog : public QDialog +{ + Q_OBJECT + +public: + explicit IconPickerDialog(QWidget *parent = 0); + ~IconPickerDialog(); + int execWithSelection(QString selection); + QString selectedIconKey; + +protected: + virtual bool eventFilter(QObject *, QEvent *); + +private: + Ui::IconPickerDialog *ui; + +private +slots: + void selectionChanged(QItemSelection, QItemSelection); + void activated(QModelIndex); + void delayed_scroll(QModelIndex); + void addNewIcon(); + void removeSelectedIcon(); + void openFolder(); +}; diff --git a/launcher/dialogs/IconPickerDialog.ui b/launcher/dialogs/IconPickerDialog.ui new file mode 100644 index 00000000..c548edfb --- /dev/null +++ b/launcher/dialogs/IconPickerDialog.ui @@ -0,0 +1,67 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>IconPickerDialog</class> + <widget class="QDialog" name="IconPickerDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>676</width> + <height>555</height> + </rect> + </property> + <property name="windowTitle"> + <string>Pick icon</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QListView" name="iconView"/> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>IconPickerDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>248</x> + <y>254</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>IconPickerDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>316</x> + <y>260</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui> diff --git a/launcher/dialogs/LoginDialog.cpp b/launcher/dialogs/LoginDialog.cpp new file mode 100644 index 00000000..32f8a48f --- /dev/null +++ b/launcher/dialogs/LoginDialog.cpp @@ -0,0 +1,110 @@ +/* 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 "LoginDialog.h" +#include "ui_LoginDialog.h" + +#include "minecraft/auth/YggdrasilTask.h" + +#include <QtWidgets/QPushButton> + +LoginDialog::LoginDialog(QWidget *parent) : QDialog(parent), ui(new Ui::LoginDialog) +{ + ui->setupUi(this); + ui->progressBar->setVisible(false); + ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); + + connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); +} + +LoginDialog::~LoginDialog() +{ + delete ui; +} + +// Stage 1: User interaction +void LoginDialog::accept() +{ + setUserInputsEnabled(false); + ui->progressBar->setVisible(true); + + // Setup the login task and start it + m_account = MojangAccount::createFromUsername(ui->userTextBox->text()); + m_loginTask = m_account->login(nullptr, ui->passTextBox->text()); + connect(m_loginTask.get(), &Task::failed, this, &LoginDialog::onTaskFailed); + connect(m_loginTask.get(), &Task::succeeded, this, + &LoginDialog::onTaskSucceeded); + connect(m_loginTask.get(), &Task::status, this, &LoginDialog::onTaskStatus); + connect(m_loginTask.get(), &Task::progress, this, &LoginDialog::onTaskProgress); + m_loginTask->start(); +} + +void LoginDialog::setUserInputsEnabled(bool enable) +{ + ui->userTextBox->setEnabled(enable); + ui->passTextBox->setEnabled(enable); + ui->buttonBox->setEnabled(enable); +} + +// Enable the OK button only when both textboxes contain something. +void LoginDialog::on_userTextBox_textEdited(const QString &newText) +{ + ui->buttonBox->button(QDialogButtonBox::Ok) + ->setEnabled(!newText.isEmpty() && !ui->passTextBox->text().isEmpty()); +} +void LoginDialog::on_passTextBox_textEdited(const QString &newText) +{ + ui->buttonBox->button(QDialogButtonBox::Ok) + ->setEnabled(!newText.isEmpty() && !ui->userTextBox->text().isEmpty()); +} + +void LoginDialog::onTaskFailed(const QString &reason) +{ + // Set message + ui->label->setText("<span style='color:red'>" + reason + "</span>"); + + // Re-enable user-interaction + setUserInputsEnabled(true); + ui->progressBar->setVisible(false); +} + +void LoginDialog::onTaskSucceeded() +{ + QDialog::accept(); +} + +void LoginDialog::onTaskStatus(const QString &status) +{ + ui->label->setText(status); +} + +void LoginDialog::onTaskProgress(qint64 current, qint64 total) +{ + ui->progressBar->setMaximum(total); + ui->progressBar->setValue(current); +} + +// Public interface +MojangAccountPtr LoginDialog::newAccount(QWidget *parent, QString msg) +{ + LoginDialog dlg(parent); + dlg.ui->label->setText(msg); + if (dlg.exec() == QDialog::Accepted) + { + return dlg.m_account; + } + return 0; +} diff --git a/launcher/dialogs/LoginDialog.h b/launcher/dialogs/LoginDialog.h new file mode 100644 index 00000000..16bdddfb --- /dev/null +++ b/launcher/dialogs/LoginDialog.h @@ -0,0 +1,58 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <QtWidgets/QDialog> +#include <QtCore/QEventLoop> + +#include "minecraft/auth/MojangAccount.h" + +namespace Ui +{ +class LoginDialog; +} + +class LoginDialog : public QDialog +{ + Q_OBJECT + +public: + ~LoginDialog(); + + static MojangAccountPtr newAccount(QWidget *parent, QString message); + +private: + explicit LoginDialog(QWidget *parent = 0); + + void setUserInputsEnabled(bool enable); + +protected +slots: + void accept(); + + void onTaskFailed(const QString &reason); + void onTaskSucceeded(); + void onTaskStatus(const QString &status); + void onTaskProgress(qint64 current, qint64 total); + + void on_userTextBox_textEdited(const QString &newText); + void on_passTextBox_textEdited(const QString &newText); + +private: + Ui::LoginDialog *ui; + MojangAccountPtr m_account; + std::shared_ptr<Task> m_loginTask; +}; diff --git a/launcher/dialogs/LoginDialog.ui b/launcher/dialogs/LoginDialog.ui new file mode 100644 index 00000000..dbdb3b93 --- /dev/null +++ b/launcher/dialogs/LoginDialog.ui @@ -0,0 +1,87 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>LoginDialog</class> + <widget class="QDialog" name="LoginDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>421</width> + <height>238</height> + </rect> + </property> + <property name="sizePolicy"> + <sizepolicy hsizetype="Fixed" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="windowTitle"> + <string>Add Account</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QLabel" name="microsoftAccountsNoticeLabel"> + <property name="text"> + <string>NOTICE: MultiMC does not currently support Microsoft accounts. This means that accounts created from December 2020 onwards cannot be used.</string> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="label"> + <property name="text"> + <string notr="true">Message label placeholder.</string> + </property> + <property name="textFormat"> + <enum>Qt::RichText</enum> + </property> + <property name="textInteractionFlags"> + <set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set> + </property> + </widget> + </item> + <item> + <widget class="QLineEdit" name="userTextBox"> + <property name="placeholderText"> + <string>Email</string> + </property> + </widget> + </item> + <item> + <widget class="QLineEdit" name="passTextBox"> + <property name="echoMode"> + <enum>QLineEdit::Password</enum> + </property> + <property name="placeholderText"> + <string>Password</string> + </property> + </widget> + </item> + <item> + <widget class="QProgressBar" name="progressBar"> + <property name="value"> + <number>24</number> + </property> + <property name="textVisible"> + <bool>false</bool> + </property> + </widget> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui> diff --git a/launcher/dialogs/NewComponentDialog.cpp b/launcher/dialogs/NewComponentDialog.cpp new file mode 100644 index 00000000..f4d6274f --- /dev/null +++ b/launcher/dialogs/NewComponentDialog.cpp @@ -0,0 +1,106 @@ +/* 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 "MultiMC.h" +#include "NewComponentDialog.h" +#include "ui_NewComponentDialog.h" + +#include <BaseVersion.h> +#include <icons/IconList.h> +#include <tasks/Task.h> +#include <InstanceList.h> + +#include "VersionSelectDialog.h" +#include "ProgressDialog.h" +#include "IconPickerDialog.h" + +#include <QLayout> +#include <QPushButton> +#include <QFileDialog> +#include <QValidator> + +#include <meta/Index.h> +#include <meta/VersionList.h> + +NewComponentDialog::NewComponentDialog(const QString & initialName, const QString & initialUid, QWidget *parent) + : QDialog(parent), ui(new Ui::NewComponentDialog) +{ + ui->setupUi(this); + resize(minimumSizeHint()); + + ui->nameTextBox->setText(initialName); + ui->uidTextBox->setText(initialUid); + + connect(ui->nameTextBox, &QLineEdit::textChanged, this, &NewComponentDialog::updateDialogState); + connect(ui->uidTextBox, &QLineEdit::textChanged, this, &NewComponentDialog::updateDialogState); + + auto groups = MMC->instances()->getGroups().toSet(); + ui->nameTextBox->setFocus(); + + originalPlaceholderText = ui->uidTextBox->placeholderText(); + updateDialogState(); +} + +NewComponentDialog::~NewComponentDialog() +{ + delete ui; +} + +void NewComponentDialog::updateDialogState() +{ + auto protoUid = ui->nameTextBox->text().toLower(); + protoUid.remove(QRegularExpression("[^a-z]")); + if(protoUid.isEmpty()) + { + ui->uidTextBox->setPlaceholderText(originalPlaceholderText); + } + else + { + QString suggestedUid = "org.multimc.custom." + protoUid; + ui->uidTextBox->setPlaceholderText(suggestedUid); + } + bool allowOK = !name().isEmpty() && !uid().isEmpty() && !uidBlacklist.contains(uid()); + ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(allowOK); +} + +QString NewComponentDialog::name() const +{ + auto result = ui->nameTextBox->text(); + if(result.size()) + { + return result.trimmed(); + } + return QString(); +} + +QString NewComponentDialog::uid() const +{ + auto result = ui->uidTextBox->text(); + if(result.size()) + { + return result.trimmed(); + } + result = ui->uidTextBox->placeholderText(); + if(result.size() && result != originalPlaceholderText) + { + return result.trimmed(); + } + return QString(); +} + +void NewComponentDialog::setBlacklist(QStringList badUids) +{ + uidBlacklist = badUids; +} diff --git a/launcher/dialogs/NewComponentDialog.h b/launcher/dialogs/NewComponentDialog.h new file mode 100644 index 00000000..8c790beb --- /dev/null +++ b/launcher/dialogs/NewComponentDialog.h @@ -0,0 +1,48 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <QDialog> + +#include <QString> +#include <QStringList> + +namespace Ui +{ +class NewComponentDialog; +} + +class NewComponentDialog : public QDialog +{ + Q_OBJECT + +public: + explicit NewComponentDialog(const QString & initialName = QString(), const QString & initialUid = QString(), QWidget *parent = 0); + virtual ~NewComponentDialog(); + void setBlacklist(QStringList badUids); + + QString name() const; + QString uid() const; + +private slots: + void updateDialogState(); + +private: + Ui::NewComponentDialog *ui; + + QString originalPlaceholderText; + QStringList uidBlacklist; +}; diff --git a/launcher/dialogs/NewComponentDialog.ui b/launcher/dialogs/NewComponentDialog.ui new file mode 100644 index 00000000..03b0d222 --- /dev/null +++ b/launcher/dialogs/NewComponentDialog.ui @@ -0,0 +1,101 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>NewComponentDialog</class> + <widget class="QDialog" name="NewComponentDialog"> + <property name="windowModality"> + <enum>Qt::ApplicationModal</enum> + </property> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>345</width> + <height>146</height> + </rect> + </property> + <property name="windowTitle"> + <string>Add Empty Component</string> + </property> + <property name="windowIcon"> + <iconset> + <normaloff>:/icons/toolbar/copy</normaloff>:/icons/toolbar/copy</iconset> + </property> + <property name="modal"> + <bool>true</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QLineEdit" name="nameTextBox"> + <property name="placeholderText"> + <string>Name</string> + </property> + </widget> + </item> + <item> + <widget class="QLineEdit" name="uidTextBox"> + <property name="placeholderText"> + <string>uid</string> + </property> + </widget> + </item> + <item> + <widget class="Line" name="line"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </widget> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </widget> + <tabstops> + <tabstop>nameTextBox</tabstop> + <tabstop>uidTextBox</tabstop> + </tabstops> + <resources> + <include location="../../graphics.qrc"/> + </resources> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>NewComponentDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>248</x> + <y>254</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>NewComponentDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>316</x> + <y>260</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui> diff --git a/launcher/dialogs/NewInstanceDialog.cpp b/launcher/dialogs/NewInstanceDialog.cpp new file mode 100644 index 00000000..86963149 --- /dev/null +++ b/launcher/dialogs/NewInstanceDialog.cpp @@ -0,0 +1,255 @@ +/* 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 "MultiMC.h" +#include "NewInstanceDialog.h" +#include "ui_NewInstanceDialog.h" + +#include <BaseVersion.h> +#include <icons/IconList.h> +#include <tasks/Task.h> +#include <InstanceList.h> + +#include "VersionSelectDialog.h" +#include "ProgressDialog.h" +#include "IconPickerDialog.h" + +#include <QLayout> +#include <QPushButton> +#include <QFileDialog> +#include <QValidator> +#include <QDialogButtonBox> + +#include "widgets/PageContainer.h" +#include <pages/modplatform/VanillaPage.h> +#include <pages/modplatform/atlauncher/AtlPage.h> +#include <pages/modplatform/ftb/FtbPage.h> +#include <pages/modplatform/legacy_ftb/Page.h> +#include <pages/modplatform/flame/FlamePage.h> +#include <pages/modplatform/ImportPage.h> +#include <pages/modplatform/technic/TechnicPage.h> + + + +NewInstanceDialog::NewInstanceDialog(const QString & initialGroup, const QString & url, QWidget *parent) + : QDialog(parent), ui(new Ui::NewInstanceDialog) +{ + ui->setupUi(this); + + setWindowIcon(MMC->getThemedIcon("new")); + + InstIconKey = "default"; + ui->iconButton->setIcon(MMC->icons()->getIcon(InstIconKey)); + + auto groups = MMC->instances()->getGroups().toSet(); + auto groupList = QStringList(groups.toList()); + groupList.sort(Qt::CaseInsensitive); + groupList.removeOne(""); + groupList.push_front(initialGroup); + groupList.push_front(""); + ui->groupBox->addItems(groupList); + int index = groupList.indexOf(initialGroup); + if(index == -1) + { + index = 0; + } + ui->groupBox->setCurrentIndex(index); + ui->groupBox->lineEdit()->setPlaceholderText(tr("No group")); + + + // NOTE: m_buttons must be initialized before PageContainer, because it indirectly accesses m_buttons through setSuggestedPack! Do not move this below. + m_buttons = new QDialogButtonBox(QDialogButtonBox::Help | QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + + m_container = new PageContainer(this); + m_container->setSizePolicy(QSizePolicy::Policy::Preferred, QSizePolicy::Policy::Expanding); + m_container->layout()->setContentsMargins(0, 0, 0, 0); + ui->verticalLayout->insertWidget(2, m_container); + + m_container->addButtons(m_buttons); + + // Bonk Qt over its stupid head and make sure it understands which button is the default one... + // See: https://stackoverflow.com/questions/24556831/qbuttonbox-set-default-button + auto OkButton = m_buttons->button(QDialogButtonBox::Ok); + OkButton->setDefault(true); + OkButton->setAutoDefault(true); + connect(OkButton, &QPushButton::clicked, this, &NewInstanceDialog::accept); + + auto CancelButton = m_buttons->button(QDialogButtonBox::Cancel); + CancelButton->setDefault(false); + CancelButton->setAutoDefault(false); + connect(CancelButton, &QPushButton::clicked, this, &NewInstanceDialog::reject); + + auto HelpButton = m_buttons->button(QDialogButtonBox::Help); + HelpButton->setDefault(false); + HelpButton->setAutoDefault(false); + connect(HelpButton, &QPushButton::clicked, m_container, &PageContainer::help); + + if(!url.isEmpty()) + { + QUrl actualUrl(url); + m_container->selectPage("import"); + importPage->setUrl(url); + } + + updateDialogState(); + + restoreGeometry(QByteArray::fromBase64(MMC->settings()->get("NewInstanceGeometry").toByteArray())); +} + +void NewInstanceDialog::reject() +{ + MMC->settings()->set("NewInstanceGeometry", saveGeometry().toBase64()); + QDialog::reject(); +} + +void NewInstanceDialog::accept() +{ + MMC->settings()->set("NewInstanceGeometry", saveGeometry().toBase64()); + importIconNow(); + QDialog::accept(); +} + +QList<BasePage *> NewInstanceDialog::getPages() +{ + importPage = new ImportPage(this); + flamePage = new FlamePage(this); + auto technicPage = new TechnicPage(this); + return + { + new VanillaPage(this), + importPage, + new AtlPage(this), + flamePage, + new FtbPage(this), + new LegacyFTB::Page(this), + technicPage + }; +} + +QString NewInstanceDialog::dialogTitle() +{ + return tr("New Instance"); +} + +NewInstanceDialog::~NewInstanceDialog() +{ + delete ui; +} + +void NewInstanceDialog::setSuggestedPack(const QString& name, InstanceTask* task) +{ + creationTask.reset(task); + ui->instNameTextBox->setPlaceholderText(name); + + if(!task) + { + ui->iconButton->setIcon(MMC->icons()->getIcon("default")); + importIcon = false; + } + + auto allowOK = task && !instName().isEmpty(); + m_buttons->button(QDialogButtonBox::Ok)->setEnabled(allowOK); +} + +void NewInstanceDialog::setSuggestedIconFromFile(const QString &path, const QString &name) +{ + importIcon = true; + importIconPath = path; + importIconName = name; + + //Hmm, for some reason they can be to small + ui->iconButton->setIcon(QIcon(path)); +} + +void NewInstanceDialog::setSuggestedIcon(const QString &key) +{ + auto icon = MMC->icons()->getIcon(key); + importIcon = false; + + ui->iconButton->setIcon(icon); +} + +InstanceTask * NewInstanceDialog::extractTask() +{ + InstanceTask * extracted = creationTask.get(); + creationTask.release(); + extracted->setName(instName()); + extracted->setGroup(instGroup()); + extracted->setIcon(iconKey()); + return extracted; +} + +void NewInstanceDialog::updateDialogState() +{ + auto allowOK = creationTask && !instName().isEmpty(); + auto OkButton = m_buttons->button(QDialogButtonBox::Ok); + if(OkButton->isEnabled() != allowOK) + { + OkButton->setEnabled(allowOK); + } +} + +QString NewInstanceDialog::instName() const +{ + auto result = ui->instNameTextBox->text().trimmed(); + if(result.size()) + { + return result; + } + result = ui->instNameTextBox->placeholderText().trimmed(); + if(result.size()) + { + return result; + } + return QString(); +} + +QString NewInstanceDialog::instGroup() const +{ + return ui->groupBox->currentText(); +} +QString NewInstanceDialog::iconKey() const +{ + return InstIconKey; +} + +void NewInstanceDialog::on_iconButton_clicked() +{ + importIconNow(); //so the user can switch back + IconPickerDialog dlg(this); + dlg.execWithSelection(InstIconKey); + + if (dlg.result() == QDialog::Accepted) + { + InstIconKey = dlg.selectedIconKey; + ui->iconButton->setIcon(MMC->icons()->getIcon(InstIconKey)); + importIcon = false; + } +} + +void NewInstanceDialog::on_instNameTextBox_textChanged(const QString &arg1) +{ + updateDialogState(); +} + +void NewInstanceDialog::importIconNow() +{ + if(importIcon) { + MMC->icons()->installIcon(importIconPath, importIconName); + InstIconKey = importIconName; + importIcon = false; + } + MMC->settings()->set("NewInstanceGeometry", saveGeometry().toBase64()); +} diff --git a/launcher/dialogs/NewInstanceDialog.h b/launcher/dialogs/NewInstanceDialog.h new file mode 100644 index 00000000..53abf8cf --- /dev/null +++ b/launcher/dialogs/NewInstanceDialog.h @@ -0,0 +1,80 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <QDialog> + +#include "BaseVersion.h" +#include "pages/BasePageProvider.h" +#include "InstanceTask.h" + +namespace Ui +{ +class NewInstanceDialog; +} + +class PageContainer; +class QDialogButtonBox; +class ImportPage; +class FlamePage; + +class NewInstanceDialog : public QDialog, public BasePageProvider +{ + Q_OBJECT + +public: + explicit NewInstanceDialog(const QString & initialGroup, const QString & url = QString(), QWidget *parent = 0); + ~NewInstanceDialog(); + + void updateDialogState(); + + void setSuggestedPack(const QString & name = QString(), InstanceTask * task = nullptr); + void setSuggestedIconFromFile(const QString &path, const QString &name); + void setSuggestedIcon(const QString &key); + + InstanceTask * extractTask(); + + QString dialogTitle() override; + QList<BasePage *> getPages() override; + + QString instName() const; + QString instGroup() const; + QString iconKey() const; + +public slots: + void accept() override; + void reject() override; + +private slots: + void on_iconButton_clicked(); + void on_instNameTextBox_textChanged(const QString &arg1); + +private: + Ui::NewInstanceDialog *ui = nullptr; + PageContainer * m_container = nullptr; + QDialogButtonBox * m_buttons = nullptr; + + QString InstIconKey; + ImportPage *importPage = nullptr; + FlamePage *flamePage = nullptr; + std::unique_ptr<InstanceTask> creationTask; + + bool importIcon = false; + QString importIconPath; + QString importIconName; + + void importIconNow(); +}; diff --git a/launcher/dialogs/NewInstanceDialog.ui b/launcher/dialogs/NewInstanceDialog.ui new file mode 100644 index 00000000..7fb19ff5 --- /dev/null +++ b/launcher/dialogs/NewInstanceDialog.ui @@ -0,0 +1,87 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>NewInstanceDialog</class> + <widget class="QDialog" name="NewInstanceDialog"> + <property name="windowModality"> + <enum>Qt::ApplicationModal</enum> + </property> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>730</width> + <height>127</height> + </rect> + </property> + <property name="windowTitle"> + <string>New Instance</string> + </property> + <property name="windowIcon"> + <iconset> + <normaloff>:/icons/toolbar/new</normaloff>:/icons/toolbar/new</iconset> + </property> + <property name="modal"> + <bool>true</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <layout class="QGridLayout" name="gridLayout_2"> + <item row="1" column="2"> + <widget class="QComboBox" name="groupBox"> + <property name="editable"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QLabel" name="groupLabel"> + <property name="text"> + <string>&Group:</string> + </property> + <property name="buddy"> + <cstring>groupBox</cstring> + </property> + </widget> + </item> + <item row="0" column="2"> + <widget class="QLineEdit" name="instNameTextBox"/> + </item> + <item row="0" column="1"> + <widget class="QLabel" name="nameLabel"> + <property name="text"> + <string>&Name:</string> + </property> + <property name="buddy"> + <cstring>instNameTextBox</cstring> + </property> + </widget> + </item> + <item row="0" column="0" rowspan="2"> + <widget class="QToolButton" name="iconButton"> + <property name="iconSize"> + <size> + <width>80</width> + <height>80</height> + </size> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="Line" name="line"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </widget> + </item> + </layout> + </widget> + <tabstops> + <tabstop>iconButton</tabstop> + <tabstop>instNameTextBox</tabstop> + <tabstop>groupBox</tabstop> + </tabstops> + <resources/> + <connections/> +</ui> diff --git a/launcher/dialogs/NotificationDialog.cpp b/launcher/dialogs/NotificationDialog.cpp new file mode 100644 index 00000000..f2a35ae2 --- /dev/null +++ b/launcher/dialogs/NotificationDialog.cpp @@ -0,0 +1,86 @@ +#include "NotificationDialog.h" +#include "ui_NotificationDialog.h" + +#include <QTimerEvent> +#include <QStyle> + +NotificationDialog::NotificationDialog(const NotificationChecker::NotificationEntry &entry, QWidget *parent) : + QDialog(parent, Qt::MSWindowsFixedSizeDialogHint | Qt::WindowTitleHint | Qt::CustomizeWindowHint), + ui(new Ui::NotificationDialog) +{ + ui->setupUi(this); + + QStyle::StandardPixmap icon; + switch (entry.type) + { + case NotificationChecker::NotificationEntry::Critical: + icon = QStyle::SP_MessageBoxCritical; + break; + case NotificationChecker::NotificationEntry::Warning: + icon = QStyle::SP_MessageBoxWarning; + break; + default: + case NotificationChecker::NotificationEntry::Information: + icon = QStyle::SP_MessageBoxInformation; + break; + } + ui->iconLabel->setPixmap(style()->standardPixmap(icon, 0, this)); + ui->messageLabel->setText(entry.message); + + m_dontShowAgainText = tr("Don't show again"); + m_closeText = tr("Close"); + + ui->dontShowAgainBtn->setText(m_dontShowAgainText + QString(" (%1)").arg(m_dontShowAgainTime)); + ui->closeBtn->setText(m_closeText + QString(" (%1)").arg(m_closeTime)); + + startTimer(1000); +} + +NotificationDialog::~NotificationDialog() +{ + delete ui; +} + +void NotificationDialog::timerEvent(QTimerEvent *event) +{ + if (m_dontShowAgainTime > 0) + { + m_dontShowAgainTime--; + if (m_dontShowAgainTime == 0) + { + ui->dontShowAgainBtn->setText(m_dontShowAgainText); + ui->dontShowAgainBtn->setEnabled(true); + } + else + { + ui->dontShowAgainBtn->setText(m_dontShowAgainText + QString(" (%1)").arg(m_dontShowAgainTime)); + } + } + if (m_closeTime > 0) + { + m_closeTime--; + if (m_closeTime == 0) + { + ui->closeBtn->setText(m_closeText); + ui->closeBtn->setEnabled(true); + } + else + { + ui->closeBtn->setText(m_closeText + QString(" (%1)").arg(m_closeTime)); + } + } + + if (m_closeTime == 0 && m_dontShowAgainTime == 0) + { + killTimer(event->timerId()); + } +} + +void NotificationDialog::on_dontShowAgainBtn_clicked() +{ + done(DontShowAgain); +} +void NotificationDialog::on_closeBtn_clicked() +{ + done(Normal); +} diff --git a/launcher/dialogs/NotificationDialog.h b/launcher/dialogs/NotificationDialog.h new file mode 100644 index 00000000..e1cbb9fa --- /dev/null +++ b/launcher/dialogs/NotificationDialog.h @@ -0,0 +1,44 @@ +#ifndef NOTIFICATIONDIALOG_H +#define NOTIFICATIONDIALOG_H + +#include <QDialog> + +#include "notifications/NotificationChecker.h" + +namespace Ui { +class NotificationDialog; +} + +class NotificationDialog : public QDialog +{ + Q_OBJECT + +public: + explicit NotificationDialog(const NotificationChecker::NotificationEntry &entry, QWidget *parent = 0); + ~NotificationDialog(); + + enum ExitCode + { + Normal, + DontShowAgain + }; + +protected: + void timerEvent(QTimerEvent *event); + +private: + Ui::NotificationDialog *ui; + + int m_dontShowAgainTime = 10; + int m_closeTime = 5; + + QString m_dontShowAgainText; + QString m_closeText; + +private +slots: + void on_dontShowAgainBtn_clicked(); + void on_closeBtn_clicked(); +}; + +#endif // NOTIFICATIONDIALOG_H diff --git a/launcher/dialogs/NotificationDialog.ui b/launcher/dialogs/NotificationDialog.ui new file mode 100644 index 00000000..3e6c22bc --- /dev/null +++ b/launcher/dialogs/NotificationDialog.ui @@ -0,0 +1,85 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>NotificationDialog</class> + <widget class="QDialog" name="NotificationDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>320</width> + <height>240</height> + </rect> + </property> + <property name="windowTitle"> + <string>Notification</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout" stretch="1,0"> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_2" stretch="0,1"> + <item> + <widget class="QLabel" name="iconLabel"> + <property name="text"> + <string notr="true">TextLabel</string> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="messageLabel"> + <property name="text"> + <string notr="true">TextLabel</string> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + <property name="openExternalLinks"> + <bool>true</bool> + </property> + <property name="textInteractionFlags"> + <set>Qt::TextBrowserInteraction</set> + </property> + </widget> + </item> + </layout> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QPushButton" name="dontShowAgainBtn"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="text"> + <string>Don't show again</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="closeBtn"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="text"> + <string>Close</string> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui> diff --git a/launcher/dialogs/ProfileSelectDialog.cpp b/launcher/dialogs/ProfileSelectDialog.cpp new file mode 100644 index 00000000..ae34709f --- /dev/null +++ b/launcher/dialogs/ProfileSelectDialog.cpp @@ -0,0 +1,116 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ProfileSelectDialog.h" +#include <SkinUtils.h> +#include "ui_ProfileSelectDialog.h" + +#include <QItemSelectionModel> + +#include <QDebug> + +#include <dialogs/ProgressDialog.h> + +#include <MultiMC.h> + +ProfileSelectDialog::ProfileSelectDialog(const QString &message, int flags, QWidget *parent) + : QDialog(parent), ui(new Ui::ProfileSelectDialog) +{ + ui->setupUi(this); + + m_accounts = MMC->accounts(); + auto view = ui->listView; + //view->setModel(m_accounts.get()); + //view->hideColumn(MojangAccountList::ActiveColumn); + view->setColumnCount(1); + view->setRootIsDecorated(false); + if(QTreeWidgetItem* header = view->headerItem()) + { + header->setText(0, tr("Name")); + } + else + { + view->setHeaderLabel(tr("Name")); + } + QList <QTreeWidgetItem *> items; + for (int i = 0; i < m_accounts->count(); i++) + { + MojangAccountPtr account = m_accounts->at(i); + for (auto profile : account->profiles()) + { + auto profileLabel = profile.name; + if(account->isInUse()) + { + profileLabel += tr(" (in use)"); + } + auto item = new QTreeWidgetItem(view); + item->setText(0, profileLabel); + item->setIcon(0, SkinUtils::getFaceFromCache(profile.id)); + item->setData(0, MojangAccountList::PointerRole, QVariant::fromValue(account)); + items.append(item); + } + } + view->addTopLevelItems(items); + + // Set the message label. + ui->msgLabel->setVisible(!message.isEmpty()); + ui->msgLabel->setText(message); + + // Flags... + ui->globalDefaultCheck->setVisible(flags & GlobalDefaultCheckbox); + ui->instDefaultCheck->setVisible(flags & InstanceDefaultCheckbox); + qDebug() << flags; + + // Select the first entry in the list. + ui->listView->setCurrentIndex(ui->listView->model()->index(0, 0)); + + connect(ui->listView, SIGNAL(doubleClicked(QModelIndex)), SLOT(on_buttonBox_accepted())); +} + +ProfileSelectDialog::~ProfileSelectDialog() +{ + delete ui; +} + +MojangAccountPtr ProfileSelectDialog::selectedAccount() const +{ + return m_selected; +} + +bool ProfileSelectDialog::useAsGlobalDefault() const +{ + return ui->globalDefaultCheck->isChecked(); +} + +bool ProfileSelectDialog::useAsInstDefaullt() const +{ + return ui->instDefaultCheck->isChecked(); +} + +void ProfileSelectDialog::on_buttonBox_accepted() +{ + QModelIndexList selection = ui->listView->selectionModel()->selectedIndexes(); + if (selection.size() > 0) + { + QModelIndex selected = selection.first(); + m_selected = selected.data(MojangAccountList::PointerRole).value<MojangAccountPtr>(); + } + close(); +} + +void ProfileSelectDialog::on_buttonBox_rejected() +{ + close(); +} diff --git a/launcher/dialogs/ProfileSelectDialog.h b/launcher/dialogs/ProfileSelectDialog.h new file mode 100644 index 00000000..9f95830c --- /dev/null +++ b/launcher/dialogs/ProfileSelectDialog.h @@ -0,0 +1,90 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <QDialog> + +#include <memory> + +#include "minecraft/auth/MojangAccountList.h" + +namespace Ui +{ +class ProfileSelectDialog; +} + +class ProfileSelectDialog : public QDialog +{ + Q_OBJECT +public: + enum Flags + { + NoFlags = 0, + + /*! + * Shows a check box on the dialog that allows the user to specify that the account + * they've selected should be used as the global default for all instances. + */ + GlobalDefaultCheckbox, + + /*! + * Shows a check box on the dialog that allows the user to specify that the account + * they've selected should be used as the default for the instance they are currently launching. + * This is not currently implemented. + */ + InstanceDefaultCheckbox, + }; + + /*! + * Constructs a new account select dialog with the given parent and message. + * The message will be shown at the top of the dialog. It is an empty string by default. + */ + explicit ProfileSelectDialog(const QString& message="", int flags=0, QWidget *parent = 0); + ~ProfileSelectDialog(); + + /*! + * Gets a pointer to the account that the user selected. + * This is null if the user clicked cancel or hasn't clicked OK yet. + */ + MojangAccountPtr selectedAccount() const; + + /*! + * Returns true if the user checked the "use as global default" checkbox. + * If the checkbox wasn't shown, this function returns false. + */ + bool useAsGlobalDefault() const; + + /*! + * Returns true if the user checked the "use as instance default" checkbox. + * If the checkbox wasn't shown, this function returns false. + */ + bool useAsInstDefaullt() const; + +public +slots: + void on_buttonBox_accepted(); + + void on_buttonBox_rejected(); + +protected: + std::shared_ptr<MojangAccountList> m_accounts; + + //! The account that was selected when the user clicked OK. + MojangAccountPtr m_selected; + +private: + Ui::ProfileSelectDialog *ui; +}; diff --git a/launcher/dialogs/ProfileSelectDialog.ui b/launcher/dialogs/ProfileSelectDialog.ui new file mode 100644 index 00000000..e779b51b --- /dev/null +++ b/launcher/dialogs/ProfileSelectDialog.ui @@ -0,0 +1,62 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>ProfileSelectDialog</class> + <widget class="QDialog" name="ProfileSelectDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>465</width> + <height>300</height> + </rect> + </property> + <property name="windowTitle"> + <string>Select an Account</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QLabel" name="msgLabel"> + <property name="text"> + <string>Select a profile.</string> + </property> + </widget> + </item> + <item> + <widget class="QTreeWidget" name="listView"> + <column> + <property name="text"> + <string notr="true">1</string> + </property> + </column> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <widget class="QCheckBox" name="globalDefaultCheck"> + <property name="text"> + <string>Use as default?</string> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="instDefaultCheck"> + <property name="text"> + <string>Use as default for this instance only?</string> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui> diff --git a/launcher/dialogs/ProgressDialog.cpp b/launcher/dialogs/ProgressDialog.cpp new file mode 100644 index 00000000..4b092859 --- /dev/null +++ b/launcher/dialogs/ProgressDialog.cpp @@ -0,0 +1,196 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ProgressDialog.h" +#include "ui_ProgressDialog.h" + +#include <QKeyEvent> +#include <QDebug> + +#include "tasks/Task.h" + +ProgressDialog::ProgressDialog(QWidget *parent) : QDialog(parent), ui(new Ui::ProgressDialog) +{ + ui->setupUi(this); + this->setWindowFlags(this->windowFlags() & ~Qt::WindowContextHelpButtonHint); + setSkipButton(false); + changeProgress(0, 100); +} + +void ProgressDialog::setSkipButton(bool present, QString label) +{ + ui->skipButton->setAutoDefault(false); + ui->skipButton->setDefault(false); + ui->skipButton->setFocusPolicy(Qt::ClickFocus); + ui->skipButton->setEnabled(present); + ui->skipButton->setVisible(present); + ui->skipButton->setText(label); + updateSize(); +} + +void ProgressDialog::on_skipButton_clicked(bool checked) +{ + Q_UNUSED(checked); + task->abort(); +} + +ProgressDialog::~ProgressDialog() +{ + delete ui; +} + +void ProgressDialog::updateSize() +{ + QSize qSize = QSize(480, minimumSizeHint().height()); + resize(qSize); + setFixedSize(qSize); +} + +int ProgressDialog::execWithTask(Task *task) +{ + this->task = task; + QDialog::DialogCode result; + + if(!task) + { + qDebug() << "Programmer error: progress dialog created with null task."; + return Accepted; + } + + if(handleImmediateResult(result)) + { + return result; + } + + // Connect signals. + connect(task, SIGNAL(started()), SLOT(onTaskStarted())); + connect(task, SIGNAL(failed(QString)), SLOT(onTaskFailed(QString))); + connect(task, SIGNAL(succeeded()), SLOT(onTaskSucceeded())); + connect(task, SIGNAL(status(QString)), SLOT(changeStatus(const QString &))); + connect(task, SIGNAL(progress(qint64, qint64)), SLOT(changeProgress(qint64, qint64))); + + // if this didn't connect to an already running task, invoke start + if(!task->isRunning()) + { + task->start(); + } + if(task->isRunning()) + { + changeProgress(task->getProgress(), task->getTotalProgress()); + changeStatus(task->getStatus()); + return QDialog::exec(); + } + else if(handleImmediateResult(result)) + { + return result; + } + else + { + return QDialog::Rejected; + } +} + +// TODO: only provide the unique_ptr overloads +int ProgressDialog::execWithTask(std::unique_ptr<Task> &&task) +{ + connect(this, &ProgressDialog::destroyed, task.get(), &Task::deleteLater); + return execWithTask(task.release()); +} +int ProgressDialog::execWithTask(std::unique_ptr<Task> &task) +{ + connect(this, &ProgressDialog::destroyed, task.get(), &Task::deleteLater); + return execWithTask(task.release()); +} + +bool ProgressDialog::handleImmediateResult(QDialog::DialogCode &result) +{ + if(task->isFinished()) + { + if(task->wasSuccessful()) + { + result = QDialog::Accepted; + } + else + { + result = QDialog::Rejected; + } + return true; + } + return false; +} + +Task *ProgressDialog::getTask() +{ + return task; +} + +void ProgressDialog::onTaskStarted() +{ +} + +void ProgressDialog::onTaskFailed(QString failure) +{ + reject(); +} + +void ProgressDialog::onTaskSucceeded() +{ + accept(); +} + +void ProgressDialog::changeStatus(const QString &status) +{ + ui->statusLabel->setText(status); + updateSize(); +} + +void ProgressDialog::changeProgress(qint64 current, qint64 total) +{ + ui->taskProgressBar->setMaximum(total); + ui->taskProgressBar->setValue(current); +} + +void ProgressDialog::keyPressEvent(QKeyEvent *e) +{ + if(ui->skipButton->isVisible()) + { + if (e->key() == Qt::Key_Escape) + { + on_skipButton_clicked(true); + return; + } + else if(e->key() == Qt::Key_Tab) + { + ui->skipButton->setFocusPolicy(Qt::StrongFocus); + ui->skipButton->setFocus(); + ui->skipButton->setAutoDefault(true); + ui->skipButton->setDefault(true); + return; + } + } + QDialog::keyPressEvent(e); +} + +void ProgressDialog::closeEvent(QCloseEvent *e) +{ + if (task && task->isRunning()) + { + e->ignore(); + } + else + { + QDialog::closeEvent(e); + } +} diff --git a/launcher/dialogs/ProgressDialog.h b/launcher/dialogs/ProgressDialog.h new file mode 100644 index 00000000..b28ad4fa --- /dev/null +++ b/launcher/dialogs/ProgressDialog.h @@ -0,0 +1,71 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <QDialog> +#include <memory> + +class Task; + +namespace Ui +{ +class ProgressDialog; +} + +class ProgressDialog : public QDialog +{ + Q_OBJECT + +public: + explicit ProgressDialog(QWidget *parent = 0); + ~ProgressDialog(); + + void updateSize(); + + int execWithTask(Task *task); + int execWithTask(std::unique_ptr<Task> &&task); + int execWithTask(std::unique_ptr<Task> &task); + + void setSkipButton(bool present, QString label = QString()); + + Task *getTask(); + +public +slots: + void onTaskStarted(); + void onTaskFailed(QString failure); + void onTaskSucceeded(); + + void changeStatus(const QString &status); + void changeProgress(qint64 current, qint64 total); + + +private +slots: + void on_skipButton_clicked(bool checked); + +protected: + virtual void keyPressEvent(QKeyEvent *e); + virtual void closeEvent(QCloseEvent *e); + +private: + bool handleImmediateResult(QDialog::DialogCode &result); + +private: + Ui::ProgressDialog *ui; + + Task *task; +}; diff --git a/launcher/dialogs/ProgressDialog.ui b/launcher/dialogs/ProgressDialog.ui new file mode 100644 index 00000000..04b8fef3 --- /dev/null +++ b/launcher/dialogs/ProgressDialog.ui @@ -0,0 +1,66 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>ProgressDialog</class> + <widget class="QDialog" name="ProgressDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>400</width> + <height>100</height> + </rect> + </property> + <property name="minimumSize"> + <size> + <width>400</width> + <height>0</height> + </size> + </property> + <property name="maximumSize"> + <size> + <width>600</width> + <height>16777215</height> + </size> + </property> + <property name="windowTitle"> + <string>Please wait...</string> + </property> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="0"> + <widget class="QLabel" name="statusLabel"> + <property name="text"> + <string>Task Status...</string> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QProgressBar" name="taskProgressBar"> + <property name="value"> + <number>24</number> + </property> + <property name="textVisible"> + <bool>false</bool> + </property> + </widget> + </item> + <item row="2" column="0"> + <widget class="QPushButton" name="skipButton"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Expanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Skip</string> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui> diff --git a/launcher/dialogs/SkinUploadDialog.cpp b/launcher/dialogs/SkinUploadDialog.cpp new file mode 100644 index 00000000..56133529 --- /dev/null +++ b/launcher/dialogs/SkinUploadDialog.cpp @@ -0,0 +1,114 @@ +#include <QFileInfo> +#include <QFileDialog> +#include <FileSystem.h> +#include <minecraft/services/SkinUpload.h> +#include "SkinUploadDialog.h" +#include "ui_SkinUploadDialog.h" +#include "ProgressDialog.h" +#include "CustomMessageBox.h" + +void SkinUploadDialog::on_buttonBox_rejected() +{ + close(); +} + +void SkinUploadDialog::on_buttonBox_accepted() +{ + AuthSessionPtr session = std::make_shared<AuthSession>(); + auto login = m_acct->login(session); + ProgressDialog prog(this); + if (prog.execWithTask((Task*)login.get()) != QDialog::Accepted) + { + //FIXME: recover with password prompt + CustomMessageBox::selectable(this, tr("Skin Upload"), tr("Failed to login!"), QMessageBox::Warning)->exec(); + close(); + return; + } + QString fileName; + QString input = ui->skinPathTextBox->text(); + QRegExp urlPrefixMatcher("^([a-z]+)://.+$"); + bool isLocalFile = false; + // it has an URL prefix -> it is an URL + if(urlPrefixMatcher.exactMatch(input)) + { + QUrl fileURL = input; + if(fileURL.isValid()) + { + // local? + if(fileURL.isLocalFile()) + { + isLocalFile = true; + fileName = fileURL.toLocalFile(); + } + else + { + CustomMessageBox::selectable( + this, + tr("Skin Upload"), + tr("Using remote URLs for setting skins is not implemented yet."), + QMessageBox::Warning + )->exec(); + close(); + return; + } + } + else + { + CustomMessageBox::selectable( + this, + tr("Skin Upload"), + tr("You cannot use an invalid URL for uploading skins."), + QMessageBox::Warning + )->exec(); + close(); + return; + } + } + else + { + // just assume it's a path then + isLocalFile = true; + fileName = ui->skinPathTextBox->text(); + } + if (isLocalFile && !QFile::exists(fileName)) + { + CustomMessageBox::selectable(this, tr("Skin Upload"), tr("Skin file does not exist!"), QMessageBox::Warning)->exec(); + close(); + return; + } + SkinUpload::Model model = SkinUpload::STEVE; + if (ui->steveBtn->isChecked()) + { + model = SkinUpload::STEVE; + } + else if (ui->alexBtn->isChecked()) + { + model = SkinUpload::ALEX; + } + SkinUploadPtr upload = std::make_shared<SkinUpload>(this, session, FS::read(fileName), model); + if (prog.execWithTask((Task*)upload.get()) != QDialog::Accepted) + { + CustomMessageBox::selectable(this, tr("Skin Upload"), tr("Failed to upload skin!"), QMessageBox::Warning)->exec(); + close(); + return; + } + CustomMessageBox::selectable(this, tr("Skin Upload"), tr("Success"), QMessageBox::Information)->exec(); + close(); +} + +void SkinUploadDialog::on_skinBrowseBtn_clicked() +{ + QString raw_path = QFileDialog::getOpenFileName(this, tr("Select Skin Texture"), QString(), "*.png"); + if (raw_path.isEmpty() || !QFileInfo::exists(raw_path)) + { + return; + } + QString cooked_path = FS::NormalizePath(raw_path); + ui->skinPathTextBox->setText(cooked_path); +} + +SkinUploadDialog::SkinUploadDialog(MojangAccountPtr acct, QWidget *parent) + :QDialog(parent), m_acct(acct), ui(new Ui::SkinUploadDialog) +{ + ui->setupUi(this); +} diff --git a/launcher/dialogs/SkinUploadDialog.h b/launcher/dialogs/SkinUploadDialog.h new file mode 100644 index 00000000..deb44eac --- /dev/null +++ b/launcher/dialogs/SkinUploadDialog.h @@ -0,0 +1,29 @@ +#pragma once + +#include <QDialog> +#include <minecraft/auth/MojangAccount.h> + +namespace Ui +{ + class SkinUploadDialog; +} + +class SkinUploadDialog : public QDialog { + Q_OBJECT +public: + explicit SkinUploadDialog(MojangAccountPtr acct, QWidget *parent = 0); + virtual ~SkinUploadDialog() {}; + +public slots: + void on_buttonBox_accepted(); + + void on_buttonBox_rejected(); + + void on_skinBrowseBtn_clicked(); + +protected: + MojangAccountPtr m_acct; + +private: + Ui::SkinUploadDialog *ui; +}; diff --git a/launcher/dialogs/SkinUploadDialog.ui b/launcher/dialogs/SkinUploadDialog.ui new file mode 100644 index 00000000..6f5307e3 --- /dev/null +++ b/launcher/dialogs/SkinUploadDialog.ui @@ -0,0 +1,85 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>SkinUploadDialog</class> + <widget class="QDialog" name="SkinUploadDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>413</width> + <height>300</height> + </rect> + </property> + <property name="windowTitle"> + <string>Skin Upload</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QGroupBox" name="fileBox"> + <property name="title"> + <string>Skin File</string> + </property> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <widget class="QLineEdit" name="skinPathTextBox"/> + </item> + <item> + <widget class="QPushButton" name="skinBrowseBtn"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Fixed" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="maximumSize"> + <size> + <width>28</width> + <height>16777215</height> + </size> + </property> + <property name="text"> + <string notr="true">...</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="modelBox"> + <property name="title"> + <string>Player Model</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_1"> + <item> + <widget class="QRadioButton" name="steveBtn"> + <property name="text"> + <string>Steve Model</string> + </property> + <property name="checked"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QRadioButton" name="alexBtn"> + <property name="text"> + <string>Alex Model</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui> diff --git a/launcher/dialogs/UpdateDialog.cpp b/launcher/dialogs/UpdateDialog.cpp new file mode 100644 index 00000000..2baaf5e9 --- /dev/null +++ b/launcher/dialogs/UpdateDialog.cpp @@ -0,0 +1,182 @@ +#include "UpdateDialog.h" +#include "ui_UpdateDialog.h" +#include <QDebug> +#include "MultiMC.h" +#include <settings/SettingsObject.h> +#include <Json.h> + +#include "BuildConfig.h" +#include "HoeDown.h" + +UpdateDialog::UpdateDialog(bool hasUpdate, QWidget *parent) : QDialog(parent), ui(new Ui::UpdateDialog) +{ + ui->setupUi(this); + auto channel = MMC->settings()->get("UpdateChannel").toString(); + if(hasUpdate) + { + ui->label->setText(tr("A new %1 update is available!").arg(channel)); + } + else + { + ui->label->setText(tr("No %1 updates found. You are running the latest version.").arg(channel)); + ui->btnUpdateNow->setHidden(true); + ui->btnUpdateLater->setText(tr("Close")); + } + ui->changelogBrowser->setHtml(tr("<center><h1>Loading changelog...</h1></center>")); + loadChangelog(); + restoreGeometry(QByteArray::fromBase64(MMC->settings()->get("UpdateDialogGeometry").toByteArray())); +} + +UpdateDialog::~UpdateDialog() +{ +} + +void UpdateDialog::loadChangelog() +{ + auto channel = MMC->settings()->get("UpdateChannel").toString(); + dljob.reset(new NetJob("Changelog")); + QString url; + if(channel == "stable") + { + url = QString("https://raw.githubusercontent.com/MultiMC/MultiMC5/%1/changelog.md").arg(channel); + m_changelogType = CHANGELOG_MARKDOWN; + } + else + { + url = QString("https://api.github.com/repos/MultiMC/MultiMC5/compare/%1...%2").arg(BuildConfig.GIT_COMMIT, channel); + m_changelogType = CHANGELOG_COMMITS; + } + dljob->addNetAction(Net::Download::makeByteArray(QUrl(url), &changelogData)); + connect(dljob.get(), &NetJob::succeeded, this, &UpdateDialog::changelogLoaded); + connect(dljob.get(), &NetJob::failed, this, &UpdateDialog::changelogFailed); + dljob->start(); +} + +QString reprocessMarkdown(QByteArray markdown) +{ + HoeDown hoedown; + QString output = hoedown.process(markdown); + + // HACK: easier than customizing hoedown + output.replace(QRegExp("GH-([0-9]+)"), "<a href=\"https://github.com/MultiMC/MultiMC5/issues/\\1\">GH-\\1</a>"); + qDebug() << output; + return output; +} + +QString reprocessCommits(QByteArray json) +{ + auto channel = MMC->settings()->get("UpdateChannel").toString(); + try + { + QString result; + auto document = Json::requireDocument(json); + auto rootobject = Json::requireObject(document); + auto status = Json::requireString(rootobject, "status"); + auto diff_url = Json::requireString(rootobject, "html_url"); + + auto print_commits = [&]() + { + result += "<table cellspacing=0 cellpadding=2 style='border-width: 1px; border-style: solid'>"; + auto commitarray = Json::requireArray(rootobject, "commits"); + for(int i = commitarray.size() - 1; i >= 0; i--) + { + const auto & commitval = commitarray[i]; + auto commitobj = Json::requireObject(commitval); + auto parents_info = Json::ensureArray(commitobj, "parents"); + // NOTE: this ignores merge commits, because they have more than one parent + if(parents_info.size() > 1) + { + continue; + } + auto commit_url = Json::requireString(commitobj, "html_url"); + auto commit_info = Json::requireObject(commitobj, "commit"); + auto commit_message = Json::requireString(commit_info, "message"); + auto lines = commit_message.split('\n'); + QRegularExpression regexp("(?<prefix>(GH-(?<issuenr>[0-9]+))|(NOISSUE)|(SCRATCH))? *(?<rest>.*) *"); + auto match = regexp.match(lines.takeFirst(), 0, QRegularExpression::NormalMatch); + auto issuenr = match.captured("issuenr"); + auto prefix = match.captured("prefix"); + auto rest = match.captured("rest"); + result += "<tr><td>"; + if(issuenr.length()) + { + result += QString("<a href=\"https://github.com/MultiMC/MultiMC5/issues/%1\">GH-%2</a>").arg(issuenr, issuenr); + } + else if(prefix.length()) + { + result += QString("<a href=\"%1\">%2</a>").arg(commit_url, prefix); + } + else + { + result += QString("<a href=\"%1\">NOISSUE</a>").arg(commit_url); + } + result += "</td>"; + lines.prepend(rest); + result += "<td><p>" + lines.join("<br />") + "</p></td></tr>"; + } + result += "</table>"; + }; + + if(status == "identical") + { + return QObject::tr("<p>There are no code changes between your current version and latest %1.</p>").arg(channel); + } + else if(status == "ahead") + { + result += QObject::tr("<p>Following commits were added since last update:</p>"); + print_commits(); + } + else if(status == "diverged") + { + auto commit_ahead = Json::requireInteger(rootobject, "ahead_by"); + auto commit_behind = Json::requireInteger(rootobject, "behind_by"); + result += QObject::tr("<p>The update removes %1 commits and adds the following %2:</p>").arg(commit_behind).arg(commit_ahead); + print_commits(); + } + result += QObject::tr("<p>You can <a href=\"%1\">look at the changes on github</a>.</p>").arg(diff_url); + return result; + } + catch (const JSONValidationError &e) + { + qWarning() << "Got an unparseable commit log from github:" << e.what(); + qDebug() << json; + } + return QString(); +} + +void UpdateDialog::changelogLoaded() +{ + QString result; + switch(m_changelogType) + { + case CHANGELOG_COMMITS: + result = reprocessCommits(changelogData); + break; + case CHANGELOG_MARKDOWN: + result = reprocessMarkdown(changelogData); + break; + } + changelogData.clear(); + ui->changelogBrowser->setHtml(result); +} + +void UpdateDialog::changelogFailed(QString reason) +{ + ui->changelogBrowser->setHtml(tr("<p align=\"center\" <span style=\"font-size:22pt;\">Failed to fetch changelog... Error: %1</span></p>").arg(reason)); +} + +void UpdateDialog::on_btnUpdateLater_clicked() +{ + reject(); +} + +void UpdateDialog::on_btnUpdateNow_clicked() +{ + done(UPDATE_NOW); +} + +void UpdateDialog::closeEvent(QCloseEvent* evt) +{ + MMC->settings()->set("UpdateDialogGeometry", saveGeometry().toBase64()); + QDialog::closeEvent(evt); +} diff --git a/launcher/dialogs/UpdateDialog.h b/launcher/dialogs/UpdateDialog.h new file mode 100644 index 00000000..ae1799c3 --- /dev/null +++ b/launcher/dialogs/UpdateDialog.h @@ -0,0 +1,67 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <QDialog> +#include "net/NetJob.h" + +namespace Ui +{ +class UpdateDialog; +} + +enum UpdateAction +{ + UPDATE_LATER = QDialog::Rejected, + UPDATE_NOW = QDialog::Accepted, +}; + +enum ChangelogType +{ + CHANGELOG_MARKDOWN, + CHANGELOG_COMMITS +}; + +class UpdateDialog : public QDialog +{ + Q_OBJECT + +public: + explicit UpdateDialog(bool hasUpdate = true, QWidget *parent = 0); + ~UpdateDialog(); + +public slots: + void on_btnUpdateNow_clicked(); + void on_btnUpdateLater_clicked(); + + /// Starts loading the changelog + void loadChangelog(); + + /// Slot for when the chengelog loads successfully. + void changelogLoaded(); + + /// Slot for when the chengelog fails to load... + void changelogFailed(QString reason); + +protected: + void closeEvent(QCloseEvent * ) override; + +private: + Ui::UpdateDialog *ui; + QByteArray changelogData; + NetJobPtr dljob; + ChangelogType m_changelogType = CHANGELOG_MARKDOWN; +}; diff --git a/launcher/dialogs/UpdateDialog.ui b/launcher/dialogs/UpdateDialog.ui new file mode 100644 index 00000000..b0b3dd83 --- /dev/null +++ b/launcher/dialogs/UpdateDialog.ui @@ -0,0 +1,91 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>UpdateDialog</class> + <widget class="QDialog" name="UpdateDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>657</width> + <height>673</height> + </rect> + </property> + <property name="windowTitle"> + <string>MultiMC Update</string> + </property> + <property name="windowIcon"> + <iconset> + <normaloff>:/icons/toolbar/checkupdate</normaloff>:/icons/toolbar/checkupdate</iconset> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_2"> + <item> + <widget class="QLabel" name="label"> + <property name="font"> + <font> + <pointsize>14</pointsize> + </font> + </property> + <property name="text"> + <string/> + </property> + <property name="alignment"> + <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set> + </property> + <property name="buddy"> + <cstring>changelogBrowser</cstring> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="QTextBrowser" name="changelogBrowser"> + <property name="openExternalLinks"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="0"> + <widget class="QPushButton" name="btnUpdateNow"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Update now</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QPushButton" name="btnUpdateLater"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Don't update yet</string> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + <tabstops> + <tabstop>changelogBrowser</tabstop> + <tabstop>btnUpdateNow</tabstop> + <tabstop>btnUpdateLater</tabstop> + </tabstops> + <resources> + <include location="../../resources/multimc/multimc.qrc"/> + </resources> + <connections/> +</ui> diff --git a/launcher/dialogs/VersionSelectDialog.cpp b/launcher/dialogs/VersionSelectDialog.cpp new file mode 100644 index 00000000..ed1210ba --- /dev/null +++ b/launcher/dialogs/VersionSelectDialog.cpp @@ -0,0 +1,141 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "VersionSelectDialog.h" + +#include <QtWidgets/QButtonGroup> +#include <QtWidgets/QDialogButtonBox> +#include <QtWidgets/QHBoxLayout> +#include <QtWidgets/QPushButton> +#include <QtWidgets/QVBoxLayout> + +#include <dialogs/ProgressDialog.h> +#include "CustomMessageBox.h" + +#include <BaseVersion.h> +#include <BaseVersionList.h> +#include <tasks/Task.h> +#include <QDebug> +#include "MultiMC.h" +#include <VersionProxyModel.h> +#include <widgets/VersionSelectWidget.h> + +VersionSelectDialog::VersionSelectDialog(BaseVersionList *vlist, QString title, QWidget *parent, bool cancelable) + : QDialog(parent) +{ + setObjectName(QStringLiteral("VersionSelectDialog")); + resize(400, 347); + m_verticalLayout = new QVBoxLayout(this); + m_verticalLayout->setObjectName(QStringLiteral("verticalLayout")); + + m_versionWidget = new VersionSelectWidget(parent); + m_verticalLayout->addWidget(m_versionWidget); + + m_horizontalLayout = new QHBoxLayout(); + m_horizontalLayout->setObjectName(QStringLiteral("horizontalLayout")); + + m_refreshButton = new QPushButton(this); + m_refreshButton->setObjectName(QStringLiteral("refreshButton")); + m_horizontalLayout->addWidget(m_refreshButton); + + m_buttonBox = new QDialogButtonBox(this); + m_buttonBox->setObjectName(QStringLiteral("buttonBox")); + m_buttonBox->setOrientation(Qt::Horizontal); + m_buttonBox->setStandardButtons(QDialogButtonBox::Cancel|QDialogButtonBox::Ok); + m_horizontalLayout->addWidget(m_buttonBox); + + m_verticalLayout->addLayout(m_horizontalLayout); + + retranslate(); + + QObject::connect(m_buttonBox, SIGNAL(accepted()), this, SLOT(accept())); + QObject::connect(m_buttonBox, SIGNAL(rejected()), this, SLOT(reject())); + + QMetaObject::connectSlotsByName(this); + setWindowModality(Qt::WindowModal); + setWindowTitle(title); + + m_vlist = vlist; + + if (!cancelable) + { + m_buttonBox->button(QDialogButtonBox::Cancel)->setEnabled(false); + } +} + +void VersionSelectDialog::retranslate() +{ + // FIXME: overrides custom title given in constructor! + setWindowTitle(tr("Choose Version")); + m_refreshButton->setToolTip(tr("Reloads the version list.")); + m_refreshButton->setText(tr("&Refresh")); +} + +void VersionSelectDialog::setCurrentVersion(const QString& version) +{ + m_currentVersion = version; + m_versionWidget->setCurrentVersion(version); +} + +void VersionSelectDialog::setEmptyString(QString emptyString) +{ + m_versionWidget->setEmptyString(emptyString); +} + +void VersionSelectDialog::setEmptyErrorString(QString emptyErrorString) +{ + m_versionWidget->setEmptyErrorString(emptyErrorString); +} + +void VersionSelectDialog::setResizeOn(int column) +{ + resizeOnColumn = column; +} + +int VersionSelectDialog::exec() +{ + QDialog::open(); + m_versionWidget->initialize(m_vlist); + if(resizeOnColumn != -1) + { + m_versionWidget->setResizeOn(resizeOnColumn); + } + return QDialog::exec(); +} + +void VersionSelectDialog::selectRecommended() +{ + m_versionWidget->selectRecommended(); +} + +BaseVersionPtr VersionSelectDialog::selectedVersion() const +{ + return m_versionWidget->selectedVersion(); +} + +void VersionSelectDialog::on_refreshButton_clicked() +{ + m_versionWidget->loadList(); +} + +void VersionSelectDialog::setExactFilter(BaseVersionList::ModelRoles role, QString filter) +{ + m_versionWidget->setExactFilter(role, filter); +} + +void VersionSelectDialog::setFuzzyFilter(BaseVersionList::ModelRoles role, QString filter) +{ + m_versionWidget->setFuzzyFilter(role, filter); +} diff --git a/launcher/dialogs/VersionSelectDialog.h b/launcher/dialogs/VersionSelectDialog.h new file mode 100644 index 00000000..ed30d3f3 --- /dev/null +++ b/launcher/dialogs/VersionSelectDialog.h @@ -0,0 +1,78 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <QDialog> +#include <QSortFilterProxyModel> + + +#include "BaseVersionList.h" + +class QVBoxLayout; +class QHBoxLayout; +class QDialogButtonBox; +class VersionSelectWidget; +class QPushButton; + +namespace Ui +{ +class VersionSelectDialog; +} + +class VersionProxyModel; + +class VersionSelectDialog : public QDialog +{ + Q_OBJECT + +public: + explicit VersionSelectDialog(BaseVersionList *vlist, QString title, QWidget *parent = 0, bool cancelable = true); + virtual ~VersionSelectDialog() {}; + + int exec() override; + + BaseVersionPtr selectedVersion() const; + + void setCurrentVersion(const QString & version); + void setFuzzyFilter(BaseVersionList::ModelRoles role, QString filter); + void setExactFilter(BaseVersionList::ModelRoles role, QString filter); + void setEmptyString(QString emptyString); + void setEmptyErrorString(QString emptyErrorString); + void setResizeOn(int column); + +private slots: + void on_refreshButton_clicked(); + +private: + void retranslate(); + void selectRecommended(); + +private: + QString m_currentVersion; + VersionSelectWidget *m_versionWidget = nullptr; + QVBoxLayout *m_verticalLayout = nullptr; + QHBoxLayout *m_horizontalLayout = nullptr; + QPushButton *m_refreshButton = nullptr; + QDialogButtonBox *m_buttonBox = nullptr; + + BaseVersionList *m_vlist = nullptr; + + VersionProxyModel *m_proxyModel = nullptr; + + int resizeOnColumn = -1; + + Task * loadTask = nullptr; +}; |