From b258eac215c791b2a8eed10cecbbf9551c87f0b9 Mon Sep 17 00:00:00 2001 From: Petr Mrázek Date: Mon, 22 Nov 2021 03:55:16 +0100 Subject: NOISSUE continue reshuffling the codebase --- launcher/ui/dialogs/AboutDialog.cpp | 149 +++++++++ launcher/ui/dialogs/AboutDialog.h | 47 +++ launcher/ui/dialogs/AboutDialog.ui | 316 ++++++++++++++++++ launcher/ui/dialogs/CopyInstanceDialog.cpp | 144 ++++++++ launcher/ui/dialogs/CopyInstanceDialog.h | 58 ++++ launcher/ui/dialogs/CopyInstanceDialog.ui | 182 ++++++++++ launcher/ui/dialogs/CustomMessageBox.cpp | 35 ++ launcher/ui/dialogs/CustomMessageBox.h | 26 ++ launcher/ui/dialogs/EditAccountDialog.cpp | 61 ++++ launcher/ui/dialogs/EditAccountDialog.h | 56 ++++ launcher/ui/dialogs/EditAccountDialog.ui | 94 ++++++ launcher/ui/dialogs/ExportInstanceDialog.cpp | 482 +++++++++++++++++++++++++++ launcher/ui/dialogs/ExportInstanceDialog.h | 54 +++ launcher/ui/dialogs/ExportInstanceDialog.ui | 83 +++++ launcher/ui/dialogs/IconPickerDialog.cpp | 163 +++++++++ launcher/ui/dialogs/IconPickerDialog.h | 49 +++ launcher/ui/dialogs/IconPickerDialog.ui | 67 ++++ launcher/ui/dialogs/LoginDialog.cpp | 119 +++++++ launcher/ui/dialogs/LoginDialog.h | 59 ++++ launcher/ui/dialogs/LoginDialog.ui | 77 +++++ launcher/ui/dialogs/MSALoginDialog.cpp | 141 ++++++++ launcher/ui/dialogs/MSALoginDialog.h | 63 ++++ launcher/ui/dialogs/MSALoginDialog.ui | 65 ++++ launcher/ui/dialogs/NewComponentDialog.cpp | 106 ++++++ launcher/ui/dialogs/NewComponentDialog.h | 48 +++ launcher/ui/dialogs/NewComponentDialog.ui | 101 ++++++ launcher/ui/dialogs/NewInstanceDialog.cpp | 255 ++++++++++++++ launcher/ui/dialogs/NewInstanceDialog.h | 80 +++++ launcher/ui/dialogs/NewInstanceDialog.ui | 87 +++++ launcher/ui/dialogs/NotificationDialog.cpp | 86 +++++ launcher/ui/dialogs/NotificationDialog.h | 44 +++ launcher/ui/dialogs/NotificationDialog.ui | 85 +++++ launcher/ui/dialogs/ProfileSelectDialog.cpp | 115 +++++++ launcher/ui/dialogs/ProfileSelectDialog.h | 90 +++++ launcher/ui/dialogs/ProfileSelectDialog.ui | 62 ++++ launcher/ui/dialogs/ProfileSetupDialog.cpp | 250 ++++++++++++++ launcher/ui/dialogs/ProfileSetupDialog.h | 88 +++++ launcher/ui/dialogs/ProfileSetupDialog.ui | 74 ++++ launcher/ui/dialogs/ProgressDialog.cpp | 196 +++++++++++ launcher/ui/dialogs/ProgressDialog.h | 71 ++++ launcher/ui/dialogs/ProgressDialog.ui | 66 ++++ launcher/ui/dialogs/SkinUploadDialog.cpp | 155 +++++++++ launcher/ui/dialogs/SkinUploadDialog.h | 29 ++ launcher/ui/dialogs/SkinUploadDialog.ui | 97 ++++++ launcher/ui/dialogs/UpdateDialog.cpp | 182 ++++++++++ launcher/ui/dialogs/UpdateDialog.h | 67 ++++ launcher/ui/dialogs/UpdateDialog.ui | 91 +++++ launcher/ui/dialogs/VersionSelectDialog.cpp | 141 ++++++++ launcher/ui/dialogs/VersionSelectDialog.h | 78 +++++ 49 files changed, 5334 insertions(+) create mode 100644 launcher/ui/dialogs/AboutDialog.cpp create mode 100644 launcher/ui/dialogs/AboutDialog.h create mode 100644 launcher/ui/dialogs/AboutDialog.ui create mode 100644 launcher/ui/dialogs/CopyInstanceDialog.cpp create mode 100644 launcher/ui/dialogs/CopyInstanceDialog.h create mode 100644 launcher/ui/dialogs/CopyInstanceDialog.ui create mode 100644 launcher/ui/dialogs/CustomMessageBox.cpp create mode 100644 launcher/ui/dialogs/CustomMessageBox.h create mode 100644 launcher/ui/dialogs/EditAccountDialog.cpp create mode 100644 launcher/ui/dialogs/EditAccountDialog.h create mode 100644 launcher/ui/dialogs/EditAccountDialog.ui create mode 100644 launcher/ui/dialogs/ExportInstanceDialog.cpp create mode 100644 launcher/ui/dialogs/ExportInstanceDialog.h create mode 100644 launcher/ui/dialogs/ExportInstanceDialog.ui create mode 100644 launcher/ui/dialogs/IconPickerDialog.cpp create mode 100644 launcher/ui/dialogs/IconPickerDialog.h create mode 100644 launcher/ui/dialogs/IconPickerDialog.ui create mode 100644 launcher/ui/dialogs/LoginDialog.cpp create mode 100644 launcher/ui/dialogs/LoginDialog.h create mode 100644 launcher/ui/dialogs/LoginDialog.ui create mode 100644 launcher/ui/dialogs/MSALoginDialog.cpp create mode 100644 launcher/ui/dialogs/MSALoginDialog.h create mode 100644 launcher/ui/dialogs/MSALoginDialog.ui create mode 100644 launcher/ui/dialogs/NewComponentDialog.cpp create mode 100644 launcher/ui/dialogs/NewComponentDialog.h create mode 100644 launcher/ui/dialogs/NewComponentDialog.ui create mode 100644 launcher/ui/dialogs/NewInstanceDialog.cpp create mode 100644 launcher/ui/dialogs/NewInstanceDialog.h create mode 100644 launcher/ui/dialogs/NewInstanceDialog.ui create mode 100644 launcher/ui/dialogs/NotificationDialog.cpp create mode 100644 launcher/ui/dialogs/NotificationDialog.h create mode 100644 launcher/ui/dialogs/NotificationDialog.ui create mode 100644 launcher/ui/dialogs/ProfileSelectDialog.cpp create mode 100644 launcher/ui/dialogs/ProfileSelectDialog.h create mode 100644 launcher/ui/dialogs/ProfileSelectDialog.ui create mode 100644 launcher/ui/dialogs/ProfileSetupDialog.cpp create mode 100644 launcher/ui/dialogs/ProfileSetupDialog.h create mode 100644 launcher/ui/dialogs/ProfileSetupDialog.ui create mode 100644 launcher/ui/dialogs/ProgressDialog.cpp create mode 100644 launcher/ui/dialogs/ProgressDialog.h create mode 100644 launcher/ui/dialogs/ProgressDialog.ui create mode 100644 launcher/ui/dialogs/SkinUploadDialog.cpp create mode 100644 launcher/ui/dialogs/SkinUploadDialog.h create mode 100644 launcher/ui/dialogs/SkinUploadDialog.ui create mode 100644 launcher/ui/dialogs/UpdateDialog.cpp create mode 100644 launcher/ui/dialogs/UpdateDialog.h create mode 100644 launcher/ui/dialogs/UpdateDialog.ui create mode 100644 launcher/ui/dialogs/VersionSelectDialog.cpp create mode 100644 launcher/ui/dialogs/VersionSelectDialog.h (limited to 'launcher/ui/dialogs') diff --git a/launcher/ui/dialogs/AboutDialog.cpp b/launcher/ui/dialogs/AboutDialog.cpp new file mode 100644 index 00000000..501156d0 --- /dev/null +++ b/launcher/ui/dialogs/AboutDialog.cpp @@ -0,0 +1,149 @@ +/* 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 +#include "Application.h" +#include "BuildConfig.h" + +#include + +#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.setCodec(QTextCodec::codecForName("UTF-8")); + stream << "
\n"; + // TODO: possibly retrieve from git history at build time? + stream << "

" << QObject::tr("Developers", "About Credits") << "

\n"; + stream << "

Andrew Okin <forkk@forkk.net>

\n"; + stream << "

Petr Mrázek <peterix@gmail.com>

\n"; + stream << "

Sky Welch <multimc@bunnies.io>

\n"; + stream << "

Jan (02JanDal) <02jandal@gmail.com>

\n"; + stream << "

RoboSky <@RoboSky_>

\n"; + stream << "
\n"; + + stream << "

" << QObject::tr("With thanks to", "About Credits") << "

\n"; + stream << "

Orochimarufan <orochimarufan.x3@gmail.com>

\n"; + stream << "

TakSuyu <taksuyu@gmail.com>

\n"; + stream << "

Kilobyte <stiepen22@gmx.de>

\n"; + stream << "

Rootbear75 <@rootbear75>

\n"; + stream << "

Zeker Zhayard <@Zeker_Zhayard>

\n"; + stream << "
\n"; + + if(!patrons.isEmpty()) { + stream << "

" << QObject::tr("Patrons", "About Credits") << "

\n"; + for (QString patron : patrons) + { + stream << "

" << patron << "

\n"; + } + } + stream << "
\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 launcherName = BuildConfig.LAUNCHER_NAME; + + setWindowTitle(tr("About %1").arg(launcherName)); + + QString chtml = getCreditsHtml(QStringList()); + ui->creditsText->setHtml(chtml); + + QString lhtml = getLicenseHtml(); + ui->licenseText->setHtml(lhtml); + + ui->urlLabel->setOpenExternalLinks(true); + + ui->icon->setPixmap(APPLICATION->getThemedIcon("logo").pixmap(64)); + ui->title->setText(launcherName); + + 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( +"

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.

\n" +"

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 MultiMC-fork in the title).

\n" +"

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 without implying that you have our blessing.

" + )); + + QString urlText("

%1

"); + ui->urlLabel->setText(urlText.arg(BuildConfig.LAUNCHER_GIT)); + + QString copyText("© 2012-2021 %1"); + ui->copyLabel->setText(copyText.arg(BuildConfig.LAUNCHER_COPYRIGHT)); + + connect(ui->closeButton, SIGNAL(clicked()), SLOT(close())); + + connect(ui->aboutQt, &QPushButton::clicked, &QApplication::aboutQt); + + loadPatronList(); +} + +AboutDialog::~AboutDialog() +{ + delete ui; +} + +void AboutDialog::loadPatronList() +{ + netJob = 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(APPLICATION->network()); +} + +void AboutDialog::patronListLoaded() +{ + QString patronListStr(dataSink); + dataSink.clear(); + QString html = getCreditsHtml(patronListStr.split("\n", QString::SkipEmptyParts)); + ui->creditsText->setHtml(html); +} + diff --git a/launcher/ui/dialogs/AboutDialog.h b/launcher/ui/dialogs/AboutDialog.h new file mode 100644 index 00000000..cc4b8850 --- /dev/null +++ b/launcher/ui/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 +#include + +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; + + NetJob::Ptr netJob; + QByteArray dataSink; +}; diff --git a/launcher/ui/dialogs/AboutDialog.ui b/launcher/ui/dialogs/AboutDialog.ui new file mode 100644 index 00000000..422e877b --- /dev/null +++ b/launcher/ui/dialogs/AboutDialog.ui @@ -0,0 +1,316 @@ + + + AboutDialog + + + + 0 + 0 + 783 + 843 + + + + + 450 + 400 + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + + 64 + 64 + + + + + 64 + 64 + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + 15 + + + + MultiMC 5 + + + Qt::AlignCenter + + + + + + + 0 + + + + About + + + + + + true + + + <html><head/><body><p>A custom launcher that makes managing Minecraft easier by allowing you to have multiple instances of Minecraft at once.</p></body></html> + + + Qt::AlignCenter + + + true + + + + + + + + 10 + + + + GIT URL + + + Qt::AlignCenter + + + + + + + + 8 + true + + + + COPYRIGHT + + + Qt::AlignCenter + + + + + + + Qt::Horizontal + + + + + + + Version: + + + Qt::AlignCenter + + + + + + + Platform: + + + Qt::AlignCenter + + + + + + + Build Number: + + + Qt::AlignCenter + + + + + + + Channel: + + + Qt::AlignCenter + + + + + + + Qt::Vertical + + + + 20 + 212 + + + + + + + + + Credits + + + + + + true + + + Qt::TextBrowserInteraction + + + + + + + + License + + + + + + + 0 + 0 + + + + + DejaVu Sans Mono + + + + true + + + Qt::TextBrowserInteraction + + + + + + + + Forking/Redistribution + + + + + + Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + + + + + + + false + + + About Qt + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Close + + + + + + + + + tabWidget + creditsText + licenseText + redistributionText + aboutQt + closeButton + + + + diff --git a/launcher/ui/dialogs/CopyInstanceDialog.cpp b/launcher/ui/dialogs/CopyInstanceDialog.cpp new file mode 100644 index 00000000..e5113981 --- /dev/null +++ b/launcher/ui/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 +#include + +#include "Application.h" +#include "CopyInstanceDialog.h" +#include "ui_CopyInstanceDialog.h" + +#include "ui/dialogs/IconPickerDialog.h" + +#include "BaseVersion.h" +#include "icons/IconList.h" +#include "tasks/Task.h" +#include "BaseInstance.h" +#include "InstanceList.h" + +CopyInstanceDialog::CopyInstanceDialog(InstancePtr original, QWidget *parent) + :QDialog(parent), ui(new Ui::CopyInstanceDialog), m_original(original) +{ + ui->setupUi(this); + resize(minimumSizeHint()); + layout()->setSizeConstraint(QLayout::SetFixedSize); + + InstIconKey = original->iconKey(); + ui->iconButton->setIcon(APPLICATION->icons()->getIcon(InstIconKey)); + ui->instNameTextBox->setText(original->name()); + ui->instNameTextBox->setFocus(); + auto groups = APPLICATION->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(APPLICATION->instances()->getInstanceGroup(m_original->id())); + if(index == -1) + { + index = 0; + } + ui->groupBox->setCurrentIndex(index); + ui->groupBox->lineEdit()->setPlaceholderText(tr("No group")); + ui->copySavesCheckbox->setChecked(m_copySaves); + ui->keepPlaytimeCheckbox->setChecked(m_keepPlaytime); +} + +CopyInstanceDialog::~CopyInstanceDialog() +{ + delete ui; +} + +void CopyInstanceDialog::updateDialogState() +{ + auto allowOK = !instName().isEmpty(); + auto OkButton = ui->buttonBox->button(QDialogButtonBox::Ok); + if(OkButton->isEnabled() != allowOK) + { + OkButton->setEnabled(allowOK); + } +} + +QString CopyInstanceDialog::instName() const +{ + auto result = ui->instNameTextBox->text().trimmed(); + if(result.size()) + { + return result; + } + return QString(); +} + +QString CopyInstanceDialog::iconKey() const +{ + return InstIconKey; +} + +QString CopyInstanceDialog::instGroup() const +{ + return ui->groupBox->currentText(); +} + +void CopyInstanceDialog::on_iconButton_clicked() +{ + IconPickerDialog dlg(this); + dlg.execWithSelection(InstIconKey); + + if (dlg.result() == QDialog::Accepted) + { + InstIconKey = dlg.selectedIconKey; + ui->iconButton->setIcon(APPLICATION->icons()->getIcon(InstIconKey)); + } +} + +void CopyInstanceDialog::on_instNameTextBox_textChanged(const QString &arg1) +{ + updateDialogState(); +} + +bool CopyInstanceDialog::shouldCopySaves() const +{ + return m_copySaves; +} + +void CopyInstanceDialog::on_copySavesCheckbox_stateChanged(int state) +{ + if(state == Qt::Unchecked) + { + m_copySaves = false; + } + else if(state == Qt::Checked) + { + m_copySaves = true; + } +} + +bool CopyInstanceDialog::shouldKeepPlaytime() const +{ + return m_keepPlaytime; +} + + +void CopyInstanceDialog::on_keepPlaytimeCheckbox_stateChanged(int state) +{ + if(state == Qt::Unchecked) + { + m_keepPlaytime = false; + } + else if(state == Qt::Checked) + { + m_keepPlaytime = true; + } +} diff --git a/launcher/ui/dialogs/CopyInstanceDialog.h b/launcher/ui/dialogs/CopyInstanceDialog.h new file mode 100644 index 00000000..bf3cd920 --- /dev/null +++ b/launcher/ui/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 +#include "BaseVersion.h" +#include + +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/ui/dialogs/CopyInstanceDialog.ui b/launcher/ui/dialogs/CopyInstanceDialog.ui new file mode 100644 index 00000000..f4b191e2 --- /dev/null +++ b/launcher/ui/dialogs/CopyInstanceDialog.ui @@ -0,0 +1,182 @@ + + + CopyInstanceDialog + + + Qt::ApplicationModal + + + + 0 + 0 + 345 + 323 + + + + Copy Instance + + + + :/icons/toolbar/copy:/icons/toolbar/copy + + + true + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + :/icons/instances/grass:/icons/instances/grass + + + + 80 + 80 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Name + + + + + + + Qt::Horizontal + + + + + + + + + &Group + + + groupBox + + + + + + + + 0 + 0 + + + + true + + + + + + + + + Copy saves + + + + + + + Keep play time + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + iconButton + instNameTextBox + groupBox + copySavesCheckbox + keepPlaytimeCheckbox + + + + + + + buttonBox + accepted() + CopyInstanceDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + CopyInstanceDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/launcher/ui/dialogs/CustomMessageBox.cpp b/launcher/ui/dialogs/CustomMessageBox.cpp new file mode 100644 index 00000000..19029f68 --- /dev/null +++ b/launcher/ui/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/ui/dialogs/CustomMessageBox.h b/launcher/ui/dialogs/CustomMessageBox.h new file mode 100644 index 00000000..712c6518 --- /dev/null +++ b/launcher/ui/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 + +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/ui/dialogs/EditAccountDialog.cpp b/launcher/ui/dialogs/EditAccountDialog.cpp new file mode 100644 index 00000000..002c064b --- /dev/null +++ b/launcher/ui/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 +#include + +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/ui/dialogs/EditAccountDialog.h b/launcher/ui/dialogs/EditAccountDialog.h new file mode 100644 index 00000000..6b5eb4aa --- /dev/null +++ b/launcher/ui/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 + +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/ui/dialogs/EditAccountDialog.ui b/launcher/ui/dialogs/EditAccountDialog.ui new file mode 100644 index 00000000..e87509bc --- /dev/null +++ b/launcher/ui/dialogs/EditAccountDialog.ui @@ -0,0 +1,94 @@ + + + EditAccountDialog + + + + 0 + 0 + 400 + 148 + + + + Login + + + + + + Message label placeholder. + + + Qt::RichText + + + Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + Email + + + + + + + QLineEdit::Password + + + Password + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + EditAccountDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + EditAccountDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/launcher/ui/dialogs/ExportInstanceDialog.cpp b/launcher/ui/dialogs/ExportInstanceDialog.cpp new file mode 100644 index 00000000..1a164875 --- /dev/null +++ b/launcher/ui/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 +#include +#include +#include +#include + +#include +#include +#include +#include +#include "MMCStrings.h" +#include "SeparatorPrefixTree.h" +#include "Application.h" +#include +#include + +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(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(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(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(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 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 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(sourceModel()); + if (!fsm) + { + return false; + } + auto blockedPath = relPath(fsm->filePath(sourceIndex)); + auto found = blocked.find(blockedPath); + if(found) + { + return !found->leaf(); + } + return false; + } + + void setBlockedPaths(QStringList paths) + { + beginResetModel(); + blocked.clear(); + blocked.insert(paths); + endResetModel(); + } + + const SeparatorPrefixTree<'/'> & blockedPaths() const + { + return blocked; + } + +protected: + bool filterAcceptsColumn(int source_column, const QModelIndex &source_parent) const + { + Q_UNUSED(source_parent) + + // adjust the columns you want to filter out here + // return false for those that will be hidden + if (source_column == 2 || source_column == 3) + return false; + + return true; + } + +private: + InstancePtr m_instance; + SeparatorPrefixTree<'/'> blocked; +}; + +ExportInstanceDialog::ExportInstanceDialog(InstancePtr instance, QWidget *parent) + : QDialog(parent), ui(new Ui::ExportInstanceDialog), m_instance(instance) +{ + ui->setupUi(this); + auto model = new QFileSystemModel(this); + proxyModel = new PackIgnoreProxy(m_instance, this); + loadPackIgnore(); + proxyModel->setSourceModel(model); + auto root = instance->instanceRoot(); + ui->treeView->setModel(proxyModel); + ui->treeView->setRootIndex(proxyModel->mapFromSource(model->index(root))); + ui->treeView->sortByColumn(0, Qt::AscendingOrder); + + connect(proxyModel, SIGNAL(rowsInserted(QModelIndex,int,int)), SLOT(rowsInserted(QModelIndex,int,int))); + + model->setFilter(QDir::AllEntries | QDir::NoDotAndDotDot | QDir::AllDirs | QDir::Hidden); + model->setRootPath(root); + auto headerView = ui->treeView->header(); + headerView->setSectionResizeMode(QHeaderView::ResizeToContents); + headerView->setSectionResizeMode(0, QHeaderView::Stretch); +} + +ExportInstanceDialog::~ExportInstanceDialog() +{ + delete ui; +} + +/// Save icon to instance's folder is needed +void SaveIcon(InstancePtr m_instance) +{ + auto iconKey = m_instance->iconKey(); + auto iconList = APPLICATION->icons(); + auto mmcIcon = iconList->icon(iconKey); + if(!mmcIcon || mmcIcon->isBuiltIn()) { + return; + } + auto path = mmcIcon->getFilePath(); + if(!path.isNull()) { + QFileInfo inInfo (path); + FS::copy(path, FS::PathCombine(m_instance->instanceRoot(), inInfo.fileName())) (); + return; + } + auto & image = mmcIcon->m_images[mmcIcon->type()]; + auto & icon = image.icon; + auto sizes = icon.availableSizes(); + if(sizes.size() == 0) + { + return; + } + auto areaOf = [](QSize size) + { + return size.width() * size.height(); + }; + QSize largest = sizes[0]; + // find variant with largest area + for(auto size: sizes) + { + if(areaOf(largest) < areaOf(size)) + { + largest = size; + } + } + auto pixmap = icon.pixmap(largest); + pixmap.save(FS::PathCombine(m_instance->instanceRoot(), iconKey + ".png")); +} + +bool ExportInstanceDialog::doExport() +{ + auto name = FS::RemoveInvalidFilenameChars(m_instance->name()); + + const QString output = QFileDialog::getSaveFileName( + this, tr("Export %1").arg(m_instance->name()), + FS::PathCombine(QDir::homePath(), name + ".zip"), "Zip (*.zip)", nullptr, QFileDialog::DontConfirmOverwrite); + if (output.isEmpty()) + { + return false; + } + if (QFile::exists(output)) + { + int ret = + QMessageBox::question(this, tr("Overwrite?"), + tr("This file already exists. Do you want to overwrite it?"), + QMessageBox::No, QMessageBox::Yes); + if (ret == QMessageBox::No) + { + return false; + } + } + + SaveIcon(m_instance); + + auto & blocked = proxyModel->blockedPaths(); + using std::placeholders::_1; + if (!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/ui/dialogs/ExportInstanceDialog.h b/launcher/ui/dialogs/ExportInstanceDialog.h new file mode 100644 index 00000000..dea02d1b --- /dev/null +++ b/launcher/ui/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 +#include +#include + +class BaseInstance; +class PackIgnoreProxy; +typedef std::shared_ptr 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/ui/dialogs/ExportInstanceDialog.ui b/launcher/ui/dialogs/ExportInstanceDialog.ui new file mode 100644 index 00000000..bcd4e84a --- /dev/null +++ b/launcher/ui/dialogs/ExportInstanceDialog.ui @@ -0,0 +1,83 @@ + + + ExportInstanceDialog + + + + 0 + 0 + 720 + 625 + + + + Export Instance + + + + + + true + + + QAbstractItemView::ExtendedSelection + + + true + + + false + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + treeView + + + + + buttonBox + accepted() + ExportInstanceDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + ExportInstanceDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/launcher/ui/dialogs/IconPickerDialog.cpp b/launcher/ui/dialogs/IconPickerDialog.cpp new file mode 100644 index 00000000..fcb645db --- /dev/null +++ b/launcher/ui/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 +#include +#include + +#include "Application.h" + +#include "IconPickerDialog.h" +#include "ui_IconPickerDialog.h" + +#include "ui/instanceview/InstanceDelegate.h" + +#include "icons/IconList.h" +#include "icons/IconUtils.h" +#include + +IconPickerDialog::IconPickerDialog(QWidget *parent) + : QDialog(parent), ui(new Ui::IconPickerDialog) +{ + ui->setupUi(this); + setWindowModality(Qt::WindowModal); + + auto contentsWidget = ui->iconView; + contentsWidget->setViewMode(QListView::IconMode); + contentsWidget->setFlow(QListView::LeftToRight); + contentsWidget->setIconSize(QSize(48, 48)); + contentsWidget->setMovement(QListView::Static); + contentsWidget->setResizeMode(QListView::Adjust); + contentsWidget->setSelectionMode(QAbstractItemView::SingleSelection); + contentsWidget->setSpacing(5); + contentsWidget->setWordWrap(false); + contentsWidget->setWrapping(true); + contentsWidget->setUniformItemSizes(true); + contentsWidget->setTextElideMode(Qt::ElideRight); + contentsWidget->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); + contentsWidget->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); + contentsWidget->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + contentsWidget->setItemDelegate(new ListViewDelegate()); + + // contentsWidget->setAcceptDrops(true); + contentsWidget->setDropIndicatorShown(true); + contentsWidget->viewport()->setAcceptDrops(true); + contentsWidget->setDragDropMode(QAbstractItemView::DropOnly); + contentsWidget->setDefaultDropAction(Qt::CopyAction); + + contentsWidget->installEventFilter(this); + + contentsWidget->setModel(APPLICATION->icons().get()); + + // NOTE: ResetRole forces the button to be on the left, while the OK/Cancel ones are on the right. We win. + auto buttonAdd = ui->buttonBox->addButton(tr("Add Icon"), QDialogButtonBox::ResetRole); + auto buttonRemove = ui->buttonBox->addButton(tr("Remove Icon"), QDialogButtonBox::ResetRole); + + connect(buttonAdd, SIGNAL(clic