diff options
Diffstat (limited to 'launcher/ui/pages')
103 files changed, 15418 insertions, 0 deletions
diff --git a/launcher/ui/pages/BasePage.h b/launcher/ui/pages/BasePage.h new file mode 100644 index 00000000..408965d0 --- /dev/null +++ b/launcher/ui/pages/BasePage.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 <QString> +#include <QIcon> +#include <memory> + +#include "BasePageContainer.h" + +class BasePage +{ +public: + virtual ~BasePage() {} + virtual QString id() const = 0; + virtual QString displayName() const = 0; + virtual QIcon icon() const = 0; + virtual bool apply() { return true; } + virtual bool shouldDisplay() const { return true; } + virtual QString helpPage() const { return QString(); } + void opened() + { + isOpened = true; + openedImpl(); + } + void closed() + { + isOpened = false; + closedImpl(); + } + virtual void openedImpl() {} + virtual void closedImpl() {} + virtual void setParentContainer(BasePageContainer * container) + { + m_container = container; + }; +public: + int stackIndex = -1; + int listIndex = -1; +protected: + BasePageContainer * m_container = nullptr; + bool isOpened = false; +}; + +typedef std::shared_ptr<BasePage> BasePagePtr; diff --git a/launcher/ui/pages/BasePageContainer.h b/launcher/ui/pages/BasePageContainer.h new file mode 100644 index 00000000..f8c7adeb --- /dev/null +++ b/launcher/ui/pages/BasePageContainer.h @@ -0,0 +1,10 @@ +#pragma once + +class BasePageContainer +{ +public: + virtual ~BasePageContainer(){}; + virtual bool selectPage(QString pageId) = 0; + virtual void refreshContainer() = 0; + virtual bool requestClose() = 0; +}; diff --git a/launcher/ui/pages/BasePageProvider.h b/launcher/ui/pages/BasePageProvider.h new file mode 100644 index 00000000..873e8dce --- /dev/null +++ b/launcher/ui/pages/BasePageProvider.h @@ -0,0 +1,68 @@ +/* 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 "ui/pages/BasePage.h" +#include <memory> +#include <functional> + +class BasePageProvider +{ +public: + virtual QList<BasePage *> getPages() = 0; + virtual QString dialogTitle() = 0; +}; + +class GenericPageProvider : public BasePageProvider +{ + typedef std::function<BasePage *()> PageCreator; +public: + explicit GenericPageProvider(const QString &dialogTitle) + : m_dialogTitle(dialogTitle) + { + } + virtual ~GenericPageProvider() {} + + QList<BasePage *> getPages() override + { + QList<BasePage *> pages; + for (PageCreator creator : m_creators) + { + pages.append(creator()); + } + return pages; + } + QString dialogTitle() override { return m_dialogTitle; } + + void setDialogTitle(const QString &title) + { + m_dialogTitle = title; + } + void addPageCreator(PageCreator page) + { + m_creators.append(page); + } + + template<typename PageClass> + void addPage() + { + addPageCreator([](){return new PageClass();}); + } + +private: + QList<PageCreator> m_creators; + QString m_dialogTitle; +}; diff --git a/launcher/ui/pages/global/AccountListPage.cpp b/launcher/ui/pages/global/AccountListPage.cpp new file mode 100644 index 00000000..816dce47 --- /dev/null +++ b/launcher/ui/pages/global/AccountListPage.cpp @@ -0,0 +1,260 @@ +/* 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 "AccountListPage.h" +#include "ui_AccountListPage.h" + +#include <QItemSelectionModel> +#include <QMenu> + +#include <QDebug> + +#include "net/NetJob.h" + +#include "ui/dialogs/ProgressDialog.h" +#include "ui/dialogs/LoginDialog.h" +#include "ui/dialogs/MSALoginDialog.h" +#include "ui/dialogs/CustomMessageBox.h" +#include "ui/dialogs/SkinUploadDialog.h" + +#include "tasks/Task.h" +#include "minecraft/auth/AccountTask.h" +#include "minecraft/services/SkinDelete.h" + +#include "Application.h" + +#include "BuildConfig.h" + +#include "Secrets.h" + +AccountListPage::AccountListPage(QWidget *parent) + : QMainWindow(parent), ui(new Ui::AccountListPage) +{ + ui->setupUi(this); + ui->listView->setEmptyString(tr( + "Welcome!\n" + "If you're new here, you can click the \"Add\" button to add your Mojang or Minecraft account." + )); + ui->listView->setEmptyMode(VersionListView::String); + ui->listView->setContextMenuPolicy(Qt::CustomContextMenu); + + m_accounts = APPLICATION->accounts(); + + ui->listView->setModel(m_accounts.get()); + ui->listView->header()->setSectionResizeMode(0, QHeaderView::Stretch); + ui->listView->header()->setSectionResizeMode(1, QHeaderView::Stretch); + ui->listView->header()->setSectionResizeMode(2, QHeaderView::ResizeToContents); + ui->listView->setSelectionMode(QAbstractItemView::SingleSelection); + + // Expand the account column + + QItemSelectionModel *selectionModel = ui->listView->selectionModel(); + + connect(selectionModel, &QItemSelectionModel::selectionChanged, [this](const QItemSelection &sel, const QItemSelection &dsel) { + updateButtonStates(); + }); + connect(ui->listView, &VersionListView::customContextMenuRequested, this, &AccountListPage::ShowContextMenu); + + connect(m_accounts.get(), &AccountList::listChanged, this, &AccountListPage::listChanged); + connect(m_accounts.get(), &AccountList::listActivityChanged, this, &AccountListPage::listChanged); + connect(m_accounts.get(), &AccountList::defaultAccountChanged, this, &AccountListPage::listChanged); + + updateButtonStates(); + + // Xbox authentication won't work without a client identifier, so disable the button if it is missing + ui->actionAddMicrosoft->setVisible(Secrets::hasMSAClientID()); +} + +AccountListPage::~AccountListPage() +{ + delete ui; +} + +void AccountListPage::ShowContextMenu(const QPoint& pos) +{ + auto menu = ui->toolBar->createContextMenu(this, tr("Context menu")); + menu->exec(ui->listView->mapToGlobal(pos)); + delete menu; +} + +void AccountListPage::changeEvent(QEvent* event) +{ + if (event->type() == QEvent::LanguageChange) + { + ui->retranslateUi(this); + } + QMainWindow::changeEvent(event); +} + +QMenu * AccountListPage::createPopupMenu() +{ + QMenu* filteredMenu = QMainWindow::createPopupMenu(); + filteredMenu->removeAction(ui->toolBar->toggleViewAction() ); + return filteredMenu; +} + + +void AccountListPage::listChanged() +{ + updateButtonStates(); +} + +void AccountListPage::on_actionAddMojang_triggered() +{ + MinecraftAccountPtr account = LoginDialog::newAccount( + this, + tr("Please enter your Mojang account email and password to add your account.") + ); + + if (account) + { + m_accounts->addAccount(account); + if (m_accounts->count() == 1) { + m_accounts->setDefaultAccount(account); + } + } +} + +void AccountListPage::on_actionAddMicrosoft_triggered() +{ + if(BuildConfig.BUILD_PLATFORM == "osx64") { + CustomMessageBox::selectable( + this, + tr("Microsoft Accounts not available"), + tr( + "Microsoft accounts are only usable on macOS 10.13 or newer, with fully updated MultiMC.\n\n" + "Please update both your operating system and MultiMC." + ), + QMessageBox::Warning + )->exec(); + return; + } + MinecraftAccountPtr account = MSALoginDialog::newAccount( + this, + tr("Please enter your Mojang account email and password to add your account.") + ); + + if (account) + { + m_accounts->addAccount(account); + if (m_accounts->count() == 1) { + m_accounts->setDefaultAccount(account); + } + } +} + +void AccountListPage::on_actionRemove_triggered() +{ + QModelIndexList selection = ui->listView->selectionModel()->selectedIndexes(); + if (selection.size() > 0) + { + QModelIndex selected = selection.first(); + m_accounts->removeAccount(selected); + } +} + +void AccountListPage::on_actionRefresh_triggered() { + QModelIndexList selection = ui->listView->selectionModel()->selectedIndexes(); + if (selection.size() > 0) { + QModelIndex selected = selection.first(); + MinecraftAccountPtr account = selected.data(AccountList::PointerRole).value<MinecraftAccountPtr>(); + AuthSessionPtr session = std::make_shared<AuthSession>(); + auto task = account->refresh(session); + if (task) { + ProgressDialog progDialog(this); + progDialog.execWithTask(task.get()); + // TODO: respond to results of the task + } + } +} + + +void AccountListPage::on_actionSetDefault_triggered() +{ + QModelIndexList selection = ui->listView->selectionModel()->selectedIndexes(); + if (selection.size() > 0) + { + QModelIndex selected = selection.first(); + MinecraftAccountPtr account = selected.data(AccountList::PointerRole).value<MinecraftAccountPtr>(); + m_accounts->setDefaultAccount(account); + } +} + +void AccountListPage::on_actionNoDefault_triggered() +{ + m_accounts->setDefaultAccount(nullptr); +} + +void AccountListPage::updateButtonStates() +{ + // If there is no selection, disable buttons that require something selected. + QModelIndexList selection = ui->listView->selectionModel()->selectedIndexes(); + bool hasSelection = selection.size() > 0; + bool accountIsReady = false; + if (hasSelection) + { + QModelIndex selected = selection.first(); + MinecraftAccountPtr account = selected.data(AccountList::PointerRole).value<MinecraftAccountPtr>(); + accountIsReady = !account->isActive(); + } + ui->actionRemove->setEnabled(accountIsReady); + ui->actionSetDefault->setEnabled(accountIsReady); + ui->actionUploadSkin->setEnabled(accountIsReady); + ui->actionDeleteSkin->setEnabled(accountIsReady); + ui->actionRefresh->setEnabled(accountIsReady); + + if(m_accounts->defaultAccount().get() == nullptr) { + ui->actionNoDefault->setEnabled(false); + ui->actionNoDefault->setChecked(true); + } + else { + ui->actionNoDefault->setEnabled(true); + ui->actionNoDefault->setChecked(false); + } +} + +void AccountListPage::on_actionUploadSkin_triggered() +{ + QModelIndexList selection = ui->listView->selectionModel()->selectedIndexes(); + if (selection.size() > 0) + { + QModelIndex selected = selection.first(); + MinecraftAccountPtr account = selected.data(AccountList::PointerRole).value<MinecraftAccountPtr>(); + SkinUploadDialog dialog(account, this); + dialog.exec(); + } +} + +void AccountListPage::on_actionDeleteSkin_triggered() +{ + QModelIndexList selection = ui->listView->selectionModel()->selectedIndexes(); + if (selection.size() <= 0) + return; + + QModelIndex selected = selection.first(); + AuthSessionPtr session = std::make_shared<AuthSession>(); + MinecraftAccountPtr account = selected.data(AccountList::PointerRole).value<MinecraftAccountPtr>(); + auto login = account->refresh(session); + ProgressDialog prog(this); + if (prog.execWithTask((Task*)login.get()) != QDialog::Accepted) { + CustomMessageBox::selectable(this, tr("Skin Delete"), tr("Failed to login!"), QMessageBox::Warning)->exec(); + return; + } + auto deleteSkinTask = std::make_shared<SkinDelete>(this, session); + if (prog.execWithTask((Task*)deleteSkinTask.get()) != QDialog::Accepted) { + CustomMessageBox::selectable(this, tr("Skin Delete"), tr("Failed to delete current skin!"), QMessageBox::Warning)->exec(); + return; + } +} diff --git a/launcher/ui/pages/global/AccountListPage.h b/launcher/ui/pages/global/AccountListPage.h new file mode 100644 index 00000000..1c65e708 --- /dev/null +++ b/launcher/ui/pages/global/AccountListPage.h @@ -0,0 +1,85 @@ +/* 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 <QMainWindow> +#include <memory> + +#include "ui/pages/BasePage.h" + +#include "minecraft/auth/AccountList.h" +#include "Application.h" + +namespace Ui +{ +class AccountListPage; +} + +class AuthenticateTask; + +class AccountListPage : public QMainWindow, public BasePage +{ + Q_OBJECT +public: + explicit AccountListPage(QWidget *parent = 0); + ~AccountListPage(); + + QString displayName() const override + { + return tr("Accounts"); + } + QIcon icon() const override + { + auto icon = APPLICATION->getThemedIcon("accounts"); + if(icon.isNull()) + { + icon = APPLICATION->getThemedIcon("noaccount"); + } + return icon; + } + QString id() const override + { + return "accounts"; + } + QString helpPage() const override + { + return "Getting-Started#adding-an-account"; + } + +public slots: + void on_actionAddMojang_triggered(); + void on_actionAddMicrosoft_triggered(); + void on_actionRemove_triggered(); + void on_actionRefresh_triggered(); + void on_actionSetDefault_triggered(); + void on_actionNoDefault_triggered(); + void on_actionUploadSkin_triggered(); + void on_actionDeleteSkin_triggered(); + + void listChanged(); + + //! Updates the states of the dialog's buttons. + void updateButtonStates(); + +protected slots: + void ShowContextMenu(const QPoint &pos); + +private: + void changeEvent(QEvent * event) override; + QMenu * createPopupMenu() override; + shared_qobject_ptr<AccountList> m_accounts; + Ui::AccountListPage *ui; +}; diff --git a/launcher/ui/pages/global/AccountListPage.ui b/launcher/ui/pages/global/AccountListPage.ui new file mode 100644 index 00000000..29738c02 --- /dev/null +++ b/launcher/ui/pages/global/AccountListPage.ui @@ -0,0 +1,129 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>AccountListPage</class> + <widget class="QMainWindow" name="AccountListPage"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>800</width> + <height>600</height> + </rect> + </property> + <widget class="QWidget" name="centralwidget"> + <layout class="QHBoxLayout" name="horizontalLayout"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="VersionListView" name="listView"> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + <property name="rootIsDecorated"> + <bool>false</bool> + </property> + <property name="itemsExpandable"> + <bool>false</bool> + </property> + <property name="allColumnsShowFocus"> + <bool>true</bool> + </property> + <attribute name="headerStretchLastSection"> + <bool>false</bool> + </attribute> + </widget> + </item> + </layout> + </widget> + <widget class="WideBar" name="toolBar"> + <attribute name="toolBarArea"> + <enum>RightToolBarArea</enum> + </attribute> + <attribute name="toolBarBreak"> + <bool>false</bool> + </attribute> + <addaction name="actionAddMicrosoft"/> + <addaction name="actionAddMojang"/> + <addaction name="actionRefresh"/> + <addaction name="actionRemove"/> + <addaction name="actionSetDefault"/> + <addaction name="actionNoDefault"/> + <addaction name="separator"/> + <addaction name="actionUploadSkin"/> + <addaction name="actionDeleteSkin"/> + </widget> + <action name="actionAddMojang"> + <property name="text"> + <string>Add Mojang</string> + </property> + </action> + <action name="actionRemove"> + <property name="text"> + <string>Remove</string> + </property> + </action> + <action name="actionSetDefault"> + <property name="text"> + <string>Set Default</string> + </property> + </action> + <action name="actionNoDefault"> + <property name="checkable"> + <bool>true</bool> + </property> + <property name="text"> + <string>No Default</string> + </property> + </action> + <action name="actionUploadSkin"> + <property name="text"> + <string>Upload Skin</string> + </property> + </action> + <action name="actionDeleteSkin"> + <property name="text"> + <string>Delete Skin</string> + </property> + <property name="toolTip"> + <string>Delete the currently active skin and go back to the default one</string> + </property> + </action> + <action name="actionAddMicrosoft"> + <property name="text"> + <string>Add Microsoft</string> + </property> + </action> + <action name="actionRefresh"> + <property name="text"> + <string>Refresh</string> + </property> + <property name="toolTip"> + <string>Refresh the account tokens</string> + </property> + </action> + </widget> + <customwidgets> + <customwidget> + <class>VersionListView</class> + <extends>QTreeView</extends> + <header>ui/widgets/VersionListView.h</header> + </customwidget> + <customwidget> + <class>WideBar</class> + <extends>QToolBar</extends> + <header>ui/widgets/WideBar.h</header> + </customwidget> + </customwidgets> + <resources/> + <connections/> +</ui> diff --git a/launcher/ui/pages/global/CustomCommandsPage.cpp b/launcher/ui/pages/global/CustomCommandsPage.cpp new file mode 100644 index 00000000..8541e3c1 --- /dev/null +++ b/launcher/ui/pages/global/CustomCommandsPage.cpp @@ -0,0 +1,51 @@ +#include "CustomCommandsPage.h" +#include <QVBoxLayout> +#include <QTabWidget> +#include <QTabBar> + +CustomCommandsPage::CustomCommandsPage(QWidget* parent): QWidget(parent) +{ + + auto verticalLayout = new QVBoxLayout(this); + verticalLayout->setObjectName(QStringLiteral("verticalLayout")); + verticalLayout->setContentsMargins(0, 0, 0, 0); + + auto tabWidget = new QTabWidget(this); + tabWidget->setObjectName(QStringLiteral("tabWidget")); + commands = new CustomCommands(this); + commands->setContentsMargins(6, 6, 6, 6); + tabWidget->addTab(commands, "Foo"); + tabWidget->tabBar()->hide(); + verticalLayout->addWidget(tabWidget); + loadSettings(); +} + +CustomCommandsPage::~CustomCommandsPage() +{ +} + +bool CustomCommandsPage::apply() +{ + applySettings(); + return true; +} + +void CustomCommandsPage::applySettings() +{ + auto s = APPLICATION->settings(); + s->set("PreLaunchCommand", commands->prelaunchCommand()); + s->set("WrapperCommand", commands->wrapperCommand()); + s->set("PostExitCommand", commands->postexitCommand()); +} + +void CustomCommandsPage::loadSettings() +{ + auto s = APPLICATION->settings(); + commands->initialize( + false, + true, + s->get("PreLaunchCommand").toString(), + s->get("WrapperCommand").toString(), + s->get("PostExitCommand").toString() + ); +} diff --git a/launcher/ui/pages/global/CustomCommandsPage.h b/launcher/ui/pages/global/CustomCommandsPage.h new file mode 100644 index 00000000..a1155e0e --- /dev/null +++ b/launcher/ui/pages/global/CustomCommandsPage.h @@ -0,0 +1,55 @@ +/* Copyright 2018-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <memory> +#include <QDialog> + +#include "ui/pages/BasePage.h" +#include <Application.h> +#include "ui/widgets/CustomCommands.h" + +class CustomCommandsPage : public QWidget, public BasePage +{ + Q_OBJECT + +public: + explicit CustomCommandsPage(QWidget *parent = 0); + ~CustomCommandsPage(); + + QString displayName() const override + { + return tr("Custom Commands"); + } + QIcon icon() const override + { + return APPLICATION->getThemedIcon("custom-commands"); + } + QString id() const override + { + return "custom-commands"; + } + QString helpPage() const override + { + return "Custom-commands"; + } + bool apply() override; + +private: + void applySettings(); + void loadSettings(); + CustomCommands * commands; +}; diff --git a/launcher/ui/pages/global/ExternalToolsPage.cpp b/launcher/ui/pages/global/ExternalToolsPage.cpp new file mode 100644 index 00000000..41d900aa --- /dev/null +++ b/launcher/ui/pages/global/ExternalToolsPage.cpp @@ -0,0 +1,233 @@ +/* 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 "ExternalToolsPage.h" +#include "ui_ExternalToolsPage.h" + +#include <QMessageBox> +#include <QFileDialog> +#include <QStandardPaths> +#include <QTabBar> + +#include "settings/SettingsObject.h" +#include "tools/BaseProfiler.h" +#include <FileSystem.h> +#include "Application.h" +#include <tools/MCEditTool.h> + +ExternalToolsPage::ExternalToolsPage(QWidget *parent) : + QWidget(parent), + ui(new Ui::ExternalToolsPage) +{ + ui->setupUi(this); + ui->tabWidget->tabBar()->hide(); + + #if QT_VERSION >= QT_VERSION_CHECK(5, 2, 0) + ui->jsonEditorTextBox->setClearButtonEnabled(true); + #endif + + ui->mceditLink->setOpenExternalLinks(true); + ui->jvisualvmLink->setOpenExternalLinks(true); + ui->jprofilerLink->setOpenExternalLinks(true); + loadSettings(); +} + +ExternalToolsPage::~ExternalToolsPage() +{ + delete ui; +} + +void ExternalToolsPage::loadSettings() +{ + auto s = APPLICATION->settings(); + ui->jprofilerPathEdit->setText(s->get("JProfilerPath").toString()); + ui->jvisualvmPathEdit->setText(s->get("JVisualVMPath").toString()); + ui->mceditPathEdit->setText(s->get("MCEditPath").toString()); + + // Editors + ui->jsonEditorTextBox->setText(s->get("JsonEditor").toString()); +} +void ExternalToolsPage::applySettings() +{ + auto s = APPLICATION->settings(); + + s->set("JProfilerPath", ui->jprofilerPathEdit->text()); + s->set("JVisualVMPath", ui->jvisualvmPathEdit->text()); + s->set("MCEditPath", ui->mceditPathEdit->text()); + + // Editors + QString jsonEditor = ui->jsonEditorTextBox->text(); + if (!jsonEditor.isEmpty() && + (!QFileInfo(jsonEditor).exists() || !QFileInfo(jsonEditor).isExecutable())) + { + QString found = QStandardPaths::findExecutable(jsonEditor); + if (!found.isEmpty()) + { + jsonEditor = found; + } + } + s->set("JsonEditor", jsonEditor); +} + +void ExternalToolsPage::on_jprofilerPathBtn_clicked() +{ + QString raw_dir = ui->jprofilerPathEdit->text(); + QString error; + do + { + raw_dir = QFileDialog::getExistingDirectory(this, tr("JProfiler Folder"), raw_dir); + if (raw_dir.isEmpty()) + { + break; + } + QString cooked_dir = FS::NormalizePath(raw_dir); + if (!APPLICATION->profilers()["jprofiler"]->check(cooked_dir, &error)) + { + QMessageBox::critical(this, tr("Error"), tr("Error while checking JProfiler install:\n%1").arg(error)); + continue; + } + else + { + ui->jprofilerPathEdit->setText(cooked_dir); + break; + } + } while (1); +} +void ExternalToolsPage::on_jprofilerCheckBtn_clicked() +{ + QString error; + if (!APPLICATION->profilers()["jprofiler"]->check(ui->jprofilerPathEdit->text(), &error)) + { + QMessageBox::critical(this, tr("Error"), tr("Error while checking JProfiler install:\n%1").arg(error)); + } + else + { + QMessageBox::information(this, tr("OK"), tr("JProfiler setup seems to be OK")); + } +} + +void ExternalToolsPage::on_jvisualvmPathBtn_clicked() +{ + QString raw_dir = ui->jvisualvmPathEdit->text(); + QString error; + do + { + raw_dir = QFileDialog::getOpenFileName(this, tr("JVisualVM Executable"), raw_dir); + if (raw_dir.isEmpty()) + { + break; + } + QString cooked_dir = FS::NormalizePath(raw_dir); + if (!APPLICATION->profilers()["jvisualvm"]->check(cooked_dir, &error)) + { + QMessageBox::critical(this, tr("Error"), tr("Error while checking JVisualVM install:\n%1").arg(error)); + continue; + } + else + { + ui->jvisualvmPathEdit->setText(cooked_dir); + break; + } + } while (1); +} +void ExternalToolsPage::on_jvisualvmCheckBtn_clicked() +{ + QString error; + if (!APPLICATION->profilers()["jvisualvm"]->check(ui->jvisualvmPathEdit->text(), &error)) + { + QMessageBox::critical(this, tr("Error"), tr("Error while checking JVisualVM install:\n%1").arg(error)); + } + else + { + QMessageBox::information(this, tr("OK"), tr("JVisualVM setup seems to be OK")); + } +} + +void ExternalToolsPage::on_mceditPathBtn_clicked() +{ + QString raw_dir = ui->mceditPathEdit->text(); + QString error; + do + { +#ifdef Q_OS_OSX + raw_dir = QFileDialog::getOpenFileName(this, tr("MCEdit Application"), raw_dir); +#else + raw_dir = QFileDialog::getExistingDirectory(this, tr("MCEdit Folder"), raw_dir); +#endif + if (raw_dir.isEmpty()) + { + break; + } + QString cooked_dir = FS::NormalizePath(raw_dir); + if (!APPLICATION->mcedit()->check(cooked_dir, error)) + { + QMessageBox::critical(this, tr("Error"), tr("Error while checking MCEdit install:\n%1").arg(error)); + continue; + } + else + { + ui->mceditPathEdit->setText(cooked_dir); + break; + } + } while (1); +} +void ExternalToolsPage::on_mceditCheckBtn_clicked() +{ + QString error; + if (!APPLICATION->mcedit()->check(ui->mceditPathEdit->text(), error)) + { + QMessageBox::critical(this, tr("Error"), tr("Error while checking MCEdit install:\n%1").arg(error)); + } + else + { + QMessageBox::information(this, tr("OK"), tr("MCEdit setup seems to be OK")); + } +} + +void ExternalToolsPage::on_jsonEditorBrowseBtn_clicked() +{ + QString raw_file = QFileDialog::getOpenFileName( + this, tr("JSON Editor"), + ui->jsonEditorTextBox->text().isEmpty() +#if defined(Q_OS_LINUX) + ? QString("/usr/bin") +#else + ? QStandardPaths::standardLocations(QStandardPaths::ApplicationsLocation).first() +#endif + : ui->jsonEditorTextBox->text()); + + if (raw_file.isEmpty()) + { + return; + } + QString cooked_file = FS::NormalizePath(raw_file); + + // it has to exist and be an executable + if (QFileInfo(cooked_file).exists() && QFileInfo(cooked_file).isExecutable()) + { + ui->jsonEditorTextBox->setText(cooked_file); + } + else + { + QMessageBox::warning(this, tr("Invalid"), + tr("The file chosen does not seem to be an executable")); + } +} + +bool ExternalToolsPage::apply() +{ + applySettings(); + return true; +} diff --git a/launcher/ui/pages/global/ExternalToolsPage.h b/launcher/ui/pages/global/ExternalToolsPage.h new file mode 100644 index 00000000..5ae6148d --- /dev/null +++ b/launcher/ui/pages/global/ExternalToolsPage.h @@ -0,0 +1,74 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <QWidget> + +#include "ui/pages/BasePage.h" +#include <Application.h> + +namespace Ui { +class ExternalToolsPage; +} + +class ExternalToolsPage : public QWidget, public BasePage +{ + Q_OBJECT + +public: + explicit ExternalToolsPage(QWidget *parent = 0); + ~ExternalToolsPage(); + + QString displayName() const override + { + return tr("External Tools"); + } + QIcon icon() const override + { + auto icon = APPLICATION->getThemedIcon("externaltools"); + if(icon.isNull()) + { + icon = APPLICATION->getThemedIcon("loadermods"); + } + return icon; + } + QString id() const override + { + return "external-tools"; + } + QString helpPage() const override + { + return "Tools"; + } + virtual bool apply() override; + +private: + void loadSettings(); + void applySettings(); + +private: + Ui::ExternalToolsPage *ui; + +private +slots: + void on_jprofilerPathBtn_clicked(); + void on_jprofilerCheckBtn_clicked(); + void on_jvisualvmPathBtn_clicked(); + void on_jvisualvmCheckBtn_clicked(); + void on_mceditPathBtn_clicked(); + void on_mceditCheckBtn_clicked(); + void on_jsonEditorBrowseBtn_clicked(); +}; diff --git a/launcher/ui/pages/global/ExternalToolsPage.ui b/launcher/ui/pages/global/ExternalToolsPage.ui new file mode 100644 index 00000000..e79e9388 --- /dev/null +++ b/launcher/ui/pages/global/ExternalToolsPage.ui @@ -0,0 +1,194 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>ExternalToolsPage</class> + <widget class="QWidget" name="ExternalToolsPage"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>673</width> + <height>751</height> + </rect> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QTabWidget" name="tabWidget"> + <property name="currentIndex"> + <number>0</number> + </property> + <widget class="QWidget" name="tab"> + <attribute name="title"> + <string notr="true">Tab 1</string> + </attribute> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <item> + <widget class="QGroupBox" name="groupBox_2"> + <property name="title"> + <string notr="true">JProfiler</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_10"> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_4"> + <item> + <widget class="QLineEdit" name="jprofilerPathEdit"/> + </item> + <item> + <widget class="QPushButton" name="jprofilerPathBtn"> + <property name="text"> + <string notr="true">...</string> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="QPushButton" name="jprofilerCheckBtn"> + <property name="text"> + <string>Check</string> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="jprofilerLink"> + <property name="text"> + <string notr="true"><html><head/><body><p><a href="https://www.ej-technologies.com/products/jprofiler/overview.html">https://www.ej-technologies.com/products/jprofiler/overview.html</a></p></body></html></string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="groupBox_3"> + <property name="title"> + <string notr="true">JVisualVM</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_11"> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_5"> + <item> + <widget class="QLineEdit" name="jvisualvmPathEdit"/> + </item> + <item> + <widget class="QPushButton" name="jvisualvmPathBtn"> + <property name="text"> + <string notr="true">...</string> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="QPushButton" name="jvisualvmCheckBtn"> + <property name="text"> + <string>Check</string> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="jvisualvmLink"> + <property name="text"> + <string notr="true"><html><head/><body><p><a href="https://visualvm.github.io/">https://visualvm.github.io/</a></p></body></html></string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="groupBox_4"> + <property name="title"> + <string notr="true">MCEdit</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_12"> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_6"> + <item> + <widget class="QLineEdit" name="mceditPathEdit"/> + </item> + <item> + <widget class="QPushButton" name="mceditPathBtn"> + <property name="text"> + <string notr="true">...</string> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="QPushButton" name="mceditCheckBtn"> + <property name="text"> + <string>Check</string> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="mceditLink"> + <property name="text"> + <string notr="true"><html><head/><body><p><a href="https://www.mcedit.net/">https://www.mcedit.net/</a></p></body></html></string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="editorsBox"> + <property name="title"> + <string>External Editors (leave empty for system default)</string> + </property> + <layout class="QGridLayout" name="foldersBoxLayout_2"> + <item row="0" column="1"> + <widget class="QLineEdit" name="jsonEditorTextBox"/> + </item> + <item row="0" column="0"> + <widget class="QLabel" name="labelJsonEditor"> + <property name="text"> + <string>Text Editor:</string> + </property> + </widget> + </item> + <item row="0" column="2"> + <widget class="QToolButton" name="jsonEditorBrowseBtn"> + <property name="text"> + <string notr="true">...</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>216</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui> diff --git a/launcher/ui/pages/global/JavaPage.cpp b/launcher/ui/pages/global/JavaPage.cpp new file mode 100644 index 00000000..bd79f11a --- /dev/null +++ b/launcher/ui/pages/global/JavaPage.cpp @@ -0,0 +1,153 @@ +/* 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 "JavaPage.h" +#include "JavaCommon.h" +#include "ui_JavaPage.h" + +#include <QFileDialog> +#include <QMessageBox> +#include <QDir> +#include <QTabBar> + +#include "ui/dialogs/VersionSelectDialog.h" + +#include "java/JavaUtils.h" +#include "java/JavaInstallList.h" + +#include "settings/SettingsObject.h" +#include <FileSystem.h> +#include "Application.h" +#include <sys.h> + +JavaPage::JavaPage(QWidget *parent) : QWidget(parent), ui(new Ui::JavaPage) +{ + ui->setupUi(this); + ui->tabWidget->tabBar()->hide(); + + auto sysMiB = Sys::getSystemRam() / Sys::mebibyte; + ui->maxMemSpinBox->setMaximum(sysMiB); + loadSettings(); +} + +JavaPage::~JavaPage() +{ + delete ui; +} + +bool JavaPage::apply() +{ + applySettings(); + return true; +} + +void JavaPage::applySettings() +{ + auto s = APPLICATION->settings(); + + // Memory + int min = ui->minMemSpinBox->value(); + int max = ui->maxMemSpinBox->value(); + if(min < max) + { + s->set("MinMemAlloc", min); + s->set("MaxMemAlloc", max); + } + else + { + s->set("MinMemAlloc", max); + s->set("MaxMemAlloc", min); + } + s->set("PermGen", ui->permGenSpinBox->value()); + + // Java Settings + s->set("JavaPath", ui->javaPathTextBox->text()); + s->set("JvmArgs", ui->jvmArgsTextBox->text()); + JavaCommon::checkJVMArgs(s->get("JvmArgs").toString(), this->parentWidget()); +} +void JavaPage::loadSettings() +{ + auto s = APPLICATION->settings(); + // Memory + int min = s->get("MinMemAlloc").toInt(); + int max = s->get("MaxMemAlloc").toInt(); + if(min < max) + { + ui->minMemSpinBox->setValue(min); + ui->maxMemSpinBox->setValue(max); + } + else + { + ui->minMemSpinBox->setValue(max); + ui->maxMemSpinBox->setValue(min); + } + ui->permGenSpinBox->setValue(s->get("PermGen").toInt()); + + // Java Settings + ui->javaPathTextBox->setText(s->get("JavaPath").toString()); + ui->jvmArgsTextBox->setText(s->get("JvmArgs").toString()); +} + +void JavaPage::on_javaDetectBtn_clicked() +{ + JavaInstallPtr java; + + VersionSelectDialog vselect(APPLICATION->javalist().get(), tr("Select a Java version"), this, true); + vselect.setResizeOn(2); + vselect.exec(); + + if (vselect.result() == QDialog::Accepted && vselect.selectedVersion()) + { + java = std::dynamic_pointer_cast<JavaInstall>(vselect.selectedVersion()); + ui->javaPathTextBox->setText(java->path); + } +} + +void JavaPage::on_javaBrowseBtn_clicked() +{ + QString raw_path = QFileDialog::getOpenFileName(this, tr("Find Java executable")); + + // do not allow current dir - it's dirty. Do not allow dirs that don't exist + if(raw_path.isEmpty()) + { + return; + } + + QString cooked_path = FS::NormalizePath(raw_path); + QFileInfo javaInfo(cooked_path);; + if(!javaInfo.exists() || !javaInfo.isExecutable()) + { + return; + } + ui->javaPathTextBox->setText(cooked_path); +} + +void JavaPage::on_javaTestBtn_clicked() +{ + if(checker) + { + return; + } + checker.reset(new JavaCommon::TestCheck( + this, ui->javaPathTextBox->text(), ui->jvmArgsTextBox->text(), + ui->minMemSpinBox->value(), ui->maxMemSpinBox->value(), ui->permGenSpinBox->value())); + connect(checker.get(), SIGNAL(finished()), SLOT(checkerFinished())); + checker->run(); +} + +void JavaPage::checkerFinished() +{ + checker.reset(); +} diff --git a/launcher/ui/pages/global/JavaPage.h b/launcher/ui/pages/global/JavaPage.h new file mode 100644 index 00000000..8f9b3323 --- /dev/null +++ b/launcher/ui/pages/global/JavaPage.h @@ -0,0 +1,72 @@ +/* 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 <memory> +#include <QDialog> +#include "ui/pages/BasePage.h" +#include "JavaCommon.h" +#include <Application.h> +#include <QObjectPtr.h> + +class SettingsObject; + +namespace Ui +{ +class JavaPage; +} + +class JavaPage : public QWidget, public BasePage +{ + Q_OBJECT + +public: + explicit JavaPage(QWidget *parent = 0); + ~JavaPage(); + + QString displayName() const override + { + return tr("Java"); + } + QIcon icon() const override + { + return APPLICATION->getThemedIcon("java"); + } + QString id() const override + { + return "java-settings"; + } + QString helpPage() const override + { + return "Java-settings"; + } + bool apply() override; + +private: + void applySettings(); + void loadSettings(); + +private +slots: + void on_javaDetectBtn_clicked(); + void on_javaTestBtn_clicked(); + void on_javaBrowseBtn_clicked(); + void checkerFinished(); + +private: + Ui::JavaPage *ui; + unique_qobject_ptr<JavaCommon::TestCheck> checker; +}; diff --git a/launcher/ui/pages/global/JavaPage.ui b/launcher/ui/pages/global/JavaPage.ui new file mode 100644 index 00000000..b67e9994 --- /dev/null +++ b/launcher/ui/pages/global/JavaPage.ui @@ -0,0 +1,260 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>JavaPage</class> + <widget class="QWidget" name="JavaPage"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>545</width> + <height>580</height> + </rect> + </property> + <property name="sizePolicy"> + <sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QTabWidget" name="tabWidget"> + <property name="currentIndex"> + <number>0</number> + </property> + <widget class="QWidget" name="tab"> + <attribute name="title"> + <string notr="true">Tab 1</string> + </attribute> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <item> + <widget class="QGroupBox" name="memoryGroupBox"> + <property name="title"> + <string>Memory</string> + </property> + <layout class="QGridLayout" name="gridLayout_2"> + <item row="1" column="1"> + <widget class="QSpinBox" name="maxMemSpinBox"> + <property name="toolTip"> + <string>The maximum amount of memory Minecraft is allowed to use.</string> + </property> + <property name="suffix"> + <string notr="true"> MiB</string> + </property> + <property name="minimum"> + <number>128</number> + </property> + <property name="maximum"> + <number>65536</number> + </property> + <property name="singleStep"> + <number>128</number> + </property> + <property name="value"> + <number>1024</number> + </property> + </widget> + </item> + <item row="0" column="0"> + <widget class="QLabel" name="labelMinMem"> + <property name="text"> + <string>Minimum memory allocation:</string> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="labelMaxMem"> + <property name="text"> + <string>Maximum memory allocation:</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QSpinBox" name="minMemSpinBox"> + <property name="toolTip"> + <string>The amount of memory Minecraft is started with.</string> + </property> + <property name="suffix"> + <string notr="true"> MiB</string> + </property> + <property name="minimum"> + <number>128</number> + </property> + <property name="maximum"> + <number>65536</number> + </property> + <property name="singleStep"> + <number>128</number> + </property> + <property name="value"> + <number>256</number> + </property> + </widget> + </item> + <item row="2" column="0"> + <widget class="QLabel" name="labelPermGen"> + <property name="text"> + <string notr="true">PermGen:</string> + </property> + </widget> + </item> + <item row="2" column="1"> + <widget class="QSpinBox" name="permGenSpinBox"> + <property name="toolTip"> + <string>The amount of memory available to store loaded Java classes.</string> + </property> + <property name="suffix"> + <string notr="true"> MiB</string> + </property> + <property name="minimum"> + <number>64</number> + </property> + <property name="maximum"> + <number>999999999</number> + </property> + <property name="singleStep"> + <number>8</number> + </property> + <property name="value"> + <number>64</number> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="javaSettingsGroupBox"> + <property name="title"> + <string>Java Runtime</string> + </property> + <layout class="QGridLayout" name="gridLayout_3"> + <item row="0" column="0"> + <widget class="QLabel" name="labelJavaPath"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Fixed" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Java path:</string> + </property> + </widget> + </item> + <item row="0" column="1" colspan="2"> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <widget class="QLineEdit" name="javaPathTextBox"/> + </item> + <item> + <widget class="QPushButton" name="javaBrowseBtn"> + <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> + </item> + <item row="2" column="1" colspan="2"> + <widget class="QLineEdit" name="jvmArgsTextBox"/> + </item> + <item row="2" column="0"> + <widget class="QLabel" name="labelJVMArgs"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Fixed" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>JVM arguments:</string> + </property> + </widget> + </item> + <item row="3" column="1"> + <widget class="QPushButton" name="javaDetectBtn"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Auto-detect...</string> + </property> + </widget> + </item> + <item row="3" column="2"> + <widget class="QPushButton" name="javaTestBtn"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Test</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + </widget> + </item> + </layout> + </widget> + <tabstops> + <tabstop>minMemSpinBox</tabstop> + <tabstop>maxMemSpinBox</tabstop> + <tabstop>permGenSpinBox</tabstop> + <tabstop>javaBrowseBtn</tabstop> + <tabstop>javaPathTextBox</tabstop> + <tabstop>jvmArgsTextBox</tabstop> + <tabstop>javaDetectBtn</tabstop> + <tabstop>javaTestBtn</tabstop> + <tabstop>tabWidget</tabstop> + </tabstops> + <resources/> + <connections/> +</ui> diff --git a/launcher/ui/pages/global/LanguagePage.cpp b/launcher/ui/pages/global/LanguagePage.cpp new file mode 100644 index 00000000..359fdeeb --- /dev/null +++ b/launcher/ui/pages/global/LanguagePage.cpp @@ -0,0 +1,51 @@ +#include "LanguagePage.h" + +#include "ui/widgets/LanguageSelectionWidget.h" +#include <QVBoxLayout> + +LanguagePage::LanguagePage(QWidget* parent) : + QWidget(parent) +{ + setObjectName(QStringLiteral("languagePage")); + auto layout = new QVBoxLayout(this); + mainWidget = new LanguageSelectionWidget(this); + layout->setContentsMargins(0,0,0,0); + layout->addWidget(mainWidget); + retranslate(); +} + +LanguagePage::~LanguagePage() +{ +} + +bool LanguagePage::apply() +{ + applySettings(); + return true; +} + +void LanguagePage::applySettings() +{ + auto settings = APPLICATION->settings(); + QString key = mainWidget->getSelectedLanguageKey(); + settings->set("Language", key); +} + +void LanguagePage::loadSettings() +{ + // NIL +} + +void LanguagePage::retranslate() +{ + mainWidget->retranslate(); +} + +void LanguagePage::changeEvent(QEvent* event) +{ + if (event->type() == QEvent::LanguageChange) + { + retranslate(); + } + QWidget::changeEvent(event); +} diff --git a/launcher/ui/pages/global/LanguagePage.h b/launcher/ui/pages/global/LanguagePage.h new file mode 100644 index 00000000..b1dd05ad --- /dev/null +++ b/launcher/ui/pages/global/LanguagePage.h @@ -0,0 +1,60 @@ +/* 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 <memory> +#include "ui/pages/BasePage.h" +#include <Application.h> +#include <QWidget> + +class LanguageSelectionWidget; + +class LanguagePage : public QWidget, public BasePage +{ + Q_OBJECT + +public: + explicit LanguagePage(QWidget *parent = 0); + virtual ~LanguagePage(); + + QString displayName() const override + { + return tr("Language"); + } + QIcon icon() const override + { + return APPLICATION->getThemedIcon("language"); + } + QString id() const override + { + return "language-settings"; + } + QString helpPage() const override + { + return "Language-settings"; + } + bool apply() override; + + void changeEvent(QEvent * ) override; + +private: + void applySettings(); + void loadSettings(); + void retranslate(); + +private: + LanguageSelectionWidget *mainWidget; +}; diff --git a/launcher/ui/pages/global/LauncherPage.cpp b/launcher/ui/pages/global/LauncherPage.cpp new file mode 100644 index 00000000..2eb73e44 --- /dev/null +++ b/launcher/ui/pages/global/LauncherPage.cpp @@ -0,0 +1,466 @@ +/* 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 "LauncherPage.h" +#include "ui_LauncherPage.h" + +#include <QFileDialog> +#include <QMessageBox> +#include <QDir> +#include <QTextCharFormat> + +#include "updater/UpdateChecker.h" + +#include "settings/SettingsObject.h" +#include <FileSystem.h> +#include "Application.h" +#include "BuildConfig.h" +#include "ui/themes/ITheme.h" + +#include <QApplication> +#include <QProcess> + +// FIXME: possibly move elsewhere +enum InstSortMode +{ + // Sort alphabetically by name. + Sort_Name, + // Sort by which instance was launched most recently. + Sort_LastLaunch +}; + +LauncherPage::LauncherPage(QWidget *parent) : QWidget(parent), ui(new Ui::LauncherPage) +{ + ui->setupUi(this); + auto origForeground = ui->fontPreview->palette().color(ui->fontPreview->foregroundRole()); + auto origBackground = ui->fontPreview->palette().color(ui->fontPreview->backgroundRole()); + m_colors.reset(new LogColorCache(origForeground, origBackground)); + + ui->sortingModeGroup->setId(ui->sortByNameBtn, Sort_Name); + ui->sortingModeGroup->setId(ui->sortLastLaunchedBtn, Sort_LastLaunch); + + defaultFormat = new QTextCharFormat(ui->fontPreview->currentCharFormat()); + + m_languageModel = APPLICATION->translations(); + loadSettings(); + + if(BuildConfig.UPDATER_ENABLED) + { + QObject::connect(APPLICATION->updateChecker().get(), &UpdateChecker::channelListLoaded, this, &LauncherPage::refreshUpdateChannelList); + + if (APPLICATION->updateChecker()->hasChannels()) + { + refreshUpdateChannelList(); + } + else + { + APPLICATION->updateChecker()->updateChanList(false); + } + } + else + { + ui->updateSettingsBox->setHidden(true); + } + // Analytics + if(BuildConfig.ANALYTICS_ID.isEmpty()) + { + ui->tabWidget->removeTab(ui->tabWidget->indexOf(ui->analyticsTab)); + } + connect(ui->fontSizeBox, SIGNAL(valueChanged(int)), SLOT(refreshFontPreview())); + connect(ui->consoleFont, SIGNAL(currentFontChanged(QFont)), SLOT(refreshFontPreview())); + + //move mac data button + QFile file(QDir::current().absolutePath() + "/dontmovemacdata"); + if (!file.exists()) + { + ui->migrateDataFolderMacBtn->setVisible(false); + } +} + +LauncherPage::~LauncherPage() +{ + delete ui; + delete defaultFormat; +} + +bool LauncherPage::apply() +{ + applySettings(); + return true; +} + +void LauncherPage::on_instDirBrowseBtn_clicked() +{ + QString raw_dir = QFileDialog::getExistingDirectory(this, tr("Instance Folder"), ui->instDirTextBox->text()); + + // do not allow current dir - it's dirty. Do not allow dirs that don't exist + if (!raw_dir.isEmpty() && QDir(raw_dir).exists()) + { + QString cooked_dir = FS::NormalizePath(raw_dir); + if (FS::checkProblemticPathJava(QDir(cooked_dir))) + { + QMessageBox warning; + warning.setText(tr("You're trying to specify an instance folder which\'s path " + "contains at least one \'!\'. " + "Java is known to cause problems if that is the case, your " + "instances (probably) won't start!")); + warning.setInformativeText( + tr("Do you really want to use this path? " + "Selecting \"No\" will close this and not alter your instance path.")); + warning.setStandardButtons(QMessageBox::Yes | QMessageBox::No); + int result = warning.exec(); + if (result == QMessageBox::Yes) + { + ui->instDirTextBox->setText(cooked_dir); + } + } + else + { + ui->instDirTextBox->setText(cooked_dir); + } + } +} + +void LauncherPage::on_iconsDirBrowseBtn_clicked() +{ + QString raw_dir = QFileDialog::getExistingDirectory(this, tr("Icons Folder"), ui->iconsDirTextBox->text()); + + // do not allow current dir - it's dirty. Do not allow dirs that don't exist + if (!raw_dir.isEmpty() && QDir(raw_dir).exists()) + { + QString cooked_dir = FS::NormalizePath(raw_dir); + ui->iconsDirTextBox->setText(cooked_dir); + } +} +void LauncherPage::on_modsDirBrowseBtn_clicked() +{ + QString raw_dir = QFileDialog::getExistingDirectory(this, tr("Mods Folder"), ui->modsDirTextBox->text()); + + // do not allow current dir - it's dirty. Do not allow dirs that don't exist + if (!raw_dir.isEmpty() && QDir(raw_dir).exists()) + { + QString cooked_dir = FS::NormalizePath(raw_dir); + ui->modsDirTextBox->setText(cooked_dir); + } +} +void LauncherPage::on_migrateDataFolderMacBtn_clicked() +{ + QFile file(QDir::current().absolutePath() + "/dontmovemacdata"); + file.remove(); + QProcess::startDetached(qApp->arguments()[0]); + qApp->quit(); +} + +void LauncherPage::refreshUpdateChannelList() +{ + // Stop listening for selection changes. It's going to change a lot while we update it and + // we don't need to update the + // description label constantly. + QObject::disconnect(ui->updateChannelComboBox, SIGNAL(currentIndexChanged(int)), this, + SLOT(updateChannelSelectionChanged(int))); + + QList<UpdateChecker::ChannelListEntry> channelList = APPLICATION->updateChecker()->getChannelList(); + ui->updateChannelComboBox->clear(); + int selection = -1; + for (int i = 0; i < channelList.count(); i++) + { + UpdateChecker::ChannelListEntry entry = channelList.at(i); + + // When it comes to selection, we'll rely on the indexes of a channel entry being the + // same in the + // combo box as it is in the update checker's channel list. + // This probably isn't very safe, but the channel list doesn't change often enough (or + // at all) for + // this to be a big deal. Hope it doesn't break... + ui->updateChannelComboBox->addItem(entry.name); + + // If the update channel we just added was the selected one, set the current index in + // the combo box to it. + if (entry.id == m_currentUpdateChannel) + { + qDebug() << "Selected index" << i << "channel id" << m_currentUpdateChannel; + selection = i; + } + } + + ui->updateChannelComboBox->setCurrentIndex(selection); + + // Start listening for selection changes again and update the description label. + QObject::connect(ui->updateChannelComboBox, SIGNAL(currentIndexChanged(int)), this, + SLOT(updateChannelSelectionChanged(int))); + refreshUpdateChannelDesc(); + + // Now that we've updated the channel list, we can enable the combo box. + // It starts off disabled so that if the channel list hasn't been loaded, it will be + // disabled. + ui->updateChannelComboBox->setEnabled(true); +} + +void LauncherPage::updateChannelSelectionChanged(int index) +{ + refreshUpdateChannelDesc(); +} + +void LauncherPage::refreshUpdateChannelDesc() +{ + // Get the channel list. + QList<UpdateChecker::ChannelListEntry> channelList = APPLICATION->updateChecker()->getChannelList(); + int selectedIndex = ui->updateChannelComboBox->currentIndex(); + if (selectedIndex < 0) + { + return; + } + if (selectedIndex < channelList.count()) + { + // Find the channel list entry with the given index. + UpdateChecker::ChannelListEntry selected = channelList.at(selectedIndex); + + // Set the description text. + ui->updateChannelDescLabel->setText(selected.description); + + // Set the currently selected channel ID. + m_currentUpdateChannel = selected.id; + } +} + +void LauncherPage::applySettings() +{ + auto s = APPLICATION->settings(); + + if (ui->resetNotificationsBtn->isChecked()) + { + s->set("ShownNotifications", QString()); + } + + // Updates + s->set("AutoUpdate", ui->autoUpdateCheckBox->isChecked()); + s->set("UpdateChannel", m_currentUpdateChannel); + auto original = s->get("IconTheme").toString(); + //FIXME: make generic + switch (ui->themeComboBox->currentIndex()) + { + case 1: + s->set("IconTheme", "pe_dark"); + break; + case 2: + s->set("IconTheme", "pe_light"); + break; + case 3: + s->set("IconTheme", "pe_blue"); + break; + case 4: + s->set("IconTheme", "pe_colored"); + break; + case 5: + s->set("IconTheme", "OSX"); + break; + case 6: + s->set("IconTheme", "iOS"); + break; + case 7: + s->set("IconTheme", "flat"); + break; + case 8: + s->set("IconTheme", "custom"); + break; + case 0: + default: + s->set("IconTheme", "multimc"); + break; + } + + if(original != s->get("IconTheme")) + { + APPLICATION->setIconTheme(s->get("IconTheme").toString()); + } + + auto originalAppTheme = s->get("ApplicationTheme").toString(); + auto newAppTheme = ui->themeComboBoxColors->currentData().toString(); + if(originalAppTheme != newAppTheme) + { + s->set("ApplicationTheme", newAppTheme); + APPLICATION->setApplicationTheme(newAppTheme, false); + } + + // Console settings + s->set("ShowConsole", ui->showConsoleCheck->isChecked()); + s->set("AutoCloseConsole", ui->autoCloseConsoleCheck->isChecked()); + s->set("ShowConsoleOnError", ui->showConsoleErrorCheck->isChecked()); + QString consoleFontFamily = ui->consoleFont->currentFont().family(); + s->set("ConsoleFont", consoleFontFamily); + s->set("ConsoleFontSize", ui->fontSizeBox->value()); + s->set("ConsoleMaxLines", ui->lineLimitSpinBox->value()); + s->set("ConsoleOverflowStop", ui->checkStopLogging->checkState() != Qt::Unchecked); + + // Folders + // TODO: Offer to move instances to new instance folder. + s->set("InstanceDir", ui->instDirTextBox->text()); + s->set("CentralModsDir", ui->modsDirTextBox->text()); + s->set("IconsDir", ui->iconsDirTextBox->text()); + + auto sortMode = (InstSortMode)ui->sortingModeGroup->checkedId(); + switch (sortMode) + { + case Sort_LastLaunch: + s->set("InstSortMode", "LastLaunch"); + break; + case Sort_Name: + default: + s->set("InstSortMode", "Name"); + break; + } + + // Analytics + if(!BuildConfig.ANALYTICS_ID.isEmpty()) + { + s->set("Analytics", ui->analyticsCheck->isChecked()); + } +} +void LauncherPage::loadSettings() +{ + auto s = APPLICATION->settings(); + // Updates + ui->autoUpdateCheckBox->setChecked(s->get("AutoUpdate").toBool()); + m_currentUpdateChannel = s->get("UpdateChannel").toString(); + //FIXME: make generic + auto theme = s->get("IconTheme").toString(); + if (theme == "pe_dark") + { + ui->themeComboBox->setCurrentIndex(1); + } + else if (theme == "pe_light") + { + ui->themeComboBox->setCurrentIndex(2); + } + else if (theme == "pe_blue") + { + ui->themeComboBox->setCurrentIndex(3); + } + else if (theme == "pe_colored") + { + ui->themeComboBox->setCurrentIndex(4); + } + else if (theme == "OSX") + { + ui->themeComboBox->setCurrentIndex(5); + } + else if (theme == "iOS") + { + ui->themeComboBox->setCurrentIndex(6); + } + else if (theme == "flat") + { + ui->themeComboBox->setCurrentIndex(7); + } + else if (theme == "custom") + { + ui->themeComboBox->setCurrentIndex(8); + } + else + { + ui->themeComboBox->setCurrentIndex(0); + } + + { + auto currentTheme = s->get("ApplicationTheme").toString(); + auto themes = APPLICATION->getValidApplicationThemes(); + int idx = 0; + for(auto &theme: themes) + { + ui->themeComboBoxColors->addItem(theme->name(), theme->id()); + if(currentTheme == theme->id()) + { + ui->themeComboBoxColors->setCurrentIndex(idx); + } + idx++; + } + } + + // Console settings + ui->showConsoleCheck->setChecked(s->get("ShowConsole").toBool()); + ui->autoCloseConsoleCheck->setChecked(s->get("AutoCloseConsole").toBool()); + ui->showConsoleErrorCheck->setChecked(s->get("ShowConsoleOnError").toBool()); + QString fontFamily = APPLICATION->settings()->get("ConsoleFont").toString(); + QFont consoleFont(fontFamily); + ui->consoleFont->setCurrentFont(consoleFont); + + bool conversionOk = true; + int fontSize = APPLICATION->settings()->get("ConsoleFontSize").toInt(&conversionOk); + if(!conversionOk) + { + fontSize = 11; + } + ui->fontSizeBox->setValue(fontSize); + refreshFontPreview(); + ui->lineLimitSpinBox->setValue(s->get("ConsoleMaxLines").toInt()); + ui->checkStopLogging->setChecked(s->get("ConsoleOverflowStop").toBool()); + + // Folders + ui->instDirTextBox->setText(s->get("InstanceDir").toString()); + ui->modsDirTextBox->setText(s->get("CentralModsDir").toString()); + ui->iconsDirTextBox->setText(s->get("IconsDir").toString()); + + QString sortMode = s->get("InstSortMode").toString(); + + if (sortMode == "LastLaunch") + { + ui->sortLastLaunchedBtn->setChecked(true); + } + else + { + ui->sortByNameBtn->setChecked(true); + } + + // Analytics + if(!BuildConfig.ANALYTICS_ID.isEmpty()) + { + ui->analyticsCheck->setChecked(s->get("Analytics").toBool()); + } +} + +void LauncherPage::refreshFontPreview() +{ + int fontSize = ui->fontSizeBox->value(); + QString fontFamily = ui->consoleFont->currentFont().family(); + ui->fontPreview->clear(); + defaultFormat->setFont(QFont(fontFamily, fontSize)); + { + QTextCharFormat format(*defaultFormat); + format.setForeground(m_colors->getFront(MessageLevel::Error)); + // append a paragraph/line + auto workCursor = ui->fontPreview->textCursor(); + workCursor.movePosition(QTextCursor::End); + workCursor.insertText(tr("[Something/ERROR] A spooky error!"), format); + workCursor.insertBlock(); + } + { + QTextCharFormat format(*defaultFormat); + format.setForeground(m_colors->getFront(MessageLevel::Message)); + // append a paragraph/line + auto workCursor = ui->fontPreview->textCursor(); + workCursor.movePosition(QTextCursor::End); + workCursor.insertText(tr("[Test/INFO] A harmless message..."), format); + workCursor.insertBlock(); + } + { + QTextCharFormat format(*defaultFormat); + format.setForeground(m_colors->getFront(MessageLevel::Warning)); + // append a paragraph/line + auto workCursor = ui->fontPreview->textCursor(); + workCursor.movePosition(QTextCursor::End); + workCursor.insertText(tr("[Something/WARN] A not so spooky warning."), format); + workCursor.insertBlock(); + } +} diff --git a/launcher/ui/pages/global/LauncherPage.h b/launcher/ui/pages/global/LauncherPage.h new file mode 100644 index 00000000..4d0cf3c9 --- /dev/null +++ b/launcher/ui/pages/global/LauncherPage.h @@ -0,0 +1,103 @@ +/* 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 <memory> +#include <QDialog> + +#include "java/JavaChecker.h" +#include "ui/pages/BasePage.h" +#include <Application.h> +#include "ui/ColorCache.h" +#include <translations/TranslationsModel.h> + +class QTextCharFormat; +class SettingsObject; + +namespace Ui +{ +class LauncherPage; +} + +class LauncherPage : public QWidget, public BasePage +{ + Q_OBJECT + +public: + explicit LauncherPage(QWidget *parent = 0); + ~LauncherPage(); + + QString displayName() const override + { + return "Launcher"; + } + QIcon icon() const override + { + return APPLICATION->getThemedIcon("launcher"); + } + QString id() const override + { + return "launcher-settings"; + } + QString helpPage() const override + { + return "Launcher-settings"; + } + bool apply() override; + +private: + void applySettings(); + void loadSettings(); + +private +slots: + void on_instDirBrowseBtn_clicked(); + void on_modsDirBrowseBtn_clicked(); + void on_iconsDirBrowseBtn_clicked(); + void on_migrateDataFolderMacBtn_clicked(); + + /*! + * Updates the list of update channels in the combo box. + */ + void refreshUpdateChannelList(); + + /*! + * Updates the channel description label. + */ + void refreshUpdateChannelDesc(); + + /*! + * Updates the font preview + */ + void refreshFontPreview(); + + void updateChannelSelectionChanged(int index); + +private: + Ui::LauncherPage *ui; + + /*! + * Stores the currently selected update channel. + */ + QString m_currentUpdateChannel; + + // default format for the font preview... + QTextCharFormat *defaultFormat; + + std::unique_ptr<LogColorCache> m_colors; + + std::shared_ptr<TranslationsModel> m_languageModel; +}; diff --git a/launcher/ui/pages/global/LauncherPage.ui b/launcher/ui/pages/global/LauncherPage.ui new file mode 100644 index 00000000..62a66d73 --- /dev/null +++ b/launcher/ui/pages/global/LauncherPage.ui @@ -0,0 +1,584 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>LauncherPage</class> + <widget class="QWidget" name="LauncherPage"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>514</width> + <height>629</height> + </rect> + </property> + <property name="sizePolicy"> + <sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <layout class="QVBoxLayout" name="mainLayout"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QTabWidget" name="tabWidget"> + <property name="toolTip"> + <string notr="true"/> + </property> + <property name="tabShape"> + <enum>QTabWidget::Rounded</enum> + </property> + <property name="currentIndex"> + <number>0</number> + </property> + <widget class="QWidget" name="featuresTab"> + <attribute name="title"> + <string>Features</string> + </attribute> + <layout class="QVBoxLayout" name="verticalLayout_9"> + <item> + <widget class="QGroupBox" name="updateSettingsBox"> + <property name="title"> + <string>Update Settings</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_7"> + <item> + <widget class="QCheckBox" name="autoUpdateCheckBox"> + <property name="text"> + <string>Check for updates on start?</string> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="updateChannelLabel"> + <property name="text"> + <string>Up&date Channel:</string> + </property> + <property name="buddy"> + <cstring>updateChannelComboBox</cstring> + </property> + </widget> + </item> + <item> + <widget class="QComboBox" name="updateChannelComboBox"> + <property name="enabled"> + <bool>false</bool> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="updateChannelDescLabel"> + <property name="text"> + <string>No channel selected.</string> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="foldersBox"> + <property name="title"> + <string>Folders</string> + </property> + <layout class="QGridLayout" name="foldersBoxLayout"> + <item row="0" column="0"> + <widget class="QLabel" name="labelInstDir"> + <property name="text"> + <string>I&nstances:</string> + </property> + <property name="buddy"> + <cstring>instDirTextBox</cstring> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QLineEdit" name="instDirTextBox"/> + </item> + <item row="0" column="2"> + <widget class="QToolButton" name="instDirBrowseBtn"> + <property name="text"> + <string notr="true">...</string> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="labelModsDir"> + <property name="text"> + <string>&Mods:</string> + </property> + <property name="buddy"> + <cstring>modsDirTextBox</cstring> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QLineEdit" name="modsDirTextBox"/> + </item> + <item row="1" column="2"> + <widget class="QToolButton" name="modsDirBrowseBtn"> + <property name="text"> + <string notr="true">...</string> + </property> + </widget> + </item> + <item row="2" column="1"> + <widget class="QLineEdit" name="iconsDirTextBox"/> + </item> + <item row="2" column="0"> + <widget class="QLabel" name="labelIconsDir"> + <property name="text"> + <string>&Icons:</string> + </property> + <property name="buddy"> + <cstring>iconsDirTextBox</cstring> + </property> + </widget> + </item> + <item row="2" column="2"> + <widget class="QToolButton" name="iconsDirBrowseBtn"> + <property name="text"> + <string notr="true">...</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QPushButton" name="migrateDataFolderMacBtn"> + <property name="text"> + <string>Move the data to new location (will restart the launcher)</string> + </property> + </widget> + </item> + <item> + <spacer name="verticalSpacer_2"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>0</width> + <height>0</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + <widget class="QWidget" name="generalTab"> + <attribute name="title"> + <string>User Interface</string> + </attribute> + <layout class="QVBoxLayout" name="verticalLayout_6"> + <item> + <widget class="QGroupBox" name="groupBox_3"> + <property name="title"> + <string>Launcher notifications</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_5"> + <item> + <widget class="QPushButton" name="resetNotificationsBtn"> + <property name="text"> + <string>Reset hidden notifications</string> + </property> + <property name="checkable"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="sortingModeBox"> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="title"> + <string>Instance view sorting mode</string> + </property> + <layout class="QHBoxLayout" name="sortingModeBoxLayout"> + <item> + <widget class="QRadioButton" name="sortLastLaunchedBtn"> + <property name="text"> + <string>By &last launched</string> + </property> + <attribute name="buttonGroup"> + <string notr="true">sortingModeGroup</string> + </attribute> + </widget> + </item> + <item> + <widget class="QRadioButton" name="sortByNameBtn"> + <property name="text"> + <string>By &name</string> + </property> + <attribute name="buttonGroup"> + <string notr="true">sortingModeGroup</string> + </attribute> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="themeBox"> + <property name="title"> + <string>Theme</string> + </property> + <layout class="QFormLayout" name="formLayout"> + <item row="0" column="0"> + <widget class="QLabel" name="label_3"> + <property name="text"> + <string>&Icons</string> + </property> + <property name="buddy"> + <cstring>themeComboBox</cstring> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QComboBox" name="themeComboBox"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="focusPolicy"> + <enum>Qt::StrongFocus</enum> + </property> + <item> + <property name="text"> + <string>Default</string> + </property> + </item> + <item> + <property name="text"> + <string>Simple (Dark Icons)</string> + </property> + </item> + <item> + <property name="text"> + <string>Simple (Light Icons)</string> + </property> + </item> + <item> + <property name="text"> + <string>Simple (Blue Icons)</string> + </property> + </item> + <item> + <property name="text"> + <string>Simple (Colored Icons)</string> + </property> + </item> + <item> + <property name="text"> + <string notr="true">OSX</string> + </property> + </item> + <item> + <property name="text"> + <string notr="true">iOS</string> + </property> + </item> + <item> + <property name="text"> + <string notr="true">Flat</string> + </property> + </item> + <item> + <property name="text"> + <string>Custom</string> + </property> + </item> + </widget> + </item> + <item row="1" column="1"> + <widget class="QComboBox" name="themeComboBoxColors"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="focusPolicy"> + <enum>Qt::StrongFocus</enum> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="label_4"> + <property name="text"> + <string>Colors</string> + </property> + <property name="buddy"> + <cstring>themeComboBoxColors</cstring> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <spacer name="generalTabSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>0</width> + <height>0</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + <widget class="QWidget" name="consoleTab"> + <attribute name="title"> + <string>Console</string> + </attribute> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <item> + <widget class="QGroupBox" name="consoleSettingsBox"> + <property name="title"> + <string>Console Settings</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_3"> + <item> + <widget class="QCheckBox" name="showConsoleCheck"> + <property name="text"> + <string>Show console while the game is running?</string> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="autoCloseConsoleCheck"> + <property name="text"> + <string>Automatically close console when the game quits?</string> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="showConsoleErrorCheck"> + <property name="text"> + <string>Show console when the game crashes?</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="groupBox_4"> + <property name="title"> + <string>History limit</string> + </property> + <layout class="QGridLayout" name="gridLayout_3"> + <item row="1" column="0"> + <widget class="QCheckBox" name="checkStopLogging"> + <property name="text"> + <string>Stop logging when log overflows</string> + </property> + </widget> + </item> + <item row="0" column="0"> + <widget class="QSpinBox" name="lineLimitSpinBox"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="suffix"> + <string> lines</string> + </property> + <property name="minimum"> + <number>10000</number> + </property> + <property name="maximum"> + <number>1000000</number> + </property> + <property name="singleStep"> + <number>10000</number> + </property> + <property name="value"> + <number>100000</number> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="themeBox_2"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="MinimumExpanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="title"> + <string>Console font</string> + </property> + <layout class="QGridLayout" name="gridLayout_2"> + <item row="1" column="0" colspan="2"> + <widget class="QTextEdit" name="fontPreview"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="MinimumExpanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="horizontalScrollBarPolicy"> + <enum>Qt::ScrollBarAlwaysOff</enum> + </property> + <property name="undoRedoEnabled"> + <bool>false</bool> + </property> + <property name="textInteractionFlags"> + <set>Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set> + </property> + </widget> + </item> + <item row="0" column="0"> + <widget class="QFontComboBox" name="consoleFont"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QSpinBox" name="fontSizeBox"> + <property name="minimum"> + <number>5</number> + </property> + <property name="maximum"> + <number>16</number> + </property> + <property name="value"> + <number>11</number> + </property> + </widget> + </item> + </layout> + </widget> + </item> + </layout> + </widget> + <widget class="QWidget" name="analyticsTab"> + <attribute name="title"> + <string>Analytics</string> + </attribute> + <layout class="QVBoxLayout" name="verticalLayout_8"> + <item> + <widget class="QGroupBox" name="consoleSettingsBox_2"> + <property name="title"> + <string>Analytics Settings</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_4"> + <item> + <widget class="QCheckBox" name="analyticsCheck"> + <property name="text"> + <string>Send anonymous usage statistics?</string> + </property> + </widget> + </item> + <item> + <widget class="Line" name="line"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="label_5"> + <property name="text"> + <string><html><head/> +<body> +<p>The launcher sends anonymous usage statistics on every start of the application.</p><p>The following data is collected:</p> +<ul> +<li>Launcher version.</li> +<li>Operating system name, version and architecture.</li> +<li>CPU architecture (kernel architecture on linux).</li> +<li>Size of system memory.</li> +<li>Java version, architecture and memory settings.</li> +</ul> +</body></html></string> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + </widget> + </item> + </layout> + </widget> + <tabstops> + <tabstop>tabWidget</tabstop> + <tabstop>autoUpdateCheckBox</tabstop> + <tabstop>updateChannelComboBox</tabstop> + <tabstop>instDirTextBox</tabstop> + <tabstop>instDirBrowseBtn</tabstop> + <tabstop>modsDirTextBox</tabstop> + <tabstop>modsDirBrowseBtn</tabstop> + <tabstop>iconsDirTextBox</tabstop> + <tabstop>iconsDirBrowseBtn</tabstop> + <tabstop>resetNotificationsBtn</tabstop> + <tabstop>sortLastLaunchedBtn</tabstop> + <tabstop>sortByNameBtn</tabstop> + <tabstop>themeComboBox</tabstop> + <tabstop>themeComboBoxColors</tabstop> + <tabstop>showConsoleCheck</tabstop> + <tabstop>autoCloseConsoleCheck</tabstop> + <tabstop>showConsoleErrorCheck</tabstop> + <tabstop>lineLimitSpinBox</tabstop> + <tabstop>checkStopLogging</tabstop> + <tabstop>consoleFont</tabstop> + <tabstop>fontSizeBox</tabstop> + <tabstop>fontPreview</tabstop> + </tabstops> + <resources/> + <connections/> + <buttongroups> + <buttongroup name="sortingModeGroup"/> + </buttongroups> +</ui> diff --git a/launcher/ui/pages/global/MinecraftPage.cpp b/launcher/ui/pages/global/MinecraftPage.cpp new file mode 100644 index 00000000..c763f8ac --- /dev/null +++ b/launcher/ui/pages/global/MinecraftPage.cpp @@ -0,0 +1,91 @@ +/* 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 "MinecraftPage.h" +#include "ui_MinecraftPage.h" + +#include <QMessageBox> +#include <QDir> +#include <QTabBar> + +#include "settings/SettingsObject.h" +#include "Application.h" + +MinecraftPage::MinecraftPage(QWidget *parent) : QWidget(parent), ui(new Ui::MinecraftPage) +{ + ui->setupUi(this); + ui->tabWidget->tabBar()->hide(); + loadSettings(); + updateCheckboxStuff(); +} + +MinecraftPage::~MinecraftPage() +{ + delete ui; +} + +bool MinecraftPage::apply() +{ + applySettings(); + return true; +} + +void MinecraftPage::updateCheckboxStuff() +{ + ui->windowWidthSpinBox->setEnabled(!ui->maximizedCheckBox->isChecked()); + ui->windowHeightSpinBox->setEnabled(!ui->maximizedCheckBox->isChecked()); +} + +void MinecraftPage::on_maximizedCheckBox_clicked(bool checked) +{ + Q_UNUSED(checked); + updateCheckboxStuff(); +} + +void MinecraftPage::applySettings() +{ + auto s = APPLICATION->settings(); + + // Window Size + s->set("LaunchMaximized", ui->maximizedCheckBox->isChecked()); + s->set("MinecraftWinWidth", ui->windowWidthSpinBox->value()); + s->set("MinecraftWinHeight", ui->windowHeightSpinBox->value()); + + // Native library workarounds + s->set("UseNativeOpenAL", ui->useNativeOpenALCheck->isChecked()); + s->set("UseNativeGLFW", ui->useNativeGLFWCheck->isChecked()); + + // Game time + s->set("ShowGameTime", ui->showGameTime->isChecked()); + s->set("ShowGlobalGameTime", ui->showGlobalGameTime->isChecked()); + s->set("RecordGameTime", ui->recordGameTime->isChecked()); +} + +void MinecraftPage::loadSettings() +{ + auto s = APPLICATION->settings(); + + // Window Size + ui->maximizedCheckBox->setChecked(s->get("LaunchMaximized").toBool()); + ui->windowWidthSpinBox->setValue(s->get("MinecraftWinWidth").toInt()); + ui->windowHeightSpinBox->setValue(s->get("MinecraftWinHeight").toInt()); + + ui->useNativeOpenALCheck->setChecked(s->get("UseNativeOpenAL").toBool()); + ui->useNativeGLFWCheck->setChecked(s->get("UseNativeGLFW").toBool()); + + ui->showGameTime->setChecked(s->get("ShowGameTime").toBool()); + ui->showGlobalGameTime->setChecked(s->get("ShowGlobalGameTime").toBool()); + ui->recordGameTime->setChecked(s->get("RecordGameTime").toBool()); +} diff --git a/launcher/ui/pages/global/MinecraftPage.h b/launcher/ui/pages/global/MinecraftPage.h new file mode 100644 index 00000000..42626d94 --- /dev/null +++ b/launcher/ui/pages/global/MinecraftPage.h @@ -0,0 +1,70 @@ +/* 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 <memory> +#include <QDialog> + +#include "java/JavaChecker.h" +#include "ui/pages/BasePage.h" +#include <Application.h> + +class SettingsObject; + +namespace Ui +{ +class MinecraftPage; +} + +class MinecraftPage : public QWidget, public BasePage +{ + Q_OBJECT + +public: + explicit MinecraftPage(QWidget *parent = 0); + ~MinecraftPage(); + + QString displayName() const override + { + return tr("Minecraft"); + } + QIcon icon() const override + { + return APPLICATION->getThemedIcon("minecraft"); + } + QString id() const override + { + return "minecraft-settings"; + } + QString helpPage() const override + { + return "Minecraft-settings"; + } + bool apply() override; + +private: + void updateCheckboxStuff(); + void applySettings(); + void loadSettings(); + +private +slots: + void on_maximizedCheckBox_clicked(bool checked); + +private: + Ui::MinecraftPage *ui; + +}; diff --git a/launcher/ui/pages/global/MinecraftPage.ui b/launcher/ui/pages/global/MinecraftPage.ui new file mode 100644 index 00000000..857b8cfb --- /dev/null +++ b/launcher/ui/pages/global/MinecraftPage.ui @@ -0,0 +1,196 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>MinecraftPage</class> + <widget class="QWidget" name="MinecraftPage"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>936</width> + <height>1134</height> + </rect> + </property> + <property name="sizePolicy"> + <sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <layout class="QVBoxLayout" name="mainLayout"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QTabWidget" name="tabWidget"> + <property name="tabShape"> + <enum>QTabWidget::Rounded</enum> + </property> + <property name="currentIndex"> + <number>0</number> + </property> + <widget class="QWidget" name="minecraftTab"> + <attribute name="title"> + <string notr="true">Minecraft</string> + </attribute> + <layout class="QVBoxLayout" name="verticalLayout_3"> + <item> + <widget class="QGroupBox" name="windowSizeGroupBox"> + <property name="title"> + <string>Window Size</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_4"> + <item> + <widget class="QCheckBox" name="maximizedCheckBox"> + <property name="text"> + <string>Start Minecraft maximized?</string> + </property> + </widget> + </item> + <item> + <layout class="QGridLayout" name="gridLayoutWindowSize"> + <item row="1" column="0"> + <widget class="QLabel" name="labelWindowHeight"> + <property name="text"> + <string>Window hei&ght:</string> + </property> + <property name="buddy"> + <cstring>windowHeightSpinBox</cstring> + </property> + </widget> + </item> + <item row="0" column="0"> + <widget class="QLabel" name="labelWindowWidth"> + <property name="text"> + <string>W&indow width:</string> + </property> + <property name="buddy"> + <cstring>windowWidthSpinBox</cstring> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QSpinBox" name="windowWidthSpinBox"> + <property name="minimum"> + <number>1</number> + </property> + <property name="maximum"> + <number>65536</number> + </property> + <property name="singleStep"> + <number>1</number> + </property> + <property name="value"> + <number>854</number> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QSpinBox" name="windowHeightSpinBox"> + <property name="minimum"> + <number>1</number> + </property> + <property name="maximum"> + <number>65536</number> + </property> + <property name="value"> + <number>480</number> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="nativeLibWorkaroundGroupBox"> + <property name="title"> + <string>Native library workarounds</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_5"> + <item> + <widget class="QCheckBox" name="useNativeGLFWCheck"> + <property name="text"> + <string>Use system installation of GLFW</string> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="useNativeOpenALCheck"> + <property name="text"> + <string>Use system installation of OpenAL</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="gameTimeGroupBox"> + <property name="title"> + <string>Game time</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_6"> + <item> + <widget class="QCheckBox" name="showGameTime"> + <property name="text"> + <string>Show time spent playing instances</string> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="showGlobalGameTime"> + <property name="text"> + <string>Show time spent playing across all instances</string> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="recordGameTime"> + <property name="text"> + <string>Record time spent playing instances</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <spacer name="verticalSpacerMinecraft"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>0</width> + <height>0</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + </widget> + </item> + </layout> + </widget> + <tabstops> + <tabstop>tabWidget</tabstop> + <tabstop>maximizedCheckBox</tabstop> + <tabstop>windowWidthSpinBox</tabstop> + <tabstop>windowHeightSpinBox</tabstop> + <tabstop>useNativeGLFWCheck</tabstop> + <tabstop>useNativeOpenALCheck</tabstop> + </tabstops> + <resources/> + <connections/> +</ui> diff --git a/launcher/ui/pages/global/PasteEEPage.cpp b/launcher/ui/pages/global/PasteEEPage.cpp new file mode 100644 index 00000000..4b375d9a --- /dev/null +++ b/launcher/ui/pages/global/PasteEEPage.cpp @@ -0,0 +1,81 @@ +/* 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 "PasteEEPage.h" +#include "ui_PasteEEPage.h" + +#include <QMessageBox> +#include <QFileDialog> +#include <QStandardPaths> +#include <QTabBar> + +#include "settings/SettingsObject.h" +#include "tools/BaseProfiler.h" +#include "Application.h" + +PasteEEPage::PasteEEPage(QWidget *parent) : + QWidget(parent), + ui(new Ui::PasteEEPage) +{ + ui->setupUi(this); + ui->tabWidget->tabBar()->hide();\ + connect(ui->customAPIkeyEdit, &QLineEdit::textEdited, this, &PasteEEPage::textEdited); + loadSettings(); +} + +PasteEEPage::~PasteEEPage() +{ + delete ui; +} + +void PasteEEPage::loadSettings() +{ + auto s = APPLICATION->settings(); + QString keyToUse = s->get("PasteEEAPIKey").toString(); + if(keyToUse == "multimc") + { + ui->multimcButton->setChecked(true); + } + else + { + ui->customButton->setChecked(true); + ui->customAPIkeyEdit->setText(keyToUse); + } +} + +void PasteEEPage::applySettings() +{ + auto s = APPLICATION->settings(); + + QString pasteKeyToUse; + if (ui->customButton->isChecked()) + pasteKeyToUse = ui->customAPIkeyEdit->text(); + else + { + pasteKeyToUse = "multimc"; + } + s->set("PasteEEAPIKey", pasteKeyToUse); +} + +bool PasteEEPage::apply() +{ + applySettings(); + return true; +} + +void PasteEEPage::textEdited(const QString& text) +{ + ui->customButton->setChecked(true); +} diff --git a/launcher/ui/pages/global/PasteEEPage.h b/launcher/ui/pages/global/PasteEEPage.h new file mode 100644 index 00000000..a1c7d434 --- /dev/null +++ b/launcher/ui/pages/global/PasteEEPage.h @@ -0,0 +1,62 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <QWidget> + +#include "ui/pages/BasePage.h" +#include <Application.h> + +namespace Ui { +class PasteEEPage; +} + +class PasteEEPage : public QWidget, public BasePage +{ + Q_OBJECT + +public: + explicit PasteEEPage(QWidget *parent = 0); + ~PasteEEPage(); + + QString displayName() const override + { + return tr("Log Upload"); + } + QIcon icon() const override + { + return APPLICATION->getThemedIcon("log"); + } + QString id() const override + { + return "log-upload"; + } + QString helpPage() const override + { + return "Log-Upload"; + } + virtual bool apply() override; + +private: + void loadSettings(); + void applySettings(); + +private slots: + void textEdited(const QString &text); + +private: + Ui::PasteEEPage *ui; +}; diff --git a/launcher/ui/pages/global/PasteEEPage.ui b/launcher/ui/pages/global/PasteEEPage.ui new file mode 100644 index 00000000..10883781 --- /dev/null +++ b/launcher/ui/pages/global/PasteEEPage.ui @@ -0,0 +1,128 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>PasteEEPage</class> + <widget class="QWidget" name="PasteEEPage"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>491</width> + <height>474</height> + </rect> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QTabWidget" name="tabWidget"> + <property name="currentIndex"> + <number>0</number> + </property> + <widget class="QWidget" name="tab"> + <attribute name="title"> + <string notr="true">Tab 1</string> + </attribute> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <item> + <widget class="QGroupBox" name="groupBox_2"> + <property name="title"> + <string>paste.ee API key</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_10"> + <item> + <widget class="QRadioButton" name="multimcButton"> + <property name="text"> + <string>MultiMC key - 12MB &upload limit</string> + </property> + <attribute name="buttonGroup"> + <string notr="true">pasteButtonGroup</string> + </attribute> + </widget> + </item> + <item> + <widget class="QRadioButton" name="customButton"> + <property name="text"> + <string>&Your own key - 12MB upload limit:</string> + </property> + <attribute name="buttonGroup"> + <string notr="true">pasteButtonGroup</string> + </attribute> + </widget> + </item> + <item> + <widget class="QLineEdit" name="customAPIkeyEdit"> + <property name="echoMode"> + <enum>QLineEdit::Password</enum> + </property> + <property name="placeholderText"> + <string>Paste your API key here!</string> + </property> + </widget> + </item> + <item> + <widget class="Line" name="line"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="label"> + <property name="text"> + <string><html><head/><body><p><a href="https://paste.ee">paste.ee</a> is used by MultiMC for log uploads. If you have a <a href="https://paste.ee">paste.ee</a> account, you can add your API key here and have your uploaded logs paired with your account.</p></body></html></string> + </property> + <property name="textFormat"> + <enum>Qt::RichText</enum> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + <property name="openExternalLinks"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>216</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + </widget> + </item> + </layout> + </widget> + <tabstops> + <tabstop>tabWidget</tabstop> + <tabstop>multimcButton</tabstop> + <tabstop>customButton</tabstop> + <tabstop>customAPIkeyEdit</tabstop> + </tabstops> + <resources/> + <connections/> + <buttongroups> + <buttongroup name="pasteButtonGroup"/> + </buttongroups> +</ui> diff --git a/launcher/ui/pages/global/ProxyPage.cpp b/launcher/ui/pages/global/ProxyPage.cpp new file mode 100644 index 00000000..5bc8199e --- /dev/null +++ b/launcher/ui/pages/global/ProxyPage.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 "ProxyPage.h" +#include "ui_ProxyPage.h" + +#include <QTabBar> + +#include "settings/SettingsObject.h" +#include "Application.h" +#include "Application.h" + +ProxyPage::ProxyPage(QWidget *parent) : QWidget(parent), ui(new Ui::ProxyPage) +{ + ui->setupUi(this); + ui->tabWidget->tabBar()->hide(); + loadSettings(); + updateCheckboxStuff(); + + connect(ui->proxyGroup, SIGNAL(buttonClicked(int)), SLOT(proxyChanged(int))); +} + +ProxyPage::~ProxyPage() +{ + delete ui; +} + +bool ProxyPage::apply() +{ + applySettings(); + return true; +} + +void ProxyPage::updateCheckboxStuff() +{ + ui->proxyAddrBox->setEnabled(!ui->proxyNoneBtn->isChecked() && + !ui->proxyDefaultBtn->isChecked()); + ui->proxyAuthBox->setEnabled(!ui->proxyNoneBtn->isChecked() && + !ui->proxyDefaultBtn->isChecked()); +} + +void ProxyPage::proxyChanged(int) +{ + updateCheckboxStuff(); +} + +void ProxyPage::applySettings() +{ + auto s = APPLICATION->settings(); + + // Proxy + QString proxyType = "None"; + if (ui->proxyDefaultBtn->isChecked()) + proxyType = "Default"; + else if (ui->proxyNoneBtn->isChecked()) + proxyType = "None"; + else if (ui->proxySOCKS5Btn->isChecked()) + proxyType = "SOCKS5"; + else if (ui->proxyHTTPBtn->isChecked()) + proxyType = "HTTP"; + + s->set("ProxyType", proxyType); + s->set("ProxyAddr", ui->proxyAddrEdit->text()); + s->set("ProxyPort", ui->proxyPortEdit->value()); + s->set("ProxyUser", ui->proxyUserEdit->text()); + s->set("ProxyPass", ui->proxyPassEdit->text()); + + APPLICATION->updateProxySettings( + proxyType, + ui->proxyAddrEdit->text(), + ui->proxyPortEdit->value(), + ui->proxyUserEdit->text(), + ui->proxyPassEdit->text() + ); +} +void ProxyPage::loadSettings() +{ + auto s = APPLICATION->settings(); + // Proxy + QString proxyType = s->get("ProxyType").toString(); + if (proxyType == "Default") + ui->proxyDefaultBtn->setChecked(true); + else if (proxyType == "None") + ui->proxyNoneBtn->setChecked(true); + else if (proxyType == "SOCKS5") + ui->proxySOCKS5Btn->setChecked(true); + else if (proxyType == "HTTP") + ui->proxyHTTPBtn->setChecked(true); + + ui->proxyAddrEdit->setText(s->get("ProxyAddr").toString()); + ui->proxyPortEdit->setValue(s->get("ProxyPort").value<uint16_t>()); + ui->proxyUserEdit->setText(s->get("ProxyUser").toString()); + ui->proxyPassEdit->setText(s->get("ProxyPass").toString()); +} diff --git a/launcher/ui/pages/global/ProxyPage.h b/launcher/ui/pages/global/ProxyPage.h new file mode 100644 index 00000000..6698c349 --- /dev/null +++ b/launcher/ui/pages/global/ProxyPage.h @@ -0,0 +1,66 @@ +/* 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 <memory> +#include <QDialog> + +#include "ui/pages/BasePage.h" +#include <Application.h> + +namespace Ui +{ +class ProxyPage; +} + +class ProxyPage : public QWidget, public BasePage +{ + Q_OBJECT + +public: + explicit ProxyPage(QWidget *parent = 0); + ~ProxyPage(); + + QString displayName() const override + { + return tr("Proxy"); + } + QIcon icon() const override + { + return APPLICATION->getThemedIcon("proxy"); + } + QString id() const override + { + return "proxy-settings"; + } + QString helpPage() const override + { + return "Proxy-settings"; + } + bool apply() override; + +private: + void updateCheckboxStuff(); + void applySettings(); + void loadSettings(); + +private +slots: + void proxyChanged(int); + +private: + Ui::ProxyPage *ui; +}; diff --git a/launcher/ui/pages/global/ProxyPage.ui b/launcher/ui/pages/global/ProxyPage.ui new file mode 100644 index 00000000..347fa86c --- /dev/null +++ b/launcher/ui/pages/global/ProxyPage.ui @@ -0,0 +1,203 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>ProxyPage</class> + <widget class="QWidget" name="ProxyPage"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>598</width> + <height>617</height> + </rect> + </property> + <property name="sizePolicy"> + <sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QTabWidget" name="tabWidget"> + <widget class="QWidget" name="tabWidgetPage1"> + <attribute name="title"> + <string notr="true"/> + </attribute> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QLabel" name="proxyPlainTextWarningLabel_2"> + <property name="text"> + <string>This only applies to the launcher. Minecraft does not accept proxy settings.</string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QGroupBox" name="proxyTypeBox"> + <property name="title"> + <string>Type</string> + </property> + <layout class="QHBoxLayout" name="horizontalLayout_3"> + <item> + <widget class="QRadioButton" name="proxyDefaultBtn"> + <property name="toolTip"> + <string>Uses your system's default proxy settings.</string> + </property> + <property name="text"> + <string>&Default</string> + </property> + <attribute name="buttonGroup"> + <string notr="true">proxyGroup</string> + </attribute> + </widget> + </item> + <item> + <widget class="QRadioButton" name="proxyNoneBtn"> + <property name="text"> + <string>&None</string> + </property> + <attribute name="buttonGroup"> + <string notr="true">proxyGroup</string> + </attribute> + </widget> + </item> + <item> + <widget class="QRadioButton" name="proxySOCKS5Btn"> + <property name="text"> + <string>SOC&KS5</string> + </property> + <attribute name="buttonGroup"> + <string notr="true">proxyGroup</string> + </attribute> + </widget> + </item> + <item> + <widget class="QRadioButton" name="proxyHTTPBtn"> + <property name="text"> + <string>H&TTP</string> + </property> + <attribute name="buttonGroup"> + <string notr="true">proxyGroup</string> + </attribute> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="proxyAddrBox"> + <property name="title"> + <string>Address and Port</string> + </property> + <layout class="QHBoxLayout" name="horizontalLayout_2"> + <item> + <widget class="QLineEdit" name="proxyAddrEdit"> + <property name="placeholderText"> + <string notr="true">127.0.0.1</string> + </property> + </widget> + </item> + <item> + <widget class="QSpinBox" name="proxyPortEdit"> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + <property name="buttonSymbols"> + <enum>QAbstractSpinBox::PlusMinus</enum> + </property> + <property name="maximum"> + <number>65535</number> + </property> + <property name="value"> + <number>8080</number> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="proxyAuthBox"> + <property name="title"> + <string>Authentication</string> + </property> + <layout class="QGridLayout" name="gridLayout_5"> + <item row="0" column="1"> + <widget class="QLineEdit" name="proxyUserEdit"/> + </item> + <item row="0" column="0"> + <widget class="QLabel" name="proxyUsernameLabel"> + <property name="text"> + <string>Username:</string> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="proxyPasswordLabel"> + <property name="text"> + <string>Password:</string> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QLineEdit" name="proxyPassEdit"> + <property name="echoMode"> + <enum>QLineEdit::Password</enum> + </property> + </widget> + </item> + <item row="2" column="0" colspan="2"> + <widget class="QLabel" name="proxyPlainTextWarningLabel"> + <property name="text"> + <string>Note: Proxy username and password are stored in plain text inside the launcher's configuration file!</string> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections/> + <buttongroups> + <buttongroup name="proxyGroup"/> + </buttongroups> +</ui> diff --git a/launcher/ui/pages/instance/GameOptionsPage.cpp b/launcher/ui/pages/instance/GameOptionsPage.cpp new file mode 100644 index 00000000..782f2ab3 --- /dev/null +++ b/launcher/ui/pages/instance/GameOptionsPage.cpp @@ -0,0 +1,37 @@ +#include "GameOptionsPage.h" +#include "ui_GameOptionsPage.h" +#include "minecraft/MinecraftInstance.h" +#include "minecraft/gameoptions/GameOptions.h" + +GameOptionsPage::GameOptionsPage(MinecraftInstance * inst, QWidget* parent) + : QWidget(parent), ui(new Ui::GameOptionsPage) +{ + ui->setupUi(this); + ui->tabWidget->tabBar()->hide(); + m_model = inst->gameOptionsModel(); + ui->optionsView->setModel(m_model.get()); + auto head = ui->optionsView->header(); + if(head->count()) + { + head->setSectionResizeMode(0, QHeaderView::ResizeToContents); + for(int i = 1; i < head->count(); i++) + { + head->setSectionResizeMode(i, QHeaderView::Stretch); + } + } +} + +GameOptionsPage::~GameOptionsPage() +{ + // m_model->save(); +} + +void GameOptionsPage::openedImpl() +{ + // m_model->observe(); +} + +void GameOptionsPage::closedImpl() +{ + // m_model->unobserve(); +} diff --git a/launcher/ui/pages/instance/GameOptionsPage.h b/launcher/ui/pages/instance/GameOptionsPage.h new file mode 100644 index 00000000..878903eb --- /dev/null +++ b/launcher/ui/pages/instance/GameOptionsPage.h @@ -0,0 +1,63 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <QWidget> +#include <QString> + +#include "ui/pages/BasePage.h" +#include <Application.h> + +namespace Ui +{ +class GameOptionsPage; +} + +class GameOptions; +class MinecraftInstance; + +class GameOptionsPage : public QWidget, public BasePage +{ + Q_OBJECT + +public: + explicit GameOptionsPage(MinecraftInstance *inst, QWidget *parent = 0); + virtual ~GameOptionsPage(); + + void openedImpl() override; + void closedImpl() override; + + virtual QString displayName() const override + { + return tr("Game Options"); + } + virtual QIcon icon() const override + { + return APPLICATION->getThemedIcon("settings"); + } + virtual QString id() const override + { + return "gameoptions"; + } + virtual QString helpPage() const override + { + return "Game-Options-management"; + } + +private: // data + Ui::GameOptionsPage *ui = nullptr; + std::shared_ptr<GameOptions> m_model; +}; diff --git a/launcher/ui/pages/instance/GameOptionsPage.ui b/launcher/ui/pages/instance/GameOptionsPage.ui new file mode 100644 index 00000000..f0a5ce0e --- /dev/null +++ b/launcher/ui/pages/instance/GameOptionsPage.ui @@ -0,0 +1,88 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>GameOptionsPage</class> + <widget class="QWidget" name="GameOptionsPage"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>706</width> + <height>575</height> + </rect> + </property> + <layout class="QGridLayout" name="gridLayout"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item row="0" column="0"> + <widget class="QTabWidget" name="tabWidget"> + <property name="currentIndex"> + <number>0</number> + </property> + <widget class="QWidget" name="tab"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <attribute name="title"> + <string notr="true">Tab 1</string> + </attribute> + <layout class="QGridLayout" name="gridLayout_2"> + <item row="0" column="0" colspan="2"> + <widget class="QTreeView" name="optionsView"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Expanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="acceptDrops"> + <bool>true</bool> + </property> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + <property name="selectionMode"> + <enum>QAbstractItemView::SingleSelection</enum> + </property> + <property name="selectionBehavior"> + <enum>QAbstractItemView::SelectRows</enum> + </property> + <property name="iconSize"> + <size> + <width>64</width> + <height>64</height> + </size> + </property> + <property name="rootIsDecorated"> + <bool>false</bool> + </property> + <attribute name="headerStretchLastSection"> + <bool>false</bool> + </attribute> + </widget> + </item> + </layout> + </widget> + </widget> + </item> + </layout> + </widget> + <tabstops> + <tabstop>tabWidget</tabstop> + <tabstop>optionsView</tabstop> + </tabstops> + <resources/> + <connections/> +</ui> diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.cpp b/launcher/ui/pages/instance/InstanceSettingsPage.cpp new file mode 100644 index 00000000..b0e18af4 --- /dev/null +++ b/launcher/ui/pages/instance/InstanceSettingsPage.cpp @@ -0,0 +1,341 @@ +#include "InstanceSettingsPage.h" +#include "ui_InstanceSettingsPage.h" + +#include <QFileDialog> +#include <QDialog> +#include <QMessageBox> + +#include <sys.h> + +#include "ui/dialogs/VersionSelectDialog.h" +#include "ui/widgets/CustomCommands.h" + +#include "JavaCommon.h" +#include "Application.h" + +#include "java/JavaInstallList.h" +#include "FileSystem.h" + + +InstanceSettingsPage::InstanceSettingsPage(BaseInstance *inst, QWidget *parent) + : QWidget(parent), ui(new Ui::InstanceSettingsPage), m_instance(inst) +{ + m_settings = inst->settings(); + ui->setupUi(this); + auto sysMB = Sys::getSystemRam() / Sys::mebibyte; + ui->maxMemSpinBox->setMaximum(sysMB); + connect(ui->openGlobalJavaSettingsButton, &QCommandLinkButton::clicked, this, &InstanceSettingsPage::globalSettingsButtonClicked); + connect(APPLICATION, &Application::globalSettingsAboutToOpen, this, &InstanceSettingsPage::applySettings); + connect(APPLICATION, &Application::globalSettingsClosed, this, &InstanceSettingsPage::loadSettings); + loadSettings(); +} + +bool InstanceSettingsPage::shouldDisplay() const +{ + return !m_instance->isRunning(); +} + +InstanceSettingsPage::~InstanceSettingsPage() +{ + delete ui; +} + +void InstanceSettingsPage::globalSettingsButtonClicked(bool) +{ + switch(ui->settingsTabs->currentIndex()) { + case 0: + APPLICATION->ShowGlobalSettings(this, "java-settings"); + return; + case 1: + APPLICATION->ShowGlobalSettings(this, "minecraft-settings"); + return; + case 2: + APPLICATION->ShowGlobalSettings(this, "custom-commands"); + return; + } +} + +bool InstanceSettingsPage::apply() +{ + applySettings(); + return true; +} + +void InstanceSettingsPage::applySettings() +{ + SettingsObject::Lock lock(m_settings); + + // Console + bool console = ui->consoleSettingsBox->isChecked(); + m_settings->set("OverrideConsole", console); + if (console) + { + m_settings->set("ShowConsole", ui->showConsoleCheck->isChecked()); + m_settings->set("AutoCloseConsole", ui->autoCloseConsoleCheck->isChecked()); + m_settings->set("ShowConsoleOnError", ui->showConsoleErrorCheck->isChecked()); + } + else + { + m_settings->reset("ShowConsole"); + m_settings->reset("AutoCloseConsole"); + m_settings->reset("ShowConsoleOnError"); + } + + // Window Size + bool window = ui->windowSizeGroupBox->isChecked(); + m_settings->set("OverrideWindow", window); + if (window) + { + m_settings->set("LaunchMaximized", ui->maximizedCheckBox->isChecked()); + m_settings->set("MinecraftWinWidth", ui->windowWidthSpinBox->value()); + m_settings->set("MinecraftWinHeight", ui->windowHeightSpinBox->value()); + } + else + { + m_settings->reset("LaunchMaximized"); + m_settings->reset("MinecraftWinWidth"); + m_settings->reset("MinecraftWinHeight"); + } + + // Memory + bool memory = ui->memoryGroupBox->isChecked(); + m_settings->set("OverrideMemory", memory); + if (memory) + { + int min = ui->minMemSpinBox->value(); + int max = ui->maxMemSpinBox->value(); + if(min < max) + { + m_settings->set("MinMemAlloc", min); + m_settings->set("MaxMemAlloc", max); + } + else + { + m_settings->set("MinMemAlloc", max); + m_settings->set("MaxMemAlloc", min); + } + m_settings->set("PermGen", ui->permGenSpinBox->value()); + } + else + { + m_settings->reset("MinMemAlloc"); + m_settings->reset("MaxMemAlloc"); + m_settings->reset("PermGen"); + } + + // Java Install Settings + bool javaInstall = ui->javaSettingsGroupBox->isChecked(); + m_settings->set("OverrideJavaLocation", javaInstall); + if (javaInstall) + { + m_settings->set("JavaPath", ui->javaPathTextBox->text()); + } + else + { + m_settings->reset("JavaPath"); + } + + // Java arguments + bool javaArgs = ui->javaArgumentsGroupBox->isChecked(); + m_settings->set("OverrideJavaArgs", javaArgs); + if(javaArgs) + { + m_settings->set("JvmArgs", ui->jvmArgsTextBox->toPlainText().replace("\n", " ")); + JavaCommon::checkJVMArgs(m_settings->get("JvmArgs").toString(), this->parentWidget()); + } + else + { + m_settings->reset("JvmArgs"); + } + + // old generic 'override both' is removed. + m_settings->reset("OverrideJava"); + + // Custom Commands + bool custcmd = ui->customCommands->checked(); + m_settings->set("OverrideCommands", custcmd); + if (custcmd) + { + m_settings->set("PreLaunchCommand", ui->customCommands->prelaunchCommand()); + m_settings->set("WrapperCommand", ui->customCommands->wrapperCommand()); + m_settings->set("PostExitCommand", ui->customCommands->postexitCommand()); + } + else + { + m_settings->reset("PreLaunchCommand"); + m_settings->reset("WrapperCommand"); + m_settings->reset("PostExitCommand"); + } + + // Workarounds + bool workarounds = ui->nativeWorkaroundsGroupBox->isChecked(); + m_settings->set("OverrideNativeWorkarounds", workarounds); + if(workarounds) + { + m_settings->set("UseNativeOpenAL", ui->useNativeOpenALCheck->isChecked()); + m_settings->set("UseNativeGLFW", ui->useNativeGLFWCheck->isChecked()); + } + else + { + m_settings->reset("UseNativeOpenAL"); + m_settings->reset("UseNativeGLFW"); + } + + // Game time + bool gameTime = ui->gameTimeGroupBox->isChecked(); + m_settings->set("OverrideGameTime", gameTime); + if (gameTime) + { + m_settings->set("ShowGameTime", ui->showGameTime->isChecked()); + m_settings->set("RecordGameTime", ui->recordGameTime->isChecked()); + } + else + { + m_settings->reset("ShowGameTime"); + m_settings->reset("RecordGameTime"); + } + + // Join server on launch + bool joinServerOnLaunch = ui->serverJoinGroupBox->isChecked(); + m_settings->set("JoinServerOnLaunch", joinServerOnLaunch); + if (joinServerOnLaunch) + { + m_settings->set("JoinServerOnLaunchAddress", ui->serverJoinAddress->text()); + } + else + { + m_settings->reset("JoinServerOnLaunchAddress"); + } +} + +void InstanceSettingsPage::loadSettings() +{ + // Console + ui->consoleSettingsBox->setChecked(m_settings->get("OverrideConsole").toBool()); + ui->showConsoleCheck->setChecked(m_settings->get("ShowConsole").toBool()); + ui->autoCloseConsoleCheck->setChecked(m_settings->get("AutoCloseConsole").toBool()); + ui->showConsoleErrorCheck->setChecked(m_settings->get("ShowConsoleOnError").toBool()); + + // Window Size + ui->windowSizeGroupBox->setChecked(m_settings->get("OverrideWindow").toBool()); + ui->maximizedCheckBox->setChecked(m_settings->get("LaunchMaximized").toBool()); + ui->windowWidthSpinBox->setValue(m_settings->get("MinecraftWinWidth").toInt()); + ui->windowHeightSpinBox->setValue(m_settings->get("MinecraftWinHeight").toInt()); + + // Memory + ui->memoryGroupBox->setChecked(m_settings->get("OverrideMemory").toBool()); + int min = m_settings->get("MinMemAlloc").toInt(); + int max = m_settings->get("MaxMemAlloc").toInt(); + if(min < max) + { + ui->minMemSpinBox->setValue(min); + ui->maxMemSpinBox->setValue(max); + } + else + { + ui->minMemSpinBox->setValue(max); + ui->maxMemSpinBox->setValue(min); + } + ui->permGenSpinBox->setValue(m_settings->get("PermGen").toInt()); + bool permGenVisible = m_settings->get("PermGenVisible").toBool(); + ui->permGenSpinBox->setVisible(permGenVisible); + ui->labelPermGen->setVisible(permGenVisible); + ui->labelPermgenNote->setVisible(permGenVisible); + + + // Java Settings + bool overrideJava = m_settings->get("OverrideJava").toBool(); + bool overrideLocation = m_settings->get("OverrideJavaLocation").toBool() || overrideJava; + bool overrideArgs = m_settings->get("OverrideJavaArgs").toBool() || overrideJava; + + ui->javaSettingsGroupBox->setChecked(overrideLocation); + ui->javaPathTextBox->setText(m_settings->get("JavaPath").toString()); + + ui->javaArgumentsGroupBox->setChecked(overrideArgs); + ui->jvmArgsTextBox->setPlainText(m_settings->get("JvmArgs").toString()); + + // Custom commands + ui->customCommands->initialize( + true, + m_settings->get("OverrideCommands").toBool(), + m_settings->get("PreLaunchCommand").toString(), + m_settings->get("WrapperCommand").toString(), + m_settings->get("PostExitCommand").toString() + ); + + // Workarounds + ui->nativeWorkaroundsGroupBox->setChecked(m_settings->get("OverrideNativeWorkarounds").toBool()); + ui->useNativeGLFWCheck->setChecked(m_settings->get("UseNativeGLFW").toBool()); + ui->useNativeOpenALCheck->setChecked(m_settings->get("UseNativeOpenAL").toBool()); + + // Miscellanous + ui->gameTimeGroupBox->setChecked(m_settings->get("OverrideGameTime").toBool()); + ui->showGameTime->setChecked(m_settings->get("ShowGameTime").toBool()); + ui->recordGameTime->setChecked(m_settings->get("RecordGameTime").toBool()); + + ui->serverJoinGroupBox->setChecked(m_settings->get("JoinServerOnLaunch").toBool()); + ui->serverJoinAddress->setText(m_settings->get("JoinServerOnLaunchAddress").toString()); +} + +void InstanceSettingsPage::on_javaDetectBtn_clicked() +{ + JavaInstallPtr java; + + VersionSelectDialog vselect(APPLICATION->javalist().get(), tr("Select a Java version"), this, true); + vselect.setResizeOn(2); + vselect.exec(); + + if (vselect.result() == QDialog::Accepted && vselect.selectedVersion()) + { + java = std::dynamic_pointer_cast<JavaInstall>(vselect.selectedVersion()); + ui->javaPathTextBox->setText(java->path); + bool visible = java->id.requiresPermGen() && m_settings->get("OverrideMemory").toBool(); + ui->permGenSpinBox->setVisible(visible); + ui->labelPermGen->setVisible(visible); + ui->labelPermgenNote->setVisible(visible); + m_settings->set("PermGenVisible", visible); + } +} + +void InstanceSettingsPage::on_javaBrowseBtn_clicked() +{ + QString raw_path = QFileDialog::getOpenFileName(this, tr("Find Java executable")); + + // do not allow current dir - it's dirty. Do not allow dirs that don't exist + if(raw_path.isEmpty()) + { + return; + } + QString cooked_path = FS::NormalizePath(raw_path); + + QFileInfo javaInfo(cooked_path); + if(!javaInfo.exists() || !javaInfo.isExecutable()) + { + return; + } + ui->javaPathTextBox->setText(cooked_path); + + // custom Java could be anything... enable perm gen option + ui->permGenSpinBox->setVisible(true); + ui->labelPermGen->setVisible(true); + ui->labelPermgenNote->setVisible(true); + m_settings->set("PermGenVisible", true); +} + +void InstanceSettingsPage::on_javaTestBtn_clicked() +{ + if(checker) + { + return; + } + checker.reset(new JavaCommon::TestCheck( + this, ui->javaPathTextBox->text(), ui->jvmArgsTextBox->toPlainText().replace("\n", " "), + ui->minMemSpinBox->value(), ui->maxMemSpinBox->value(), ui->permGenSpinBox->value())); + connect(checker.get(), SIGNAL(finished()), SLOT(checkerFinished())); + checker->run(); +} + +void InstanceSettingsPage::checkerFinished() +{ + checker.reset(); +} diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.h b/launcher/ui/pages/instance/InstanceSettingsPage.h new file mode 100644 index 00000000..5c8c8e66 --- /dev/null +++ b/launcher/ui/pages/instance/InstanceSettingsPage.h @@ -0,0 +1,76 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <QWidget> + +#include "java/JavaChecker.h" +#include "BaseInstance.h" +#include <QObjectPtr.h> +#include "ui/pages/BasePage.h" +#include "JavaCommon.h" +#include "Application.h" + +class JavaChecker; +namespace Ui +{ +class InstanceSettingsPage; +} + +class InstanceSettingsPage : public QWidget, public BasePage +{ + Q_OBJECT + +public: + explicit InstanceSettingsPage(BaseInstance *inst, QWidget *parent = 0); + virtual ~InstanceSettingsPage(); + virtual QString displayName() const override + { + return tr("Settings"); + } + virtual QIcon icon() const override + { + return APPLICATION->getThemedIcon("instance-settings"); + } + virtual QString id() const override + { + return "settings"; + } + virtual bool apply() override; + virtual QString helpPage() const override + { + return "Instance-settings"; + } + virtual bool shouldDisplay() const override; + +private slots: + void on_javaDetectBtn_clicked(); + void on_javaTestBtn_clicked(); + void on_javaBrowseBtn_clicked(); + + void applySettings(); + void loadSettings(); + + void checkerFinished(); + + void globalSettingsButtonClicked(bool checked); + +private: + Ui::InstanceSettingsPage *ui; + BaseInstance *m_instance; + SettingsObjectPtr m_settings; + unique_qobject_ptr<JavaCommon::TestCheck> checker; +}; diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.ui b/launcher/ui/pages/instance/InstanceSettingsPage.ui new file mode 100644 index 00000000..729f8e2a --- /dev/null +++ b/launcher/ui/pages/instance/InstanceSettingsPage.ui @@ -0,0 +1,548 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>InstanceSettingsPage</class> + <widget class="QWidget" name="InstanceSettingsPage"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>691</width> + <height>581</height> + </rect> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QCommandLinkButton" name="openGlobalJavaSettingsButton"> + <property name="text"> + <string>Open Global Settings</string> + </property> + <property name="description"> + <string>The settings here are overrides for global settings.</string> + </property> + </widget> + </item> + <item> + <widget class="QTabWidget" name="settingsTabs"> + <property name="tabShape"> + <enum>QTabWidget::Rounded</enum> + </property> + <property name="currentIndex"> + <number>0</number> + </property> + <widget class="QWidget" name="minecraftTab"> + <attribute name="title"> + <string notr="true">Java</string> + </attribute> + <layout class="QVBoxLayout" name="verticalLayout_5"> + <item> + <widget class="QGroupBox" name="javaSettingsGroupBox"> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="title"> + <string>Java insta&llation</string> + </property> + <property name="checkable"> + <bool>true</bool> + </property> + <property name="checked"> + <bool>false</bool> + </property> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="0" colspan="3"> + <widget class="QLineEdit" name="javaPathTextBox"/> + </item> + <item row="1" column="0"> + <widget class="QPushButton" name="javaDetectBtn"> + <property name="text"> + <string>Auto-detect...</string> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QPushButton" name="javaBrowseBtn"> + <property name="text"> + <string>Browse...</string> + </property> + </widget> + </item> + <item row="1" column="2"> + <widget class="QPushButton" name="javaTestBtn"> + <property name="text"> + <string>Test</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="memoryGroupBox"> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="title"> + <string>Memor&y</string> + </property> + <property name="checkable"> + <bool>true</bool> + </property> + <property name="checked"> + <bool>false</bool> + </property> + <layout class="QGridLayout" name="gridLayout_2"> + <item row="0" column="0"> + <widget class="QLabel" name="labelMinMem"> + <property name="text"> + <string>Minimum memory allocation:</string> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QSpinBox" name="maxMemSpinBox"> + <property name="toolTip"> + <string>The maximum amount of memory Minecraft is allowed to use.</string> + </property> + <property name="suffix"> + <string notr="true"> MiB</string> + </property> + <property name="minimum"> + <number>128</number> + </property> + <property name="maximum"> + <number>65536</number> + </property> + <property name="singleStep"> + <number>128</number> + </property> + <property name="value"> + <number>1024</number> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QSpinBox" name="minMemSpinBox"> + <property name="toolTip"> + <string>The amount of memory Minecraft is started with.</string> + </property> + <property name="suffix"> + <string notr="true"> MiB</string> + </property> + <property name="minimum"> + <number>128</number> + </property> + <property name="maximum"> + <number>65536</number> + </property> + <property name="singleStep"> + <number>128</number> + </property> + <property name="value"> + <number>256</number> + </property> + </widget> + </item> + <item row="2" column="1"> + <widget class="QSpinBox" name="permGenSpinBox"> + <property name="toolTip"> + <string>The amount of memory available to store loaded Java classes.</string> + </property> + <property name="suffix"> + <string notr="true"> MiB</string> + </property> + <property name="minimum"> + <number>64</number> + </property> + <property name="maximum"> + <number>999999999</number> + </property> + <property name="singleStep"> + <number>8</number> + </property> + <property name="value"> + <number>64</number> + </property> + </widget> + </item> + <item row="2" column="0"> + <widget class="QLabel" name="labelPermGen"> + <property name="text"> + <string notr="true">PermGen:</string> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="labelMaxMem"> + <property name="text"> + <string>Maximum memory allocation:</string> + </property> + </widget> + </item> + <item row="3" column="0" colspan="2"> + <widget class="QLabel" name="labelPermgenNote"> + <property name="text"> + <string>Note: Permgen is set automatically by Java 8 and later</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="javaArgumentsGroupBox"> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="title"> + <string>Java argumen&ts</string> + </property> + <property name="checkable"> + <bool>true</bool> + </property> + <property name="checked"> + <bool>false</bool> + </property> + <layout class="QGridLayout" name="gridLayout_5"> + <item row="1" column="1"> + <widget class="QPlainTextEdit" name="jvmArgsTextBox"/> + </item> + </layout> + </widget> + </item> + </layout> + </widget> + <widget class="QWidget" name="javaTab"> + <attribute name="title"> + <string>Game windows</string> + </attribute> + <layout class="QVBoxLayout" name="verticalLayout_3"> + <item> + <widget class="QGroupBox" name="windowSizeGroupBox"> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="title"> + <string>Game Window</string> + </property> + <property name="checkable"> + <bool>true</bool> + </property> + <property name="checked"> + <bool>false</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout_4"> + <item> + <widget class="QCheckBox" name="maximizedCheckBox"> + <property name="text"> + <string>Start Minecraft maximized?</string> + </property> + </widget> + </item> + <item> + <layout class="QGridLayout" name="gridLayoutWindowSize"> + <item row="1" column="0"> + <widget class="QLabel" name="labelWindowHeight"> + <property name="text"> + <string>Window height:</string> + </property> + </widget> + </item> + <item row="0" column="0"> + <widget class="QLabel" name="labelWindowWidth"> + <property name="text"> + <string>Window width:</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QSpinBox" name="windowWidthSpinBox"> + <property name="minimum"> + <number>1</number> + </property> + <property name="maximum"> + <number>65536</number> + </property> + <property name="singleStep"> + <number>1</number> + </property> + <property name="value"> + <number>854</number> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QSpinBox" name="windowHeightSpinBox"> + <property name="minimum"> + <number>1</number> + </property> + <property name="maximum"> + <number>65536</number> + </property> + <property name="value"> + <number>480</number> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="consoleSettingsBox"> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="title"> + <string>Conso&le Settings</string> + </property> + <property name="checkable"> + <bool>true</bool> + </property> + <property name="checked"> + <bool>false</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <item> + <widget class="QCheckBox" name="showConsoleCheck"> + <property name="text"> + <string>Show console while the game is running?</string> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="autoCloseConsoleCheck"> + <property name="text"> + <string>Automatically close console when the game quits?</string> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="showConsoleErrorCheck"> + <property name="text"> + <string>Show console when the game crashes?</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <spacer name="verticalSpacerMinecraft_2"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>88</width> + <height>125</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + <widget class="QWidget" name="tab"> + <attribute name="title"> + <string>Custom commands</string> + </attribute> + <layout class="QVBoxLayout" name="verticalLayout_6"> + <item> + <widget class="CustomCommands" name="customCommands" native="true"/> + </item> + </layout> + </widget> + <widget class="QWidget" name="workaroundsPage"> + <attribute name="title"> + <string>Workarounds</string> + </attribute> + <layout class="QVBoxLayout" name="verticalLayout_8"> + <item> + <widget class="QGroupBox" name="nativeWorkaroundsGroupBox"> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="title"> + <string>Native libraries</string> + </property> + <property name="checkable"> + <bool>true</bool> + </property> + <property name="checked"> + <bool>false</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout_7"> + <item> + <widget class="QCheckBox" name="useNativeGLFWCheck"> + <property name="text"> + <string>Use system installation of GLFW</string> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="useNativeOpenALCheck"> + <property name="text"> + <string>Use system installation of OpenAL</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + <widget class="QWidget" name="miscellaneousPage"> + <attribute name="title"> + <string>Miscellaneous</string> + </attribute> + <layout class="QVBoxLayout" name="verticalLayout_9"> + <item> + <widget class="QGroupBox" name="gameTimeGroupBox"> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="title"> + <string>Override global game time settings</string> + </property> + <property name="checkable"> + <bool>true</bool> + </property> + <property name="checked"> + <bool>false</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout_10"> + <item> + <widget class="QCheckBox" name="showGameTime"> + <property name="text"> + <string>Show time spent playing this instance</string> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="recordGameTime"> + <property name="text"> + <string>Record time spent playing this instance</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="serverJoinGroupBox"> + <property name="title"> + <string>Set a server to join on launch</string> + </property> + <property name="checkable"> + <bool>true</bool> + </property> + <property name="checked"> + <bool>false</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout_11"> + <item> + <layout class="QGridLayout" name="serverJoinLayout"> + <item row="0" column="0"> + <widget class="QLabel" name="serverJoinAddressLabel"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Server address:</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QLineEdit" name="serverJoinAddress"/> + </item> + </layout> + </item> + </layout> + </widget> + </item> + <item> + <spacer name="verticalSpacerMiscellaneous"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + </widget> + </item> + </layout> + </widget> + <customwidgets> + <customwidget> + <class>CustomCommands</class> + <extends>QWidget</extends> + <header>ui/widgets/CustomCommands.h</header> + <container>1</container> + </customwidget> + </customwidgets> + <tabstops> + <tabstop>openGlobalJavaSettingsButton</tabstop> + <tabstop>settingsTabs</tabstop> + <tabstop>javaSettingsGroupBox</tabstop> + <tabstop>javaPathTextBox</tabstop> + <tabstop>javaDetectBtn</tabstop> + <tabstop>javaBrowseBtn</tabstop> + <tabstop>javaTestBtn</tabstop> + <tabstop>memoryGroupBox</tabstop> + <tabstop>minMemSpinBox</tabstop> + <tabstop>maxMemSpinBox</tabstop> + <tabstop>permGenSpinBox</tabstop> + <tabstop>javaArgumentsGroupBox</tabstop> + <tabstop>jvmArgsTextBox</tabstop> + <tabstop>windowSizeGroupBox</tabstop> + <tabstop>maximizedCheckBox</tabstop> + <tabstop>windowWidthSpinBox</tabstop> + <tabstop>windowHeightSpinBox</tabstop> + <tabstop>consoleSettingsBox</tabstop> + <tabstop>showConsoleCheck</tabstop> + <tabstop>autoCloseConsoleCheck</tabstop> + <tabstop>showConsoleErrorCheck</tabstop> + <tabstop>nativeWorkaroundsGroupBox</tabstop> + <tabstop>useNativeGLFWCheck</tabstop> + <tabstop>useNativeOpenALCheck</tabstop> + <tabstop>showGameTime</tabstop> + <tabstop>recordGameTime</tabstop> + </tabstops> + <resources/> + <connections/> +</ui> diff --git a/launcher/ui/pages/instance/LegacyUpgradePage.cpp b/launcher/ui/pages/instance/LegacyUpgradePage.cpp new file mode 100644 index 00000000..cb78af02 --- /dev/null +++ b/launcher/ui/pages/instance/LegacyUpgradePage.cpp @@ -0,0 +1,51 @@ +#include "LegacyUpgradePage.h" +#include "ui_LegacyUpgradePage.h" + +#include "InstanceList.h" +#include "minecraft/legacy/LegacyInstance.h" +#include "minecraft/legacy/LegacyUpgradeTask.h" +#include "Application.h" + +#include "ui/dialogs/CustomMessageBox.h" +#include "ui/dialogs/ProgressDialog.h" + +LegacyUpgradePage::LegacyUpgradePage(InstancePtr inst, QWidget *parent) + : QWidget(parent), ui(new Ui::LegacyUpgradePage), m_inst(inst) +{ + ui->setupUi(this); +} + +LegacyUpgradePage::~LegacyUpgradePage() +{ + delete ui; +} + +void LegacyUpgradePage::runModalTask(Task *task) +{ + connect(task, &Task::failed, [this](QString reason) + { + CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Warning)->show(); + }); + ProgressDialog loadDialog(this); + loadDialog.setSkipButton(true, tr("Abort")); + if(loadDialog.execWithTask(task) == QDialog::Accepted) + { + m_container->requestClose(); + } +} + +void LegacyUpgradePage::on_upgradeButton_clicked() +{ + QString newName = tr("%1 (Migrated)").arg(m_inst->name()); + auto upgradeTask = new LegacyUpgradeTask(m_inst); + upgradeTask->setName(newName); + upgradeTask->setGroup(APPLICATION->instances()->getInstanceGroup(m_inst->id())); + upgradeTask->setIcon(m_inst->iconKey()); + unique_qobject_ptr<Task> task(APPLICATION->instances()->wrapInstanceTask(upgradeTask)); + runModalTask(task.get()); +} + +bool LegacyUpgradePage::shouldDisplay() const +{ + return !m_inst->isRunning(); +} diff --git a/launcher/ui/pages/instance/LegacyUpgradePage.h b/launcher/ui/pages/instance/LegacyUpgradePage.h new file mode 100644 index 00000000..7c51956b --- /dev/null +++ b/launcher/ui/pages/instance/LegacyUpgradePage.h @@ -0,0 +1,64 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <QWidget> + +#include "minecraft/legacy/LegacyInstance.h" +#include "ui/pages/BasePage.h" +#include <Application.h> +#include "tasks/Task.h" + +namespace Ui +{ +class LegacyUpgradePage; +} + +class LegacyUpgradePage : public QWidget, public BasePage +{ + Q_OBJECT + +public: + explicit LegacyUpgradePage(InstancePtr inst, QWidget *parent = 0); + virtual ~LegacyUpgradePage(); + virtual QString displayName() const override + { + return tr("Upgrade"); + } + virtual QIcon icon() const override + { + return APPLICATION->getThemedIcon("checkupdate"); + } + virtual QString id() const override + { + return "upgrade"; + } + virtual QString helpPage() const override + { + return "Legacy-upgrade"; + } + virtual bool shouldDisplay() const override; + +private slots: + void on_upgradeButton_clicked(); + +private: + void runModalTask(Task *task); + +private: + Ui::LegacyUpgradePage *ui; + InstancePtr m_inst; +}; diff --git a/launcher/ui/pages/instance/LegacyUpgradePage.ui b/launcher/ui/pages/instance/LegacyUpgradePage.ui new file mode 100644 index 00000000..085919e3 --- /dev/null +++ b/launcher/ui/pages/instance/LegacyUpgradePage.ui @@ -0,0 +1,47 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>LegacyUpgradePage</class> + <widget class="QWidget" name="LegacyUpgradePage"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>546</width> + <height>405</height> + </rect> + </property> + <layout class="QVBoxLayout" name="verticalLayout_5"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QTextBrowser" name="textBrowser"> + <property name="html"> + <string><html><body><h1>Upgrade is required</h1><p>MultiMC now supports old Minecraft versions and all the required features in the new (OneSix) instance format. As a consequence, the old (Legacy) format has been entirely disabled and old instances need to be upgraded.</p><p>The upgrade will create a new instance with the same contents as the current one, in the new format. The original instance will remain untouched, in case anything goes wrong in the process.</p><p>Please report any issues on our <a href="https://github.com/MultiMC/Launcher/issues">github issues page</a>.</p><p>There is also a <a href="https://discord.gg/GtPmv93">discord channel for testing here</a>.</p></body></html></string> + </property> + <property name="openExternalLinks"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QCommandLinkButton" name="upgradeButton"> + <property name="text"> + <string>Upgrade the instance</string> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui> diff --git a/launcher/ui/pages/instance/LogPage.cpp b/launcher/ui/pages/instance/LogPage.cpp new file mode 100644 index 00000000..b66c6cc7 --- /dev/null +++ b/launcher/ui/pages/instance/LogPage.cpp @@ -0,0 +1,330 @@ +#include "LogPage.h" +#include "ui_LogPage.h" + +#include "Application.h" + +#include <QIcon> +#include <QScrollBar> +#include <QShortcut> + +#include "launch/LaunchTask.h" +#include "settings/Setting.h" + +#include "ui/GuiUtil.h" +#include "ui/ColorCache.h" + +#include <BuildConfig.h> + +class LogFormatProxyModel : public QIdentityProxyModel +{ +public: + LogFormatProxyModel(QObject* parent = nullptr) : QIdentityProxyModel(parent) + { + } + QVariant data(const QModelIndex &index, int role) const override + { + switch(role) + { + case Qt::FontRole: + return m_font; + case Qt::TextColorRole: + { + MessageLevel::Enum level = (MessageLevel::Enum) QIdentityProxyModel::data(index, LogModel::LevelRole).toInt(); + return m_colors->getFront(level); + } + case Qt::BackgroundRole: + { + MessageLevel::Enum level = (MessageLevel::Enum) QIdentityProxyModel::data(index, LogModel::LevelRole).toInt(); + return m_colors->getBack(level); + } + default: + return QIdentityProxyModel::data(index, role); + } + } + + void setFont(QFont font) + { + m_font = font; + } + + void setColors(LogColorCache* colors) + { + m_colors.reset(colors); + } + + QModelIndex find(const QModelIndex &start, const QString &value, bool reverse) const + { + QModelIndex parentIndex = parent(start); + auto compare = [&](int r) -> QModelIndex + { + QModelIndex idx = index(r, start.column(), parentIndex); + if (!idx.isValid() || idx == start) + { + return QModelIndex(); + } + QVariant v = data(idx, Qt::DisplayRole); + QString t = v.toString(); + if (t.contains(value, Qt::CaseInsensitive)) + return idx; + return QModelIndex(); + }; + if(reverse) + { + int from = start.row(); + int to = 0; + + for (int i = 0; i < 2; ++i) + { + for (int r = from; (r >= to); --r) + { + auto idx = compare(r); + if(idx.isValid()) + return idx; + } + // prepare for the next iteration + from = rowCount() - 1; + to = start.row(); + } + } + else + { + int from = start.row(); + int to = rowCount(parentIndex); + + for (int i = 0; i < 2; ++i) + { + for (int r = from; (r < to); ++r) + { + auto idx = compare(r); + if(idx.isValid()) + return idx; + } + // prepare for the next iteration + from = 0; + to = start.row(); + } + } + return QModelIndex(); + } +private: + QFont m_font; + std::unique_ptr<LogColorCache> m_colors; +}; + +LogPage::LogPage(InstancePtr instance, QWidget *parent) + : QWidget(parent), ui(new Ui::LogPage), m_instance(instance) +{ + ui->setupUi(this); + ui->tabWidget->tabBar()->hide(); + + m_proxy = new LogFormatProxyModel(this); + // set up text colors in the log proxy and adapt them to the current theme foreground and background + { + auto origForeground = ui->text->palette().color(ui->text->foregroundRole()); + auto origBackground = ui->text->palette().color(ui->text->backgroundRole()); + m_proxy->setColors(new LogColorCache(origForeground, origBackground)); + } + + // set up fonts in the log proxy + { + QString fontFamily = APPLICATION->settings()->get("ConsoleFont").toString(); + bool conversionOk = false; + int fontSize = APPLICATION->settings()->get("ConsoleFontSize").toInt(&conversionOk); + if(!conversionOk) + { + fontSize = 11; + } + m_proxy->setFont(QFont(fontFamily, fontSize)); + } + + ui->text->setModel(m_proxy); + + // set up instance and launch process recognition + { + auto launchTask = m_instance->getLaunchTask(); + if(launchTask) + { + setInstanceLaunchTaskChanged(launchTask, true); + } + connect(m_instance.get(), &BaseInstance::launchTaskChanged, this, &LogPage::onInstanceLaunchTaskChanged); + } + + auto findShortcut = new QShortcut(QKeySequence(QKeySequence::Find), this); + connect(findShortcut, SIGNAL(activated()), SLOT(findActivated())); + auto findNextShortcut = new QShortcut(QKeySequence(QKeySequence::FindNext), this); + connect(findNextShortcut, SIGNAL(activated()), SLOT(findNextActivated())); + connect(ui->searchBar, SIGNAL(returnPressed()), SLOT(on_findButton_clicked())); + auto findPreviousShortcut = new QShortcut(QKeySequence(QKeySequence::FindPrevious), this); + connect(findPreviousShortcut, SIGNAL(activated()), SLOT(findPreviousActivated())); +} + +LogPage::~LogPage() +{ + delete ui; +} + +void LogPage::modelStateToUI() +{ + if(m_model->wrapLines()) + { + ui->text->setWordWrap(true); + ui->wrapCheckbox->setCheckState(Qt::Checked); + } + else + { + ui->text->setWordWrap(false); + ui->wrapCheckbox->setCheckState(Qt::Unchecked); + } + if(m_model->suspended()) + { + ui->trackLogCheckbox->setCheckState(Qt::Unchecked); + } + else + { + ui->trackLogCheckbox->setCheckState(Qt::Checked); + } +} + +void LogPage::UIToModelState() +{ + if(!m_model) + { + return; + } + m_model->setLineWrap(ui->wrapCheckbox->checkState() == Qt::Checked); + m_model->suspend(ui->trackLogCheckbox->checkState() != Qt::Checked); +} + +void LogPage::setInstanceLaunchTaskChanged(shared_qobject_ptr<LaunchTask> proc, bool initial) +{ + m_process = proc; + if(m_process) + { + m_model = proc->getLogModel(); + m_proxy->setSourceModel(m_model.get()); + if(initial) + { + modelStateToUI(); + } + else + { + UIToModelState(); + } + } + else + { + m_proxy->setSourceModel(nullptr); + m_model.reset(); + } +} + +void LogPage::onInstanceLaunchTaskChanged(shared_qobject_ptr<LaunchTask> proc) +{ + setInstanceLaunchTaskChanged(proc, false); +} + +bool LogPage::apply() +{ + return true; +} + +bool LogPage::shouldDisplay() const +{ + return m_instance->isRunning() || m_proxy->rowCount() > 0; +} + +void LogPage::on_btnPaste_clicked() +{ + if(!m_model) + return; + + //FIXME: turn this into a proper task and move the upload logic out of GuiUtil! + m_model->append( + MessageLevel::Launcher, + QString("%2: Log upload triggered at: %1").arg( + QDateTime::currentDateTime().toString(Qt::RFC2822Date), + BuildConfig.LAUNCHER_NAME + ) + ); + auto url = GuiUtil::uploadPaste(m_model->toPlainText(), this); + if(!url.isEmpty()) + { + m_model->append( + MessageLevel::Launcher, + QString("%2: Log uploaded to: %1").arg( + url, + BuildConfig.LAUNCHER_NAME + ) + ); + } + else + { + m_model->append( + MessageLevel::Error, + QString("%1: Log upload failed!").arg(BuildConfig.LAUNCHER_NAME) + ); + } +} + +void LogPage::on_btnCopy_clicked() +{ + if(!m_model) + return; + m_model->append(MessageLevel::Launcher, QString("Clipboard copy at: %1").arg(QDateTime::currentDateTime().toString(Qt::RFC2822Date))); + GuiUtil::setClipboardText(m_model->toPlainText()); +} + +void LogPage::on_btnClear_clicked() +{ + if(!m_model) + return; + m_model->clear(); + m_container->refreshContainer(); +} + +void LogPage::on_btnBottom_clicked() +{ + ui->text->scrollToBottom(); +} + +void LogPage::on_trackLogCheckbox_clicked(bool checked) +{ + if(!m_model) + return; + m_model->suspend(!checked); +} + +void LogPage::on_wrapCheckbox_clicked(bool checked) +{ + ui->text->setWordWrap(checked); + if(!m_model) + return; + m_model->setLineWrap(checked); +} + +void LogPage::on_findButton_clicked() +{ + auto modifiers = QApplication::keyboardModifiers(); + bool reverse = modifiers & Qt::ShiftModifier; + ui->text->findNext(ui->searchBar->text(), reverse); +} + +void LogPage::findNextActivated() +{ + ui->text->findNext(ui->searchBar->text(), false); +} + +void LogPage::findPreviousActivated() +{ + ui->text->findNext(ui->searchBar->text(), true); +} + +void LogPage::findActivated() +{ + // focus the search bar if it doesn't have focus + if (!ui->searchBar->hasFocus()) + { + ui->searchBar->setFocus(); + ui->searchBar->selectAll(); + } +} diff --git a/launcher/ui/pages/instance/LogPage.h b/launcher/ui/pages/instance/LogPage.h new file mode 100644 index 00000000..cab25563 --- /dev/null +++ b/launcher/ui/pages/instance/LogPage.h @@ -0,0 +1,86 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <QWidget> + +#include "BaseInstance.h" +#include "launch/LaunchTask.h" +#include "ui/pages/BasePage.h" +#include <Application.h> + +namespace Ui +{ +class LogPage; +} +class QTextCharFormat; +class LogFormatProxyModel; + +class LogPage : public QWidget, public BasePage +{ + Q_OBJECT + +public: + explicit LogPage(InstancePtr instance, QWidget *parent = 0); + virtual ~LogPage(); + virtual QString displayName() const override + { + return tr("Minecraft Log"); + } + virtual QIcon icon() const override + { + return APPLICATION->getThemedIcon("log"); + } + virtual QString id() const override + { + return "console"; + } + virtual bool apply() override; + virtual QString helpPage() const override + { + return "Minecraft-Logs"; + } + virtual bool shouldDisplay() const override; + +private slots: + void on_btnPaste_clicked(); + void on_btnCopy_clicked(); + void on_btnClear_clicked(); + void on_btnBottom_clicked(); + + void on_trackLogCheckbox_clicked(bool checked); + void on_wrapCheckbox_clicked(bool checked); + + void on_findButton_clicked(); + void findActivated(); + void findNextActivated(); + void findPreviousActivated(); + + void onInstanceLaunchTaskChanged(shared_qobject_ptr<LaunchTask> proc); + +private: + void modelStateToUI(); + void UIToModelState(); + void setInstanceLaunchTaskChanged(shared_qobject_ptr<LaunchTask> proc, bool initial); + +private: + Ui::LogPage *ui; + InstancePtr m_instance; + shared_qobject_ptr<LaunchTask> m_process; + + LogFormatProxyModel * m_proxy; + shared_qobject_ptr <LogModel> m_model; +}; diff --git a/launcher/ui/pages/instance/LogPage.ui b/launcher/ui/pages/instance/LogPage.ui new file mode 100644 index 00000000..ccfc1551 --- /dev/null +++ b/launcher/ui/pages/instance/LogPage.ui @@ -0,0 +1,182 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>LogPage</class> + <widget class="QWidget" name="LogPage"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>825</width> + <height>782</height> + </rect> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QTabWidget" name="tabWidget"> + <property name="currentIndex"> + <number>0</number> + </property> + <widget class="QWidget" name="tab"> + <attribute name="title"> + <string notr="true">Tab 1</string> + </attribute> + <layout class="QGridLayout" name="gridLayout"> + <item row="1" column="0" colspan="5"> + <widget class="LogView" name="text"> + <property name="undoRedoEnabled"> + <bool>false</bool> + </property> + <property name="readOnly"> + <bool>true</bool> + </property> + <property name="plainText"> + <string notr="true"/> + </property> + <property name="textInteractionFlags"> + <set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set> + </property> + <property name="centerOnScroll"> + <bool>false</bool> + </property> + </widget> + </item> + <item row="0" column="0" colspan="5"> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <widget class="QCheckBox" name="trackLogCheckbox"> + <property name="text"> + <string>Keep updating</string> + </property> + <property name="checked"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="wrapCheckbox"> + <property name="text"> + <string>Wrap lines</string> + </property> + <property name="checked"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QPushButton" name="btnCopy"> + <property name="toolTip"> + <string>Copy the whole log into the clipboard</string> + </property> + <property name="text"> + <string>&Copy</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="btnPaste"> + <property name="toolTip"> + <string>Upload the log to paste.ee - it will stay online for a month</string> + </property> + <property name="text"> + <string>Upload</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="btnClear"> + <property name="toolTip"> + <string>Clear the log</string> + </property> + <property name="text"> + <string>Clear</string> + </property> + </widget> + </item> + </layout> + </item> + <item row="2" column="0"> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Search:</string> + </property> + </widget> + </item> + <item row="2" column="2"> + <widget class="QPushButton" name="findButton"> + <property name="text"> + <string>Find</string> + </property> + </widget> + </item> + <item row="2" column="1"> + <widget class="QLineEdit" name="searchBar"/> + </item> + <item row="2" column="4"> + <widget class="QPushButton" name="btnBottom"> + <property name="toolTip"> + <string>Scroll all the way to bottom</string> + </property> + <property name="text"> + <string>Bottom</string> + </property> + </widget> + </item> + <item row="2" column="3"> + <widget class="Line" name="line"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + </widget> + </item> + </layout> + </widget> + </widget> + </item> + </layout> + </widget> + <customwidgets> + <customwidget> + <class>LogView</class> + <extends>QPlainTextEdit</extends> + <header>ui/widgets/LogView.h</header> + </customwidget> + </customwidgets> + <tabstops> + <tabstop>tabWidget</tabstop> + <tabstop>trackLogCheckbox</tabstop> + <tabstop>wrapCheckbox</tabstop> + <tabstop>btnCopy</tabstop> + <tabstop>btnPaste</tabstop> + <tabstop>btnClear</tabstop> + <tabstop>text</tabstop> + <tabstop>searchBar</tabstop> + <tabstop>findButton</tabstop> + </tabstops> + <resources/> + <connections/> +</ui> diff --git a/launcher/ui/pages/instance/ModFolderPage.cpp b/launcher/ui/pages/instance/ModFolderPage.cpp new file mode 100644 index 00000000..e63b1434 --- /dev/null +++ b/launcher/ui/pages/instance/ModFolderPage.cpp @@ -0,0 +1,366 @@ +/* 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 "ModFolderPage.h" +#include "ui_ModFolderPage.h" + +#include <QMessageBox> +#include <QEvent> +#include <QKeyEvent> +#include <QAbstractItemModel> +#include <QMenu> +#include <QSortFilterProxyModel> + +#include "Application.h" + +#include "ui/dialogs/CustomMessageBox.h" +#include "ui/GuiUtil.h" + +#include "DesktopServices.h" + +#include "minecraft/mod/ModFolderModel.h" +#include "minecraft/mod/Mod.h" +#include "minecraft/VersionFilterData.h" +#include "minecraft/PackProfile.h" + +#include "Version.h" + +namespace { + // FIXME: wasteful + void RemoveThePrefix(QString & string) { + QRegularExpression regex(QStringLiteral("^(([Tt][Hh][eE])|([Tt][eE][Hh])) +")); + string.remove(regex); + string = string.trimmed(); + } +} + +class ModSortProxy : public QSortFilterProxyModel +{ +public: + explicit ModSortProxy(QObject *parent = 0) : QSortFilterProxyModel(parent) + { + } + +protected: + bool filterAcceptsRow(int source_row, const QModelIndex & source_parent) const override { + ModFolderModel *model = qobject_cast<ModFolderModel *>(sourceModel()); + if(!model) { + return false; + } + const auto &mod = model->at(source_row); + if(mod.name().contains(filterRegExp())) { + return true; + } + if(mod.description().contains(filterRegExp())) { + return true; + } + for(auto & author: mod.authors()) { + if (author.contains(filterRegExp())) { + return true; + } + } + return false; + } + + bool lessThan(const QModelIndex & source_left, const QModelIndex & source_right) const override + { + ModFolderModel *model = qobject_cast<ModFolderModel *>(sourceModel()); + if( + !model || + !source_left.isValid() || + !source_right.isValid() || + source_left.column() != source_right.column() + ) { + return QSortFilterProxyModel::lessThan(source_left, source_right); + } + + // we are now guaranteed to have two valid indexes in the same column... we love the provided invariants unconditionally and proceed. + + auto column = (ModFolderModel::Columns) source_left.column(); + bool invert = false; + switch(column) { + // GH-2550 - sort by enabled/disabled + case ModFolderModel::ActiveColumn: { + auto dataL = source_left.data(Qt::CheckStateRole).toBool(); + auto dataR = source_right.data(Qt::CheckStateRole).toBool(); + if(dataL != dataR) { + return dataL > dataR; + } + // fallthrough + invert = sortOrder() == Qt::DescendingOrder; + } + // GH-2722 - sort mod names in a way that discards "The" prefixes + case ModFolderModel::NameColumn: { + auto dataL = model->data(model->index(source_left.row(), ModFolderModel::NameColumn)).toString(); + RemoveThePrefix(dataL); + auto dataR = model->data(model->index(source_right.row(), ModFolderModel::NameColumn)).toString(); + RemoveThePrefix(dataR); + + auto less = dataL.compare(dataR, sortCaseSensitivity()); + if(less != 0) { + return invert ? (less > 0) : (less < 0); + } + // fallthrough + invert = sortOrder() == Qt::DescendingOrder; + } + // GH-2762 - sort versions by parsing them as versions + case ModFolderModel::VersionColumn: { + auto dataL = Version(model->data(model->index(source_left.row(), ModFolderModel::VersionColumn)).toString()); + auto dataR = Version(model->data(model->index(source_right.row(), ModFolderModel::VersionColumn)).toString()); + return invert ? (dataL > dataR) : (dataL < dataR); + } + default: { + return QSortFilterProxyModel::lessThan(source_left, source_right); + } + } + } +}; + +ModFolderPage::ModFolderPage( + BaseInstance *inst, + std::shared_ptr<ModFolderModel> mods, + QString id, + QString iconName, + QString displayName, + QString helpPage, + QWidget *parent +) : + QMainWindow(parent), + ui(new Ui::ModFolderPage) +{ + ui->setupUi(this); + ui->actionsToolbar->insertSpacer(ui->actionView_configs); + + m_inst = inst; + on_RunningState_changed(m_inst && m_inst->isRunning()); + m_mods = mods; + m_id = id; + m_displayName = displayName; + m_iconName = iconName; + m_helpName = helpPage; + m_fileSelectionFilter = "%1 (*.zip *.jar)"; + m_filterModel = new ModSortProxy(this); + m_filterModel->setDynamicSortFilter(true); + m_filterModel->setFilterCaseSensitivity(Qt::CaseInsensitive); + m_filterModel->setSortCaseSensitivity(Qt::CaseInsensitive); + m_filterModel->setSourceModel(m_mods.get()); + m_filterModel->setFilterKeyColumn(-1); + ui->modTreeView->setModel(m_filterModel); + ui->modTreeView->installEventFilter(this); + ui->modTreeView->sortByColumn(1, Qt::AscendingOrder); + ui->modTreeView->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->modTreeView, &ModListView::customContextMenuRequested, this, &ModFolderPage::ShowContextMenu); + connect(ui->modTreeView, &ModListView::activated, this, &ModFolderPage::modItemActivated); + + auto smodel = ui->modTreeView->selectionModel(); + connect(smodel, &QItemSelectionModel::currentChanged, this, &ModFolderPage::modCurrent); + connect(ui->filterEdit, &QLineEdit::textChanged, this, &ModFolderPage::on_filterTextChanged); + connect(m_inst, &BaseInstance::runningStatusChanged, this, &ModFolderPage::on_RunningState_changed); +} + +void ModFolderPage::modItemActivated(const QModelIndex&) +{ + if(!m_controlsEnabled) { + return; + } + auto selection = m_filterModel->mapSelectionToSource(ui->modTreeView->selectionModel()->selection()); + m_mods->setModStatus(selection.indexes(), ModFolderModel::Toggle); +} + +QMenu * ModFolderPage::createPopupMenu() +{ + QMenu* filteredMenu = QMainWindow::createPopupMenu(); + filteredMenu->removeAction(ui->actionsToolbar->toggleViewAction() ); + return filteredMenu; +} + +void ModFolderPage::ShowContextMenu(const QPoint& pos) +{ + auto menu = ui->actionsToolbar->createContextMenu(this, tr("Context menu")); + menu->exec(ui->modTreeView->mapToGlobal(pos)); + delete menu; +} + +void ModFolderPage::openedImpl() +{ + m_mods->startWatching(); +} + +void ModFolderPage::closedImpl() +{ + m_mods->stopWatching(); +} + +void ModFolderPage::on_filterTextChanged(const QString& newContents) +{ + m_viewFilter = newContents; + m_filterModel->setFilterFixedString(m_viewFilter); +} + + +CoreModFolderPage::CoreModFolderPage(BaseInstance *inst, std::shared_ptr<ModFolderModel> mods, + QString id, QString iconName, QString displayName, + QString helpPage, QWidget *parent) + : ModFolderPage(inst, mods, id, iconName, displayName, helpPage, parent) +{ +} + +ModFolderPage::~ModFolderPage() +{ + m_mods->stopWatching(); + delete ui; +} + +void ModFolderPage::on_RunningState_changed(bool running) +{ + if(m_controlsEnabled == !running) { + return; + } + m_controlsEnabled = !running; + ui->actionAdd->setEnabled(m_controlsEnabled); + ui->actionDisable->setEnabled(m_controlsEnabled); + ui->actionEnable->setEnabled(m_controlsEnabled); + ui->actionRemove->setEnabled(m_controlsEnabled); +} + +bool ModFolderPage::shouldDisplay() const +{ + return true; +} + +bool CoreModFolderPage::shouldDisplay() const +{ + if (ModFolderPage::shouldDisplay()) + { + auto inst = dynamic_cast<MinecraftInstance *>(m_inst); + if (!inst) + return true; + auto version = inst->getPackProfile(); + if (!version) + return true; + if(!version->getComponent("net.minecraftforge")) + { + return false; + } + if(!version->getComponent("net.minecraft")) + { + return false; + } + if(version->getComponent("net.minecraft")->getReleaseDateTime() < g_VersionFilterData.legacyCutoffDate) + { + return true; + } + } + return false; +} + +bool ModFolderPage::modListFilter(QKeyEvent *keyEvent) +{ + switch (keyEvent->key()) + { + case Qt::Key_Delete: + on_actionRemove_triggered(); + return true; + case Qt::Key_Plus: + on_actionAdd_triggered(); + return true; + default: + break; + } + return QWidget::eventFilter(ui->modTreeView, keyEvent); +} + +bool ModFolderPage::eventFilter(QObject *obj, QEvent *ev) +{ + if (ev->type() != QEvent::KeyPress) + { + return QWidget::eventFilter(obj, ev); + } + QKeyEvent *keyEvent = static_cast<QKeyEvent *>(ev); + if (obj == ui->modTreeView) + return modListFilter(keyEvent); + return QWidget::eventFilter(obj, ev); +} + +void ModFolderPage::on_actionAdd_triggered() +{ + if(!m_controlsEnabled) { + return; + } + auto list = GuiUtil::BrowseForFiles( + m_helpName, + tr("Select %1", + "Select whatever type of files the page contains. Example: 'Loader Mods'") + .arg(m_displayName), + m_fileSelectionFilter.arg(m_displayName), APPLICATION->settings()->get("CentralModsDir").toString(), + this->parentWidget()); + if (!list.empty()) + { + for (auto filename : list) + { + m_mods->installMod(filename); + } + } +} + +void ModFolderPage::on_actionEnable_triggered() +{ + if(!m_controlsEnabled) { + return; + } + auto selection = m_filterModel->mapSelectionToSource(ui->modTreeView->selectionModel()->selection()); + m_mods->setModStatus(selection.indexes(), ModFolderModel::Enable); +} + +void ModFolderPage::on_actionDisable_triggered() +{ + if(!m_controlsEnabled) { + return; + } + auto selection = m_filterModel->mapSelectionToSource(ui->modTreeView->selectionModel()->selection()); + m_mods->setModStatus(selection.indexes(), ModFolderModel::Disable); +} + +void ModFolderPage::on_actionRemove_triggered() +{ + if(!m_controlsEnabled) { + return; + } + auto selection = m_filterModel->mapSelectionToSource(ui->modTreeView->selectionModel()->selection()); + m_mods->deleteMods(selection.indexes()); +} + +void ModFolderPage::on_actionView_configs_triggered() +{ + DesktopServices::openDirectory(m_inst->instanceConfigFolder(), true); +} + +void ModFolderPage::on_actionView_Folder_triggered() +{ + DesktopServices::openDirectory(m_mods->dir().absolutePath(), true); +} + +void ModFolderPage::modCurrent(const QModelIndex ¤t, const QModelIndex &previous) +{ + if (!current.isValid()) + { + ui->frame->clear(); + return; + } + auto sourceCurrent = m_filterModel->mapToSource(current); + int row = sourceCurrent.row(); + Mod &m = m_mods->operator[](row); + ui->frame->updateWithMod(m); +} diff --git a/launcher/ui/pages/instance/ModFolderPage.h b/launcher/ui/pages/instance/ModFolderPage.h new file mode 100644 index 00000000..8ef7559b --- /dev/null +++ b/launcher/ui/pages/instance/ModFolderPage.h @@ -0,0 +1,120 @@ +/* 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 <QMainWindow> + +#include "minecraft/MinecraftInstance.h" +#include "ui/pages/BasePage.h" + +#include <Application.h> + +class ModFolderModel; +namespace Ui +{ +class ModFolderPage; +} + +class ModFolderPage : public QMainWindow, public BasePage +{ + Q_OBJECT + +public: + explicit ModFolderPage( + BaseInstance *inst, + std::shared_ptr<ModFolderModel> mods, + QString id, + QString iconName, + QString displayName, + QString helpPage = "", + QWidget *parent = 0 + ); + virtual ~ModFolderPage(); + + void setFilter(const QString & filter) + { + m_fileSelectionFilter = filter; + } + + virtual QString displayName() const override + { + return m_displayName; + } + virtual QIcon icon() const override + { + return APPLICATION->getThemedIcon(m_iconName); + } + virtual QString id() const override + { + return m_id; + } + virtual QString helpPage() const override + { + return m_helpName; + } + virtual bool shouldDisplay() const override; + + virtual void openedImpl() override; + virtual void closedImpl() override; +protected: + bool eventFilter(QObject *obj, QEvent *ev) override; + bool modListFilter(QKeyEvent *ev); + QMenu * createPopupMenu() override; + +protected: + BaseInstance *m_inst = nullptr; + +protected: + Ui::ModFolderPage *ui = nullptr; + std::shared_ptr<ModFolderModel> m_mods; + QSortFilterProxyModel *m_filterModel = nullptr; + QString m_iconName; + QString m_id; + QString m_displayName; + QString m_helpName; + QString m_fileSelectionFilter; + QString m_viewFilter; + bool m_controlsEnabled = true; + +public +slots: + void modCurrent(const QModelIndex ¤t, const QModelIndex &previous); + +private +slots: + void modItemActivated(const QModelIndex &index); + void on_filterTextChanged(const QString & newContents); + void on_RunningState_changed(bool running); + void on_actionAdd_triggered(); + void on_actionRemove_triggered(); + void on_actionEnable_triggered(); + void on_actionDisable_triggered(); + void on_actionView_Folder_triggered(); + void on_actionView_configs_triggered(); + void ShowContextMenu(const QPoint &pos); +}; + +class CoreModFolderPage : public ModFolderPage +{ +public: + explicit CoreModFolderPage(BaseInstance *inst, std::shared_ptr<ModFolderModel> mods, QString id, + QString iconName, QString displayName, QString helpPage = "", + QWidget *parent = 0); + virtual ~CoreModFolderPage() + { + } + virtual bool shouldDisplay() const; +}; diff --git a/launcher/ui/pages/instance/ModFolderPage.ui b/launcher/ui/pages/instance/ModFolderPage.ui new file mode 100644 index 00000000..0fb51e84 --- /dev/null +++ b/launcher/ui/pages/instance/ModFolderPage.ui @@ -0,0 +1,164 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>ModFolderPage</class> + <widget class="QMainWindow" name="ModFolderPage"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>1042</width> + <height>501</height> + </rect> + </property> + <widget class="QWidget" name="centralwidget"> + <layout class="QGridLayout" name="gridLayout"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item row="4" column="1" colspan="3"> + <layout class="QGridLayout" name="gridLayout_2"> + <item row="0" column="1"> + <widget class="QLineEdit" name="filterEdit"> + <property name="clearButtonEnabled"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="0" column="0"> + <widget class="QLabel" name="filterLabel"> + <property name="text"> + <string>Filter:</string> + </property> + </widget> + </item> + </layout> + </item> + <item row="2" column="1" colspan="3"> + <widget class="MCModInfoFrame" name="frame"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Minimum"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + </widget> + </item> + <item row="1" column="1" colspan="3"> + <widget class="ModListView" name="modTreeView"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Expanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="acceptDrops"> + <bool>true</bool> + </property> + <property name="dragDropMode"> + <enum>QAbstractItemView::DropOnly</enum> + </property> + </widget> + </item> + </layout> + </widget> + <widget class="WideBar" name="actionsToolbar"> + <property name="windowTitle"> + <string>Actions</string> + </property> + <property name="toolButtonStyle"> + <enum>Qt::ToolButtonTextOnly</enum> + </property> + <attribute name="toolBarArea"> + <enum>RightToolBarArea</enum> + </attribute> + <attribute name="toolBarBreak"> + <bool>false</bool> + </attribute> + <addaction name="actionAdd"/> + <addaction name="separator"/> + <addaction name="actionRemove"/> + <addaction name="actionEnable"/> + <addaction name="actionDisable"/> + <addaction name="actionView_configs"/> + <addaction name="actionView_Folder"/> + </widget> + <action name="actionAdd"> + <property name="text"> + <string>&Add</string> + </property> + <property name="toolTip"> + <string>Add mods</string> + </property> + </action> + <action name="actionRemove"> + <property name="text"> + <string>&Remove</string> + </property> + <property name="toolTip"> + <string>Remove selected mods</string> + </property> + </action> + <action name="actionEnable"> + <property name="text"> + <string>&Enable</string> + </property> + <property name="toolTip"> + <string>Enable selected mods</string> + </property> + </action> + <action name="actionDisable"> + <property name="text"> + <string>&Disable</string> + </property> + <property name="toolTip"> + <string>Disable selected mods</string> + </property> + </action> + <action name="actionView_configs"> + <property name="text"> + <string>View &Configs</string> + </property> + <property name="toolTip"> + <string>Open the 'config' folder in the system file manager.</string> + </property> + </action> + <action name="actionView_Folder"> + <property name="text"> + <string>View &Folder</string> + </property> + </action> + </widget> + <customwidgets> + <customwidget> + <class>ModListView</class> + <extends>QTreeView</extends> + <header>ui/widgets/ModListView.h</header> + </customwidget> + <customwidget> + <class>MCModInfoFrame</class> + <extends>QFrame</extends> + <header>ui/widgets/MCModInfoFrame.h</header> + <container>1</container> + </customwidget> + <customwidget> + <class>WideBar</class> + <extends>QToolBar</extends> + <header>ui/widgets/WideBar.h</header> + </customwidget> + </customwidgets> + <tabstops> + <tabstop>modTreeView</tabstop> + <tabstop>filterEdit</tabstop> + </tabstops> + <resources/> + <connections/> +</ui> diff --git a/launcher/ui/pages/instance/NotesPage.cpp b/launcher/ui/pages/instance/NotesPage.cpp new file mode 100644 index 00000000..fa966c91 --- /dev/null +++ b/launcher/ui/pages/instance/NotesPage.cpp @@ -0,0 +1,21 @@ +#include "NotesPage.h" +#include "ui_NotesPage.h" +#include <QTabBar> + +NotesPage::NotesPage(BaseInstance *inst, QWidget *parent) + : QWidget(parent), ui(new Ui::NotesPage), m_inst(inst) +{ + ui->setupUi(this); + ui->noteEditor->setText(m_inst->notes()); +} + +NotesPage::~NotesPage() +{ + delete ui; +} + +bool NotesPage::apply() +{ + m_inst->setNotes(ui->noteEditor->toPlainText()); + return true; +} diff --git a/launcher/ui/pages/instance/NotesPage.h b/launcher/ui/pages/instance/NotesPage.h new file mode 100644 index 00000000..539401ee --- /dev/null +++ b/launcher/ui/pages/instance/NotesPage.h @@ -0,0 +1,60 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <QWidget> + +#include "BaseInstance.h" +#include "ui/pages/BasePage.h" +#include <Application.h> + +namespace Ui +{ +class NotesPage; +} + +class NotesPage : public QWidget, public BasePage +{ + Q_OBJECT + +public: + explicit NotesPage(BaseInstance *inst, QWidget *parent = 0); + virtual ~NotesPage(); + virtual QString displayName() const override + { + return tr("Notes"); + } + virtual QIcon icon() const override + { + auto icon = APPLICATION->getThemedIcon("notes"); + if(icon.isNull()) + icon = APPLICATION->getThemedIcon("news"); + return icon; + } + virtual QString id() const override + { + return "notes"; + } + virtual bool apply() override; + virtual QString helpPage() const override + { + return "Notes"; + } + +private: + Ui::NotesPage *ui; + BaseInstance *m_inst; +}; diff --git a/launcher/ui/pages/instance/NotesPage.ui b/launcher/ui/pages/instance/NotesPage.ui new file mode 100644 index 00000000..67cb261c --- /dev/null +++ b/launcher/ui/pages/instance/NotesPage.ui @@ -0,0 +1,49 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>NotesPage</class> + <widget class="QWidget" name="NotesPage"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>731</width> + <height>538</height> + </rect> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QTextEdit" name="noteEditor"> + <property name="verticalScrollBarPolicy"> + <enum>Qt::ScrollBarAlwaysOn</enum> + </property> + <property name="tabChangesFocus"> + <bool>true</bool> + </property> + <property name="acceptRichText"> + <bool>false</bool> + </property> + <property name="textInteractionFlags"> + <set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextEditable|Qt::TextEditorInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set> + </property> + </widget> + </item> + </layout> + </widget> + <tabstops> + <tabstop>noteEditor</tabstop> + </tabstops> + <resources/> + <connections/> +</ui> diff --git a/launcher/ui/pages/instance/OtherLogsPage.cpp b/launcher/ui/pages/instance/OtherLogsPage.cpp new file mode 100644 index 00000000..0131c5c1 --- /dev/null +++ b/launcher/ui/pages/instance/OtherLogsPage.cpp @@ -0,0 +1,314 @@ +/* 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 "OtherLogsPage.h" +#include "ui_OtherLogsPage.h" + +#include <QMessageBox> + +#include "ui/GuiUtil.h" + +#include "RecursiveFileSystemWatcher.h" +#include <GZip.h> +#include <FileSystem.h> +#include <QShortcut> + +OtherLogsPage::OtherLogsPage(QString path, IPathMatcher::Ptr fileFilter, QWidget *parent) + : QWidget(parent), ui(new Ui::OtherLogsPage), m_path(path), m_fileFilter(fileFilter), + m_watcher(new RecursiveFileSystemWatcher(this)) +{ + ui->setupUi(this); + ui->tabWidget->tabBar()->hide(); + + m_watcher->setMatcher(fileFilter); + m_watcher->setRootDir(QDir::current().absoluteFilePath(m_path)); + + connect(m_watcher, &RecursiveFileSystemWatcher::filesChanged, this, &OtherLogsPage::populateSelectLogBox); + populateSelectLogBox(); + + auto findShortcut = new QShortcut(QKeySequence(QKeySequence::Find), this); + connect(findShortcut, &QShortcut::activated, this, &OtherLogsPage::findActivated); + + auto findNextShortcut = new QShortcut(QKeySequence(QKeySequence::FindNext), this); + connect(findNextShortcut, &QShortcut::activated, this, &OtherLogsPage::findNextActivated); + + auto findPreviousShortcut = new QShortcut(QKeySequence(QKeySequence::FindPrevious), this); + connect(findPreviousShortcut, &QShortcut::activated, this, &OtherLogsPage::findPreviousActivated); + + connect(ui->searchBar, &QLineEdit::returnPressed, this, &OtherLogsPage::on_findButton_clicked); +} + +OtherLogsPage::~OtherLogsPage() +{ + delete ui; +} + +void OtherLogsPage::openedImpl() +{ + m_watcher->enable(); +} +void OtherLogsPage::closedImpl() +{ + m_watcher->disable(); +} + +void OtherLogsPage::populateSelectLogBox() +{ + ui->selectLogBox->clear(); + ui->selectLogBox->addItems(m_watcher->files()); + if (m_currentFile.isEmpty()) + { + setControlsEnabled(false); + ui->selectLogBox->setCurrentIndex(-1); + } + else + { + const int index = ui->selectLogBox->findText(m_currentFile); + if (index != -1) + { + ui->selectLogBox->setCurrentIndex(index); + setControlsEnabled(true); + } + else + { + setControlsEnabled(false); + } + } +} + +void OtherLogsPage::on_selectLogBox_currentIndexChanged(const int index) +{ + QString file; + if (index != -1) + { + file = ui->selectLogBox->itemText(index); + } + + if (file.isEmpty() || !QFile::exists(FS::PathCombine(m_path, file))) + { + m_currentFile = QString(); + ui->text->clear(); + setControlsEnabled(false); + } + else + { + m_currentFile = file; + on_btnReload_clicked(); + setControlsEnabled(true); + } +} + +void OtherLogsPage::on_btnReload_clicked() +{ + if(m_currentFile.isEmpty()) + { + setControlsEnabled(false); + return; + } + QFile file(FS::PathCombine(m_path, m_currentFile)); + if (!file.open(QFile::ReadOnly)) + { + setControlsEnabled(false); + ui->btnReload->setEnabled(true); // allow reload + m_currentFile = QString(); + QMessageBox::critical(this, tr("Error"), tr("Unable to open %1 for reading: %2") + .arg(m_currentFile, file.errorString())); + } + else + { + auto setPlainText = [&](const QString & text) + { + QString fontFamily = APPLICATION->settings()->get("ConsoleFont").toString(); + bool conversionOk = false; + int fontSize = APPLICATION->settings()->get("ConsoleFontSize").toInt(&conversionOk); + if(!conversionOk) + { + fontSize = 11; + } + QTextDocument *doc = ui->text->document(); + doc->setDefaultFont(QFont(fontFamily, fontSize)); + ui->text->setPlainText(text); + }; + auto showTooBig = [&]() + { + setPlainText( + tr("The file (%1) is too big. You may want to open it in a viewer optimized " + "for large files.").arg(file.fileName())); + }; + if(file.size() > (1024ll * 1024ll * 12ll)) + { + showTooBig(); + return; + } + QString content; + if(file.fileName().endsWith(".gz")) + { + QByteArray temp; + if(!GZip::unzip(file.readAll(), temp)) + { + setPlainText( + tr("The file (%1) is not readable.").arg(file.fileName())); + return; + } + content = QString::fromUtf8(temp); + } + else + { + content = QString::fromUtf8(file.readAll()); + } + if (content.size() >= 50000000ll) + { + showTooBig(); + return; + } + setPlainText(content); + } +} + +void OtherLogsPage::on_btnPaste_clicked() +{ + GuiUtil::uploadPaste(ui->text->toPlainText(), this); +} + +void OtherLogsPage::on_btnCopy_clicked() +{ + GuiUtil::setClipboardText(ui->text->toPlainText()); +} + +void OtherLogsPage::on_btnDelete_clicked() +{ + if(m_currentFile.isEmpty()) + { + setControlsEnabled(false); + return; + } + if (QMessageBox::question(this, tr("Delete"), + tr("Do you really want to delete %1?").arg(m_currentFile), + QMessageBox::Yes, QMessageBox::No) == QMessageBox::No) + { + return; + } + QFile file(FS::PathCombine(m_path, m_currentFile)); + if (!file.remove()) + { + QMessageBox::critical(this, tr("Error"), tr("Unable to delete %1: %2") + .arg(m_currentFile, file.errorString())); + } +} + + + +void OtherLogsPage::on_btnClean_clicked() +{ + auto toDelete = m_watcher->files(); + if(toDelete.isEmpty()) + { + return; + } + QMessageBox *messageBox = new QMessageBox(this); + messageBox->setWindowTitle(tr("Clean up")); + if(toDelete.size() > 5) + { + messageBox->setText(tr("Do you really want to delete all log files?")); + messageBox->setDetailedText(toDelete.join('\n')); + } + else + { + messageBox->setText(tr("Do you really want to delete these files?\n%1").arg(toDelete.join('\n'))); + } + messageBox->setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel); + messageBox->setDefaultButton(QMessageBox::Ok); + messageBox->setTextInteractionFlags(Qt::TextSelectableByMouse); + messageBox->setIcon(QMessageBox::Question); + messageBox->setTextInteractionFlags(Qt::TextBrowserInteraction); + + if (messageBox->exec() != QMessageBox::Ok) + { + return; + } + QStringList failed; + for(auto item: toDelete) + { + QFile file(FS::PathCombine(m_path, item)); + if (!file.remove()) + { + failed.push_back(item); + } + } + if(!failed.empty()) + { + QMessageBox *messageBox = new QMessageBox(this); + messageBox->setWindowTitle(tr("Error")); + if(failed.size() > 5) + { + messageBox->setText(tr("Couldn't delete some files!")); + messageBox->setDetailedText(failed.join('\n')); + } + else + { + messageBox->setText(tr("Couldn't delete some files:\n%1").arg(failed.join('\n'))); + } + messageBox->setStandardButtons(QMessageBox::Ok); + messageBox->setDefaultButton(QMessageBox::Ok); + messageBox->setTextInteractionFlags(Qt::TextSelectableByMouse); + messageBox->setIcon(QMessageBox::Critical); + messageBox->setTextInteractionFlags(Qt::TextBrowserInteraction); + messageBox->exec(); + } +} + + +void OtherLogsPage::setControlsEnabled(const bool enabled) +{ + ui->btnReload->setEnabled(enabled); + ui->btnDelete->setEnabled(enabled); + ui->btnCopy->setEnabled(enabled); + ui->btnPaste->setEnabled(enabled); + ui->text->setEnabled(enabled); + ui->btnClean->setEnabled(enabled); +} + +// FIXME: HACK, use LogView instead? +static void findNext(QPlainTextEdit * _this, const QString& what, bool reverse) +{ + _this->find(what, reverse ? QTextDocument::FindFlag::FindBackward : QTextDocument::FindFlag(0)); +} + +void OtherLogsPage::on_findButton_clicked() +{ + auto modifiers = QApplication::keyboardModifiers(); + bool reverse = modifiers & Qt::ShiftModifier; + findNext(ui->text, ui->searchBar->text(), reverse); +} + +void OtherLogsPage::findNextActivated() +{ + findNext(ui->text, ui->searchBar->text(), false); +} + +void OtherLogsPage::findPreviousActivated() +{ + findNext(ui->text, ui->searchBar->text(), true); +} + +void OtherLogsPage::findActivated() +{ + // focus the search bar if it doesn't have focus + if (!ui->searchBar->hasFocus()) + { + ui->searchBar->setFocus(); + ui->searchBar->selectAll(); + } +} diff --git a/launcher/ui/pages/instance/OtherLogsPage.h b/launcher/ui/pages/instance/OtherLogsPage.h new file mode 100644 index 00000000..b2b2a91b --- /dev/null +++ b/launcher/ui/pages/instance/OtherLogsPage.h @@ -0,0 +1,81 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <QWidget> + +#include "ui/pages/BasePage.h" +#include <Application.h> +#include <pathmatcher/IPathMatcher.h> + +namespace Ui +{ +class OtherLogsPage; +} + +class RecursiveFileSystemWatcher; + +class OtherLogsPage : public QWidget, public BasePage +{ + Q_OBJECT + +public: + explicit OtherLogsPage(QString path, IPathMatcher::Ptr fileFilter, QWidget *parent = 0); + ~OtherLogsPage(); + + QString id() const override + { + return "logs"; + } + QString displayName() const override + { + return tr("Other logs"); + } + QIcon icon() const override + { + return APPLICATION->getThemedIcon("log"); + } + QString helpPage() const override + { + return "Minecraft-Logs"; + } + void openedImpl() override; + void closedImpl() override; + +private slots: + void populateSelectLogBox(); + void on_selectLogBox_currentIndexChanged(const int index); + void on_btnReload_clicked(); + void on_btnPaste_clicked(); + void on_btnCopy_clicked(); + void on_btnDelete_clicked(); + void on_btnClean_clicked(); + + void on_findButton_clicked(); + void findActivated(); + void findNextActivated(); + void findPreviousActivated(); + +private: + void setControlsEnabled(const bool enabled); + +private: + Ui::OtherLogsPage *ui; + QString m_path; + QString m_currentFile; + IPathMatcher::Ptr m_fileFilter; + RecursiveFileSystemWatcher *m_watcher; +}; diff --git a/launcher/ui/pages/instance/OtherLogsPage.ui b/launcher/ui/pages/instance/OtherLogsPage.ui new file mode 100644 index 00000000..56ff3b62 --- /dev/null +++ b/launcher/ui/pages/instance/OtherLogsPage.ui @@ -0,0 +1,150 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>OtherLogsPage</class> + <widget class="QWidget" name="OtherLogsPage"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>657</width> + <height>538</height> + </rect> + </property> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QTabWidget" name="tabWidget"> + <property name="currentIndex"> + <number>0</number> + </property> + <widget class="QWidget" name="tab"> + <attribute name="title"> + <string notr="true">Tab 1</string> + </attribute> + <layout class="QGridLayout" name="gridLayout_2"> + <item row="2" column="1"> + <widget class="QLineEdit" name="searchBar"/> + </item> + <item row="2" column="2"> + <widget class="QPushButton" name="findButton"> + <property name="text"> + <string>Find</string> + </property> + </widget> + </item> + <item row="1" column="0" colspan="4"> + <widget class="QPlainTextEdit" name="text"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="verticalScrollBarPolicy"> + <enum>Qt::ScrollBarAlwaysOn</enum> + </property> + <property name="readOnly"> + <bool>true</bool> + </property> + <property name="textInteractionFlags"> + <set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set> + </property> + </widget> + </item> + <item row="0" column="0" colspan="4"> + <layout class="QGridLayout" name="gridLayout"> + <item row="3" column="1"> + <widget class="QPushButton" name="btnCopy"> + <property name="toolTip"> + <string>Copy the whole log into the clipboard</string> + </property> + <property name="text"> + <string>&Copy</string> + </property> + </widget> + </item> + <item row="3" column="3"> + <widget class="QPushButton" name="btnDelete"> + <property name="toolTip"> + <string>Clear the log</string> + </property> + <property name="text"> + <string>Delete</string> + </property> + </widget> + </item> + <item row="3" column="2"> + <widget class="QPushButton" name="btnPaste"> + <property name="toolTip"> + <string>Upload the log to paste.ee - it will stay online for a month</string> + </property> + <property name="text"> + <string>Upload</string> + </property> + </widget> + </item> + <item row="3" column="4"> + <widget class="QPushButton" name="btnClean"> + <property name="toolTip"> + <string>Clear the log</string> + </property> + <property name="text"> + <string>Clean</string> + </property> + </widget> + </item> + <item row="3" column="0"> + <widget class="QPushButton" name="btnReload"> + <property name="text"> + <string>Reload</string> + </property> + </widget> + </item> + <item row="0" column="0" colspan="5"> + <widget class="QComboBox" name="selectLogBox"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + </widget> + </item> + </layout> + </item> + <item row="2" column="0"> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Search:</string> + </property> + </widget> + </item> + </layout> + </widget> + </widget> + </item> + </layout> + </widget> + <tabstops> + <tabstop>tabWidget</tabstop> + <tabstop>selectLogBox</tabstop> + <tabstop>btnReload</tabstop> + <tabstop>btnCopy</tabstop> + <tabstop>btnPaste</tabstop> + <tabstop>btnDelete</tabstop> + <tabstop>btnClean</tabstop> + <tabstop>text</tabstop> + <tabstop>searchBar</tabstop> + <tabstop>findButton</tabstop> + </tabstops> + <resources/> + <connections/> +</ui> diff --git a/launcher/ui/pages/instance/ResourcePackPage.h b/launcher/ui/pages/instance/ResourcePackPage.h new file mode 100644 index 00000000..1486bf52 --- /dev/null +++ b/launcher/ui/pages/instance/ResourcePackPage.h @@ -0,0 +1,23 @@ +#pragma once + +#include "ModFolderPage.h" +#include "ui_ModFolderPage.h" + +class ResourcePackPage : public ModFolderPage +{ + Q_OBJECT +public: + explicit ResourcePackPage(MinecraftInstance *instance, QWidget *parent = 0) + : ModFolderPage(instance, instance->resourcePackList(), "resourcepacks", + "resourcepacks", tr("Resource packs"), "Resource-packs", parent) + { + ui->actionView_configs->setVisible(false); + } + virtual ~ResourcePackPage() {} + + virtual bool shouldDisplay() const override + { + return !m_inst->traits().contains("no-texturepacks") && + !m_inst->traits().contains("texturepacks"); + } +}; diff --git a/launcher/ui/pages/instance/ScreenshotsPage.cpp b/launcher/ui/pages/instance/ScreenshotsPage.cpp new file mode 100644 index 00000000..06c4379f --- /dev/null +++ b/launcher/ui/pages/instance/ScreenshotsPage.cpp @@ -0,0 +1,423 @@ +#include "ScreenshotsPage.h" +#include "ui_ScreenshotsPage.h" + +#include <QModelIndex> +#include <QMutableListIterator> +#include <QMap> +#include <QSet> +#include <QFileIconProvider> +#include <QFileSystemModel> +#include <QStyledItemDelegate> +#include <QLineEdit> +#include <QEvent> +#include <QPainter> +#include <QClipboard> +#include <QKeyEvent> +#include <QMenu> + +#include <Application.h> + +#include "ui/dialogs/ProgressDialog.h" +#include "ui/dialogs/CustomMessageBox.h" + +#include "net/NetJob.h" +#include "screenshots/ImgurUpload.h" +#include "screenshots/ImgurAlbumCreation.h" +#include "tasks/SequentialTask.h" + +#include "RWStorage.h" +#include <FileSystem.h> +#include <DesktopServices.h> + +typedef RWStorage<QString, QIcon> SharedIconCache; +typedef std::shared_ptr<SharedIconCache> SharedIconCachePtr; + +class ThumbnailingResult : public QObject +{ + Q_OBJECT +public slots: + inline void emitResultsReady(const QString &path) { emit resultsReady(path); } + inline void emitResultsFailed(const QString &path) { emit resultsFailed(path); } +signals: + void resultsReady(const QString &path); + void resultsFailed(const QString &path); +}; + +class ThumbnailRunnable : public QRunnable +{ +public: + ThumbnailRunnable(QString path, SharedIconCachePtr cache) + { + m_path = path; + m_cache = cache; + } + void run() + { + QFileInfo info(m_path); + if (info.isDir()) + return; + if ((info.suffix().compare("png", Qt::CaseInsensitive) != 0)) + return; + int tries = 5; + while (tries) + { + if (!m_cache->stale(m_path)) + return; + QImage image(m_path); + if (image.isNull()) + { + QThread::msleep(500); + tries--; + continue; + } + QImage small; + if (image.width() > image.height()) + small = image.scaledToWidth(512).scaledToWidth(256, Qt::SmoothTransformation); + else + small = image.scaledToHeight(512).scaledToHeight(256, Qt::SmoothTransformation); + QPoint offset((256 - small.width()) / 2, (256 - small.height()) / 2); + QImage square(QSize(256, 256), QImage::Format_ARGB32); + square.fill(Qt::transparent); + + QPainter painter(&square); + painter.drawImage(offset, small); + painter.end(); + + QIcon icon(QPixmap::fromImage(square)); + m_cache->add(m_path, icon); + m_resultEmitter.emitResultsReady(m_path); + return; + } + m_resultEmitter.emitResultsFailed(m_path); + } + QString m_path; + SharedIconCachePtr m_cache; + ThumbnailingResult m_resultEmitter; +}; + +// this is about as elegant and well written as a bag of bricks with scribbles done by insane +// asylum patients. +class FilterModel : public QIdentityProxyModel +{ + Q_OBJECT +public: + explicit FilterModel(QObject *parent = 0) : QIdentityProxyModel(parent) + { + m_thumbnailingPool.setMaxThreadCount(4); + m_thumbnailCache = std::make_shared<SharedIconCache>(); + m_thumbnailCache->add("placeholder", APPLICATION->getThemedIcon("screenshot-placeholder")); + connect(&watcher, SIGNAL(fileChanged(QString)), SLOT(fileChanged(QString))); + // FIXME: the watched file set is not updated when files are removed + } + virtual ~FilterModel() { m_thumbnailingPool.waitForDone(500); } + virtual QVariant data(const QModelIndex &proxyIndex, int role = Qt::DisplayRole) const + { + auto model = sourceModel(); + if (!model) + return QVariant(); + if (role == Qt::DisplayRole || role == Qt::EditRole) + { + QVariant result = sourceModel()->data(mapToSource(proxyIndex), role); + return result.toString().remove(QRegExp("\\.png$")); + } + if (role == Qt::DecorationRole) + { + QVariant result = + sourceModel()->data(mapToSource(proxyIndex), QFileSystemModel::FilePathRole); + QString filePath = result.toString(); + QIcon temp; + if (!watched.contains(filePath)) + { + ((QFileSystemWatcher &)watcher).addPath(filePath); + ((QSet<QString> &)watched).insert(filePath); + } + if (m_thumbnailCache->get(filePath, temp)) + { + return temp; + } + if (!m_failed.contains(filePath)) + { + ((FilterModel *)this)->thumbnailImage(filePath); + } + return (m_thumbnailCache->get("placeholder")); + } + return sourceModel()->data(mapToSource(proxyIndex), role); + } + virtual bool setData(const QModelIndex &index, const QVariant &value, + int role = Qt::EditRole) + { + auto model = sourceModel(); + if (!model) + return false; + if (role != Qt::EditRole) + return false; + // FIXME: this is a workaround for a bug in QFileSystemModel, where it doesn't + // sort after renames + { + ((QFileSystemModel *)model)->setNameFilterDisables(true); + ((QFileSystemModel *)model)->setNameFilterDisables(false); + } + return model->setData(mapToSource(index), value.toString() + ".png", role); + } + +private: + void thumbnailImage(QString path) + { + auto runnable = new ThumbnailRunnable(path, m_thumbnailCache); + connect(&(runnable->m_resultEmitter), SIGNAL(resultsReady(QString)), + SLOT(thumbnailReady(QString))); + connect(&(runnable->m_resultEmitter), SIGNAL(resultsFailed(QString)), + SLOT(thumbnailFailed(QString))); + ((QThreadPool &)m_thumbnailingPool).start(runnable); + } +private slots: + void thumbnailReady(QString path) { emit layoutChanged(); } + void thumbnailFailed(QString path) { m_failed.insert(path); } + void fileChanged(QString filepath) + { + m_thumbnailCache->setStale(filepath); + thumbnailImage(filepath); + // reinsert the path... + watcher.removePath(filepath); + watcher.addPath(filepath); + } + +private: + SharedIconCachePtr m_thumbnailCache; + QThreadPool m_thumbnailingPool; + QSet<QString> m_failed; + QSet<QString> watched; + QFileSystemWatcher watcher; +}; + +class CenteredEditingDelegate : public QStyledItemDelegate +{ +public: + explicit CenteredEditingDelegate(QObject *parent = 0) : QStyledItemDelegate(parent) {} + virtual ~CenteredEditingDelegate() {} + virtual QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, + const QModelIndex &index) const + { + auto widget = QStyledItemDelegate::createEditor(parent, option, index); + auto foo = dynamic_cast<QLineEdit *>(widget); + if (foo) + { + foo->setAlignment(Qt::AlignHCenter); + foo->setFrame(true); + foo->setMaximumWidth(192); + } + return widget; + } +}; + +ScreenshotsPage::ScreenshotsPage(QString path, QWidget *parent) + : QMainWindow(parent), ui(new Ui::ScreenshotsPage) +{ + m_model.reset(new QFileSystemModel()); + m_filterModel.reset(new FilterModel()); + m_filterModel->setSourceModel(m_model.get()); + m_model->setFilter(QDir::Files | QDir::Writable | QDir::Readable); + m_model->setReadOnly(false); + m_model->setNameFilters({"*.png"}); + m_model->setNameFilterDisables(false); + m_folder = path; + m_valid = FS::ensureFolderPathExists(m_folder); + + ui->setupUi(this); + ui->toolBar->insertSpacer(ui->actionView_Folder); + + ui->listView->setIconSize(QSize(128, 128)); + ui->listView->setGridSize(QSize(192, 160)); + ui->listView->setSpacing(9); + // ui->listView->setUniformItemSizes(true); + ui->listView->setLayoutMode(QListView::Batched); + ui->listView->setViewMode(QListView::IconMode); + ui->listView->setResizeMode(QListView::Adjust); + ui->listView->installEventFilter(this); + ui->listView->setEditTriggers(0); + ui->listView->setItemDelegate(new CenteredEditingDelegate(this)); + ui->listView->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->listView, &QListView::customContextMenuRequested, this, &ScreenshotsPage::ShowContextMenu); + connect(ui->listView, SIGNAL(activated(QModelIndex)), SLOT(onItemActivated(QModelIndex))); +} + +bool ScreenshotsPage::eventFilter(QObject *obj, QEvent *evt) +{ + if (obj != ui->listView) + return QWidget::eventFilter(obj, evt); + if (evt->type() != QEvent::KeyPress) + { + return QWidget::eventFilter(obj, evt); + } + QKeyEvent *keyEvent = static_cast<QKeyEvent *>(evt); + switch (keyEvent->key()) + { + case Qt::Key_Delete: + on_actionDelete_triggered(); + return true; + case Qt::Key_F2: + on_actionRename_triggered(); + return true; + default: + break; + } + return QWidget::eventFilter(obj, evt); +} + +ScreenshotsPage::~ScreenshotsPage() +{ + delete ui; +} + +void ScreenshotsPage::ShowContextMenu(const QPoint& pos) +{ + auto menu = ui->toolBar->createContextMenu(this, tr("Context menu")); + menu->exec(ui->listView->mapToGlobal(pos)); + delete menu; +} + +QMenu * ScreenshotsPage::createPopupMenu() +{ + QMenu* filteredMenu = QMainWindow::createPopupMenu(); + filteredMenu->removeAction( ui->toolBar->toggleViewAction() ); + return filteredMenu; +} + +void ScreenshotsPage::onItemActivated(QModelIndex index) +{ + if (!index.isValid()) + return; + auto info = m_model->fileInfo(index); + QString fileName = info.absoluteFilePath(); + DesktopServices::openFile(info.absoluteFilePath()); +} + +void ScreenshotsPage::on_actionView_Folder_triggered() +{ + DesktopServices::openDirectory(m_folder, true); +} + +void ScreenshotsPage::on_actionUpload_triggered() +{ + auto selection = ui->listView->selectionModel()->selectedRows(); + if (selection.isEmpty()) + return; + + QList<ScreenShot::Ptr> uploaded; + auto job = NetJob::Ptr(new NetJob("Screenshot Upload")); + if(selection.size() < 2) + { + auto item = selection.at(0); + auto info = m_model->fileInfo(item); + auto screenshot = std::make_shared<ScreenShot>(info); + job->addNetAction(ImgurUpload::make(screenshot)); + + m_uploadActive = true; + ProgressDialog dialog(this); + if(dialog.execWithTask(job.get()) != QDialog::Accepted) + { + CustomMessageBox::selectable(this, tr("Failed to upload screenshots!"), + tr("Unknown error"), QMessageBox::Warning)->exec(); + } + else + { + auto link = screenshot->m_url; + QClipboard *clipboard = QApplication::clipboard(); + clipboard->setText(link); + CustomMessageBox::selectable( + this, + tr("Upload finished"), + tr("The <a href=\"%1\">link to the uploaded screenshot</a> has been placed in your clipboard.") + .arg(link), + QMessageBox::Information + )->exec(); + } + + m_uploadActive = false; + return; + } + + for (auto item : selection) + { + auto info = m_model->fileInfo(item); + auto screenshot = std::make_shared<ScreenShot>(info); + uploaded.push_back(screenshot); + job->addNetAction(ImgurUpload::make(screenshot)); + } + SequentialTask task; + auto albumTask = NetJob::Ptr(new NetJob("Imgur Album Creation")); + auto imgurAlbum = ImgurAlbumCreation::make(uploaded); + albumTask->addNetAction(imgurAlbum); + task.addTask(job); + task.addTask(albumTask); + m_uploadActive = true; + ProgressDialog prog(this); + if (prog.execWithTask(&task) != QDialog::Accepted) + { + CustomMessageBox::selectable(this, tr("Failed to upload screenshots!"), + tr("Unknown error"), QMessageBox::Warning)->exec(); + } + else + { + auto link = QString("https://imgur.com/a/%1").arg(imgurAlbum->id()); + QClipboard *clipboard = QApplication::clipboard(); + clipboard->setText(link); + CustomMessageBox::selectable( + this, + tr("Upload finished"), + tr("The <a href=\"%1\">link to the uploaded album</a> has been placed in your clipboard.") .arg(link), + QMessageBox::Information + )->exec(); + } + m_uploadActive = false; +} + +void ScreenshotsPage::on_actionDelete_triggered() +{ + auto mbox = CustomMessageBox::selectable( + this, tr("Are you sure?"), tr("This will delete all selected screenshots."), + QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No); + std::unique_ptr<QMessageBox> box(mbox); + + if (box->exec() != QMessageBox::Yes) + return; + + auto selected = ui->listView->selectionModel()->selectedIndexes(); + for (auto item : selected) + { + m_model->remove(item); + } +} + +void ScreenshotsPage::on_actionRename_triggered() +{ + auto selection = ui->listView->selectionModel()->selectedIndexes(); + if (selection.isEmpty()) + return; + ui->listView->edit(selection[0]); + // TODO: mass renaming +} + +void ScreenshotsPage::openedImpl() +{ + if(!m_valid) + { + m_valid = FS::ensureFolderPathExists(m_folder); + } + if (m_valid) + { + QString path = QDir(m_folder).absolutePath(); + auto idx = m_model->setRootPath(path); + if(idx.isValid()) + { + ui->listView->setModel(m_filterModel.get()); + ui->listView->setRootIndex(m_filterModel->mapFromSource(idx)); + } + else + { + ui->listView->setModel(nullptr); + } + } +} + +#include "ScreenshotsPage.moc" diff --git a/launcher/ui/pages/instance/ScreenshotsPage.h b/launcher/ui/pages/instance/ScreenshotsPage.h new file mode 100644 index 00000000..d2f44837 --- /dev/null +++ b/launcher/ui/pages/instance/ScreenshotsPage.h @@ -0,0 +1,89 @@ +/* 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 <QMainWindow> + +#include "ui/pages/BasePage.h" +#include <Application.h> + +class QFileSystemModel; +class QIdentityProxyModel; +namespace Ui +{ +class ScreenshotsPage; +} + +struct ScreenShot; +class ScreenshotList; +class ImgurAlbumCreation; + +class ScreenshotsPage : public QMainWindow, public BasePage +{ + Q_OBJECT + +public: + explicit ScreenshotsPage(QString path, QWidget *parent = 0); + virtual ~ScreenshotsPage(); + + virtual void openedImpl() override; + + enum + { + NothingDone = 0x42 + }; + + virtual bool eventFilter(QObject *, QEvent *) override; + virtual QString displayName() const override + { + return tr("Screenshots"); + } + virtual QIcon icon() const override + { + return APPLICATION->getThemedIcon("screenshots"); + } + virtual QString id() const override + { + return "screenshots"; + } + virtual QString helpPage() const override + { + return "Screenshots-management"; + } + virtual bool apply() override + { + return !m_uploadActive; + } + +protected: + QMenu * createPopupMenu() override; + +private slots: + void on_actionUpload_triggered(); + void on_actionDelete_triggered(); + void on_actionRename_triggered(); + void on_actionView_Folder_triggered(); + void onItemActivated(QModelIndex); + void ShowContextMenu(const QPoint &pos); + +private: + Ui::ScreenshotsPage *ui; + std::shared_ptr<QFileSystemModel> m_model; + std::shared_ptr<QIdentityProxyModel> m_filterModel; + QString m_folder; + bool m_valid = false; + bool m_uploadActive = false; +}; diff --git a/launcher/ui/pages/instance/ScreenshotsPage.ui b/launcher/ui/pages/instance/ScreenshotsPage.ui new file mode 100644 index 00000000..ec461087 --- /dev/null +++ b/launcher/ui/pages/instance/ScreenshotsPage.ui @@ -0,0 +1,87 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>ScreenshotsPage</class> + <widget class="QMainWindow" name="ScreenshotsPage"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>800</width> + <height>600</height> + </rect> + </property> + <widget class="QWidget" name="centralwidget"> + <layout class="QHBoxLayout" name="horizontalLayout"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QListView" name="listView"> + <property name="selectionMode"> + <enum>QAbstractItemView::ExtendedSelection</enum> + </property> + <property name="selectionBehavior"> + <enum>QAbstractItemView::SelectRows</enum> + </property> + </widget> + </item> + </layout> + </widget> + <widget class="WideBar" name="toolBar"> + <property name="windowTitle"> + <string>Actions</string> + </property> + <property name="toolButtonStyle"> + <enum>Qt::ToolButtonTextOnly</enum> + </property> + <attribute name="toolBarArea"> + <enum>RightToolBarArea</enum> + </attribute> + <attribute name="toolBarBreak"> + <bool>false</bool> + </attribute> + <addaction name="actionUpload"/> + <addaction name="actionDelete"/> + <addaction name="actionRename"/> + <addaction name="actionView_Folder"/> + </widget> + <action name="actionUpload"> + <property name="text"> + <string>Upload</string> + </property> + </action> + <action name="actionDelete"> + <property name="text"> + <string>Delete</string> + </property> + </action> + <action name="actionRename"> + <property name="text"> + <string>Rename</string> + </property> + </action> + <action name="actionView_Folder"> + <property name="text"> + <string>View Folder</string> + </property> + </action> + </widget> + <customwidgets> + <customwidget> + <class>WideBar</class> + <extends>QToolBar</extends> + <header>ui/widgets/WideBar.h</header> + </customwidget> + </customwidgets> + <resources/> + <connections/> +</ui> diff --git a/launcher/ui/pages/instance/ServersPage.cpp b/launcher/ui/pages/instance/ServersPage.cpp new file mode 100644 index 00000000..8116d2bf --- /dev/null +++ b/launcher/ui/pages/instance/ServersPage.cpp @@ -0,0 +1,768 @@ +#include "ServersPage.h" +#include "ui_ServersPage.h" + +#include <FileSystem.h> +#include <sstream> +#include <io/stream_reader.h> +#include <tag_string.h> +#include <tag_primitive.h> +#include <tag_list.h> +#include <tag_compound.h> +#include <minecraft/MinecraftInstance.h> + +#include <QFileSystemWatcher> +#include <QMenu> + +static const int COLUMN_COUNT = 2; // 3 , TBD: latency and other nice things. + +struct Server +{ + // Types + enum class AcceptsTextures : int + { + ASK = 0, + ALWAYS = 1, + NEVER = 2 + }; + + // Methods + Server() + { + m_name = QObject::tr("Minecraft Server"); + } + Server(const QString & name, const QString & address) + { + m_name = name; + m_address = address; + } + Server(nbt::tag_compound& server) + { + std::string addressStr(server["ip"]); + m_address = QString::fromUtf8(addressStr.c_str()); + + std::string nameStr(server["name"]); + m_name = QString::fromUtf8(nameStr.c_str()); + + if(server["icon"]) + { + std::string base64str(server["icon"]); + m_icon = QByteArray::fromBase64(base64str.c_str()); + } + + if(server.has_key("acceptTextures", nbt::tag_type::Byte)) + { + bool value = server["acceptTextures"].as<nbt::tag_byte>().get(); + if(value) + { + m_acceptsTextures = AcceptsTextures::ALWAYS; + } + else + { + m_acceptsTextures = AcceptsTextures::NEVER; + } + } + } + + void serialize(nbt::tag_compound& server) + { + server.insert("name", m_name.trimmed().toUtf8().toStdString()); + server.insert("ip", m_address.trimmed().toUtf8().toStdString()); + if(m_icon.size()) + { + server.insert("icon", m_icon.toBase64().toStdString()); + } + if(m_acceptsTextures != AcceptsTextures::ASK) + { + server.insert("acceptTextures", nbt::tag_byte(m_acceptsTextures == AcceptsTextures::ALWAYS)); + } + } + + // Data - persistent and user changeable + QString m_name; + QString m_address; + AcceptsTextures m_acceptsTextures = AcceptsTextures::ASK; + + // Data - persistent and automatically updated + QByteArray m_icon; + + // Data - temporary + bool m_checked = false; + bool m_up = false; + QString m_motd; // https://mctools.org/motd-creator + int m_ping = 0; + int m_currentPlayers = 0; + int m_maxPlayers = 0; +}; + +static std::unique_ptr <nbt::tag_compound> parseServersDat(const QString& filename) +{ + try + { + QByteArray input = FS::read(filename); + std::istringstream foo(std::string(input.constData(), input.size())); + auto pair = nbt::io::read_compound(foo); + + if(pair.first != "") + return nullptr; + + if(pair.second == nullptr) + return nullptr; + + return std::move(pair.second); + } + catch (...) + { + return nullptr; + } +} + +static bool serializeServerDat(const QString& filename, nbt::tag_compound * levelInfo) +{ + try + { + if(!FS::ensureFilePathExists(filename)) + { + return false; + } + std::ostringstream s; + nbt::io::write_tag("", *levelInfo, s); + QByteArray val(s.str().data(), (int) s.str().size() ); + FS::write(filename, val); + return true; + } + catch (...) + { + return false; + } +} + +class ServersModel: public QAbstractListModel +{ + Q_OBJECT +public: + enum Roles + { + ServerPtrRole = Qt::UserRole, + }; + explicit ServersModel(const QString &path, QObject *parent = 0) + : QAbstractListModel(parent) + { + m_path = path; + m_watcher = new QFileSystemWatcher(this); + connect(m_watcher, &QFileSystemWatcher::fileChanged, this, &ServersModel::fileChanged); + connect(m_watcher, &QFileSystemWatcher::directoryChanged, this, &ServersModel::dirChanged); + m_saveTimer.setSingleShot(true); + m_saveTimer.setInterval(5000); + connect(&m_saveTimer, &QTimer::timeout, this, &ServersModel::save_internal); + } + virtual ~ServersModel() {}; + + void observe() + { + if(m_observed) + { + return; + } + m_observed = true; + + if(!m_loaded) + { + load(); + } + + updateFSObserver(); + } + + void unobserve() + { + if(!m_observed) + { + return; + } + m_observed = false; + + updateFSObserver(); + } + + void lock() + { + if(m_locked) + { + return; + } + saveNow(); + + m_locked = true; + updateFSObserver(); + } + + void unlock() + { + if(!m_locked) + { + return; + } + m_locked = false; + + updateFSObserver(); + } + + int addEmptyRow(int position) + { + if(m_locked) + { + return -1; + } + if(position < 0 || position >= rowCount()) + { + position = rowCount(); + } + beginInsertRows(QModelIndex(), position, position); + m_servers.insert(position, Server()); + endInsertRows(); + scheduleSave(); + return position; + } + + bool removeRow(int row) + { + if(m_locked) + { + return false; + } + if(row < 0 || row >= rowCount()) + { + return false; + } + beginRemoveRows(QModelIndex(), row, row); + m_servers.removeAt(row); + endRemoveRows(); // does absolutely nothing, the selected server stays as the next line... + scheduleSave(); + return true; + } + + bool moveUp(int row) + { + if(m_locked) + { + return false; + } + if(row <= 0) + { + return false; + } + beginMoveRows(QModelIndex(), row, row, QModelIndex(), row - 1); + m_servers.swap(row-1, row); + endMoveRows(); + scheduleSave(); + return true; + } + + bool moveDown(int row) + { + if(m_locked) + { + return false; + } + int count = rowCount(); + if(row + 1 >= count) + { + return false; + } + beginMoveRows(QModelIndex(), row, row, QModelIndex(), row + 2); + m_servers.swap(row+1, row); + endMoveRows(); + scheduleSave(); + return true; + } + + QVariant headerData(int section, Qt::Orientation orientation, int role) const override + { + if (section < 0 || section >= COLUMN_COUNT) + return QVariant(); + + if(role == Qt::DisplayRole) + { + switch(section) + { + case 0: + return tr("Name"); + case 1: + return tr("Address"); + case 2: + return tr("Latency"); + } + } + + return QAbstractListModel::headerData(section, orientation, role); + } + + virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override + { + if (!index.isValid()) + return QVariant(); + + int row = index.row(); + int column = index.column(); + if(column < 0 || column >= COLUMN_COUNT) + return QVariant(); + + if (row < 0 || row >= m_servers.size()) + return QVariant(); + + switch(column) + { + case 0: + switch (role) + { + case Qt::DecorationRole: + { + auto & bytes = m_servers[row].m_icon; + if(bytes.size()) + { + QPixmap px; + if(px.loadFromData(bytes)) + return QIcon(px); + } + return APPLICATION->getThemedIcon("unknown_server"); + } + case Qt::DisplayRole: + return m_servers[row].m_name; + case ServerPtrRole: + return QVariant::fromValue<void *>((void *)&m_servers[row]); + default: + return QVariant(); + } + case 1: + switch (role) + { + case Qt::DisplayRole: + return m_servers[row].m_address; + default: + return QVariant(); + } + case 2: + switch (role) + { + case Qt::DisplayRole: + return m_servers[row].m_ping; + default: + return QVariant(); + } + default: + return QVariant(); + } + } + + virtual int rowCount(const QModelIndex &parent = QModelIndex()) const override + { + return m_servers.size(); + } + int columnCount(const QModelIndex & parent) const override + { + return COLUMN_COUNT; + } + + Server * at(int index) + { + if(index < 0 || index >= rowCount()) + { + return nullptr; + } + return &m_servers[index]; + } + + void setName(int row, const QString & name) + { + if(m_locked) + { + return; + } + auto server = at(row); + if(!server || server->m_name == name) + { + return; + } + server->m_name = name; + emit dataChanged(index(row, 0), index(row, COLUMN_COUNT - 1)); + scheduleSave(); + } + + void setAddress(int row, const QString & address) + { + if(m_locked) + { + return; + } + auto server = at(row); + if(!server || server->m_address == address) + { + return; + } + server->m_address = address; + emit dataChanged(index(row, 0), index(row, COLUMN_COUNT - 1)); + scheduleSave(); + } + + void setAcceptsTextures(int row, Server::AcceptsTextures textures) + { + if(m_locked) + { + return; + } + auto server = at(row); + if(!server || server->m_acceptsTextures == textures) + { + return; + } + server->m_acceptsTextures = textures; + emit dataChanged(index(row, 0), index(row, COLUMN_COUNT - 1)); + scheduleSave(); + } + + void load() + { + cancelSave(); + beginResetModel(); + QList<Server> servers; + auto serversDat = parseServersDat(serversPath()); + if(serversDat) + { + auto &serversList = serversDat->at("servers").as<nbt::tag_list>(); + for(auto iter = serversList.begin(); iter != serversList.end(); iter++) + { + auto & serverTag = (*iter).as<nbt::tag_compound>(); + Server s(serverTag); + servers.append(s); + } + } + m_servers.swap(servers); + m_loaded = true; + endResetModel(); + } + + void saveNow() + { + if(saveIsScheduled()) + { + save_internal(); + } + } + + +public slots: + void dirChanged(const QString& path) + { + qDebug() << "Changed:" << path; + load(); + } + void fileChanged(const QString& path) + { + qDebug() << "Changed:" << path; + } + +private slots: + void save_internal() + { + cancelSave(); + QString path = serversPath(); + qDebug() << "Server list about to be saved to" << path; + + nbt::tag_compound out; + nbt::tag_list list; + for(auto & server: m_servers) + { + nbt::tag_compound serverNbt; + server.serialize(serverNbt); + list.push_back(std::move(serverNbt)); + } + out.insert("servers", nbt::value(std::move(list))); + + if(!serializeServerDat(path, &out)) + { + qDebug() << "Failed to save server list:" << path << "Will try again."; + scheduleSave(); + } + } + +private: + void scheduleSave() + { + if(!m_loaded) + { + qDebug() << "Server list should never save if it didn't successfully load, path:" << m_path; + return; + } + if(!m_dirty) + { + m_dirty = true; + qDebug() << "Server list save is scheduled for" << m_path; + } + m_saveTimer.start(); + } + + void cancelSave() + { + m_dirty = false; + m_saveTimer.stop(); + } + + bool saveIsScheduled() const + { + return m_dirty; + } + + void updateFSObserver() + { + bool observingFS = m_watcher->directories().contains(m_path); + if(m_observed && m_locked) + { + if(!observingFS) + { + qWarning() << "Will watch" << m_path; + if(!m_watcher->addPath(m_path)) + { + qWarning() << "Failed to start watching" << m_path; + } + } + } + else + { + if(observingFS) + { + qWarning() << "Will stop watching" << m_path; + if(!m_watcher->removePath(m_path)) + { + qWarning() << "Failed to stop watching" << m_path; + } + } + } + } + + QString serversPath() + { + QFileInfo foo(FS::PathCombine(m_path, "servers.dat")); + return foo.filePath(); + } + +private: + bool m_loaded = false; + bool m_locked = false; + bool m_observed = false; + bool m_dirty = false; + QString m_path; + QList<Server> m_servers; + QFileSystemWatcher *m_watcher = nullptr; + QTimer m_saveTimer; +}; + +ServersPage::ServersPage(InstancePtr inst, QWidget* parent) + : QMainWindow(parent), ui(new Ui::ServersPage) +{ + ui->setupUi(this); + m_inst = inst; + m_model = new ServersModel(inst->gameRoot(), this); + ui->serversView->setIconSize(QSize(64,64)); + ui->serversView->setModel(m_model); + ui->serversView->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->serversView, &QTreeView::customContextMenuRequested, this, &ServersPage::ShowContextMenu); + + auto head = ui->serversView->header(); + if(head->count()) + { + head->setSectionResizeMode(0, QHeaderView::Stretch); + for(int i = 1; i < head->count(); i++) + { + head->setSectionResizeMode(i, QHeaderView::ResizeToContents); + } + } + + auto selectionModel = ui->serversView->selectionModel(); + connect(selectionModel, &QItemSelectionModel::currentChanged, this, &ServersPage::currentChanged); + connect(m_inst.get(), &MinecraftInstance::runningStatusChanged, this, &ServersPage::on_RunningState_changed); + connect(ui->nameLine, &QLineEdit::textEdited, this, &ServersPage::nameEdited); + connect(ui->addressLine, &QLineEdit::textEdited, this, &ServersPage::addressEdited); + connect(ui->resourceComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(resourceIndexChanged(int))); + connect(m_model, &QAbstractItemModel::rowsRemoved, this, &ServersPage::rowsRemoved); + + m_locked = m_inst->isRunning(); + if(m_locked) + { + m_model->lock(); + } + + updateState(); +} + +ServersPage::~ServersPage() +{ + m_model->saveNow(); + delete ui; +} + +void ServersPage::ShowContextMenu(const QPoint& pos) +{ + auto menu = ui->toolBar->createContextMenu(this, tr("Context menu")); + menu->exec(ui->serversView->mapToGlobal(pos)); + delete menu; +} + +QMenu * ServersPage::createPopupMenu() +{ + QMenu* filteredMenu = QMainWindow::createPopupMenu(); + filteredMenu->removeAction( ui->toolBar->toggleViewAction() ); + return filteredMenu; +} + +void ServersPage::on_RunningState_changed(bool running) +{ + if(m_locked == running) + { + return; + } + m_locked = running; + if(m_locked) + { + m_model->lock(); + } + else + { + m_model->unlock(); + } + updateState(); +} + +void ServersPage::currentChanged(const QModelIndex ¤t, const QModelIndex &previous) +{ + int nextServer = -1; + if (!current.isValid()) + { + nextServer = -1; + } + else + { + nextServer = current.row(); + } + currentServer = nextServer; + updateState(); +} + +// WARNING: this is here because currentChanged is not accurate when removing rows. the current item needs to be fixed up after removal. +void ServersPage::rowsRemoved(const QModelIndex& parent, int first, int last) +{ + if(currentServer < first) + { + // current was before the removal + return; + } + else if(currentServer >= first && currentServer <= last) + { + // current got removed... + return; + } + else + { + // current was past the removal + int count = last - first + 1; + currentServer -= count; + } +} + +void ServersPage::nameEdited(const QString& name) +{ + m_model->setName(currentServer, name); +} + +void ServersPage::addressEdited(const QString& address) +{ + m_model->setAddress(currentServer, address); +} + +void ServersPage::resourceIndexChanged(int index) +{ + auto acceptsTextures = Server::AcceptsTextures(index); + m_model->setAcceptsTextures(currentServer, acceptsTextures); +} + +void ServersPage::updateState() +{ + auto server = m_model->at(currentServer); + + bool serverEditEnabled = server && !m_locked; + ui->addressLine->setEnabled(serverEditEnabled); + ui->nameLine->setEnabled(serverEditEnabled); + ui->resourceComboBox->setEnabled(serverEditEnabled); + ui->actionMove_Down->setEnabled(serverEditEnabled); + ui->actionMove_Up->setEnabled(serverEditEnabled); + ui->actionRemove->setEnabled(serverEditEnabled); + ui->actionJoin->setEnabled(serverEditEnabled); + + if(server) + { + ui->addressLine->setText(server->m_address); + ui->nameLine->setText(server->m_name); + ui->resourceComboBox->setCurrentIndex(int(server->m_acceptsTextures)); + } + else + { + ui->addressLine->setText(QString()); + ui->nameLine->setText(QString()); + ui->resourceComboBox->setCurrentIndex(0); + } + + ui->actionAdd->setDisabled(m_locked); +} + +void ServersPage::openedImpl() +{ + m_model->observe(); +} + +void ServersPage::closedImpl() +{ + m_model->unobserve(); +} + +void ServersPage::on_actionAdd_triggered() +{ + int position = m_model->addEmptyRow(currentServer + 1); + if(position < 0) + { + return; + } + // select the new row + ui->serversView->selectionModel()->setCurrentIndex( + m_model->index(position), + QItemSelectionModel::SelectCurrent | QItemSelectionModel::Clear | QItemSelectionModel::Rows + ); + currentServer = position; +} + +void ServersPage::on_actionRemove_triggered() +{ + m_model->removeRow(currentServer); +} + +void ServersPage::on_actionMove_Up_triggered() +{ + if(m_model->moveUp(currentServer)) + { + currentServer --; + } +} + +void ServersPage::on_actionMove_Down_triggered() +{ + if(m_model->moveDown(currentServer)) + { + currentServer ++; + } +} + +void ServersPage::on_actionJoin_triggered() +{ + const auto &address = m_model->at(currentServer)->m_address; + APPLICATION->launch(m_inst, true, nullptr, std::make_shared<MinecraftServerTarget>(MinecraftServerTarget::parse(address))); +} + +#include "ServersPage.moc" diff --git a/launcher/ui/pages/instance/ServersPage.h b/launcher/ui/pages/instance/ServersPage.h new file mode 100644 index 00000000..d91da2ae --- /dev/null +++ b/launcher/ui/pages/instance/ServersPage.h @@ -0,0 +1,94 @@ +/* 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 <QMainWindow> +#include <QString> + +#include "ui/pages/BasePage.h" +#include <Application.h> + +namespace Ui +{ +class ServersPage; +} + +struct Server; +class ServersModel; +class MinecraftInstance; + +class ServersPage : public QMainWindow, public BasePage +{ + Q_OBJECT + +public: + explicit ServersPage(InstancePtr inst, QWidget *parent = 0); + virtual ~ServersPage(); + + void openedImpl() override; + void closedImpl() override; + + virtual QString displayName() const override + { + return tr("Servers"); + } + virtual QIcon icon() const override + { + return APPLICATION->getThemedIcon("unknown_server"); + } + virtual QString id() const override + { + return "servers"; + } + virtual QString helpPage() const override + { + return "Servers-management"; + } + +protected: + QMenu * createPopupMenu() override; + +private: + void updateState(); + void scheduleSave(); + bool saveIsScheduled() const; + +private slots: + void currentChanged(const QModelIndex ¤t, const QModelIndex &previous); + void rowsRemoved(const QModelIndex &parent, int first, int last); + + void on_actionAdd_triggered(); + void on_actionRemove_triggered(); + void on_actionMove_Up_triggered(); + void on_actionMove_Down_triggered(); + void on_actionJoin_triggered(); + + void on_RunningState_changed(bool running); + + void nameEdited(const QString & name); + void addressEdited(const QString & address); + void resourceIndexChanged(int index);\ + + void ShowContextMenu(const QPoint &pos); + +private: // data + int currentServer = -1; + bool m_locked = true; + Ui::ServersPage *ui = nullptr; + ServersModel * m_model = nullptr; + InstancePtr m_inst = nullptr; +}; + diff --git a/launcher/ui/pages/instance/ServersPage.ui b/launcher/ui/pages/instance/ServersPage.ui new file mode 100644 index 00000000..e8f79cf2 --- /dev/null +++ b/launcher/ui/pages/instance/ServersPage.ui @@ -0,0 +1,194 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>ServersPage</class> + <widget class="QMainWindow" name="ServersPage"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>1318</width> + <height>879</height> + </rect> + </property> + <widget class="QWidget" name="centralwidget"> + <layout class="QVBoxLayout" name="verticalLayout"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QTreeView" name="serversView"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Expanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="acceptDrops"> + <bool>true</bool> + </property> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + <property name="selectionMode"> + <enum>QAbstractItemView::SingleSelection</enum> + </property> + <property name="selectionBehavior"> + <enum>QAbstractItemView::SelectRows</enum> + </property> + <property name="iconSize"> + <size> + <width>64</width> + <height>64</height> + </size> + </property> + <property name="rootIsDecorated"> + <bool>false</bool> + </property> + <attribute name="headerStretchLastSection"> + <bool>false</bool> + </attribute> + </widget> + </item> + <item> + <layout class="QGridLayout" name="gridLayout_2"> + <property name="leftMargin"> + <number>6</number> + </property> + <property name="rightMargin"> + <number>6</number> + </property> + <item row="0" column="0"> + <widget class="QLabel" name="nameLabel"> + <property name="text"> + <string>&Name</string> + </property> + <property name="buddy"> + <cstring>nameLine</cstring> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QLineEdit" name="nameLine"/> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="addressLabel"> + <property name="text"> + <string>Address</string> + </property> + <property name="buddy"> + <cstring>addressLine</cstring> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QLineEdit" name="addressLine"/> + </item> + <item row="2" column="0"> + <widget class="QLabel" name="resourcesLabel"> + <property name="text"> + <string>Reso&urces</string> + </property> + <property name="buddy"> + <cstring>resourceComboBox</cstring> + </property> + </widget> + </item> + <item row="2" column="1"> + <widget class="QComboBox" name="resourceComboBox"> + <item> + <property name="text"> + <string>Ask to download</string> + </property> + </item> + <item> + <property name="text"> + <string>Always download</string> + </property> + </item> + <item> + <property name="text"> + <string>Never download</string> + </property> + </item> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + <widget class="WideBar" name="toolBar"> + <property name="windowTitle"> + <string>Actions</string> + </property> + <property name="allowedAreas"> + <set>Qt::LeftToolBarArea|Qt::RightToolBarArea</set> + </property> + <property name="toolButtonStyle"> + <enum>Qt::ToolButtonTextOnly</enum> + </property> + <property name="floatable"> + <bool>false</bool> + </property> + <attribute name="toolBarArea"> + <enum>RightToolBarArea</enum> + </attribute> + <attribute name="toolBarBreak"> + <bool>false</bool> + </attribute> + <addaction name="actionAdd"/> + <addaction name="actionRemove"/> + <addaction name="actionMove_Up"/> + <addaction name="actionMove_Down"/> + <addaction name="actionJoin"/> + </widget> + <action name="actionAdd"> + <property name="text"> + <string>Add</string> + </property> + </action> + <action name="actionRemove"> + <property name="text"> + <string>Remove</string> + </property> + </action> + <action name="actionMove_Up"> + <property name="text"> + <string>Move Up</string> + </property> + </action> + <action name="actionMove_Down"> + <property name="text"> + <string>Move Down</string> + </property> + </action> + <action name="actionJoin"> + <property name="text"> + <string>Join</string> + </property> + </action> + </widget> + <customwidgets> + <customwidget> + <class>WideBar</class> + <extends>QToolBar</extends> + <header>ui/widgets/WideBar.h</header> + </customwidget> + </customwidgets> + <tabstops> + <tabstop>serversView</tabstop> + <tabstop>nameLine</tabstop> + <tabstop>addressLine</tabstop> + <tabstop>resourceComboBox</tabstop> + </tabstops> + <resources/> + <connections/> +</ui> diff --git a/launcher/ui/pages/instance/ShaderPackPage.h b/launcher/ui/pages/instance/ShaderPackPage.h new file mode 100644 index 00000000..36724992 --- /dev/null +++ b/launcher/ui/pages/instance/ShaderPackPage.h @@ -0,0 +1,22 @@ +#pragma once + +#include "ModFolderPage.h" +#include "ui_ModFolderPage.h" + +class ShaderPackPage : public ModFolderPage +{ + Q_OBJECT +public: + explicit ShaderPackPage(MinecraftInstance *instance, QWidget *parent = 0) + : ModFolderPage(instance, instance->shaderPackList(), "shaderpacks", + "shaderpacks", tr("Shader packs"), "Resource-packs", parent) + { + ui->actionView_configs->setVisible(false); + } + virtual ~ShaderPackPage() {} + + virtual bool shouldDisplay() const override + { + return true; + } +}; diff --git a/launcher/ui/pages/instance/TexturePackPage.h b/launcher/ui/pages/instance/TexturePackPage.h new file mode 100644 index 00000000..3f04997d --- /dev/null +++ b/launcher/ui/pages/instance/TexturePackPage.h @@ -0,0 +1,22 @@ +#pragma once + +#include "ModFolderPage.h" +#include "ui_ModFolderPage.h" + +class TexturePackPage : public ModFolderPage +{ + Q_OBJECT +public: + explicit TexturePackPage(MinecraftInstance *instance, QWidget *parent = 0) + : ModFolderPage(instance, instance->texturePackList(), "texturepacks", "resourcepacks", + tr("Texture packs"), "Texture-packs", parent) + { + ui->actionView_configs->setVisible(false); + } + virtual ~TexturePackPage() {} + + virtual bool shouldDisplay() const override + { + return m_inst->traits().contains("texturepacks"); + } +}; diff --git a/launcher/ui/pages/instance/VersionPage.cpp b/launcher/ui/pages/instance/VersionPage.cpp new file mode 100644 index 00000000..715059ff --- /dev/null +++ b/launcher/ui/pages/instance/VersionPage.cpp @@ -0,0 +1,641 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Application.h" + +#include <QMessageBox> +#include <QLabel> +#include <QEvent> +#include <QKeyEvent> +#include <QMenu> +#include <QAbstractItemModel> +#include <QMessageBox> +#include <QListView> +#include <QString> +#include <QUrl> + +#include "VersionPage.h" +#include "ui_VersionPage.h" + +#include "ui/dialogs/CustomMessageBox.h" +#include "ui/dialogs/VersionSelectDialog.h" +#include "ui/dialogs/NewComponentDialog.h" +#include "ui/dialogs/ProgressDialog.h" + +#include "ui/GuiUtil.h" + +#include "minecraft/PackProfile.h" +#include "minecraft/auth/AccountList.h" +#include "minecraft/mod/Mod.h" +#include "icons/IconList.h" +#include "Exception.h" +#include "Version.h" +#include "DesktopServices.h" + +#include "meta/Index.h" +#include "meta/VersionList.h" + +class IconProxy : public QIdentityProxyModel +{ + Q_OBJECT +public: + + IconProxy(QWidget *parentWidget) : QIdentityProxyModel(parentWidget) + { + connect(parentWidget, &QObject::destroyed, this, &IconProxy::widgetGone); + m_parentWidget = parentWidget; + } + + virtual QVariant data(const QModelIndex &proxyIndex, int role = Qt::DisplayRole) const override + { + QVariant var = QIdentityProxyModel::data(proxyIndex, role); + int column = proxyIndex.column(); + if(column == 0 && role == Qt::DecorationRole && m_parentWidget) + { + if(!var.isNull()) + { + auto string = var.toString(); + if(string == "warning") + { + return APPLICATION->getThemedIcon("status-yellow"); + } + else if(string == "error") + { + return APPLICATION->getThemedIcon("status-bad"); + } + } + return APPLICATION->getThemedIcon("status-good"); + } + return var; + } +private slots: + void widgetGone() + { + m_parentWidget = nullptr; + } + +private: + QWidget *m_parentWidget = nullptr; +}; + +QIcon VersionPage::icon() const +{ + return APPLICATION->icons()->getIcon(m_inst->iconKey()); +} +bool VersionPage::shouldDisplay() const +{ + return true; +} + +QMenu * VersionPage::createPopupMenu() +{ + QMenu* filteredMenu = QMainWindow::createPopupMenu(); + filteredMenu->removeAction( ui->toolBar->toggleViewAction() ); + return filteredMenu; +} + +VersionPage::VersionPage(MinecraftInstance *inst, QWidget *parent) + : QMainWindow(parent), ui(new Ui::VersionPage), m_inst(inst) +{ + ui->setupUi(this); + + ui->toolBar->insertSpacer(ui->actionReload); + + m_profile = m_inst->getPackProfile(); + + reloadPackProfile(); + + auto proxy = new IconProxy(ui->packageView); + proxy->setSourceModel(m_profile.get()); + + m_filterModel = new QSortFilterProxyModel(); + m_filterModel->setDynamicSortFilter(true); + m_filterModel->setFilterCaseSensitivity(Qt::CaseInsensitive); + m_filterModel->setSortCaseSensitivity(Qt::CaseInsensitive); + m_filterModel->setSourceModel(proxy); + m_filterModel->setFilterKeyColumn(-1); + + ui->packageView->setModel(m_filterModel); + ui->packageView->installEventFilter(this); + ui->packageView->setSelectionMode(QAbstractItemView::SingleSelection); + ui->packageView->setContextMenuPolicy(Qt::CustomContextMenu); + + connect(ui->packageView->selectionModel(), &QItemSelectionModel::currentChanged, this, &VersionPage::versionCurrent); + auto smodel = ui->packageView->selectionModel(); + connect(smodel, &QItemSelectionModel::currentChanged, this, &VersionPage::packageCurrent); + + connect(m_profile.get(), &PackProfile::minecraftChanged, this, &VersionPage::updateVersionControls); + controlsEnabled = !m_inst->isRunning(); + updateVersionControls(); + preselect(0); + connect(m_inst, &BaseInstance::runningStatusChanged, this, &VersionPage::updateRunningStatus); + connect(ui->packageView, &ModListView::customContextMenuRequested, this, &VersionPage::showContextMenu); + connect(ui->filterEdit, &QLineEdit::textChanged, this, &VersionPage::onFilterTextChanged); +} + +VersionPage::~VersionPage() +{ + delete ui; +} + +void VersionPage::showContextMenu(const QPoint& pos) +{ + auto menu = ui->toolBar->createContextMenu(this, tr("Context menu")); + menu->exec(ui->packageView->mapToGlobal(pos)); + delete menu; +} + +void VersionPage::packageCurrent(const QModelIndex ¤t, const QModelIndex &previous) +{ + if (!current.isValid()) + { + ui->frame->clear(); + return; + } + int row = current.row(); + auto patch = m_profile->getComponent(row); + auto severity = patch->getProblemSeverity(); + switch(severity) + { + case ProblemSeverity::Warning: + ui->frame->setModText(tr("%1 possibly has issues.").arg(patch->getName())); + break; + case ProblemSeverity::Error: + ui->frame->setModText(tr("%1 has issues!").arg(patch->getName())); + break; + default: + case ProblemSeverity::None: + ui->frame->clear(); + return; + } + + auto &problems = patch->getProblems(); + QString problemOut; + for (auto &problem: problems) + { + if(problem.m_severity == ProblemSeverity::Error) + { + problemOut += tr("Error: "); + } + else if(problem.m_severity == ProblemSeverity::Warning) + { + problemOut += tr("Warning: "); + } + problemOut += problem.m_description; + problemOut += "\n"; + } + ui->frame->setModDescription(problemOut); +} + +void VersionPage::updateRunningStatus(bool running) +{ + if(controlsEnabled == running) { + controlsEnabled = !running; + updateVersionControls(); + } +} + +void VersionPage::updateVersionControls() +{ + // FIXME: this is a dirty hack + auto minecraftVersion = Version(m_profile->getComponentVersion("net.minecraft")); + + bool supportsFabric = minecraftVersion >= Version("1.14"); + ui->actionInstall_Fabric->setEnabled(controlsEnabled && supportsFabric); + + bool supportsForge = minecraftVersion <= Version("1.16.5"); + ui->actionInstall_Forge->setEnabled(controlsEnabled && supportsForge); + + bool supportsLiteLoader = minecraftVersion <= Version("1.12.2"); + ui->actionInstall_LiteLoader->setEnabled(controlsEnabled && supportsLiteLoader); + + updateButtons(); +} + +void VersionPage::updateButtons(int row) +{ + if(row == -1) + row = currentRow(); + auto patch = m_profile->getComponent(row); + ui->actionRemove->setEnabled(controlsEnabled && patch && patch->isRemovable()); + ui->actionMove_down->setEnabled(controlsEnabled && patch && patch->isMoveable()); + ui->actionMove_up->setEnabled(controlsEnabled && patch && patch->isMoveable()); + ui->actionChange_version->setEnabled(controlsEnabled && patch && patch->isVersionChangeable()); + ui->actionEdit->setEnabled(controlsEnabled && patch && patch->isCustom()); + ui->actionCustomize->setEnabled(controlsEnabled && patch && patch->isCustomizable()); + ui->actionRevert->setEnabled(controlsEnabled && patch && patch->isRevertible()); + ui->actionDownload_All->setEnabled(controlsEnabled); + ui->actionAdd_Empty->setEnabled(controlsEnabled); + ui->actionReload->setEnabled(controlsEnabled); + ui->actionInstall_mods->setEnabled(controlsEnabled); + ui->actionReplace_Minecraft_jar->setEnabled(controlsEnabled); + ui->actionAdd_to_Minecraft_jar->setEnabled(controlsEnabled); +} + +bool VersionPage::reloadPackProfile() +{ + try + { + m_profile->reload(Net::Mode::Online); + return true; + } + catch (const Exception &e) + { + QMessageBox::critical(this, tr("Error"), e.cause()); + return false; + } + catch (...) + { + QMessageBox::critical( + this, tr("Error"), + tr("Couldn't load the instance profile.")); + return false; + } +} + +void VersionPage::on_actionReload_triggered() +{ + reloadPackProfile(); + m_container->refreshContainer(); +} + +void VersionPage::on_actionRemove_triggered() +{ + if (ui->packageView->currentIndex().isValid()) + { + // FIXME: use actual model, not reloading. + if (!m_profile->remove(ui->packageView->currentIndex().row())) + { + QMessageBox::critical(this, tr("Error"), tr("Couldn't remove file")); + } + } + updateButtons(); + reloadPackProfile(); + m_container->refreshContainer(); +} + +void VersionPage::on_actionInstall_mods_triggered() +{ + if(m_container) + { + m_container->selectPage("mods"); + } +} + +void VersionPage::on_actionAdd_to_Minecraft_jar_triggered() +{ + auto list = GuiUtil::BrowseForFiles("jarmod", tr("Select jar mods"), tr("Minecraft.jar mods (*.zip *.jar)"), APPLICATION->settings()->get("CentralModsDir").toString(), this->parentWidget()); + if(!list.empty()) + { + m_profile->installJarMods(list); + } + updateButtons(); +} + +void VersionPage::on_actionReplace_Minecraft_jar_triggered() +{ + auto jarPath = GuiUtil::BrowseForFile("jar", tr("Select jar"), tr("Minecraft.jar replacement (*.jar)"), APPLICATION->settings()->get("CentralModsDir").toString(), this->parentWidget()); + if(!jarPath.isEmpty()) + { + m_profile->installCustomJar(jarPath); + } + updateButtons(); +} + +void VersionPage::on_actionMove_up_triggered() +{ + try + { + m_profile->move(currentRow(), PackProfile::MoveUp); + } + catch (const Exception &e) + { + QMessageBox::critical(this, tr("Error"), e.cause()); + } + updateButtons(); +} + +void VersionPage::on_actionMove_down_triggered() +{ + try + { + m_profile->move(currentRow(), PackProfile::MoveDown); + } + catch (const Exception &e) + { + QMessageBox::critical(this, tr("Error"), e.cause()); + } + updateButtons(); +} + +void VersionPage::on_actionChange_version_triggered() +{ + auto versionRow = currentRow(); + if(versionRow == -1) + { + return; + } + auto patch = m_profile->getComponent(versionRow); + auto name = patch->getName(); + auto list = patch->getVersionList(); + if(!list) + { + return; + } + auto uid = list->uid(); + // FIXME: this is a horrible HACK. Get version filtering information from the actual metadata... + if(uid == "net.minecraftforge") + { + on_actionInstall_Forge_triggered(); + return; + } + else if (uid == "com.mumfrey.liteloader") + { + on_actionInstall_LiteLoader_triggered(); + return; + } + VersionSelectDialog vselect(list.get(), tr("Change %1 version").arg(name), this); + if (uid == "net.fabricmc.intermediary") + { + vselect.setEmptyString(tr("No intermediary mappings versions are currently available.")); + vselect.setEmptyErrorString(tr("Couldn't load or download the intermediary mappings version lists!")); + vselect.setExactFilter(BaseVersionList::ParentVersionRole, m_profile->getComponentVersion("net.minecraft")); + } + auto currentVersion = patch->getVersion(); + if(!currentVersion.isEmpty()) + { + vselect.setCurrentVersion(currentVersion); + } + if (!vselect.exec() || !vselect.selectedVersion()) + return; + + qDebug() << "Change" << uid << "to" << vselect.selectedVersion()->descriptor(); + bool important = false; + if(uid == "net.minecraft") + { + important = true; + } + m_profile->setComponentVersion(uid, vselect.selectedVersion()->descriptor(), important); + m_profile->resolve(Net::Mode::Online); + m_container->refreshContainer(); +} + +void VersionPage::on_actionDownload_All_triggered() +{ + if (!APPLICATION->accounts()->anyAccountIsValid()) + { + CustomMessageBox::selectable( + this, tr("Error"), + tr("MultiMC cannot download Minecraft or update instances unless you have at least " + "one account added.\nPlease add your Mojang or Minecraft account."), + QMessageBox::Warning)->show(); + return; + } + + auto updateTask = m_inst->createUpdateTask(Net::Mode::Online); + if (!updateTask) + { + return; + } + ProgressDialog tDialog(this); + connect(updateTask.get(), SIGNAL(failed(QString)), SLOT(onGameUpdateError(QString))); + // FIXME: unused return value + tDialog.execWithTask(updateTask.get()); + updateButtons(); + m_container->refreshContainer(); +} + +void VersionPage::on_actionInstall_Forge_triggered() +{ + auto vlist = APPLICATION->metadataIndex()->get("net.minecraftforge"); + if(!vlist) + { + return; + } + VersionSelectDialog vselect(vlist.get(), tr("Select Forge version"), this); + vselect.setExactFilter(BaseVersionList::ParentVersionRole, m_profile->getComponentVersion("net.minecraft")); + vselect.setEmptyString(tr("No Forge versions are currently available for Minecraft ") + m_profile->getComponentVersion("net.minecraft")); + vselect.setEmptyErrorString(tr("Couldn't load or download the Forge version lists!")); + + auto currentVersion = m_profile->getComponentVersion("net.minecraftforge"); + if(!currentVersion.isEmpty()) + { + vselect.setCurrentVersion(currentVersion); + } + + if (vselect.exec() && vselect.selectedVersion()) + { + auto vsn = vselect.selectedVersion(); + m_profile->setComponentVersion("net.minecraftforge", vsn->descriptor()); + m_profile->resolve(Net::Mode::Online); + // m_profile->installVersion(); + preselect(m_profile->rowCount(QModelIndex())-1); + m_container->refreshContainer(); + } +} + +void VersionPage::on_actionInstall_Fabric_triggered() +{ + auto vlist = APPLICATION->metadataIndex()->get("net.fabricmc.fabric-loader"); + if(!vlist) + { + return; + } + VersionSelectDialog vselect(vlist.get(), tr("Select Fabric Loader version"), this); + vselect.setEmptyString(tr("No Fabric Loader versions are currently available.")); + vselect.setEmptyErrorString(tr("Couldn't load or download the Fabric Loader version lists!")); + + auto currentVersion = m_profile->getComponentVersion("net.fabricmc.fabric-loader"); + if(!currentVersion.isEmpty()) + { + vselect.setCurrentVersion(currentVersion); + } + + if (vselect.exec() && vselect.selectedVersion()) + { + auto vsn = vselect.selectedVersion(); + m_profile->setComponentVersion("net.fabricmc.fabric-loader", vsn->descriptor()); + m_profile->resolve(Net::Mode::Online); + preselect(m_profile->rowCount(QModelIndex())-1); + m_container->refreshContainer(); + } +} + +void VersionPage::on_actionAdd_Empty_triggered() +{ + NewComponentDialog compdialog(QString(), QString(), this); + QStringList blacklist; + for(int i = 0; i < m_profile->rowCount(); i++) + { + auto comp = m_profile->getComponent(i); + blacklist.push_back(comp->getID()); + } + compdialog.setBlacklist(blacklist); + if (compdialog.exec()) + { + qDebug() << "name:" << compdialog.name(); + qDebug() << "uid:" << compdialog.uid(); + m_profile->installEmpty(compdialog.uid(), compdialog.name()); + } +} + +void VersionPage::on_actionInstall_LiteLoader_triggered() +{ + auto vlist = APPLICATION->metadataIndex()->get("com.mumfrey.liteloader"); + if(!vlist) + { + return; + } + VersionSelectDialog vselect(vlist.get(), tr("Select LiteLoader version"), this); + vselect.setExactFilter(BaseVersionList::ParentVersionRole, m_profile->getComponentVersion("net.minecraft")); + vselect.setEmptyString(tr("No LiteLoader versions are currently available for Minecraft ") + m_profile->getComponentVersion("net.minecraft")); + vselect.setEmptyErrorString(tr("Couldn't load or download the LiteLoader version lists!")); + + auto currentVersion = m_profile->getComponentVersion("com.mumfrey.liteloader"); + if(!currentVersion.isEmpty()) + { + vselect.setCurrentVersion(currentVersion); + } + + if (vselect.exec() && vselect.selectedVersion()) + { + auto vsn = vselect.selectedVersion(); + m_profile->setComponentVersion("com.mumfrey.liteloader", vsn->descriptor()); + m_profile->resolve(Net::Mode::Online); + // m_profile->installVersion(vselect.selectedVersion()); + preselect(m_profile->rowCount(QModelIndex())-1); + m_container->refreshContainer(); + } +} + +void VersionPage::on_actionLibrariesFolder_triggered() +{ + DesktopServices::openDirectory(m_inst->getLocalLibraryPath(), true); +} + +void VersionPage::on_actionMinecraftFolder_triggered() +{ + DesktopServices::openDirectory(m_inst->gameRoot(), true); +} + +void VersionPage::versionCurrent(const QModelIndex ¤t, const QModelIndex &previous) +{ + currentIdx = current.row(); + updateButtons(currentIdx); +} + +void VersionPage::preselect(int row) +{ + if(row < 0) + { + row = 0; + } + if(row >= m_profile->rowCount(QModelIndex())) + { + row = m_profile->rowCount(QModelIndex()) - 1; + } + if(row < 0) + { + return; + } + auto model_index = m_profile->index(row); + ui->packageView->selectionModel()->select(model_index, QItemSelectionModel::SelectCurrent | QItemSelectionModel::Rows); + updateButtons(row); +} + +void VersionPage::onGameUpdateError(QString error) +{ + CustomMessageBox::selectable(this, tr("Error updating instance"), error, QMessageBox::Warning)->show(); +} + +Component * VersionPage::current() +{ + auto row = currentRow(); + if(row < 0) + { + return nullptr; + } + return m_profile->getComponent(row); +} + +int VersionPage::currentRow() +{ + if (ui->packageView->selectionModel()->selectedRows().isEmpty()) + { + return -1; + } + return ui->packageView->selectionModel()->selectedRows().first().row(); +} + +void VersionPage::on_actionCustomize_triggered() +{ + auto version = currentRow(); + if(version == -1) + { + return; + } + auto patch = m_profile->getComponent(version); + if(!patch->getVersionFile()) + { + // TODO: wait for the update task to finish here... + return; + } + if(!m_profile->customize(version)) + { + // TODO: some error box here + } + updateButtons(); + preselect(currentIdx); +} + +void VersionPage::on_actionEdit_triggered() +{ + auto version = current(); + if(!version) + { + return; + } + auto filename = version->getFilename(); + if(!QFileInfo::exists(filename)) + { + qWarning() << "file" << filename << "can't be opened for editing, doesn't exist!"; + return; + } + APPLICATION->openJsonEditor(filename); +} + +void VersionPage::on_actionRevert_triggered() +{ + auto version = currentRow(); + if(version == -1) + { + return; + } + if(!m_profile->revertToBase(version)) + { + // TODO: some error box here + } + updateButtons(); + preselect(currentIdx); + m_container->refreshContainer(); +} + +void VersionPage::onFilterTextChanged(const QString &newContents) +{ + m_filterModel->setFilterFixedString(newContents); +} + +#include "VersionPage.moc" + diff --git a/launcher/ui/pages/instance/VersionPage.h b/launcher/ui/pages/instance/VersionPage.h new file mode 100644 index 00000000..b5ce4064 --- /dev/null +++ b/launcher/ui/pages/instance/VersionPage.h @@ -0,0 +1,104 @@ +/* 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 <QMainWindow> + +#include "minecraft/MinecraftInstance.h" +#include "minecraft/PackProfile.h" +#include "ui/pages/BasePage.h" + +namespace Ui +{ +class VersionPage; +} + +class VersionPage : public QMainWindow, public BasePage +{ + Q_OBJECT + +public: + explicit VersionPage(MinecraftInstance *inst, QWidget *parent = 0); + virtual ~VersionPage(); + virtual QString displayName() const override + { + return tr("Version"); + } + virtual QIcon icon() const override; + virtual QString id() const override + { + return "version"; + } + virtual QString helpPage() const override + { + return "Instance-Version"; + } + virtual bool shouldDisplay() const override; + +private slots: + void on_actionChange_version_triggered(); + void on_actionInstall_Forge_triggered(); + void on_actionInstall_Fabric_triggered(); + void on_actionAdd_Empty_triggered(); + void on_actionInstall_LiteLoader_triggered(); + void on_actionReload_triggered(); + void on_actionRemove_triggered(); + void on_actionMove_up_triggered(); + void on_actionMove_down_triggered(); + void on_actionAdd_to_Minecraft_jar_triggered(); + void on_actionReplace_Minecraft_jar_triggered(); + void on_actionRevert_triggered(); + void on_actionEdit_triggered(); + void on_actionInstall_mods_triggered(); + void on_actionCustomize_triggered(); + void on_actionDownload_All_triggered(); + + void on_actionMinecraftFolder_triggered(); + void on_actionLibrariesFolder_triggered(); + + void updateVersionControls(); + +private: + Component * current(); + int currentRow(); + void updateButtons(int row = -1); + void preselect(int row = 0); + int doUpdate(); + +protected: + QMenu * createPopupMenu() override; + + /// FIXME: this shouldn't be necessary! + bool reloadPackProfile(); + +private: + Ui::VersionPage *ui; + QSortFilterProxyModel *m_filterModel; + std::shared_ptr<PackProfile> m_profile; + MinecraftInstance *m_inst; + int currentIdx = 0; + bool controlsEnabled = false; + +public slots: + void versionCurrent(const QModelIndex ¤t, const QModelIndex &previous); + +private slots: + void updateRunningStatus(bool running); + void onGameUpdateError(QString error); + void packageCurrent(const QModelIndex ¤t, const QModelIndex &previous); + void showContextMenu(const QPoint &pos); + void onFilterTextChanged(const QString & newContents); +}; diff --git a/launcher/ui/pages/instance/VersionPage.ui b/launcher/ui/pages/instance/VersionPage.ui new file mode 100644 index 00000000..a4990ff3 --- /dev/null +++ b/launcher/ui/pages/instance/VersionPage.ui @@ -0,0 +1,285 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>VersionPage</class> + <widget class="QMainWindow" name="VersionPage"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>961</width> + <height>1091</height> + </rect> + </property> + <widget class="QWidget" name="centralwidget"> + <layout class="QHBoxLayout" name="horizontalLayout"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="ModListView" name="packageView"> + <property name="verticalScrollBarPolicy"> + <enum>Qt::ScrollBarAlwaysOn</enum> + </property> + <property name="horizontalScrollBarPolicy"> + <enum>Qt::ScrollBarAlwaysOff</enum> + </property> + <property name="sortingEnabled"> + <bool>false</bool> + </property> + <property name="headerHidden"> + <bool>false</bool> + </property> + <attribute name="headerVisible"> + <bool>true</bool> + </attribute> + </widget> + </item> + <item> + <layout class="QGridLayout" name="gridLayout_2"> + <item row="0" column="1"> + <widget class="QLineEdit" name="filterEdit"> + <property name="clearButtonEnabled"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="0" column="0"> + <widget class="QLabel" name="filterLabel"> + <property name="text"> + <string>Filter:</string> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="MCModInfoFrame" name="frame"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Minimum"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + <widget class="WideBar" name="toolBar"> + <property name="windowTitle"> + <string>Actions</string> + </property> + <property name="allowedAreas"> + <set>Qt::LeftToolBarArea|Qt::RightToolBarArea</set> + </property> + <property name="toolButtonStyle"> + <enum>Qt::ToolButtonTextOnly</enum> + </property> + <property name="floatable"> + <bool>false</bool> + </property> + <attribute name="toolBarArea"> + <enum>RightToolBarArea</enum> + </attribute> + <attribute name="toolBarBreak"> + <bool>false</bool> + </attribute> + <addaction name="actionChange_version"/> + <addaction name="actionMove_up"/> + <addaction name="actionMove_down"/> + <addaction name="actionRemove"/> + <addaction name="separator"/> + <addaction name="actionCustomize"/> + <addaction name="actionEdit"/> + <addaction name="actionRevert"/> + <addaction name="separator"/> + <addaction name="actionInstall_Forge"/> + <addaction name="actionInstall_Fabric"/> + <addaction name="actionInstall_LiteLoader"/> + <addaction name="actionInstall_mods"/> + <addaction name="separator"/> + <addaction name="actionAdd_to_Minecraft_jar"/> + <addaction name="actionReplace_Minecraft_jar"/> + <addaction name="actionAdd_Empty"/> + <addaction name="separator"/> + <addaction name="actionMinecraftFolder"/> + <addaction name="actionLibrariesFolder"/> + <addaction name="separator"/> + <addaction name="actionReload"/> + <addaction name="actionDownload_All"/> + </widget> + <action name="actionChange_version"> + <property name="text"> + <string>Change version</string> + </property> + <property name="toolTip"> + <string>Change version of the selected package.</string> + </property> + </action> + <action name="actionMove_up"> + <property name="text"> + <string>Move up</string> + </property> + <property name="toolTip"> + <string>Make the selected package apply sooner.</string> + </property> + </action> + <action name="actionMove_down"> + <property name="text"> + <string>Move down</string> + </property> + <property name="toolTip"> + <string>Make the selected package apply later.</string> + </property> + </action> + <action name="actionRemove"> + <property name="text"> + <string>Remove</string> + </property> + <property name="toolTip"> + <string>Remove selected package from the instance.</string> + </property> + </action> + <action name="actionCustomize"> + <property name="text"> + <string>Customize</string> + </property> + <property name="toolTip"> + <string>Customize selected package.</string> + </property> + </action> + <action name="actionEdit"> + <property name="text"> + <string>Edit</string> + </property> + <property name="toolTip"> + <string>Edit selected package.</string> + </property> + </action> + <action name="actionRevert"> + <property name="text"> + <string>Revert</string> + </property> + <property name="toolTip"> + <string>Revert the selected package to default.</string> + </property> + </action> + <action name="actionInstall_Forge"> + <property name="text"> + <string>Install Forge</string> + </property> + <property name="toolTip"> + <string>Install the Minecraft Forge package.</string> + </property> + </action> + <action name="actionInstall_Fabric"> + <property name="text"> + <string>Install Fabric</string> + </property> + <property name="toolTip"> + <string>Install the Fabric Loader package.</string> + </property> + </action> + <action name="actionInstall_LiteLoader"> + <property name="text"> + <string>Install LiteLoader</string> + </property> + <property name="toolTip"> + <string>Install the LiteLoader package.</string> + </property> + </action> + <action name="actionInstall_mods"> + <property name="text"> + <string>Install mods</string> + </property> + <property name="toolTip"> + <string>Install normal mods.</string> + </property> + </action> + <action name="actionAdd_to_Minecraft_jar"> + <property name="text"> + <string>Add to Minecraft.jar</string> + </property> + <property name="toolTip"> + <string>Add a mod into the Minecraft jar file.</string> + </property> + </action> + <action name="actionReplace_Minecraft_jar"> + <property name="text"> + <string>Replace Minecraft.jar</string> + </property> + </action> + <action name="actionAdd_Empty"> + <property name="text"> + <string>Add Empty</string> + </property> + <property name="toolTip"> + <string>Add an empty custom package.</string> + </property> + </action> + <action name="actionReload"> + <property name="text"> + <string>Reload</string> + </property> + <property name="toolTip"> + <string>Reload all packages.</string> + </property> + </action> + <action name="actionDownload_All"> + <property name="text"> + <string>Download All</string> + </property> + <property name="toolTip"> + <string>Download the files needed to launch the instance now.</string> + </property> + </action> + <action name="actionMinecraftFolder"> + <property name="text"> + <string>Open .minecraft</string> + </property> + <property name="toolTip"> + <string>Open the instance's .minecraft folder.</string> + </property> + </action> + <action name="actionLibrariesFolder"> + <property name="text"> + <string>Open libraries</string> + </property> + <property name="toolTip"> + <string>Open the instance's local libraries folder.</string> + </property> + </action> + </widget> + <customwidgets> + <customwidget> + <class>ModListView</class> + <extends>QTreeView</extends> + <header>ui/widgets/ModListView.h</header> + </customwidget> + <customwidget> + <class>MCModInfoFrame</class> + <extends>QFrame</extends> + <header>ui/widgets/MCModInfoFrame.h</header> + <container>1</container> + </customwidget> + <customwidget> + <class>WideBar</class> + <extends>QToolBar</extends> + <header>ui/widgets/WideBar.h</header> + </customwidget> + </customwidgets> + <resources/> + <connections/> +</ui> diff --git a/launcher/ui/pages/instance/WorldListPage.cpp b/launcher/ui/pages/instance/WorldListPage.cpp new file mode 100644 index 00000000..d2bf63bd --- /dev/null +++ b/launcher/ui/pages/instance/WorldListPage.cpp @@ -0,0 +1,412 @@ +/* Copyright 2015-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 "WorldListPage.h" +#include "ui_WorldListPage.h" +#include "minecraft/WorldList.h" + +#include <QEvent> +#include <QMenu> +#include <QKeyEvent> +#include <QClipboard> +#include <QMessageBox> +#include <QTreeView> +#include <QInputDialog> +#include <QProcess> + +#include "tools/MCEditTool.h" +#include "FileSystem.h" + +#include "ui/GuiUtil.h" +#include "DesktopServices.h" + +#include "Application.h" + + +class WorldListProxyModel : public QSortFilterProxyModel +{ + Q_OBJECT + +public: + WorldListProxyModel(QObject *parent) : QSortFilterProxyModel(parent) {} + + virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const + { + QModelIndex sourceIndex = mapToSource(index); + + if (index.column() == 0 && role == Qt::DecorationRole) + { + WorldList *worlds = qobject_cast<WorldList *>(sourceModel()); + auto iconFile = worlds->data(sourceIndex, WorldList::IconFileRole).toString(); + if(iconFile.isNull()) { + // NOTE: Minecraft uses the same placeholder for servers AND worlds + return APPLICATION->getThemedIcon("unknown_server"); + } + return QIcon(iconFile); + } + + return sourceIndex.data(role); + } +}; + + +WorldListPage::WorldListPage(BaseInstance *inst, std::shared_ptr<WorldList> worlds, QWidget *parent) + : QMainWindow(parent), m_inst(inst), ui(new Ui::WorldListPage), m_worlds(worlds) +{ + ui->setupUi(this); + + ui->toolBar->insertSpacer(ui->actionRefresh); + + WorldListProxyModel * proxy = new WorldListProxyModel(this); + proxy->setSortCaseSensitivity(Qt::CaseInsensitive); + proxy->setSourceModel(m_worlds.get()); + ui->worldTreeView->setSortingEnabled(true); + ui->worldTreeView->setModel(proxy); + ui->worldTreeView->installEventFilter(this); + ui->worldTreeView->setContextMenuPolicy(Qt::CustomContextMenu); + ui->worldTreeView->setIconSize(QSize(64,64)); + connect(ui->worldTreeView, &QTreeView::customContextMenuRequested, this, &WorldListPage::ShowContextMenu); + + auto head = ui->worldTreeView->header(); + head->setSectionResizeMode(0, QHeaderView::Stretch); + head->setSectionResizeMode(1, QHeaderView::ResizeToContents); + + connect(ui->worldTreeView->selectionModel(), &QItemSelectionModel::currentChanged, this, &WorldListPage::worldChanged); + worldChanged(QModelIndex(), QModelIndex()); +} + +void WorldListPage::openedImpl() +{ + m_worlds->startWatching(); +} + +void WorldListPage::closedImpl() +{ + m_worlds->stopWatching(); +} + +WorldListPage::~WorldListPage() +{ + m_worlds->stopWatching(); + delete ui; +} + +void WorldListPage::ShowContextMenu(const QPoint& pos) +{ + auto menu = ui->toolBar->createContextMenu(this, tr("Context menu")); + menu->exec(ui->worldTreeView->mapToGlobal(pos)); + delete menu; +} + +QMenu * WorldListPage::createPopupMenu() +{ + QMenu* filteredMenu = QMainWindow::createPopupMenu(); + filteredMenu->removeAction( ui->toolBar->toggleViewAction() ); + return filteredMenu; +} + +bool WorldListPage::shouldDisplay() const +{ + return true; +} + +bool WorldListPage::worldListFilter(QKeyEvent *keyEvent) +{ + switch (keyEvent->key()) + { + case Qt::Key_Delete: + on_actionRemove_triggered(); + return true; + default: + break; + } + return QWidget::eventFilter(ui->worldTreeView, keyEvent); +} + +bool WorldListPage::eventFilter(QObject *obj, QEvent *ev) +{ + if (ev->type() != QEvent::KeyPress) + { + return QWidget::eventFilter(obj, ev); + } + QKeyEvent *keyEvent = static_cast<QKeyEvent *>(ev); + if (obj == ui->worldTreeView) + return worldListFilter(keyEvent); + return QWidget::eventFilter(obj, ev); +} + +void WorldListPage::on_actionRemove_triggered() +{ + auto proxiedIndex = getSelectedWorld(); + + if(!proxiedIndex.isValid()) + return; + + auto result = QMessageBox::question(this, + tr("Are you sure?"), + tr("This will remove the selected world permenantly.\n" + "The world will be gone forever (A LONG TIME).\n" + "\n" + "Do you want to continue?")); + if(result != QMessageBox::Yes) + { + return; + } + m_worlds->stopWatching(); + m_worlds->deleteWorld(proxiedIndex.row()); + m_worlds->startWatching(); +} + +void WorldListPage::on_actionView_Folder_triggered() +{ + DesktopServices::openDirectory(m_worlds->dir().absolutePath(), true); +} + +void WorldListPage::on_actionDatapacks_triggered() +{ + QModelIndex index = getSelectedWorld(); + + if (!index.isValid()) + { + return; + } + + if(!worldSafetyNagQuestion()) + return; + + auto fullPath = m_worlds->data(index, WorldList::FolderRole).toString(); + + DesktopServices::openDirectory(FS::PathCombine(fullPath, "datapacks"), true); +} + + +void WorldListPage::on_actionReset_Icon_triggered() +{ + auto proxiedIndex = getSelectedWorld(); + + if(!proxiedIndex.isValid()) + return; + + if(m_worlds->resetIcon(proxiedIndex.row())) { + ui->actionReset_Icon->setEnabled(false); + } +} + + +QModelIndex WorldListPage::getSelectedWorld() +{ + auto index = ui->worldTreeView->selectionModel()->currentIndex(); + + auto proxy = (QSortFilterProxyModel *) ui->worldTreeView->model(); + return proxy->mapToSource(index); +} + +void WorldListPage::on_actionCopy_Seed_triggered() +{ + QModelIndex index = getSelectedWorld(); + + if (!index.isValid()) + { + return; + } + int64_t seed = m_worlds->data(index, WorldList::SeedRole).toLongLong(); + APPLICATION->clipboard()->setText(QString::number(seed)); +} + +void WorldListPage::on_actionMCEdit_triggered() +{ + if(m_mceditStarting) + return; + + auto mcedit = APPLICATION->mcedit(); + + const QString mceditPath = mcedit->path(); + + QModelIndex index = getSelectedWorld(); + + if (!index.isValid()) + { + return; + } + + if(!worldSafetyNagQuestion()) + return; + + auto fullPath = m_worlds->data(index, WorldList::FolderRole).toString(); + + auto program = mcedit->getProgramPath(); + if(program.size()) + { +#ifdef Q_OS_WIN32 + if(!QProcess::startDetached(program, {fullPath}, mceditPath)) + { + mceditError(); + } +#else + m_mceditProcess.reset(new LoggedProcess()); + m_mceditProcess->setDetachable(true); + connect(m_mceditProcess.get(), &LoggedProcess::stateChanged, this, &WorldListPage::mceditState); + m_mceditProcess->start(program, {fullPath}); + m_mceditProcess->setWorkingDirectory(mceditPath); + m_mceditStarting = true; +#endif + } + else + { + QMessageBox::warning( + this->parentWidget(), + tr("No MCEdit found or set up!"), + tr("You do not have MCEdit set up or it was moved.\nYou can set it up in the global settings.") + ); + } +} + +void WorldListPage::mceditError() +{ + QMessageBox::warning( + this->parentWidget(), + tr("MCEdit failed to start!"), + tr("MCEdit failed to start.\nIt may be necessary to reinstall it.") + ); +} + +void WorldListPage::mceditState(LoggedProcess::State state) +{ + bool failed = false; + switch(state) + { + case LoggedProcess::NotRunning: + case LoggedProcess::Starting: + return; + case LoggedProcess::FailedToStart: + case LoggedProcess::Crashed: + case LoggedProcess::Aborted: + { + failed = true; + } + case LoggedProcess::Running: + case LoggedProcess::Finished: + { + m_mceditStarting = false; + break; + } + } + if(failed) + { + mceditError(); + } +} + +void WorldListPage::worldChanged(const QModelIndex ¤t, const QModelIndex &previous) +{ + QModelIndex index = getSelectedWorld(); + bool enable = index.isValid(); + ui->actionCopy_Seed->setEnabled(enable); + ui->actionMCEdit->setEnabled(enable); + ui->actionRemove->setEnabled(enable); + ui->actionCopy->setEnabled(enable); + ui->actionRename->setEnabled(enable); + ui->actionDatapacks->setEnabled(enable); + bool hasIcon = !index.data(WorldList::IconFileRole).isNull(); + ui->actionReset_Icon->setEnabled(enable && hasIcon); +} + +void WorldListPage::on_actionAdd_triggered() +{ + auto list = GuiUtil::BrowseForFiles( + displayName(), + tr("Select a Minecraft world zip"), + tr("Minecraft World Zip File (*.zip)"), QString(), this->parentWidget()); + if (!list.empty()) + { + m_worlds->stopWatching(); + for (auto filename : list) + { + m_worlds->installWorld(QFileInfo(filename)); + } + m_worlds->startWatching(); + } +} + +bool WorldListPage::isWorldSafe(QModelIndex) +{ + return !m_inst->isRunning(); +} + +bool WorldListPage::worldSafetyNagQuestion() +{ + if(!isWorldSafe(getSelectedWorld())) + { + auto result = QMessageBox::question(this, tr("Copy World"), tr("Changing a world while Minecraft is running is potentially unsafe.\nDo you wish to proceed?")); + if(result == QMessageBox::No) + { + return false; + } + } + return true; +} + + +void WorldListPage::on_actionCopy_triggered() +{ + QModelIndex index = getSelectedWorld(); + if (!index.isValid()) + { + return; + } + + if(!worldSafetyNagQuestion()) + return; + + auto worldVariant = m_worlds->data(index, WorldList::ObjectRole); + auto world = (World *) worldVariant.value<void *>(); + bool ok = false; + QString name = QInputDialog::getText(this, tr("World name"), tr("Enter a new name for the copy."), QLineEdit::Normal, world->name(), &ok); + + if (ok && name.length() > 0) + { + world->install(m_worlds->dir().absolutePath(), name); + } +} + +void WorldListPage::on_actionRename_triggered() +{ + QModelIndex index = getSelectedWorld(); + if (!index.isValid()) + { + return; + } + + if(!worldSafetyNagQuestion()) + return; + + auto worldVariant = m_worlds->data(index, WorldList::ObjectRole); + auto world = (World *) worldVariant.value<void *>(); + + bool ok = false; + QString name = QInputDialog::getText(this, tr("World name"), tr("Enter a new world name."), QLineEdit::Normal, world->name(), &ok); + + if (ok && name.length() > 0) + { + world->rename(name); + } +} + +void WorldListPage::on_actionRefresh_triggered() +{ + m_worlds->update(); +} + +#include "WorldListPage.moc" diff --git a/launcher/ui/pages/instance/WorldListPage.h b/launcher/ui/pages/instance/WorldListPage.h new file mode 100644 index 00000000..e07d5794 --- /dev/null +++ b/launcher/ui/pages/instance/WorldListPage.h @@ -0,0 +1,99 @@ +/* Copyright 2015-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 <QMainWindow> + +#include "minecraft/MinecraftInstance.h" +#include "ui/pages/BasePage.h" +#include <Application.h> +#include <LoggedProcess.h> + +class WorldList; +namespace Ui +{ +class WorldListPage; +} + +class WorldListPage : public QMainWindow, public BasePage +{ + Q_OBJECT + +public: + explicit WorldListPage( + BaseInstance *inst, + std::shared_ptr<WorldList> worlds, + QWidget *parent = 0 + ); + virtual ~WorldListPage(); + + virtual QString displayName() const override + { + return tr("Worlds"); + } + virtual QIcon icon() const override + { + return APPLICATION->getThemedIcon("worlds"); + } + virtual QString id() const override + { + return "worlds"; + } + virtual QString helpPage() const override + { + return "Worlds"; + } + virtual bool shouldDisplay() const override; + + virtual void openedImpl() override; + virtual void closedImpl() override; + +protected: + bool eventFilter(QObject *obj, QEvent *ev) override; + bool worldListFilter(QKeyEvent *ev); + QMenu * createPopupMenu() override; + +protected: + BaseInstance *m_inst; + +private: + QModelIndex getSelectedWorld(); + bool isWorldSafe(QModelIndex index); + bool worldSafetyNagQuestion(); + void mceditError(); + +private: + Ui::WorldListPage *ui; + std::shared_ptr<WorldList> m_worlds; + unique_qobject_ptr<LoggedProcess> m_mceditProcess; + bool m_mceditStarting = false; + +private slots: + void on_actionCopy_Seed_triggered(); + void on_actionMCEdit_triggered(); + void on_actionRemove_triggered(); + void on_actionAdd_triggered(); + void on_actionCopy_triggered(); + void on_actionRename_triggered(); + void on_actionRefresh_triggered(); + void on_actionView_Folder_triggered(); + void on_actionDatapacks_triggered(); + void on_actionReset_Icon_triggered(); + void worldChanged(const QModelIndex ¤t, const QModelIndex &previous); + void mceditState(LoggedProcess::State state); + + void ShowContextMenu(const QPoint &pos); +}; diff --git a/launcher/ui/pages/instance/WorldListPage.ui b/launcher/ui/pages/instance/WorldListPage.ui new file mode 100644 index 00000000..7c68bfae --- /dev/null +++ b/launcher/ui/pages/instance/WorldListPage.ui @@ -0,0 +1,161 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>WorldListPage</class> + <widget class="QMainWindow" name="WorldListPage"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>800</width> + <height>600</height> + </rect> + </property> + <widget class="QWidget" name="centralwidget"> + <layout class="QHBoxLayout" name="horizontalLayout"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QTreeView" name="worldTreeView"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Expanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="acceptDrops"> + <bool>true</bool> + </property> + <property name="dragDropMode"> + <enum>QAbstractItemView::DragDrop</enum> + </property> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + <property name="rootIsDecorated"> + <bool>false</bool> + </property> + <property name="itemsExpandable"> + <bool>false</bool> + </property> + <property name="sortingEnabled"> + <bool>true</bool> + </property> + <property name="allColumnsShowFocus"> + <bool>true</bool> + </property> + <attribute name="headerStretchLastSection"> + <bool>false</bool> + </attribute> + </widget> + </item> + </layout> + </widget> + <widget class="WideBar" name="toolBar"> + <property name="windowTitle"> + <string>Actions</string> + </property> + <property name="allowedAreas"> + <set>Qt::LeftToolBarArea|Qt::RightToolBarArea</set> + </property> + <property name="toolButtonStyle"> + <enum>Qt::ToolButtonTextOnly</enum> + </property> + <property name="floatable"> + <bool>false</bool> + </property> + <attribute name="toolBarArea"> + <enum>RightToolBarArea</enum> + </attribute> + <attribute name="toolBarBreak"> + <bool>false</bool> + </attribute> + <addaction name="actionAdd"/> + <addaction name="separator"/> + <addaction name="actionRename"/> + <addaction name="actionCopy"/> + <addaction name="actionRemove"/> + <addaction name="actionMCEdit"/> + <addaction name="actionDatapacks"/> + <addaction name="actionReset_Icon"/> + <addaction name="separator"/> + <addaction name="actionCopy_Seed"/> + <addaction name="actionRefresh"/> + <addaction name="actionView_Folder"/> + </widget> + <action name="actionAdd"> + <property name="text"> + <string>Add</string> + </property> + </action> + <action name="actionRename"> + <property name="text"> + <string>Rename</string> + </property> + </action> + <action name="actionCopy"> + <property name="text"> + <string>Copy</string> + </property> + </action> + <action name="actionRemove"> + <property name="text"> + <string>Remove</string> + </property> + </action> + <action name="actionMCEdit"> + <property name="text"> + <string>MCEdit</string> + </property> + </action> + <action name="actionCopy_Seed"> + <property name="text"> + <string>Copy Seed</string> + </property> + </action> + <action name="actionRefresh"> + <property name="text"> + <string>Refresh</string> + </property> + </action> + <action name="actionView_Folder"> + <property name="text"> + <string>View Folder</string> + </property> + </action> + <action name="actionReset_Icon"> + <property name="text"> + <string>Reset Icon</string> + </property> + <property name="toolTip"> + <string>Remove world icon to make the game re-generate it on next load.</string> + </property> + </action> + <action name="actionDatapacks"> + <property name="text"> + <string>Datapacks</string> + </property> + <property name="toolTip"> + <string>Manage datapacks inside the world.</string> + </property> + </action> + </widget> + <customwidgets> + <customwidget> + <class>WideBar</class> + <extends>QToolBar</extends> + <header>ui/widgets/WideBar.h</header> + </customwidget> + </customwidgets> + <resources/> + <connections/> +</ui> diff --git a/launcher/ui/pages/modplatform/ImportPage.cpp b/launcher/ui/pages/modplatform/ImportPage.cpp new file mode 100644 index 00000000..c9e24ead --- /dev/null +++ b/launcher/ui/pages/modplatform/ImportPage.cpp @@ -0,0 +1,132 @@ +#include "ImportPage.h" +#include "ui_ImportPage.h" + +#include <QFileDialog> +#include <QValidator> + +#include "ui/dialogs/NewInstanceDialog.h" + +#include "InstanceImportTask.h" + + +class UrlValidator : public QValidator +{ +public: + using QValidator::QValidator; + + State validate(QString &in, int &pos) const + { + const QUrl url(in); + if (url.isValid() && !url.isRelative() && !url.isEmpty()) + { + return Acceptable; + } + else if (QFile::exists(in)) + { + return Acceptable; + } + else + { + return Intermediate; + } + } +}; + +ImportPage::ImportPage(NewInstanceDialog* dialog, QWidget *parent) + : QWidget(parent), ui(new Ui::ImportPage), dialog(dialog) +{ + ui->setupUi(this); + ui->modpackEdit->setValidator(new UrlValidator(ui->modpackEdit)); + connect(ui->modpackEdit, &QLineEdit::textChanged, this, &ImportPage::updateState); +} + +ImportPage::~ImportPage() +{ + delete ui; +} + +bool ImportPage::shouldDisplay() const +{ + return true; +} + +void ImportPage::openedImpl() +{ + updateState(); +} + +void ImportPage::updateState() +{ + if(!isOpened) + { + return; + } + if(ui->modpackEdit->hasAcceptableInput()) + { + QString input = ui->modpackEdit->text(); + auto url = QUrl::fromUserInput(input); + if(url.isLocalFile()) + { + // FIXME: actually do some validation of what's inside here... this is fake AF + QFileInfo fi(input); + if(fi.exists() && fi.suffix() == "zip") + { + QFileInfo fi(url.fileName()); + dialog->setSuggestedPack(fi.completeBaseName(), new InstanceImportTask(url)); + dialog->setSuggestedIcon("default"); + } + } + else + { + if(input.endsWith("?client=y")) { + input.chop(9); + input.append("/file"); + url = QUrl::fromUserInput(input); + } + // hook, line and sinker. + QFileInfo fi(url.fileName()); + dialog->setSuggestedPack(fi.completeBaseName(), new InstanceImportTask(url)); + dialog->setSuggestedIcon("default"); + } + } + else + { + dialog->setSuggestedPack(); + } +} + +void ImportPage::setUrl(const QString& url) +{ + ui->modpackEdit->setText(url); + updateState(); +} + +void ImportPage::on_modpackBtn_clicked() +{ + const QUrl url = QFileDialog::getOpenFileUrl(this, tr("Choose modpack"), modpackUrl(), tr("Zip (*.zip)")); + if (url.isValid()) + { + if (url.isLocalFile()) + { + ui->modpackEdit->setText(url.toLocalFile()); + } + else + { + ui->modpackEdit->setText(url.toString()); + } + } +} + + +QUrl ImportPage::modpackUrl() const +{ + const QUrl url(ui->modpackEdit->text()); + if (url.isValid() && !url.isRelative() && !url.host().isEmpty()) + { + return url; + } + else + { + return QUrl::fromLocalFile(ui->modpackEdit->text()); + } +} diff --git a/launcher/ui/pages/modplatform/ImportPage.h b/launcher/ui/pages/modplatform/ImportPage.h new file mode 100644 index 00000000..aba4def0 --- /dev/null +++ b/launcher/ui/pages/modplatform/ImportPage.h @@ -0,0 +1,70 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <QWidget> + +#include "ui/pages/BasePage.h" +#include <Application.h> +#include "tasks/Task.h" + +namespace Ui +{ +class ImportPage; +} + +class NewInstanceDialog; + +class ImportPage : public QWidget, public BasePage +{ + Q_OBJECT + +public: + explicit ImportPage(NewInstanceDialog* dialog, QWidget *parent = 0); + virtual ~ImportPage(); + virtual QString displayName() const override + { + return tr("Import from zip"); + } + virtual QIcon icon() const override + { + return APPLICATION->getThemedIcon("viewfolder"); + } + virtual QString id() const override + { + return "import"; + } + virtual QString helpPage() const override + { + return "Zip-import"; + } + virtual bool shouldDisplay() const override; + + void setUrl(const QString & url); + void openedImpl() override; + +private slots: + void on_modpackBtn_clicked(); + void updateState(); + +private: + QUrl modpackUrl() const; + +private: + Ui::ImportPage *ui = nullptr; + NewInstanceDialog* dialog = nullptr; +}; + diff --git a/launcher/ui/pages/modplatform/ImportPage.ui b/launcher/ui/pages/modplatform/ImportPage.ui new file mode 100644 index 00000000..eb63cbe9 --- /dev/null +++ b/launcher/ui/pages/modplatform/ImportPage.ui @@ -0,0 +1,52 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>ImportPage</class> + <widget class="QWidget" name="ImportPage"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>546</width> + <height>405</height> + </rect> + </property> + <layout class="QGridLayout" name="gridLayout"> + <item row="1" column="1"> + <widget class="QPushButton" name="modpackBtn"> + <property name="text"> + <string>Browse</string> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLineEdit" name="modpackEdit"> + <property name="placeholderText"> + <string notr="true">http://</string> + </property> + </widget> + </item> + <item row="0" column="0" colspan="2"> + <widget class="QLabel" name="modpackLabel"> + <property name="text"> + <string>Local file or link to a direct download:</string> + </property> + </widget> + </item> + <item row="2" column="0" colspan="2"> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui> diff --git a/launcher/ui/pages/modplatform/VanillaPage.cpp b/launcher/ui/pages/modplatform/VanillaPage.cpp new file mode 100644 index 00000000..5c58c1f1 --- /dev/null +++ b/launcher/ui/pages/modplatform/VanillaPage.cpp @@ -0,0 +1,103 @@ +#include "VanillaPage.h" +#include "ui_VanillaPage.h" + +#include <QTabBar> + +#include "Application.h" +#include "meta/Index.h" +#include "meta/VersionList.h" +#include "ui/dialogs/NewInstanceDialog.h" +#include "Filter.h" +#include "InstanceCreationTask.h" + +VanillaPage::VanillaPage(NewInstanceDialog *dialog, QWidget *parent) + : QWidget(parent), dialog(dialog), ui(new Ui::VanillaPage) +{ + ui->setupUi(this); + ui->tabWidget->tabBar()->hide(); + connect(ui->versionList, &VersionSelectWidget::selectedVersionChanged, this, &VanillaPage::setSelectedVersion); + filterChanged(); + connect(ui->alphaFilter, &QCheckBox::stateChanged, this, &VanillaPage::filterChanged); + connect(ui->betaFilter, &QCheckBox::stateChanged, this, &VanillaPage::filterChanged); + connect(ui->snapshotFilter, &QCheckBox::stateChanged, this, &VanillaPage::filterChanged); + connect(ui->oldSnapshotFilter, &QCheckBox::stateChanged, this, &VanillaPage::filterChanged); + connect(ui->releaseFilter, &QCheckBox::stateChanged, this, &VanillaPage::filterChanged); + connect(ui->experimentsFilter, &QCheckBox::stateChanged, this, &VanillaPage::filterChanged); + connect(ui->refreshBtn, &QPushButton::clicked, this, &VanillaPage::refresh); +} + +void VanillaPage::openedImpl() +{ + if(!initialized) + { + auto vlist = APPLICATION->metadataIndex()->get("net.minecraft"); + ui->versionList->initialize(vlist.get()); + initialized = true; + } + else + { + suggestCurrent(); + } +} + +void VanillaPage::refresh() +{ + ui->versionList->loadList(); +} + +void VanillaPage::filterChanged() +{ + QStringList out; + if(ui->alphaFilter->isChecked()) + out << "(old_alpha)"; + if(ui->betaFilter->isChecked()) + out << "(old_beta)"; + if(ui->snapshotFilter->isChecked()) + out << "(snapshot)"; + if(ui->oldSnapshotFilter->isChecked()) + out << "(old_snapshot)"; + if(ui->releaseFilter->isChecked()) + out << "(release)"; + if(ui->experimentsFilter->isChecked()) + out << "(experiment)"; + auto regexp = out.join('|'); + ui->versionList->setFilter(BaseVersionList::TypeRole, new RegexpFilter(regexp, false)); +} + +VanillaPage::~VanillaPage() +{ + delete ui; +} + +bool VanillaPage::shouldDisplay() const +{ + return true; +} + +BaseVersionPtr VanillaPage::selectedVersion() const +{ + return m_selectedVersion; +} + +void VanillaPage::suggestCurrent() +{ + if (!isOpened) + { + return; + } + + if(!m_selectedVersion) + { + dialog->setSuggestedPack(); + return; + } + + dialog->setSuggestedPack(m_selectedVersion->descriptor(), new InstanceCreationTask(m_selectedVersion)); + dialog->setSuggestedIcon("default"); +} + +void VanillaPage::setSelectedVersion(BaseVersionPtr version) +{ + m_selectedVersion = version; + suggestCurrent(); +} diff --git a/launcher/ui/pages/modplatform/VanillaPage.h b/launcher/ui/pages/modplatform/VanillaPage.h new file mode 100644 index 00000000..fd4c2daa --- /dev/null +++ b/launcher/ui/pages/modplatform/VanillaPage.h @@ -0,0 +1,75 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <QWidget> + +#include "ui/pages/BasePage.h" +#include <Application.h> +#include "tasks/Task.h" + +namespace Ui +{ +class VanillaPage; +} + +class NewInstanceDialog; + +class VanillaPage : public QWidget, public BasePage +{ + Q_OBJECT + +public: + explicit VanillaPage(NewInstanceDialog *dialog, QWidget *parent = 0); + virtual ~VanillaPage(); + virtual QString displayName() const override + { + return tr("Vanilla"); + } + virtual QIcon icon() const override + { + return APPLICATION->getThemedIcon("minecraft"); + } + virtual QString id() const override + { + return "vanilla"; + } + virtual QString helpPage() const override + { + return "Vanilla-platform"; + } + virtual bool shouldDisplay() const override; + void openedImpl() override; + + BaseVersionPtr selectedVersion() const; + +public slots: + void setSelectedVersion(BaseVersionPtr version); + +private slots: + void filterChanged(); + +private: + void refresh(); + void suggestCurrent(); + +private: + bool initialized = false; + NewInstanceDialog *dialog = nullptr; + Ui::VanillaPage *ui = nullptr; + bool m_versionSetByUser = false; + BaseVersionPtr m_selectedVersion; +}; diff --git a/launcher/ui/pages/modplatform/VanillaPage.ui b/launcher/ui/pages/modplatform/VanillaPage.ui new file mode 100644 index 00000000..870ff161 --- /dev/null +++ b/launcher/ui/pages/modplatform/VanillaPage.ui @@ -0,0 +1,169 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>VanillaPage</class> + <widget class="QWidget" name="VanillaPage"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>815</width> + <height>607</height> + </rect> + </property> + <layout class="QVBoxLayout" name="verticalLayout_5"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QTabWidget" name="tabWidget"> + <property name="currentIndex"> + <number>0</number> + </property> + <widget class="QWidget" name="tab"> + <attribute name="title"> + <string notr="true"/> + </attribute> + <layout class="QGridLayout" name="gridLayout_2"> + <item row="0" column="1"> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Filter</string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="releaseFilter"> + <property name="text"> + <string>Releases</string> + </property> + <property name="checkable"> + <bool>true</bool> + </property> + <property name="checked"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="snapshotFilter"> + <property name="text"> + <string>Snapshots</string> + </property> + <property name="checkable"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="oldSnapshotFilter"> + <property name="text"> + <string>Old Snapshots</string> + </property> + <property name="checkable"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="betaFilter"> + <property name="text"> + <string>Betas</string> + </property> + <property name="checkable"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="alphaFilter"> + <property name="text"> + <string>Alphas</string> + </property> + <property name="checkable"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="experimentsFilter"> + <property name="text"> + <string>Experiments</string> + </property> + <property name="checkable"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QPushButton" name="refreshBtn"> + <property name="text"> + <string>Refresh</string> + </property> + </widget> + </item> + </layout> + </item> + <item row="0" column="0"> + <widget class="VersionSelectWidget" name="versionList" native="true"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Expanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + </widget> + </item> + </layout> + </widget> + </widget> + </item> + </layout> + </widget> + <customwidgets> + <customwidget> + <class>VersionSelectWidget</class> + <extends>QWidget</extends> + <header>ui/widgets/VersionSelectWidget.h</header> + <container>1</container> + </customwidget> + </customwidgets> + <tabstops> + <tabstop>tabWidget</tabstop> + <tabstop>releaseFilter</tabstop> + <tabstop>snapshotFilter</tabstop> + <tabstop>oldSnapshotFilter</tabstop> + <tabstop>betaFilter</tabstop> + <tabstop>alphaFilter</tabstop> + <tabstop>experimentsFilter</tabstop> + <tabstop>refreshBtn</tabstop> + </tabstops> + <resources/> + <connections/> +</ui> diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlFilterModel.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlFilterModel.cpp new file mode 100644 index 00000000..b5d8f22b --- /dev/null +++ b/launcher/ui/pages/modplatform/atlauncher/AtlFilterModel.cpp @@ -0,0 +1,81 @@ +#include "AtlFilterModel.h" + +#include <QDebug> + +#include <modplatform/atlauncher/ATLPackIndex.h> +#include <Version.h> +#include <MMCStrings.h> + +namespace Atl { + +FilterModel::FilterModel(QObject *parent) : QSortFilterProxyModel(parent) +{ + currentSorting = Sorting::ByPopularity; + sortings.insert(tr("Sort by popularity"), Sorting::ByPopularity); + sortings.insert(tr("Sort by name"), Sorting::ByName); + sortings.insert(tr("Sort by game version"), Sorting::ByGameVersion); + + searchTerm = ""; +} + +const QMap<QString, FilterModel::Sorting> FilterModel::getAvailableSortings() +{ + return sortings; +} + +QString FilterModel::translateCurrentSorting() +{ + return sortings.key(currentSorting); +} + +void FilterModel::setSorting(Sorting sorting) +{ + currentSorting = sorting; + invalidate(); +} + +FilterModel::Sorting FilterModel::getCurrentSorting() +{ + return currentSorting; +} + +void FilterModel::setSearchTerm(const QString term) +{ + searchTerm = term.trimmed(); + invalidate(); +} + +bool FilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const +{ + if (searchTerm.isEmpty()) { + return true; + } + + QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent); + ATLauncher::IndexedPack pack = sourceModel()->data(index, Qt::UserRole).value<ATLauncher::IndexedPack>(); + return pack.name.contains(searchTerm, Qt::CaseInsensitive); +} + +bool FilterModel::lessThan(const QModelIndex &left, const QModelIndex &right) const +{ + ATLauncher::IndexedPack leftPack = sourceModel()->data(left, Qt::UserRole).value<ATLauncher::IndexedPack>(); + ATLauncher::IndexedPack rightPack = sourceModel()->data(right, Qt::UserRole).value<ATLauncher::IndexedPack>(); + + if (currentSorting == ByPopularity) { + return leftPack.position > rightPack.position; + } + else if (currentSorting == ByGameVersion) { + Version lv(leftPack.versions.at(0).minecraft); + Version rv(rightPack.versions.at(0).minecraft); + return lv < rv; + } + else if (currentSorting == ByName) { + return Strings::naturalCompare(leftPack.name, rightPack.name, Qt::CaseSensitive) >= 0; + } + + // Invalid sorting set, somehow... + qWarning() << "Invalid sorting set!"; + return true; +} + +} diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlFilterModel.h b/launcher/ui/pages/modplatform/atlauncher/AtlFilterModel.h new file mode 100644 index 00000000..bd72ad91 --- /dev/null +++ b/launcher/ui/pages/modplatform/atlauncher/AtlFilterModel.h @@ -0,0 +1,34 @@ +#pragma once + +#include <QtCore/QSortFilterProxyModel> + +namespace Atl { + +class FilterModel : public QSortFilterProxyModel +{ + Q_OBJECT +public: + FilterModel(QObject* parent = Q_NULLPTR); + enum Sorting { + ByPopularity, + ByGameVersion, + ByName, + }; + const QMap<QString, Sorting> getAvailableSortings(); + QString translateCurrentSorting(); + void setSorting(Sorting sorting); + Sorting getCurrentSorting(); + void setSearchTerm(QString term); + +protected: + bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override; + bool lessThan(const QModelIndex &left, const QModelIndex &right) const override; + +private: + QMap<QString, Sorting> sortings; + Sorting currentSorting; + QString searchTerm; + +}; + +} diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp new file mode 100644 index 00000000..e8c6deee --- /dev/null +++ b/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp @@ -0,0 +1,193 @@ +#include "AtlListModel.h" + +#include <BuildConfig.h> +#include <Application.h> +#include <Json.h> + +namespace Atl { + +ListModel::ListModel(QObject *parent) : QAbstractListModel(parent) +{ +} + +ListModel::~ListModel() +{ +} + +int ListModel::rowCount(const QModelIndex &parent) const +{ + return modpacks.size(); +} + +int ListModel::columnCount(const QModelIndex &parent) const +{ + return 1; +} + +QVariant ListModel::data(const QModelIndex &index, int role) const +{ + int pos = index.row(); + if(pos >= modpacks.size() || pos < 0 || !index.isValid()) + { + return QString("INVALID INDEX %1").arg(pos); + } + + ATLauncher::IndexedPack pack = modpacks.at(pos); + if(role == Qt::DisplayRole) + { + return pack.name; + } + else if (role == Qt::ToolTipRole) + { + return pack.name; + } + else if(role == Qt::DecorationRole) + { + if(m_logoMap.contains(pack.safeName)) + { + return (m_logoMap.value(pack.safeName)); + } + auto icon = APPLICATION->getThemedIcon("atlauncher-placeholder"); + + auto url = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "launcher/images/%1.png").arg(pack.safeName.toLower()); + ((ListModel *)this)->requestLogo(pack.safeName, url); + + return icon; + } + else if(role == Qt::UserRole) + { + QVariant v; + v.setValue(pack); + return v; + } + + return QVariant(); +} + +void ListModel::request() +{ + beginResetModel(); + modpacks.clear(); + endResetModel(); + + auto *netJob = new NetJob("Atl::Request"); + auto url = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "launcher/json/packsnew.json"); + netJob->addNetAction(Net::Download::makeByteArray(QUrl(url), &response)); + jobPtr = netJob; + jobPtr->start(APPLICATION->network()); + + QObject::connect(netJob, &NetJob::succeeded, this, &ListModel::requestFinished); + QObject::connect(netJob, &NetJob::failed, this, &ListModel::requestFailed); +} + +void ListModel::requestFinished() +{ + jobPtr.reset(); + + QJsonParseError parse_error; + QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error); + if(parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from ATL at " << parse_error.offset << " reason: " << parse_error.errorString(); + qWarning() << response; + return; + } + + QList<ATLauncher::IndexedPack> newList; + + auto packs = doc.array(); + for(auto packRaw : packs) { + auto packObj = packRaw.toObject(); + + ATLauncher::IndexedPack pack; + + try { + ATLauncher::loadIndexedPack(pack, packObj); + } + catch (const JSONValidationError &e) { + qDebug() << QString::fromUtf8(response); + qWarning() << "Error while reading pack manifest from ATLauncher: " << e.cause(); + return; + } + + // ignore packs without a published version + if(pack.versions.length() == 0) continue; + // only display public packs (for now) + if(pack.type != ATLauncher::PackType::Public) continue; + // ignore "system" packs (Vanilla, Vanilla with Forge, etc) + if(pack.system) continue; + + newList.append(pack); + } + + beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size() + newList.size() - 1); + modpacks.append(newList); + endInsertRows(); +} + +void ListModel::requestFailed(QString reason) +{ + jobPtr.reset(); +} + +void ListModel::getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback) +{ + if(m_logoMap.contains(logo)) + { + callback(APPLICATION->metacache()->resolveEntry("ATLauncherPacks", QString("logos/%1").arg(logo.section(".", 0, 0)))->getFullPath()); + } + else + { + requestLogo(logo, logoUrl); + } +} + +void ListModel::logoFailed(QString logo) +{ + m_failedLogos.append(logo); + m_loadingLogos.removeAll(logo); +} + +void ListModel::logoLoaded(QString logo, QIcon out) +{ + m_loadingLogos.removeAll(logo); + m_logoMap.insert(logo, out); + + for(int i = 0; i < modpacks.size(); i++) { + if(modpacks[i].safeName == logo) { + emit dataChanged(createIndex(i, 0), createIndex(i, 0), {Qt::DecorationRole}); + } + } +} + +void ListModel::requestLogo(QString file, QString url) +{ + if(m_loadingLogos.contains(file) || m_failedLogos.contains(file)) + { + return; + } + + MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("ATLauncherPacks", QString("logos/%1").arg(file.section(".", 0, 0))); + NetJob *job = new NetJob(QString("ATLauncher Icon Download %1").arg(file)); + job->addNetAction(Net::Download::makeCached(QUrl(url), entry)); + + auto fullPath = entry->getFullPath(); + QObject::connect(job, &NetJob::succeeded, this, [this, file, fullPath] + { + emit logoLoaded(file, QIcon(fullPath)); + if(waitingCallbacks.contains(file)) + { + waitingCallbacks.value(file)(fullPath); + } + }); + + QObject::connect(job, &NetJob::failed, this, [this, file] + { + emit logoFailed(file); + }); + + job->start(APPLICATION->network()); + + m_loadingLogos.append(file); +} + +} diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlListModel.h b/launcher/ui/pages/modplatform/atlauncher/AtlListModel.h new file mode 100644 index 00000000..79aa8180 --- /dev/null +++ b/launcher/ui/pages/modplatform/atlauncher/AtlListModel.h @@ -0,0 +1,52 @@ +#pragma once + +#include <QAbstractListModel> + +#include "net/NetJob.h" +#include <QIcon> +#include <modplatform/atlauncher/ATLPackIndex.h> + +namespace Atl { + +typedef QMap<QString, QIcon> LogoMap; +typedef std::function<void(QString)> LogoCallback; + +class ListModel : public QAbstractListModel +{ + Q_OBJECT + +public: + ListModel(QObject *parent); + virtual ~ListModel(); + + int rowCount(const QModelIndex &parent) const override; + int columnCount(const QModelIndex &parent) const override; + QVariant data(const QModelIndex &index, int role) const override; + + void request(); + + void getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback); + +private slots: + void requestFinished(); + void requestFailed(QString reason); + + void logoFailed(QString logo); + void logoLoaded(QString logo, QIcon out); + +private: + void requestLogo(QString file, QString url); + +private: + QList<ATLauncher::IndexedPack> modpacks; + + QStringList m_failedLogos; + QStringList m_loadingLogos; + LogoMap m_logoMap; + QMap<QString, LogoCallback> waitingCallbacks; + + NetJob::Ptr jobPtr; + QByteArray response; +}; + +} diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp new file mode 100644 index 00000000..14bbd18b --- /dev/null +++ b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp @@ -0,0 +1,209 @@ +#include "AtlOptionalModDialog.h" +#include "ui_AtlOptionalModDialog.h" + +AtlOptionalModListModel::AtlOptionalModListModel(QWidget *parent, QVector<ATLauncher::VersionMod> mods) + : QAbstractListModel(parent), m_mods(mods) { + + // fill mod index + for (int i = 0; i < m_mods.size(); i++) { + auto mod = m_mods.at(i); + m_index[mod.name] = i; + } + // set initial state + for (int i = 0; i < m_mods.size(); i++) { + auto mod = m_mods.at(i); + m_selection[mod.name] = false; + setMod(mod, i, mod.selected, false); + } +} + +QVector<QString> AtlOptionalModListModel::getResult() { + QVector<QString> result; + + for (const auto& mod : m_mods) { + if (m_selection[mod.name]) { + result.push_back(mod.name); + } + } + + return result; +} + +int AtlOptionalModListModel::rowCount(const QModelIndex &parent) const { + return m_mods.size(); +} + +int AtlOptionalModListModel::columnCount(const QModelIndex &parent) const { + // Enabled, Name, Description + return 3; +} + +QVariant AtlOptionalModListModel::data(const QModelIndex &index, int role) const { + auto row = index.row(); + auto mod = m_mods.at(row); + + if (role == Qt::DisplayRole) { + if (index.column() == NameColumn) { + return mod.name; + } + if (index.column() == DescriptionColumn) { + return mod.description; + } + } + else if (role == Qt::ToolTipRole) { + if (index.column() == DescriptionColumn) { + return mod.description; + } + } + else if (role == Qt::CheckStateRole) { + if (index.column() == EnabledColumn) { + return m_selection[mod.name] ? Qt::Checked : Qt::Unchecked; + } + } + + return QVariant(); +} + +bool AtlOptionalModListModel::setData(const QModelIndex &index, const QVariant &value, int role) { + if (role == Qt::CheckStateRole) { + auto row = index.row(); + auto mod = m_mods.at(row); + + toggleMod(mod, row); + return true; + } + + return false; +} + +QVariant AtlOptionalModListModel::headerData(int section, Qt::Orientation orientation, int role) const { + if (role == Qt::DisplayRole && orientation == Qt::Horizontal) { + switch (section) { + case EnabledColumn: + return QString(); + case NameColumn: + return QString("Name"); + case DescriptionColumn: + return QString("Description"); + } + } + + return QVariant(); +} + +Qt::ItemFlags AtlOptionalModListModel::flags(const QModelIndex &index) const { + auto flags = QAbstractListModel::flags(index); + if (index.isValid() && index.column() == EnabledColumn) { + flags |= Qt::ItemIsUserCheckable; + } + return flags; +} + +void AtlOptionalModListModel::selectRecommended() { + for (const auto& mod : m_mods) { + m_selection[mod.name] = mod.recommended; + } + + emit dataChanged(AtlOptionalModListModel::index(0, EnabledColumn), + AtlOptionalModListModel::index(m_mods.size() - 1, EnabledColumn)); +} + +void AtlOptionalModListModel::clearAll() { + for (const auto& mod : m_mods) { + m_selection[mod.name] = false; + } + + emit dataChanged(AtlOptionalModListModel::index(0, EnabledColumn), + AtlOptionalModListModel::index(m_mods.size() - 1, EnabledColumn)); +} + +void AtlOptionalModListModel::toggleMod(ATLauncher::VersionMod mod, int index) { + setMod(mod, index, !m_selection[mod.name]); +} + +void AtlOptionalModListModel::setMod(ATLauncher::VersionMod mod, int index, bool enable, bool shouldEmit) { + if (m_selection[mod.name] == enable) return; + + m_selection[mod.name] = enable; + + // disable other mods in the group, if applicable + if (enable && !mod.group.isEmpty()) { + for (int i = 0; i < m_mods.size(); i++) { + if (index == i) continue; + auto other = m_mods.at(i); + + if (mod.group == other.group) { + setMod(other, i, false, shouldEmit); + } + } + } + + for (const auto& dependencyName : mod.depends) { + auto dependencyIndex = m_index[dependencyName]; + auto dependencyMod = m_mods.at(dependencyIndex); + + // enable/disable dependencies + if (enable) { + setMod(dependencyMod, dependencyIndex, true, shouldEmit); + } + + // if the dependency is 'effectively hidden', then track which mods + // depend on it - so we can efficiently disable it when no more dependents + // depend on it. + auto dependants = m_dependants[dependencyName]; + + if (enable) { + dependants.append(mod.name); + } + else { + dependants.removeAll(mod.name); + + // if there are no longer any dependents, let's disable the mod + if (dependencyMod.effectively_hidden && dependants.isEmpty()) { + setMod(dependencyMod, dependencyIndex, false, shouldEmit); + } + } + } + + // disable mods that depend on this one, if disabling + if (!enable) { + auto dependants = m_dependants[mod.name]; + for (const auto& dependencyName : dependants) { + auto dependencyIndex = m_index[dependencyName]; + auto dependencyMod = m_mods.at(dependencyIndex); + + setMod(dependencyMod, dependencyIndex, false, shouldEmit); + } + } + + if (shouldEmit) { + emit dataChanged(AtlOptionalModListModel::index(index, EnabledColumn), + AtlOptionalModListModel::index(index, EnabledColumn)); + } +} + + +AtlOptionalModDialog::AtlOptionalModDialog(QWidget *parent, QVector<ATLauncher::VersionMod> mods) + : QDialog(parent), ui(new Ui::AtlOptionalModDialog) { + ui->setupUi(this); + + listModel = new AtlOptionalModListModel(this, mods); + ui->treeView->setModel(listModel); + + ui->treeView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + ui->treeView->header()->setSectionResizeMode( + AtlOptionalModListModel::NameColumn, QHeaderView::ResizeToContents); + ui->treeView->header()->setSectionResizeMode( + AtlOptionalModListModel::DescriptionColumn, QHeaderView::Stretch); + + connect(ui->selectRecommendedButton, &QPushButton::pressed, + listModel, &AtlOptionalModListModel::selectRecommended); + connect(ui->clearAllButton, &QPushButton::pressed, + listModel, &AtlOptionalModListModel::clearAll); + connect(ui->installButton, &QPushButton::pressed, + this, &QDialog::close); +} + +AtlOptionalModDialog::~AtlOptionalModDialog() { + delete ui; +} diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h new file mode 100644 index 00000000..a1df43f6 --- /dev/null +++ b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h @@ -0,0 +1,66 @@ +#pragma once + +#include <QDialog> +#include <QAbstractListModel> + +#include "modplatform/atlauncher/ATLPackIndex.h" + +namespace Ui { +class AtlOptionalModDialog; +} + +class AtlOptionalModListModel : public QAbstractListModel { + Q_OBJECT + +public: + enum Columns + { + EnabledColumn = 0, + NameColumn, + DescriptionColumn, + }; + + AtlOptionalModListModel(QWidget *parent, QVector<ATLauncher::VersionMod> mods); + + QVector<QString> getResult(); + + int rowCount(const QModelIndex &parent) const override; + int columnCount(const QModelIndex &parent) const override; + + QVariant data(const QModelIndex &index, int role) const override; + bool setData(const QModelIndex &index, const QVariant &value, int role) override; + QVariant headerData(int section, Qt::Orientation orientation, int role) const override; + + Qt::ItemFlags flags(const QModelIndex &index) const override; + +public slots: + void selectRecommended(); + void clearAll(); + +private: + void toggleMod(ATLauncher::VersionMod mod, int index); + void setMod(ATLauncher::VersionMod mod, int index, bool enable, bool shouldEmit = true); + +private: + QVector<ATLauncher::VersionMod> m_mods; + QMap<QString, bool> m_selection; + QMap<QString, int> m_index; + QMap<QString, QVector<QString>> m_dependants; +}; + +class AtlOptionalModDialog : public QDialog { + Q_OBJECT + +public: + AtlOptionalModDialog(QWidget *parent, QVector<ATLauncher::VersionMod> mods); + ~AtlOptionalModDialog() override; + + QVector<QString> getResult() { + return listModel->getResult(); + } + +private: + Ui::AtlOptionalModDialog *ui; + + AtlOptionalModListModel *listModel; +}; diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.ui b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.ui new file mode 100644 index 00000000..4c5c2ec5 --- /dev/null +++ b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.ui @@ -0,0 +1,65 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>AtlOptionalModDialog</class> + <widget class="QDialog" name="AtlOptionalModDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>550</width> + <height>310</height> + </rect> + </property> + <property name="windowTitle"> + <string>Select Mods To Install</string> + </property> + <layout class="QGridLayout" name="gridLayout_2"> + <item row="1" column="3"> + <widget class="QPushButton" name="installButton"> + <property name="text"> + <string>Install</string> + </property> + <property name="default"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QPushButton" name="selectRecommendedButton"> + <property name="text"> + <string>Select Recommended</string> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QPushButton" name="shareCodeButton"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="text"> + <string>Use Share Code</string> + </property> + </widget> + </item> + <item row="1" column="2"> + <widget class="QPushButton" name="clearAllButton"> + <property name="text"> + <string>Clear All</string> + </property> + </widget> + </item> + <item row="0" column="0" colspan="4"> + <widget class="ModListView" name="treeView"/> + </item> + </layout> + </widget> + <customwidgets> + <customwidget> + <class>ModListView</class> + <extends>QTreeView</extends> + <header>ui/widgets/ModListView.h</header> + </customwidget> + </customwidgets> + <resources/> + <connections/> +</ui> diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp new file mode 100644 index 00000000..5f6a1396 --- /dev/null +++ b/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp @@ -0,0 +1,171 @@ +#include "AtlPage.h" +#include "ui_AtlPage.h" + +#include "modplatform/atlauncher/ATLPackInstallTask.h" + +#include "AtlOptionalModDialog.h" +#include "ui/dialogs/NewInstanceDialog.h" +#include "ui/dialogs/VersionSelectDialog.h" + +#include <BuildConfig.h> + +AtlPage::AtlPage(NewInstanceDialog* dialog, QWidget *parent) + : QWidget(parent), ui(new Ui::AtlPage), dialog(dialog) +{ + ui->setupUi(this); + + filterModel = new Atl::FilterModel(this); + listModel = new Atl::ListModel(this); + filterModel->setSourceModel(listModel); + ui->packView->setModel(filterModel); + ui->packView->setSortingEnabled(true); + + ui->packView->header()->hide(); + ui->packView->setIndentation(0); + + ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); + ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300); + + for(int i = 0; i < filterModel->getAvailableSortings().size(); i++) + { + ui->sortByBox->addItem(filterModel->getAvailableSortings().keys().at(i)); + } + ui->sortByBox->setCurrentText(filterModel->translateCurrentSorting()); + + connect(ui->searchEdit, &QLineEdit::textChanged, this, &AtlPage::triggerSearch); + connect(ui->sortByBox, &QComboBox::currentTextChanged, this, &AtlPage::onSortingSelectionChanged); + connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &AtlPage::onSelectionChanged); + connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &AtlPage::onVersionSelectionChanged); +} + +AtlPage::~AtlPage() +{ + delete ui; +} + +bool AtlPage::shouldDisplay() const +{ + return true; +} + +void AtlPage::openedImpl() +{ + if(!initialized) + { + listModel->request(); + initialized = true; + } + + suggestCurrent(); +} + +void AtlPage::suggestCurrent() +{ + if(!isOpened) + { + return; + } + + if (selectedVersion.isEmpty()) + { + dialog->setSuggestedPack(); + return; + } + + dialog->setSuggestedPack(selected.name + " " + selectedVersion, new ATLauncher::PackInstallTask(this, selected.safeName, selectedVersion)); + auto editedLogoName = selected.safeName; + auto url = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "launcher/images/%1.png").arg(selected.safeName.toLower()); + listModel->getLogo(selected.safeName, url, [this, editedLogoName](QString logo) + { + dialog->setSuggestedIconFromFile(logo, editedLogoName); + }); +} + +void AtlPage::triggerSearch() +{ + filterModel->setSearchTerm(ui->searchEdit->text()); +} + +void AtlPage::onSortingSelectionChanged(QString data) +{ + auto toSet = filterModel->getAvailableSortings().value(data); + filterModel->setSorting(toSet); +} + +void AtlPage::onSelectionChanged(QModelIndex first, QModelIndex second) +{ + ui->versionSelectionBox->clear(); + + if(!first.isValid()) + { + if(isOpened) + { + dialog->setSuggestedPack(); + } + return; + } + + selected = filterModel->data(first, Qt::UserRole).value<ATLauncher::IndexedPack>(); + + ui->packDescription->setHtml(selected.description.replace("\n", "<br>")); + + for(const auto& version : selected.versions) { + ui->versionSelectionBox->addItem(version.version); + } + + suggestCurrent(); +} + +void AtlPage::onVersionSelectionChanged(QString data) +{ + if(data.isNull() || data.isEmpty()) + { + selectedVersion = ""; + return; + } + + selectedVersion = data; + suggestCurrent(); +} + +QVector<QString> AtlPage::chooseOptionalMods(QVector<ATLauncher::VersionMod> mods) { + AtlOptionalModDialog optionalModDialog(this, mods); + optionalModDialog.exec(); + return optionalModDialog.getResult(); +} + +QString AtlPage::chooseVersion(Meta::VersionListPtr vlist, QString minecraftVersion) { + VersionSelectDialog vselect(vlist.get(), "Choose Version", APPLICATION->activeWindow(), false); + if (minecraftVersion != Q_NULLPTR) { + vselect.setExactFilter(BaseVersionList::ParentVersionRole, minecraftVersion); + vselect.setEmptyString(tr("No versions are currently available for Minecraft %1").arg(minecraftVersion)); + } + else { + vselect.setEmptyString(tr("No versions are currently available")); + } + vselect.setEmptyErrorString(tr("Couldn't load or download the version lists!")); + + // select recommended build + for (int i = 0; i < vlist->versions().size(); i++) { + auto version = vlist->versions().at(i); + auto reqs = version->requires(); + + // filter by minecraft version, if the loader depends on a certain version. + if (minecraftVersion != Q_NULLPTR) { + auto iter = std::find_if(reqs.begin(), reqs.end(), [](const Meta::Require &req) { + return req.uid == "net.minecraft"; + }); + if (iter == reqs.end()) continue; + if (iter->equalsVersion != minecraftVersion) continue; + } + + // first recommended build we find, we use. + if (version->isRecommended()) { + vselect.setCurrentVersion(version->descriptor()); + break; + } + } + + vselect.exec(); + return vselect.selectedVersion()->descriptor(); +} diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlPage.h b/launcher/ui/pages/modplatform/atlauncher/AtlPage.h new file mode 100644 index 00000000..b95b3d9e --- /dev/null +++ b/launcher/ui/pages/modplatform/atlauncher/AtlPage.h @@ -0,0 +1,86 @@ +/* Copyright 2013-2019 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 "AtlFilterModel.h" +#include "AtlListModel.h" + +#include <QWidget> +#include <modplatform/atlauncher/ATLPackInstallTask.h> + +#include "Application.h" +#include "ui/pages/BasePage.h" +#include "tasks/Task.h" + +namespace Ui +{ + class AtlPage; +} + +class NewInstanceDialog; + +class AtlPage : public QWidget, public BasePage, public ATLauncher::UserInteractionSupport +{ +Q_OBJECT + +public: + explicit AtlPage(NewInstanceDialog* dialog, QWidget *parent = 0); + virtual ~AtlPage(); + virtual QString displayName() const override + { + return tr("ATLauncher"); + } + virtual QIcon icon() const override + { + return APPLICATION->getThemedIcon("atlauncher"); + } + virtual QString id() const override + { + return "atl"; + } + virtual QString helpPage() const override + { + return "ATL-platform"; + } + virtual bool shouldDisplay() const override; + + void openedImpl() override; + +private: + void suggestCurrent(); + + QString chooseVersion(Meta::VersionListPtr vlist, QString minecraftVersion) override; + QVector<QString> chooseOptionalMods(QVector<ATLauncher::VersionMod> mods) override; + +private slots: + void triggerSearch(); + + void onSortingSelectionChanged(QString data); + + void onSelectionChanged(QModelIndex first, QModelIndex second); + void onVersionSelectionChanged(QString data); + +private: + Ui::AtlPage *ui = nullptr; + NewInstanceDialog* dialog = nullptr; + Atl::ListModel* listModel = nullptr; + Atl::FilterModel* filterModel = nullptr; + + ATLauncher::IndexedPack selected; + QString selectedVersion; + + bool initialized = false; +}; diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlPage.ui b/launcher/ui/pages/modplatform/atlauncher/AtlPage.ui new file mode 100644 index 00000000..9085766a --- /dev/null +++ b/launcher/ui/pages/modplatform/atlauncher/AtlPage.ui @@ -0,0 +1,92 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>AtlPage</class> + <widget class="QWidget" name="AtlPage"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>837</width> + <height>685</height> + </rect> + </property> + <layout class="QGridLayout" name="gridLayout"> + <item row="1" column="0" colspan="2"> + <layout class="QGridLayout" name="gridLayout_3"> + <item row="1" column="0"> + <widget class="QTreeView" name="packView"> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + <property name="iconSize"> + <size> + <width>96</width> + <height>48</height> + </size> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QTextBrowser" name="packDescription"> + <property name="openExternalLinks"> + <bool>true</bool> + </property> + <property name="openLinks"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="0" column="0" colspan="2"> + <widget class="QLabel" name="label_2"> + <property name="text"> + <string>Warning: This is still a work in progress. If you run into issues with the imported modpack, it may be a bug.</string> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </item> + <item row="2" column="0" colspan="2"> + <layout class="QGridLayout" name="gridLayout_4" columnstretch="0,0,0" rowminimumheight="0" columnminimumwidth="0,0,0"> + <item row="0" column="2"> + <widget class="QComboBox" name="versionSelectionBox"/> + </item> + <item row="0" column="1"> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Version selected:</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + </widget> + </item> + <item row="0" column="0"> + <widget class="QComboBox" name="sortByBox"/> + </item> + </layout> + </item> + <item row="0" column="0"> + <widget class="QLineEdit" name="searchEdit"> + <property name="placeholderText"> + <string>Search and filter ...</string> + </property> + <property name="clearButtonEnabled"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </widget> + <tabstops> + <tabstop>searchEdit</tabstop> + <tabstop>packView</tabstop> + <tabstop>packDescription</tabstop> + <tabstop>sortByBox</tabstop> + <tabstop>versionSelectionBox</tabstop> + </tabstops> + <resources/> + <connections/> +</ui> diff --git a/launcher/ui/pages/modplatform/flame/FlameModel.cpp b/launcher/ui/pages/modplatform/flame/FlameModel.cpp new file mode 100644 index 00000000..a05ab641 --- /dev/null +++ b/launcher/ui/pages/modplatform/flame/FlameModel.cpp @@ -0,0 +1,258 @@ +#include "FlameModel.h" +#include "Application.h" +#include <Json.h> + +#include <MMCStrings.h> +#include <Version.h> + +#include <QtMath> +#include <QLabel> + +#include <RWStorage.h> + +namespace Flame { + +ListModel::ListModel(QObject *parent) : QAbstractListModel(parent) +{ +} + +ListModel::~ListModel() +{ +} + +int ListModel::rowCount(const QModelIndex &parent) const +{ + return modpacks.size(); +} + +int ListModel::columnCount(const QModelIndex &parent) const +{ + return 1; +} + +QVariant ListModel::data(const QModelIndex &index, int role) const +{ + int pos = index.row(); + if(pos >= modpacks.size() || pos < 0 || !index.isValid()) + { + return QString("INVALID INDEX %1").arg(pos); + } + + IndexedPack pack = modpacks.at(pos); + if(role == Qt::DisplayRole) + { + return pack.name; + } + else if (role == Qt::ToolTipRole) + { + if(pack.description.length() > 100) + { + //some magic to prevent to long tooltips and replace html linebreaks + QString edit = pack.description.left(97); + edit = edit.left(edit.lastIndexOf("<br>")).left(edit.lastIndexOf(" ")).append("..."); + return edit; + + } + return pack.description; + } + else if(role == Qt::DecorationRole) + { + if(m_logoMap.contains(pack.logoName)) + { + return (m_logoMap.value(pack.logoName)); + } + QIcon icon = APPLICATION->getThemedIcon("screenshot-placeholder"); + ((ListModel *)this)->requestLogo(pack.logoName, pack.logoUrl); + return icon; + } + else if(role == Qt::UserRole) + { + QVariant v; + v.setValue(pack); + return v; + } + + return QVariant(); +} + +void ListModel::logoLoaded(QString logo, QIcon out) +{ + m_loadingLogos.removeAll(logo); + m_logoMap.insert(logo, out); + for(int i = 0; i < modpacks.size(); i++) { + if(modpacks[i].logoName == logo) { + emit dataChanged(createIndex(i, 0), createIndex(i, 0), {Qt::DecorationRole}); + } + } +} + +void ListModel::logoFailed(QString logo) +{ + m_failedLogos.append(logo); + m_loadingLogos.removeAll(logo); +} + +void ListModel::requestLogo(QString logo, QString url) +{ + if(m_loadingLogos.contains(logo) || m_failedLogos.contains(logo)) + { + return; + } + + MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("FlamePacks", QString("logos/%1").arg(logo.section(".", 0, 0))); + NetJob *job = new NetJob(QString("Flame Icon Download %1").arg(logo)); + job->addNetAction(Net::Download::makeCached(QUrl(url), entry)); + + auto fullPath = entry->getFullPath(); + QObject::connect(job, &NetJob::succeeded, this, [this, logo, fullPath] + { + emit logoLoaded(logo, QIcon(fullPath)); + if(waitingCallbacks.contains(logo)) + { + waitingCallbacks.value(logo)(fullPath); + } + }); + + QObject::connect(job, &NetJob::failed, this, [this, logo] + { + emit logoFailed(logo); + }); + + job->start(APPLICATION->network()); + + m_loadingLogos.append(logo); +} + +void ListModel::getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback) +{ + if(m_logoMap.contains(logo)) + { + callback(APPLICATION->metacache()->resolveEntry("FlamePacks", QString("logos/%1").arg(logo.section(".", 0, 0)))->getFullPath()); + } + else + { + requestLogo(logo, logoUrl); + } +} + +Qt::ItemFlags ListModel::flags(const QModelIndex &index) const +{ + return QAbstractListModel::flags(index); +} + +bool ListModel::canFetchMore(const QModelIndex& parent) const +{ + return searchState == CanPossiblyFetchMore; +} + +void ListModel::fetchMore(const QModelIndex& parent) +{ + if (parent.isValid()) + return; + if(nextSearchOffset == 0) { + qWarning() << "fetchMore with 0 offset is wrong..."; + return; + } + performPaginatedSearch(); +} + +void ListModel::performPaginatedSearch() +{ + NetJob *netJob = new NetJob("Flame::Search"); + auto searchUrl = QString( + "https://addons-ecs.forgesvc.net/api/v2/addon/search?" + "categoryId=0&" + "gameId=432&" + "index=%1&" + "pageSize=25&" + "searchFilter=%2&" + "sectionId=4471&" + "sort=%3" + ).arg(nextSearchOffset).arg(currentSearchTerm).arg(currentSort); + netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response)); + jobPtr = netJob; + jobPtr->start(APPLICATION->network()); + QObject::connect(netJob, &NetJob::succeeded, this, &ListModel::searchRequestFinished); + QObject::connect(netJob, &NetJob::failed, this, &ListModel::searchRequestFailed); +} + +void ListModel::searchWithTerm(const QString& term, int sort) +{ + if(currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull() && currentSort == sort) { + return; + } + currentSearchTerm = term; + currentSort = sort; + if(jobPtr) { + jobPtr->abort(); + searchState = ResetRequested; + return; + } + else { + beginResetModel(); + modpacks.clear(); + endResetModel(); + searchState = None; + } + nextSearchOffset = 0; + performPaginatedSearch(); +} + +void Flame::ListModel::searchRequestFinished() +{ + jobPtr.reset(); + + QJsonParseError parse_error; + QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error); + if(parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from CurseForge at " << parse_error.offset << " reason: " << parse_error.errorString(); + qWarning() << response; + return; + } + + QList<Flame::IndexedPack> newList; + auto packs = doc.array(); + for(auto packRaw : packs) { + auto packObj = packRaw.toObject(); + + Flame::IndexedPack pack; + try + { + Flame::loadIndexedPack(pack, packObj); + newList.append(pack); + } + catch(const JSONValidationError &e) + { + qWarning() << "Error while loading pack from CurseForge: " << e.cause(); + continue; + } + } + if(packs.size() < 25) { + searchState = Finished; + } else { + nextSearchOffset += 25; + searchState = CanPossiblyFetchMore; + } + beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size() + newList.size() - 1); + modpacks.append(newList); + endInsertRows(); +} + +void Flame::ListModel::searchRequestFailed(QString reason) +{ + jobPtr.reset(); + + if(searchState == ResetRequested) { + beginResetModel(); + modpacks.clear(); + endResetModel(); + + nextSearchOffset = 0; + performPaginatedSearch(); + } else { + searchState = Finished; + } +} + +} + diff --git a/launcher/ui/pages/modplatform/flame/FlameModel.h b/launcher/ui/pages/modplatform/flame/FlameModel.h new file mode 100644 index 00000000..536f6add --- /dev/null +++ b/launcher/ui/pages/modplatform/flame/FlameModel.h @@ -0,0 +1,76 @@ +#pragma once + +#include <RWStorage.h> + +#include <QAbstractListModel> +#include <QSortFilterProxyModel> +#include <QThreadPool> +#include <QIcon> +#include <QStyledItemDelegate> +#include <QList> +#include <QString> +#include <QStringList> +#include <QMetaType> + +#include <functional> +#include <net/NetJob.h> + +#include <modplatform/flame/FlamePackIndex.h> + +namespace Flame { + + +typedef QMap<QString, QIcon> LogoMap; +typedef std::function<void(QString)> LogoCallback; + +class ListModel : public QAbstractListModel +{ + Q_OBJECT + +public: + ListModel(QObject *parent); + virtual ~ListModel(); + + int rowCount(const QModelIndex &parent) const override; + int columnCount(const QModelIndex &parent) const override; + QVariant data(const QModelIndex &index, int role) const override; + Qt::ItemFlags flags(const QModelIndex &index) const override; + bool canFetchMore(const QModelIndex & parent) const override; + void fetchMore(const QModelIndex & parent) override; + + void getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback); + void searchWithTerm(const QString & term, const int sort); + +private slots: + void performPaginatedSearch(); + + void logoFailed(QString logo); + void logoLoaded(QString logo, QIcon out); + + void searchRequestFinished(); + void searchRequestFailed(QString reason); + +private: + void requestLogo(QString file, QString url); + +private: + QList<IndexedPack> modpacks; + QStringList m_failedLogos; + QStringList m_loadingLogos; + LogoMap m_logoMap; + QMap<QString, LogoCallback> waitingCallbacks; + + QString currentSearchTerm; + int currentSort = 0; + int nextSearchOffset = 0; + enum SearchState { + None, + CanPossiblyFetchMore, + ResetRequested, + Finished + } searchState = None; + NetJob::Ptr jobPtr; + QByteArray response; +}; + +} diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.cpp b/launcher/ui/pages/modplatform/flame/FlamePage.cpp new file mode 100644 index 00000000..cb1185f7 --- /dev/null +++ b/launcher/ui/pages/modplatform/flame/FlamePage.cpp @@ -0,0 +1,186 @@ +#include "FlamePage.h" +#include "ui_FlamePage.h" + +#include <QKeyEvent> + +#include "Application.h" +#include "Json.h" +#include "ui/dialogs/NewInstanceDialog.h" +#include "InstanceImportTask.h" +#include "FlameModel.h" + +FlamePage::FlamePage(NewInstanceDialog* dialog, QWidget *parent) + : QWidget(parent), ui(new Ui::FlamePage), dialog(dialog) +{ + ui->setupUi(this); + connect(ui->searchButton, &QPushButton::clicked, this, &FlamePage::triggerSearch); + ui->searchEdit->installEventFilter(this); + listModel = new Flame::ListModel(this); + ui->packView->setModel(listModel); + + ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); + ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300); + + // index is used to set the sorting with the curseforge api + ui->sortByBox->addItem(tr("Sort by featured")); + ui->sortByBox->addItem(tr("Sort by popularity")); + ui->sortByBox->addItem(tr("Sort by last updated")); + ui->sortByBox->addItem(tr("Sort by name")); + ui->sortByBox->addItem(tr("Sort by author")); + ui->sortByBox->addItem(tr("Sort by total downloads")); + + connect(ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch())); + connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FlamePage::onSelectionChanged); + connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &FlamePage::onVersionSelectionChanged); +} + +FlamePage::~FlamePage() +{ + delete ui; +} + +bool FlamePage::eventFilter(QObject* watched, QEvent* event) +{ + if (watched == ui->searchEdit && event->type() == QEvent::KeyPress) { + QKeyEvent* keyEvent = static_cast<QKeyEvent*>(event); + if (keyEvent->key() == Qt::Key_Return) { + triggerSearch(); + keyEvent->accept(); + return true; + } + } + return QWidget::eventFilter(watched, event); +} + +bool FlamePage::shouldDisplay() const +{ + return true; +} + +void FlamePage::openedImpl() +{ + suggestCurrent(); + triggerSearch(); +} + +void FlamePage::triggerSearch() +{ + listModel->searchWithTerm(ui->searchEdit->text(), ui->sortByBox->currentIndex()); +} + +void FlamePage::onSelectionChanged(QModelIndex first, QModelIndex second) +{ + ui->versionSelectionBox->clear(); + + if(!first.isValid()) + { + if(isOpened) + { + dialog->setSuggestedPack(); + } + return; + } + + current = listModel->data(first, Qt::UserRole).value<Flame::IndexedPack>(); + QString text = ""; + QString name = current.name; + + if (current.websiteUrl.isEmpty()) + text = name; + else + text = "<a href=\"" + current.websiteUrl + "\">" + name + "</a>"; + if (!current.authors.empty()) { + auto authorToStr = [](Flame::ModpackAuthor & author) { + if(author.url.isEmpty()) { + return author.name; + } + return QString("<a href=\"%1\">%2</a>").arg(author.url, author.name); + }; + QStringList authorStrs; + for(auto & author: current.authors) { + authorStrs.push_back(authorToStr(author)); + } + text += "<br>" + tr(" by ") + authorStrs.join(", "); + } + text += "<br><br>"; + + ui->packDescription->setHtml(text + current.description); + + if (current.versionsLoaded == false) + { + qDebug() << "Loading flame modpack versions"; + NetJob *netJob = new NetJob(QString("Flame::PackVersions(%1)").arg(current.name)); + std::shared_ptr<QByteArray> response = std::make_shared<QByteArray>(); + int addonId = current.addonId; + netJob->addNetAction(Net::Download::makeByteArray(QString("https://addons-ecs.forgesvc.net/api/v2/addon/%1/files").arg(addonId), response.get())); + + QObject::connect(netJob, &NetJob::succeeded, this, [this, response] + { + QJsonParseError parse_error; + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + if(parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from CurseForge at " << parse_error.offset << " reason: " << parse_error.errorString(); + qWarning() << *response; + return; + } + QJsonArray arr = doc.array(); + try + { + Flame::loadIndexedPackVersions(current, arr); + } + catch(const JSONValidationError &e) + { + qDebug() << *response; + qWarning() << "Error while reading flame modpack version: " << e.cause(); + } + + for(auto version : current.versions) { + ui->versionSelectionBox->addItem(version.version, QVariant(version.downloadUrl)); + } + + suggestCurrent(); + }); + netJob->start(APPLICATION->network()); + } + else + { + for(auto version : current.versions) { + ui->versionSelectionBox->addItem(version.version, QVariant(version.downloadUrl)); + } + + suggestCurrent(); + } +} + +void FlamePage::suggestCurrent() +{ + if(!isOpened) + { + return; + } + + if (selectedVersion.isEmpty()) + { + dialog->setSuggestedPack(); + return; + } + + dialog->setSuggestedPack(current.name, new InstanceImportTask(selectedVersion)); + QString editedLogoName; + editedLogoName = "curseforge_" + current.logoName.section(".", 0, 0); + listModel->getLogo(current.logoName, current.logoUrl, [this, editedLogoName](QString logo) + { + dialog->setSuggestedIconFromFile(logo, editedLogoName); + }); +} + +void FlamePage::onVersionSelectionChanged(QString data) +{ + if(data.isNull() || data.isEmpty()) + { + selectedVersion = ""; + return; + } + selectedVersion = ui->versionSelectionBox->currentData().toString(); + suggestCurrent(); +} diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.h b/launcher/ui/pages/modplatform/flame/FlamePage.h new file mode 100644 index 00000000..5cfe21dc --- /dev/null +++ b/launcher/ui/pages/modplatform/flame/FlamePage.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 <QWidget> + +#include "ui/pages/BasePage.h" +#include <Application.h> +#include "tasks/Task.h" +#include <modplatform/flame/FlamePackIndex.h> + +namespace Ui +{ +class FlamePage; +} + +class NewInstanceDialog; + +namespace Flame { + class ListModel; +} + +class FlamePage : public QWidget, public BasePage +{ + Q_OBJECT + +public: + explicit FlamePage(NewInstanceDialog* dialog, QWidget *parent = 0); + virtual ~FlamePage(); + virtual QString displayName() const override + { + return tr("CurseForge"); + } + virtual QIcon icon() const override + { + return APPLICATION->getThemedIcon("flame"); + } + virtual QString id() const override + { + return "flame"; + } + virtual QString helpPage() const override + { + return "Flame-platform"; + } + virtual bool shouldDisplay() const override; + + void openedImpl() override; + + bool eventFilter(QObject * watched, QEvent * event) override; + +private: + void suggestCurrent(); + +private slots: + void triggerSearch(); + void onSelectionChanged(QModelIndex first, QModelIndex second); + void onVersionSelectionChanged(QString data); + +private: + Ui::FlamePage *ui = nullptr; + NewInstanceDialog* dialog = nullptr; + Flame::ListModel* listModel = nullptr; + Flame::IndexedPack current; + + QString selectedVersion; +}; diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.ui b/launcher/ui/pages/modplatform/flame/FlamePage.ui new file mode 100644 index 00000000..9723815a --- /dev/null +++ b/launcher/ui/pages/modplatform/flame/FlamePage.ui @@ -0,0 +1,90 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>FlamePage</class> + <widget class="QWidget" name="FlamePage"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>837</width> + <height>685</height> + </rect> + </property> + <layout class="QGridLayout" name="gridLayout"> + <item row="1" column="0" colspan="2"> + <layout class="QGridLayout" name="gridLayout_3"> + <item row="1" column="0"> + <widget class="QListView" name="packView"> + <property name="iconSize"> + <size> + <width>48</width> + <height>48</height> + </size> + </property> + <property name="horizontalScrollBarPolicy"> + <enum>Qt::ScrollBarAlwaysOff</enum> + </property> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QTextBrowser" name="packDescription"> + <property name="openExternalLinks"> + <bool>true</bool> + </property> + <property name="openLinks"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </item> + <item row="2" column="0" colspan="2"> + <layout class="QGridLayout" name="gridLayout_4" columnstretch="0,0,0" rowminimumheight="0" columnminimumwidth="0,0,0"> + <item row="0" column="2"> + <widget class="QComboBox" name="versionSelectionBox"/> + </item> + <item row="0" column="1"> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Version selected:</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + </widget> + </item> + <item row="0" column="0"> + <widget class="QComboBox" name="sortByBox"/> + </item> + </layout> + </item> + <item row="0" column="1"> + <widget class="QPushButton" name="searchButton"> + <property name="text"> + <string>Search</string> + </property> + </widget> + </item> + <item row="0" column="0"> + <widget class="QLineEdit" name="searchEdit"> + <property name="placeholderText"> + <string>Search and filter ...</string> + </property> + </widget> + </item> + </layout> + </widget> + <tabstops> + <tabstop>searchEdit</tabstop> + <tabstop>searchButton</tabstop> + <tabstop>packView</tabstop> + <tabstop>packDescription</tabstop> + <tabstop>sortByBox</tabstop> + <tabstop>versionSelectionBox</tabstop> + </tabstops> + <resources/> + <connections/> +</ui> diff --git a/launcher/ui/pages/modplatform/ftb/FtbFilterModel.cpp b/launcher/ui/pages/modplatform/ftb/FtbFilterModel.cpp new file mode 100644 index 00000000..793b8769 --- /dev/null +++ b/launcher/ui/pages/modplatform/ftb/FtbFilterModel.cpp @@ -0,0 +1,76 @@ +#include "FtbFilterModel.h" + +#include <QDebug> + +#include "modplatform/modpacksch/FTBPackManifest.h" +#include <MMCStrings.h> + +namespace Ftb { + +FilterModel::FilterModel(QObject *parent) : QSortFilterProxyModel(parent) +{ + currentSorting = Sorting::ByPlays; + sortings.insert(tr("Sort by plays"), Sorting::ByPlays); + sortings.insert(tr("Sort by installs"), Sorting::ByInstalls); + sortings.insert(tr("Sort by name"), Sorting::ByName); +} + +const QMap<QString, FilterModel::Sorting> FilterModel::getAvailableSortings() +{ + return sortings; +} + +QString FilterModel::translateCurrentSorting() +{ + return sortings.key(currentSorting); +} + +void FilterModel::setSorting(Sorting sorting) +{ + currentSorting = sorting; + invalidate(); +} + +FilterModel::Sorting FilterModel::getCurrentSorting() +{ + return currentSorting; +} + +void FilterModel::setSearchTerm(const QString& term) +{ + searchTerm = term.trimmed(); + invalidate(); +} + +bool FilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const +{ + if (searchTerm.isEmpty()) { + return true; + } + + auto index = sourceModel()->index(sourceRow, 0, sourceParent); + auto pack = sourceModel()->data(index, Qt::UserRole).value<ModpacksCH::Modpack>(); + return pack.name.contains(searchTerm, Qt::CaseInsensitive); +} + +bool FilterModel::lessThan(const QModelIndex &left, const QModelIndex &right) const +{ + ModpacksCH::Modpack leftPack = sourceModel()->data(left, Qt::UserRole).value<ModpacksCH::Modpack>(); + ModpacksCH::Modpack rightPack = sourceModel()->data(right, Qt::UserRole).value<ModpacksCH::Modpack>(); + + if (currentSorting == ByPlays) { + return leftPack.plays < rightPack.plays; + } + else if (currentSorting == ByInstalls) { + return leftPack.installs < rightPack.installs; + } + else if (currentSorting == ByName) { + return Strings::naturalCompare(leftPack.name, rightPack.name, Qt::CaseSensitive) >= 0; + } + + // Invalid sorting set, somehow... + qWarning() << "Invalid sorting set!"; + return true; +} + +} diff --git a/launcher/ui/pages/modplatform/ftb/FtbFilterModel.h b/launcher/ui/pages/modplatform/ftb/FtbFilterModel.h new file mode 100644 index 00000000..2e712c7d --- /dev/null +++ b/launcher/ui/pages/modplatform/ftb/FtbFilterModel.h @@ -0,0 +1,35 @@ +#pragma once + +#include <QtCore/QSortFilterProxyModel> + +namespace Ftb { + +class FilterModel : public QSortFilterProxyModel +{ + Q_OBJECT + +public: + FilterModel(QObject* parent = Q_NULLPTR); + enum Sorting { + ByPlays, + ByInstalls, + ByName, + }; + const QMap<QString, Sorting> getAvailableSortings(); + QString translateCurrentSorting(); + void setSorting(Sorting sorting); + Sorting getCurrentSorting(); + void setSearchTerm(const QString& term); + +protected: + bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override; + bool lessThan(const QModelIndex &left, const QModelIndex &right) const override; + +private: + QMap<QString, Sorting> sortings; + Sorting currentSorting; + QString searchTerm { "" }; + +}; + +} diff --git a/launcher/ui/pages/modplatform/ftb/FtbListModel.cpp b/launcher/ui/pages/modplatform/ftb/FtbListModel.cpp new file mode 100644 index 00000000..59cd0b85 --- /dev/null +++ b/launcher/ui/pages/modplatform/ftb/FtbListModel.cpp @@ -0,0 +1,278 @@ +#include "FtbListModel.h" + +#include "BuildConfig.h" +#include "Application.h" +#include "Json.h" + +#include <QPainter> + +namespace Ftb { + +ListModel::ListModel(QObject *parent) : QAbstractListModel(parent) +{ +} + +ListModel::~ListModel() +{ +} + +int ListModel::rowCount(const QModelIndex &parent) const +{ + return modpacks.size(); +} + +int ListModel::columnCount(const QModelIndex &parent) const +{ + return 1; +} + +QVariant ListModel::data(const QModelIndex &index, int role) const +{ + int pos = index.row(); + if(pos >= modpacks.size() || pos < 0 || !index.isValid()) + { + return QString("INVALID INDEX %1").arg(pos); + } + + ModpacksCH::Modpack pack = modpacks.at(pos); + if(role == Qt::DisplayRole) + { + return pack.name; + } + else if (role == Qt::ToolTipRole) + { + return pack.synopsis; + } + else if(role == Qt::DecorationRole) + { + QIcon placeholder = APPLICATION->getThemedIcon("screenshot-placeholder"); + + auto iter = m_logoMap.find(pack.name); + if (iter != m_logoMap.end()) { + auto & logo = *iter; + if(!logo.result.isNull()) { + return logo.result; + } + return placeholder; + } + + for(auto art : pack.art) { + if(art.type == "square") { + ((ListModel *)this)->requestLogo(pack.name, art.url); + } + } + return placeholder; + } + else if(role == Qt::UserRole) + { + QVariant v; + v.setValue(pack); + return v; + } + + return QVariant(); +} + +void ListModel::getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback) +{ + if(m_logoMap.contains(logo)) + { + callback(APPLICATION->metacache()->resolveEntry("ModpacksCHPacks", QString("logos/%1").arg(logo.section(".", 0, 0)))->getFullPath()); + } + else + { + requestLogo(logo, logoUrl); + } +} + +void ListModel::request() +{ + beginResetModel(); + modpacks.clear(); + endResetModel(); + + auto *netJob = new NetJob("Ftb::Request"); + auto url = QString(BuildConfig.MODPACKSCH_API_BASE_URL + "public/modpack/all"); + netJob->addNetAction(Net::Download::makeByteArray(QUrl(url), &response)); + jobPtr = netJob; + jobPtr->start(APPLICATION->network()); + + QObject::connect(netJob, &NetJob::succeeded, this, &ListModel::requestFinished); + QObject::connect(netJob, &NetJob::failed, this, &ListModel::requestFailed); +} + +void ListModel::requestFinished() +{ + jobPtr.reset(); + remainingPacks.clear(); + + QJsonParseError parse_error; + QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error); + if(parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from FTB at " << parse_error.offset << " reason: " << parse_error.errorString(); + qWarning() << response; + return; + } + + auto packs = doc.object().value("packs").toArray(); + for(auto pack : packs) { + auto packId = pack.toInt(); + remainingPacks.append(packId); + } + + if(!remainingPacks.isEmpty()) { + currentPack = remainingPacks.at(0); + requestPack(); + } +} + +void ListModel::requestFailed(QString reason) +{ + jobPtr.reset(); + remainingPacks.clear(); +} + +void ListModel::requestPack() +{ + auto *netJob = new NetJob("Ftb::Search"); + auto searchUrl = QString(BuildConfig.MODPACKSCH_API_BASE_URL + "public/modpack/%1") + .arg(currentPack); + netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response)); + jobPtr = netJob; + jobPtr->start(APPLICATION->network()); + + QObject::connect(netJob, &NetJob::succeeded, this, &ListModel::packRequestFinished); + QObject::connect(netJob, &NetJob::failed, this, &ListModel::packRequestFailed); +} + +void ListModel::packRequestFinished() +{ + jobPtr.reset(); + remainingPacks.removeOne(currentPack); + + QJsonParseError parse_error; + QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error); + + if(parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from FTB at " << parse_error.offset << " reason: " << parse_error.errorString(); + qWarning() << response; + return; + } + + auto obj = doc.object(); + + ModpacksCH::Modpack pack; + try + { + ModpacksCH::loadModpack(pack, obj); + } + catch (const JSONValidationError &e) + { + qDebug() << QString::fromUtf8(response); + qWarning() << "Error while reading pack manifest from FTB: " << e.cause(); + return; + } + + // Since there is no guarantee that packs have a version, this will just + // ignore those "dud" packs. + if (pack.versions.empty()) + { + qWarning() << "FTB Pack " << pack.id << " ignored. reason: lacking any versions"; + } + else + { + beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size()); + modpacks.append(pack); + endInsertRows(); + } + + if(!remainingPacks.isEmpty()) { + currentPack = remainingPacks.at(0); + requestPack(); + } +} + +void ListModel::packRequestFailed(QString reason) +{ + jobPtr.reset(); + remainingPacks.removeOne(currentPack); +} + +void ListModel::logoLoaded(QString logo, bool stale) +{ + auto & logoObj = m_logoMap[logo]; + logoObj.downloadJob.reset(); + QString smallPath = logoObj.fullpath + ".small"; + + QFileInfo smallInfo(smallPath); + + if(stale || !smallInfo.exists()) { + QImage image(logoObj.fullpath); + if (image.isNull()) + { + logoObj.failed = true; + return; + } + QImage small; + if (image.width() > image.height()) { + small = image.scaledToWidth(512).scaledToWidth(256, Qt::SmoothTransformation); + } + else { + small = image.scaledToHeight(512).scaledToHeight(256, Qt::SmoothTransformation); + } + QPoint offset((256 - small.width()) / 2, (256 - small.height()) / 2); + QImage square(QSize(256, 256), QImage::Format_ARGB32); + square.fill(Qt::transparent); + + QPainter painter(&square); + painter.drawImage(offset, small); + painter.end(); + + square.save(logoObj.fullpath + ".small", "PNG"); + } + + logoObj.result = QIcon(logoObj.fullpath + ".small"); + for(int i = 0; i < modpacks.size(); i++) { + if(modpacks[i].name == logo) { + emit dataChanged(createIndex(i, 0), createIndex(i, 0), {Qt::DecorationRole}); + } + } +} + +void ListModel::logoFailed(QString logo) +{ + m_logoMap[logo].failed = true; + m_logoMap[logo].downloadJob.reset(); +} + +void ListModel::requestLogo(QString logo, QString url) +{ + if(m_logoMap.contains(logo)) { + return; + } + + MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("ModpacksCHPacks", QString("logos/%1").arg(logo.section(".", 0, 0))); + + bool stale = entry->isStale(); + + NetJob *job = new NetJob(QString("FTB Icon Download %1").arg(logo)); + job->addNetAction(Net::Download::makeCached(QUrl(url), entry)); + + auto fullPath = entry->getFullPath(); + QObject::connect(job, &NetJob::finished, this, [this, logo, fullPath, stale] + { + logoLoaded(logo, stale); + }); + + QObject::connect(job, &NetJob::failed, this, [this, logo] + { + logoFailed(logo); + }); + + auto &newLogoEntry = m_logoMap[logo]; + newLogoEntry.downloadJob = job; + newLogoEntry.fullpath = fullPath; + job->start(APPLICATION->network()); +} + +} diff --git a/launcher/ui/pages/modplatform/ftb/FtbListModel.h b/launcher/ui/pages/modplatform/ftb/FtbListModel.h new file mode 100644 index 00000000..e2b73c25 --- /dev/null +++ b/launcher/ui/pages/modplatform/ftb/FtbListModel.h @@ -0,0 +1,61 @@ +#pragma once + +#include <QAbstractListModel> + +#include "modplatform/modpacksch/FTBPackManifest.h" +#include "net/NetJob.h" +#include <QIcon> + +namespace Ftb { + +struct Logo { + QString fullpath; + NetJob::Ptr downloadJob; + QIcon result; + bool failed = false; +}; + +typedef QMap<QString, Logo> LogoMap; +typedef std::function<void(QString)> LogoCallback; + +class ListModel : public QAbstractListModel +{ + Q_OBJECT + +public: + ListModel(QObject *parent); + virtual ~ListModel(); + + int rowCount(const QModelIndex &parent) const override; + int columnCount(const QModelIndex &parent) const override; + QVariant data(const QModelIndex &index, int role) const override; + + void request(); + + void getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback); + +private slots: + void requestFinished(); + void requestFailed(QString reason); + + void requestPack(); + void packRequestFinished(); + void packRequestFailed(QString reason); + + void logoFailed(QString logo); + void logoLoaded(QString logo, bool stale); + +private: + void requestLogo(QString file, QString url); + +private: + QList<ModpacksCH::Modpack> modpacks; + LogoMap m_logoMap; + + NetJob::Ptr jobPtr; + int currentPack; + QList<int> remainingPacks; + QByteArray response; +}; + +} diff --git a/launcher/ui/pages/modplatform/ftb/FtbPage.cpp b/launcher/ui/pages/modplatform/ftb/FtbPage.cpp new file mode 100644 index 00000000..a82de1d6 --- /dev/null +++ b/launcher/ui/pages/modplatform/ftb/FtbPage.cpp @@ -0,0 +1,150 @@ +#include "FtbPage.h" +#include "ui_FtbPage.h" + +#include <QKeyEvent> + +#include "ui/dialogs/NewInstanceDialog.h" +#include "modplatform/modpacksch/FTBPackInstallTask.h" + +#include "HoeDown.h" + +FtbPage::FtbPage(NewInstanceDialog* dialog, QWidget *parent) + : QWidget(parent), ui(new Ui::FtbPage), dialog(dialog) +{ + ui->setupUi(this); + + filterModel = new Ftb::FilterModel(this); + listModel = new Ftb::ListModel(this); + filterModel->setSourceModel(listModel); + ui->packView->setModel(filterModel); + ui->packView->setSortingEnabled(true); + ui->packView->header()->hide(); + ui->packView->setIndentation(0); + + ui->searchEdit->installEventFilter(this); + + ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); + ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300); + + for(int i = 0; i < filterModel->getAvailableSortings().size(); i++) + { + ui->sortByBox->addItem(filterModel->getAvailableSortings().keys().at(i)); + } + ui->sortByBox->setCurrentText(filterModel->translateCurrentSorting()); + + connect(ui->searchEdit, &QLineEdit::textChanged, this, &FtbPage::triggerSearch); + connect(ui->sortByBox, &QComboBox::currentTextChanged, this, &FtbPage::onSortingSelectionChanged); + connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FtbPage::onSelectionChanged); + connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &FtbPage::onVersionSelectionChanged); +} + +FtbPage::~FtbPage() +{ + delete ui; +} + +bool FtbPage::eventFilter(QObject* watched, QEvent* event) +{ + if (watched == ui->searchEdit && event->type() == QEvent::KeyPress) { + QKeyEvent* keyEvent = static_cast<QKeyEvent*>(event); + if (keyEvent->key() == Qt::Key_Return) { + triggerSearch(); + keyEvent->accept(); + return true; + } + } + return QWidget::eventFilter(watched, event); +} + +bool FtbPage::shouldDisplay() const +{ + return true; +} + +void FtbPage::openedImpl() +{ + if(!initialised) + { + listModel->request(); + initialised = true; + } + + suggestCurrent(); +} + +void FtbPage::suggestCurrent() +{ + if(!isOpened) + { + return; + } + + if (selectedVersion.isEmpty()) + { + dialog->setSuggestedPack(); + return; + } + + dialog->setSuggestedPack(selected.name + " " + selectedVersion, new ModpacksCH::PackInstallTask(selected, selectedVersion)); + for(auto art : selected.art) { + if(art.type == "square") { + QString editedLogoName; + editedLogoName = selected.name; + + listModel->getLogo(selected.name, art.url, [this, editedLogoName](QString logo) + { + dialog->setSuggestedIconFromFile(logo + ".small", editedLogoName); + }); + } + } +} + +void FtbPage::triggerSearch() +{ + filterModel->setSearchTerm(ui->searchEdit->text()); +} + +void FtbPage::onSortingSelectionChanged(QString data) +{ + auto toSet = filterModel->getAvailableSortings().value(data); + filterModel->setSorting(toSet); +} + +void FtbPage::onSelectionChanged(QModelIndex first, QModelIndex second) +{ + ui->versionSelectionBox->clear(); + + if(!first.isValid()) + { + if(isOpened) + { + dialog->setSuggestedPack(); + } + return; + } + + selected = filterModel->data(first, Qt::UserRole).value<ModpacksCH::Modpack>(); + + HoeDown hoedown; + QString output = hoedown.process(selected.description.toUtf8()); + ui->packDescription->setHtml(output); + + // reverse foreach, so that the newest versions are first + for (auto i = selected.versions.size(); i--;) { + ui->versionSelectionBox->addItem(selected.versions.at(i).name); + } + + suggestCurrent(); +} + +void FtbPage::onVersionSelectionChanged(QString data) +{ + if(data.isNull() || data.isEmpty()) + { + selectedVersion = ""; + return; + } + + selectedVersion = data; + suggestCurrent(); +} diff --git a/launcher/ui/pages/modplatform/ftb/FtbPage.h b/launcher/ui/pages/modplatform/ftb/FtbPage.h new file mode 100644 index 00000000..28a189f0 --- /dev/null +++ b/launcher/ui/pages/modplatform/ftb/FtbPage.h @@ -0,0 +1,83 @@ +/* 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 "FtbFilterModel.h" +#include "FtbListModel.h" + +#include <QWidget> + +#include "Application.h" +#include "ui/pages/BasePage.h" +#include "tasks/Task.h" + +namespace Ui +{ + class FtbPage; +} + +class NewInstanceDialog; + +class FtbPage : public QWidget, public BasePage +{ +Q_OBJECT + +public: + explicit FtbPage(NewInstanceDialog* dialog, QWidget *parent = 0); + virtual ~FtbPage(); + virtual QString displayName() const override + { + return tr("FTB"); + } + virtual QIcon icon() const override + { + return APPLICATION->getThemedIcon("ftb_logo"); + } + virtual QString id() const override + { + return "ftb"; + } + virtual QString helpPage() const override + { + return "FTB-platform"; + } + virtual bool shouldDisplay() const override; + + void openedImpl() override; + + bool eventFilter(QObject * watched, QEvent * event) override; + +private: + void suggestCurrent(); + +private slots: + void triggerSearch(); + + void onSortingSelectionChanged(QString data); + void onSelectionChanged(QModelIndex first, QModelIndex second); + void onVersionSelectionChanged(QString data); + +private: + Ui::FtbPage *ui = nullptr; + NewInstanceDialog* dialog = nullptr; + Ftb::ListModel* listModel = nullptr; + Ftb::FilterModel* filterModel = nullptr; + + ModpacksCH::Modpack selected; + QString selectedVersion; + + bool initialised { false }; +}; diff --git a/launcher/ui/pages/modplatform/ftb/FtbPage.ui b/launcher/ui/pages/modplatform/ftb/FtbPage.ui new file mode 100644 index 00000000..e9c783e3 --- /dev/null +++ b/launcher/ui/pages/modplatform/ftb/FtbPage.ui @@ -0,0 +1,79 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>FtbPage</class> + <widget class="QWidget" name="FtbPage"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>875</width> + <height>745</height> + </rect> + </property> + <layout class="QGridLayout" name="gridLayout"> + <item row="2" column="0" colspan="2"> + <layout class="QGridLayout" name="gridLayout_4" columnstretch="0,0,0" rowminimumheight="0" columnminimumwidth="0,0,0"> + <item row="0" column="2"> + <widget class="QComboBox" name="versionSelectionBox"/> + </item> + <item row="0" column="1"> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Version selected:</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + </widget> + </item> + <item row="0" column="0"> + <widget class="QComboBox" name="sortByBox"/> + </item> + </layout> + </item> + <item row="0" column="0"> + <widget class="QLineEdit" name="searchEdit"> + <property name="placeholderText"> + <string>Search and filter ...</string> + </property> + <property name="clearButtonEnabled"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="1" column="0" colspan="2"> + <layout class="QGridLayout" name="gridLayout_3"> + <item row="0" column="0"> + <widget class="QTreeView" name="packView"> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + <property name="iconSize"> + <size> + <width>48</width> + <height>48</height> + </size> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QTextBrowser" name="packDescription"> + <property name="openExternalLinks"> + <bool>true</bool> + </property> + <property name="openLinks"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + <tabstops> + <tabstop>searchEdit</tabstop> + <tabstop>versionSelectionBox</tabstop> + </tabstops> + <resources/> + <connections/> +</ui> diff --git a/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp b/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp new file mode 100644 index 00000000..5fa932b7 --- /dev/null +++ b/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp @@ -0,0 +1,259 @@ +#include "ListModel.h" +#include "Application.h" + +#include <MMCStrings.h> +#include <Version.h> + +#include <QtMath> +#include <QLabel> + +#include <RWStorage.h> + +#include <BuildConfig.h> + +namespace LegacyFTB { + +FilterModel::FilterModel(QObject *parent) : QSortFilterProxyModel(parent) +{ + currentSorting = Sorting::ByGameVersion; + sortings.insert(tr("Sort by name"), Sorting::ByName); + sortings.insert(tr("Sort by game version"), Sorting::ByGameVersion); +} + +bool FilterModel::lessThan(const QModelIndex &left, const QModelIndex &right) const +{ + Modpack leftPack = sourceModel()->data(left, Qt::UserRole).value<Modpack>(); + Modpack rightPack = sourceModel()->data(right, Qt::UserRole).value<Modpack>(); + + if(currentSorting == Sorting::ByGameVersion) { + Version lv(leftPack.mcVersion); + Version rv(rightPack.mcVersion); + return lv < rv; + + } else if(currentSorting == Sorting::ByName) { + return Strings::naturalCompare(leftPack.name, rightPack.name, Qt::CaseSensitive) >= 0; + } + + //UHM, some inavlid value set?! + qWarning() << "Invalid sorting set!"; + return true; +} + +bool FilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const +{ + return true; +} + +const QMap<QString, FilterModel::Sorting> FilterModel::getAvailableSortings() +{ + return sortings; +} + +QString FilterModel::translateCurrentSorting() +{ + return sortings.key(currentSorting); +} + +void FilterModel::setSorting(Sorting s) +{ + currentSorting = s; + invalidate(); +} + +FilterModel::Sorting FilterModel::getCurrentSorting() +{ + return currentSorting; +} + +ListModel::ListModel(QObject *parent) : QAbstractListModel(parent) +{ +} + +ListModel::~ListModel() +{ +} + +QString ListModel::translatePackType(PackType type) const +{ + switch(type) + { + case PackType::Public: + return tr("Public Modpack"); + case PackType::ThirdParty: + return tr("Third Party Modpack"); + case PackType::Private: + return tr("Private Modpack"); + } + qWarning() << "Unknown FTB modpack type:" << int(type); + return QString(); +} + +int ListModel::rowCount(const QModelIndex &parent) const +{ + return modpacks.size(); +} + +int ListModel::columnCount(const QModelIndex &parent) const +{ + return 1; +} + +QVariant ListModel::data(const QModelIndex &index, int role) const +{ + int pos = index.row(); + if(pos >= modpacks.size() || pos < 0 || !index.isValid()) + { + return QString("INVALID INDEX %1").arg(pos); + } + + Modpack pack = modpacks.at(pos); + if(role == Qt::DisplayRole) + { + return pack.name + "\n" + translatePackType(pack.type); + } + else if (role == Qt::ToolTipRole) + { + if(pack.description.length() > 100) + { + //some magic to prevent to long tooltips and replace html linebreaks + QString edit = pack.description.left(97); + edit = edit.left(edit.lastIndexOf("<br>")).left(edit.lastIndexOf(" ")).append("..."); + return edit; + + } + return pack.description; + } + else if(role == Qt::DecorationRole) + { + if(m_logoMap.contains(pack.logo)) + { + return (m_logoMap.value(pack.logo)); + } + QIcon icon = APPLICATION->getThemedIcon("screenshot-placeholder"); + ((ListModel *)this)->requestLogo(pack.logo); + return icon; + } + else if(role == Qt::TextColorRole) + { + if(pack.broken) + { + //FIXME: Hardcoded color + return QColor(255, 0, 50); + } + else if(pack.bugged) + { + //FIXME: Hardcoded color + //bugged pack, currently only indicates bugged xml + return QColor(244, 229, 66); + } + } + else if(role == Qt::UserRole) + { + QVariant v; + v.setValue(pack); + return v; + } + + return QVariant(); +} + +void ListModel::fill(ModpackList modpacks) +{ + beginResetModel(); + this->modpacks = modpacks; + endResetModel(); +} + +void ListModel::addPack(Modpack modpack) +{ + beginResetModel(); + this->modpacks.append(modpack); + endResetModel(); +} + +void ListModel::clear() +{ + beginResetModel(); + modpacks.clear(); + endResetModel(); +} + +Modpack ListModel::at(int row) +{ + return modpacks.at(row); +} + +void ListModel::remove(int row) +{ + if(row < 0 || row >= modpacks.size()) + { + qWarning() << "Attempt to remove FTB modpacks with invalid row" << row; + return; + } + beginRemoveRows(QModelIndex(), row, row); + modpacks.removeAt(row); + endRemoveRows(); +} + +void ListModel::logoLoaded(QString logo, QIcon out) +{ + m_loadingLogos.removeAll(logo); + m_logoMap.insert(logo, out); + emit dataChanged(createIndex(0, 0), createIndex(1, 0)); +} + +void ListModel::logoFailed(QString logo) +{ + m_failedLogos.append(logo); + m_loadingLogos.removeAll(logo); +} + +void ListModel::requestLogo(QString file) +{ + if(m_loadingLogos.contains(file) || m_failedLogos.contains(file)) + { + return; + } + + MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("FTBPacks", QString("logos/%1").arg(file.section(".", 0, 0))); + NetJob *job = new NetJob(QString("FTB Icon Download for %1").arg(file)); + job->addNetAction(Net::Download::makeCached(QUrl(QString(BuildConfig.LEGACY_FTB_CDN_BASE_URL + "static/%1").arg(file)), entry)); + + auto fullPath = entry->getFullPath(); + QObject::connect(job, &NetJob::finished, this, [this, file, fullPath] + { + emit logoLoaded(file, QIcon(fullPath)); + if(waitingCallbacks.contains(file)) + { + waitingCallbacks.value(file)(fullPath); + } + }); + + QObject::connect(job, &NetJob::failed, this, [this, file] + { + emit logoFailed(file); + }); + + job->start(APPLICATION->network()); + + m_loadingLogos.append(file); +} + +void ListModel::getLogo(const QString &logo, LogoCallback callback) +{ + if(m_logoMap.contains(logo)) + { + callback(APPLICATION->metacache()->resolveEntry("FTBPacks", QString("logos/%1").arg(logo.section(".", 0, 0)))->getFullPath()); + } + else + { + requestLogo(logo); + } +} + +Qt::ItemFlags ListModel::flags(const QModelIndex &index) const +{ + return QAbstractListModel::flags(index); +} + +} diff --git a/launcher/ui/pages/modplatform/legacy_ftb/ListModel.h b/launcher/ui/pages/modplatform/legacy_ftb/ListModel.h new file mode 100644 index 00000000..c55df000 --- /dev/null +++ b/launcher/ui/pages/modplatform/legacy_ftb/ListModel.h @@ -0,0 +1,78 @@ +#pragma once + +#include <modplatform/legacy_ftb/PackHelpers.h> +#include <RWStorage.h> + +#include <QAbstractListModel> +#include <QSortFilterProxyModel> +#include <QThreadPool> +#include <QIcon> +#include <QStyledItemDelegate> + +#include <functional> + +namespace LegacyFTB { + +typedef QMap<QString, QIcon> FTBLogoMap; +typedef std::function<void(QString)> LogoCallback; + +class FilterModel : public QSortFilterProxyModel +{ + Q_OBJECT +public: + FilterModel(QObject* parent = Q_NULLPTR); + enum Sorting { + ByName, + ByGameVersion + }; + const QMap<QString, Sorting> getAvailableSortings(); + QString translateCurrentSorting(); + void setSorting(Sorting sorting); + Sorting getCurrentSorting(); + +protected: + bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override; + bool lessThan(const QModelIndex &left, const QModelIndex &right) const override; + +private: + QMap<QString, Sorting> sortings; + Sorting currentSorting; + +}; + +class ListModel : public QAbstractListModel +{ + Q_OBJECT +private: + ModpackList modpacks; + QStringList m_failedLogos; + QStringList m_loadingLogos; + FTBLogoMap m_logoMap; + QMap<QString, LogoCallback> waitingCallbacks; + + void requestLogo(QString file); + QString translatePackType(PackType type) const; + + +private slots: + void logoFailed(QString logo); + void logoLoaded(QString logo, QIcon out); + +public: + ListModel(QObject *parent); + ~ListModel(); + int rowCount(const QModelIndex &parent) const override; + int columnCount(const QModelIndex &parent) const override; + QVariant data(const QModelIndex &index, int role) const override; + Qt::ItemFlags flags(const QModelIndex &index) const override; + + void fill(ModpackList modpacks); + void addPack(Modpack modpack); + void clear(); + void remove(int row); + + Modpack at(int row); + void getLogo(const QString &logo, LogoCallback callback); +}; + +} diff --git a/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp b/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp new file mode 100644 index 00000000..891704de --- /dev/null +++ b/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp @@ -0,0 +1,371 @@ +#include "Page.h" +#include "ui_Page.h" + +#include <QInputDialog> + +#include "Application.h" + +#include "ui/dialogs/CustomMessageBox.h" +#include "ui/dialogs/NewInstanceDialog.h" + +#include "modplatform/legacy_ftb/PackFetchTask.h" +#include "modplatform/legacy_ftb/PackInstallTask.h" +#include "modplatform/legacy_ftb/PrivatePackManager.h" +#include "ListModel.h" + +namespace LegacyFTB { + +Page::Page(NewInstanceDialog* dialog, QWidget *parent) + : QWidget(parent), dialog(dialog), ui(new Ui::Page) +{ + ftbFetchTask.reset(new PackFetchTask(APPLICATION->network())); + ftbPrivatePacks.reset(new PrivatePackManager()); + + ui->setupUi(this); + + { + publicFilterModel = new FilterModel(this); + publicListModel = new ListModel(this); + publicFilterModel->setSourceModel(publicListModel); + + ui->publicPackList->setModel(publicFilterModel); + ui->publicPackList->setSortingEnabled(true); + ui->publicPackList->header()->hide(); + ui->publicPackList->setIndentation(0); + ui->publicPackList->setIconSize(QSize(42, 42)); + + for(int i = 0; i < publicFilterModel->getAvailableSortings().size(); i++) + { + ui->sortByBox->addItem(publicFilterModel->getAvailableSortings().keys().at(i)); + } + + ui->sortByBox->setCurrentText(publicFilterModel->translateCurrentSorting()); + } + + { + thirdPartyFilterModel = new FilterModel(this); + thirdPartyModel = new ListModel(this); + thirdPartyFilterModel->setSourceModel(thirdPartyModel); + + ui->thirdPartyPackList->setModel(thirdPartyFilterModel); + ui->thirdPartyPackList->setSortingEnabled(true); + ui->thirdPartyPackList->header()->hide(); + ui->thirdPartyPackList->setIndentation(0); + ui->thirdPartyPackList->setIconSize(QSize(42, 42)); + + thirdPartyFilterModel->setSorting(publicFilterModel->getCurrentSorting()); + } + + { + privateFilterModel = new FilterModel(this); + privateListModel = new ListModel(this); + privateFilterModel->setSourceModel(privateListModel); + + ui->privatePackList->setModel(privateFilterModel); + ui->privatePackList->setSortingEnabled(true); + ui->privatePackList->header()->hide(); + ui->privatePackList->setIndentation(0); + ui->privatePackList->setIconSize(QSize(42, 42)); + + privateFilterModel->setSorting(publicFilterModel->getCurrentSorting()); + } + + ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); + ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300); + + connect(ui->sortByBox, &QComboBox::currentTextChanged, this, &Page::onSortingSelectionChanged); + connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &Page::onVersionSelectionItemChanged); + + connect(ui->publicPackList->selectionModel(), &QItemSelectionModel::currentChanged, this, &Page::onPublicPackSelectionChanged); + connect(ui->thirdPartyPackList->selectionModel(), &QItemSelectionModel::currentChanged, this, &Page::onThirdPartyPackSelectionChanged); + connect(ui->privatePackList->selectionModel(), &QItemSelectionModel::currentChanged, this, &Page::onPrivatePackSelectionChanged); + + connect(ui->addPackBtn, &QPushButton::pressed, this, &Page::onAddPackClicked); + connect(ui->removePackBtn, &QPushButton::pressed, this, &Page::onRemovePackClicked); + + connect(ui->tabWidget, &QTabWidget::currentChanged, this, &Page::onTabChanged); + + // ui->modpackInfo->setOpenExternalLinks(true); + + ui->publicPackList->selectionModel()->reset(); + ui->thirdPartyPackList->selectionModel()->reset(); + ui->privatePackList->selectionModel()->reset(); + + onTabChanged(ui->tabWidget->currentIndex()); +} + +Page::~Page() +{ + delete ui; +} + +bool Page::shouldDisplay() const +{ + return true; +} + +void Page::openedImpl() +{ + if(!initialized) + { + connect(ftbFetchTask.get(), &PackFetchTask::finished, this, &Page::ftbPackDataDownloadSuccessfully); + connect(ftbFetchTask.get(), &PackFetchTask::failed, this, &Page::ftbPackDataDownloadFailed); + + connect(ftbFetchTask.get(), &PackFetchTask::privateFileDownloadFinished, this, &Page::ftbPrivatePackDataDownloadSuccessfully); + connect(ftbFetchTask.get(), &PackFetchTask::privateFileDownloadFailed, this, &Page::ftbPrivatePackDataDownloadFailed); + + ftbFetchTask->fetch(); + ftbPrivatePacks->load(); + ftbFetchTask->fetchPrivate(ftbPrivatePacks->getCurrentPackCodes().toList()); + initialized = true; + } + suggestCurrent(); +} + +void Page::suggestCurrent() +{ + if(!isOpened) + { + return; + } + + if(selected.broken || selectedVersion.isEmpty()) + { + dialog->setSuggestedPack(); + return; + } + + dialog->setSuggestedPack(selected.name, new PackInstallTask(APPLICATION->network(), selected, selectedVersion)); + QString editedLogoName; + if(selected.logo.toLower().startsWith("ftb")) + { + editedLogoName = selected.logo; + } + else + { + editedLogoName = "ftb_" + selected.logo; + } + + editedLogoName = editedLogoName.left(editedLogoName.lastIndexOf(".png")); + + if(selected.type == PackType::Public) + { + publicListModel->getLogo(selected.logo, [this, editedLogoName](QString logo) + { + dialog->setSuggestedIconFromFile(logo, editedLogoName); + }); + } + else if (selected.type == PackType::ThirdParty) + { + thirdPartyModel->getLogo(selected.logo, [this, editedLogoName](QString logo) + { + dialog->setSuggestedIconFromFile(logo, editedLogoName); + }); + } + else if (selected.type == PackType::Private) + { + privateListModel->getLogo(selected.logo, [this, editedLogoName](QString logo) + { + dialog->setSuggestedIconFromFile(logo, editedLogoName); + }); + } +} + +void Page::ftbPackDataDownloadSuccessfully(ModpackList publicPacks, ModpackList thirdPartyPacks) +{ + publicListModel->fill(publicPacks); + thirdPartyModel->fill(thirdPartyPacks); +} + +void Page::ftbPackDataDownloadFailed(QString reason) +{ + //TODO: Display the error +} + +void Page::ftbPrivatePackDataDownloadSuccessfully(Modpack pack) +{ + privateListModel->addPack(pack); +} + +void Page::ftbPrivatePackDataDownloadFailed(QString reason, QString packCode) +{ + auto reply = QMessageBox::question( + this, + tr("FTB private packs"), + tr("Failed to download pack information for code %1.\nShould it be removed now?").arg(packCode) + ); + if(reply == QMessageBox::Yes) + { + ftbPrivatePacks->remove(packCode); + } +} + +void Page::onPublicPackSelectionChanged(QModelIndex now, QModelIndex prev) +{ + if(!now.isValid()) + { + onPackSelectionChanged(); + return; + } + Modpack selectedPack = publicFilterModel->data(now, Qt::UserRole).value<Modpack>(); + onPackSelectionChanged(&selectedPack); +} + +void Page::onThirdPartyPackSelectionChanged(QModelIndex now, QModelIndex prev) +{ + if(!now.isValid()) + { + onPackSelectionChanged(); + return; + } + Modpack selectedPack = thirdPartyFilterModel->data(now, Qt::UserRole).value<Modpack>(); + onPackSelectionChanged(&selectedPack); +} + +void Page::onPrivatePackSelectionChanged(QModelIndex now, QModelIndex prev) +{ + if(!now.isValid()) + { + onPackSelectionChanged(); + return; + } + Modpack selectedPack = privateFilterModel->data(now, Qt::UserRole).value<Modpack>(); + onPackSelectionChanged(&selectedPack); +} + +void Page::onPackSelectionChanged(Modpack* pack) +{ + ui->versionSelectionBox->clear(); + if(pack) + { + currentModpackInfo->setHtml("Pack by <b>" + pack->author + "</b>" + + "<br>Minecraft " + pack->mcVersion + "<br>" + "<br>" + pack->description + "<ul><li>" + pack->mods.replace(";", "</li><li>") + + "</li></ul>"); + bool currentAdded = false; + + for(int i = 0; i < pack->oldVersions.size(); i++) + { + if(pack->currentVersion == pack->oldVersions.at(i)) + { + currentAdded = true; + } + ui->versionSelectionBox->addItem(pack->oldVersions.at(i)); + } + + if(!currentAdded) + { + ui->versionSelectionBox->addItem(pack->currentVersion); + } + selected = *pack; + } + else + { + currentModpackInfo->setHtml(""); + ui->versionSelectionBox->clear(); + if(isOpened) + { + dialog->setSuggestedPack(); + } + return; + } + suggestCurrent(); +} + +void Page::onVersionSelectionItemChanged(QString data) +{ + if(data.isNull() || data.isEmpty()) + { + selectedVersion = ""; + return; + } + + selectedVersion = data; + suggestCurrent(); +} + +void Page::onSortingSelectionChanged(QString data) +{ + FilterModel::Sorting toSet = publicFilterModel->getAvailableSortings().value(data); + publicFilterModel->setSorting(toSet); + thirdPartyFilterModel->setSorting(toSet); + privateFilterModel->setSorting(toSet); +} + +void Page::onTabChanged(int tab) +{ + if(tab == 1) + { + currentModel = thirdPartyFilterModel; + currentList = ui->thirdPartyPackList; + currentModpackInfo = ui->thirdPartyPackDescription; + } + else if(tab == 2) + { + currentModel = privateFilterModel; + currentList = ui->privatePackList; + currentModpackInfo = ui->privatePackDescription; + } + else + { + currentModel = publicFilterModel; + currentList = ui->publicPackList; + currentModpackInfo = ui->publicPackDescription; + } + + currentList->selectionModel()->reset(); + QModelIndex idx = currentList->currentIndex(); + if(idx.isValid()) + { + auto pack = currentModel->data(idx, Qt::UserRole).value<Modpack>(); + onPackSelectionChanged(&pack); + } + else + { + onPackSelectionChanged(); + } +} + +void Page::onAddPackClicked() +{ + bool ok; + QString text = QInputDialog::getText( + this, + tr("Add FTB pack"), + tr("Enter pack code:"), + QLineEdit::Normal, + QString(), + &ok + ); + if(ok && !text.isEmpty()) + { + ftbPrivatePacks->add(text); + ftbFetchTask->fetchPrivate({text}); + } +} + +void Page::onRemovePackClicked() +{ + auto index = ui->privatePackList->currentIndex(); + if(!index.isValid()) + { + return; + } + auto row = index.row(); + Modpack pack = privateListModel->at(row); + auto answer = QMessageBox::question( + this, + tr("Remove pack"), + tr("Are you sure you want to remove pack %1?").arg(pack.name), + QMessageBox::Yes | QMessageBox::No + ); + if(answer != QMessageBox::Yes) + { + return; + } + + ftbPrivatePacks->remove(pack.packCode); + privateListModel->remove(row); + onPackSelectionChanged(); +} + +} diff --git a/launcher/ui/pages/modplatform/legacy_ftb/Page.h b/launcher/ui/pages/modplatform/legacy_ftb/Page.h new file mode 100644 index 00000000..d8225e11 --- /dev/null +++ b/launcher/ui/pages/modplatform/legacy_ftb/Page.h @@ -0,0 +1,119 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <QWidget> +#include <QTreeView> +#include <QTextBrowser> + +#include "ui/pages/BasePage.h" +#include <Application.h> +#include "tasks/Task.h" +#include "modplatform/legacy_ftb/PackHelpers.h" +#include "modplatform/legacy_ftb/PackFetchTask.h" +#include "QObjectPtr.h" + +class NewInstanceDialog; + +namespace LegacyFTB { + +namespace Ui +{ +class Page; +} + +class ListModel; +class FilterModel; +class PrivatePackListModel; +class PrivatePackFilterModel; +class PrivatePackManager; + +class Page : public QWidget, public BasePage +{ + Q_OBJECT + +public: + explicit Page(NewInstanceDialog * dialog, QWidget *parent = 0); + virtual ~Page(); + QString displayName() const override + { + return tr("FTB Legacy"); + } + QIcon icon() const override + { + return APPLICATION->getThemedIcon("ftb_logo"); + } + QString id() const override + { + return "legacy_ftb"; + } + QString helpPage() const override + { + return "FTB-platform"; + } + bool shouldDisplay() const override; + void openedImpl() override; + +private: + void suggestCurrent(); + void onPackSelectionChanged(Modpack *pack = nullptr); + +private slots: + void ftbPackDataDownloadSuccessfully(ModpackList publicPacks, ModpackList thirdPartyPacks); + void ftbPackDataDownloadFailed(QString reason); + + void ftbPrivatePackDataDownloadSuccessfully(Modpack pack); + void ftbPrivatePackDataDownloadFailed(QString reason, QString packCode); + + void onSortingSelectionChanged(QString data); + void onVersionSelectionItemChanged(QString data); + + void onPublicPackSelectionChanged(QModelIndex first, QModelIndex second); + void onThirdPartyPackSelectionChanged(QModelIndex first, QModelIndex second); + void onPrivatePackSelectionChanged(QModelIndex first, QModelIndex second); + + void onTabChanged(int tab); + + void onAddPackClicked(); + void onRemovePackClicked(); + +private: + FilterModel* currentModel = nullptr; + QTreeView* currentList = nullptr; + QTextBrowser* currentModpackInfo = nullptr; + + bool initialized = false; + Modpack selected; + QString selectedVersion; + + ListModel* publicListModel = nullptr; + FilterModel* publicFilterModel = nullptr; + + ListModel *thirdPartyModel = nullptr; + FilterModel *thirdPartyFilterModel = nullptr; + + ListModel *privateListModel = nullptr; + FilterModel *privateFilterModel = nullptr; + + unique_qobject_ptr<PackFetchTask> ftbFetchTask; + std::unique_ptr<PrivatePackManager> ftbPrivatePacks; + + NewInstanceDialog* dialog = nullptr; + + Ui::Page *ui = nullptr; +}; + +} diff --git a/launcher/ui/pages/modplatform/legacy_ftb/Page.ui b/launcher/ui/pages/modplatform/legacy_ftb/Page.ui new file mode 100644 index 00000000..15e5d432 --- /dev/null +++ b/launcher/ui/pages/modplatform/legacy_ftb/Page.ui @@ -0,0 +1,135 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>LegacyFTB::Page</class> + <widget class="QWidget" name="LegacyFTB::Page"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>709</width> + <height>602</height> + </rect> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QTabWidget" name="tabWidget"> + <property name="currentIndex"> + <number>0</number> + </property> + <widget class="QWidget" name="tab"> + <attribute name="title"> + <string>Public</string> + </attribute> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="0"> + <widget class="QTreeView" name="publicPackList"> + <property name="maximumSize"> + <size> + <width>250</width> + <height>16777215</height> + </size> + </property> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QTextBrowser" name="publicPackDescription"/> + </item> + </layout> + </widget> + <widget class="QWidget" name="tab_2"> + <attribute name="title"> + <string>3rd Party</string> + </attribute> + <layout class="QGridLayout" name="gridLayout_3"> + <item row="0" column="1"> + <widget class="QTextBrowser" name="thirdPartyPackDescription"/> + </item> + <item row="0" column="0"> + <widget class="QTreeView" name="thirdPartyPackList"> + <property name="maximumSize"> + <size> + <width>250</width> + <height>16777215</height> + </size> + </property> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </widget> + <widget class="QWidget" name="tab_3"> + <attribute name="title"> + <string>Private</string> + </attribute> + <layout class="QGridLayout" name="gridLayout_2"> + <item row="0" column="0"> + <widget class="QTreeView" name="privatePackList"> + <property name="maximumSize"> + <size> + <width>250</width> + <height>16777215</height> + </size> + </property> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QPushButton" name="addPackBtn"> + <property name="text"> + <string>Add pack</string> + </property> + </widget> + </item> + <item row="2" column="0"> + <widget class="QPushButton" name="removePackBtn"> + <property name="text"> + <string>Remove selected pack</string> + </property> + </widget> + </item> + <item row="0" column="1" rowspan="3"> + <widget class="QTextBrowser" name="privatePackDescription"/> + </item> + </layout> + </widget> + </widget> + </item> + <item> + <layout class="QGridLayout" name="gridLayout_4"> + <item row="0" column="1"> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Version selected:</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + </widget> + </item> + <item row="0" column="2"> + <widget class="QComboBox" name="versionSelectionBox"/> + </item> + <item row="0" column="0"> + <widget class="QComboBox" name="sortByBox"> + <property name="minimumSize"> + <size> + <width>265</width> + <height>0</height> + </size> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui> diff --git a/launcher/ui/pages/modplatform/technic/TechnicData.h b/launcher/ui/pages/modplatform/technic/TechnicData.h new file mode 100644 index 00000000..50fd75e8 --- /dev/null +++ b/launcher/ui/pages/modplatform/technic/TechnicData.h @@ -0,0 +1,42 @@ +/* Copyright 2020-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 <QList> +#include <QString> + +namespace Technic { +struct Modpack { + QString slug; + + QString name; + QString logoUrl; + QString logoName; + + bool broken = true; + + QString url; + bool isSolder = false; + QString minecraftVersion; + + bool metadataLoaded = false; + QString websiteUrl; + QString author; + QString description; +}; +} + +Q_DECLARE_METATYPE(Technic::Modpack) diff --git a/launcher/ui/pages/modplatform/technic/TechnicModel.cpp b/launcher/ui/pages/modplatform/technic/TechnicModel.cpp new file mode 100644 index 00000000..63c2d4c4 --- /dev/null +++ b/launcher/ui/pages/modplatform/technic/TechnicModel.cpp @@ -0,0 +1,237 @@ +/* Copyright 2020-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 "TechnicModel.h" +#include "Application.h" +#include "Json.h" + +#include <QIcon> + +Technic::ListModel::ListModel(QObject *parent) : QAbstractListModel(parent) +{ +} + +Technic::ListModel::~ListModel() +{ +} + +QVariant Technic::ListModel::data(const QModelIndex& index, int role) const +{ + int pos = index.row(); + if(pos >= modpacks.size() || pos < 0 || !index.isValid()) + { + return QString("INVALID INDEX %1").arg(pos); + } + + Modpack pack = modpacks.at(pos); + if(role == Qt::DisplayRole) + { + return pack.name; + } + else if(role == Qt::DecorationRole) + { + if(m_logoMap.contains(pack.logoName)) + { + return (m_logoMap.value(pack.logoName)); + } + QIcon icon = APPLICATION->getThemedIcon("screenshot-placeholder"); + ((ListModel *)this)->requestLogo(pack.logoName, pack.logoUrl); + return icon; + } + else if(role == Qt::UserRole) + { + QVariant v; + v.setValue(pack); + return v; + } + return QVariant(); +} + +int Technic::ListModel::columnCount(const QModelIndex&) const +{ + return 1; +} + +int Technic::ListModel::rowCount(const QModelIndex&) const +{ + return modpacks.size(); +} + +void Technic::ListModel::searchWithTerm(const QString& term) +{ + if(currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull()) { + return; + } + currentSearchTerm = term; + if(jobPtr) { + jobPtr->abort(); + searchState = ResetRequested; + return; + } + else { + beginResetModel(); + modpacks.clear(); + endResetModel(); + searchState = None; + } + performSearch(); +} + +void Technic::ListModel::performSearch() +{ + NetJob *netJob = new NetJob("Technic::Search"); + QString searchUrl = ""; + if (currentSearchTerm.isEmpty()) { + searchUrl = "https://api.technicpack.net/trending?build=multimc"; + } + else + { + searchUrl = QString( + "https://api.technicpack.net/search?build=multimc&q=%1" + ).arg(currentSearchTerm); + } + netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response)); + jobPtr = netJob; + jobPtr->start(APPLICATION->network()); + QObject::connect(netJob, &NetJob::succeeded, this, &ListModel::searchRequestFinished); + QObject::connect(netJob, &NetJob::failed, this, &ListModel::searchRequestFailed); +} + +void Technic::ListModel::searchRequestFinished() +{ + jobPtr.reset(); + + QJsonParseError parse_error; + QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error); + if(parse_error.error != QJsonParseError::NoError) + { + qWarning() << "Error while parsing JSON response from Technic at " << parse_error.offset << " reason: " << parse_error.errorString(); + qWarning() << response; + return; + } + + QList<Modpack> newList; + try { + auto root = Json::requireObject(doc); + auto objs = Json::requireArray(root, "modpacks"); + for (auto technicPack: objs) { + Modpack pack; + auto technicPackObject = Json::requireObject(technicPack); + pack.name = Json::requireString(technicPackObject, "name"); + pack.slug = Json::requireString(technicPackObject, "slug"); + if (pack.slug == "vanilla") + continue; + + auto rawURL = Json::ensureString(technicPackObject, "iconUrl", "null"); + if(rawURL == "null") { + pack.logoUrl = "null"; + pack.logoName = "null"; + } + else { + pack.logoUrl = rawURL; + pack.logoName = rawURL.section(QLatin1Char('/'), -1).section(QLatin1Char('.'), 0, 0); + } + pack.broken = false; + newList.append(pack); + } + } + catch (const JSONValidationError &err) + { + qCritical() << "Couldn't parse technic search results:" << err.cause() ; + return; + } + searchState = Finished; + beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size() + newList.size() - 1); + modpacks.append(newList); + endInsertRows(); +} + +void Technic::ListModel::getLogo(const QString& logo, const QString& logoUrl, Technic::LogoCallback callback) +{ + if(m_logoMap.contains(logo)) + { + callback(APPLICATION->metacache()->resolveEntry("TechnicPacks", QString("logos/%1").arg(logo))->getFullPath()); + } + else + { + requestLogo(logo, logoUrl); + } +} + +void Technic::ListModel::searchRequestFailed() +{ + jobPtr.reset(); + + if(searchState == ResetRequested) + { + beginResetModel(); + modpacks.clear(); + endResetModel(); + + performSearch(); + } + else + { + searchState = Finished; + } +} + + +void Technic::ListModel::logoLoaded(QString logo, QString out) +{ + m_loadingLogos.removeAll(logo); + m_logoMap.insert(logo, QIcon(out)); + for(int i = 0; i < modpacks.size(); i++) + { + if(modpacks[i].logoName == logo) + { + emit dataChanged(createIndex(i, 0), createIndex(i, 0), {Qt::DecorationRole}); + } + } +} + +void Technic::ListModel::logoFailed(QString logo) +{ + m_failedLogos.append(logo); + m_loadingLogos.removeAll(logo); +} + +void Technic::ListModel::requestLogo(QString logo, QString url) +{ + if(m_loadingLogos.contains(logo) || m_failedLogos.contains(logo) || logo == "null") + { + return; + } + + MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("TechnicPacks", QString("logos/%1").arg(logo)); + NetJob *job = new NetJob(QString("Technic Icon Download %1").arg(logo)); + job->addNetAction(Net::Download::makeCached(QUrl(url), entry)); + + auto fullPath = entry->getFullPath(); + + QObject::connect(job, &NetJob::succeeded, this, [this, logo, fullPath] + { + logoLoaded(logo, fullPath); + }); + + QObject::connect(job, &NetJob::failed, this, [this, logo] + { + logoFailed(logo); + }); + + job->start(APPLICATION->network()); + + m_loadingLogos.append(logo); +} diff --git a/launcher/ui/pages/modplatform/technic/TechnicModel.h b/launcher/ui/pages/modplatform/technic/TechnicModel.h new file mode 100644 index 00000000..e80e6e7c --- /dev/null +++ b/launcher/ui/pages/modplatform/technic/TechnicModel.h @@ -0,0 +1,70 @@ +/* Copyright 2020-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 <QModelIndex> + +#include "TechnicData.h" +#include "net/NetJob.h" + +namespace Technic { + +typedef std::function<void(QString)> LogoCallback; + +class ListModel : public QAbstractListModel +{ + Q_OBJECT + +public: + ListModel(QObject *parent); + virtual ~ListModel(); + + virtual QVariant data(const QModelIndex& index, int role) const; + virtual int columnCount(const QModelIndex& parent) const; + virtual int rowCount(const QModelIndex& parent) const; + + void getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback); + void searchWithTerm(const QString & term); + +private slots: + void searchRequestFinished(); + void searchRequestFailed(); + + void logoFailed(QString logo); + void logoLoaded(QString logo, QString out); + +private: + void performSearch(); + void requestLogo(QString logo, QString url); + +private: + QList<Modpack> modpacks; + QStringList m_failedLogos; + QStringList m_loadingLogos; + QMap<QString, QIcon> m_logoMap; + QMap<QString, LogoCallback> waitingCallbacks; + + QString currentSearchTerm; + enum SearchState { + None, + ResetRequested, + Finished + } searchState = None; + NetJob::Ptr jobPtr; + QByteArray response; +}; + +} diff --git a/launcher/ui/pages/modplatform/technic/TechnicPage.cpp b/launcher/ui/pages/modplatform/technic/TechnicPage.cpp new file mode 100644 index 00000000..ac69675c --- /dev/null +++ b/launcher/ui/pages/modplatform/technic/TechnicPage.cpp @@ -0,0 +1,201 @@ +/* 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 "TechnicPage.h" +#include "ui_TechnicPage.h" + +#include <QKeyEvent> + +#include "ui/dialogs/NewInstanceDialog.h" + +#include "TechnicModel.h" +#include "modplatform/technic/SingleZipPackInstallTask.h" +#include "modplatform/technic/SolderPackInstallTask.h" +#include "Json.h" + +#include "Application.h" + +TechnicPage::TechnicPage(NewInstanceDialog* dialog, QWidget *parent) + : QWidget(parent), ui(new Ui::TechnicPage), dialog(dialog) +{ + ui->setupUi(this); + connect(ui->searchButton, &QPushButton::clicked, this, &TechnicPage::triggerSearch); + ui->searchEdit->installEventFilter(this); + model = new Technic::ListModel(this); + ui->packView->setModel(model); + connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &TechnicPage::onSelectionChanged); +} + +bool TechnicPage::eventFilter(QObject* watched, QEvent* event) +{ + if (watched == ui->searchEdit && event->type() == QEvent::KeyPress) { + QKeyEvent* keyEvent = static_cast<QKeyEvent*>(event); + if (keyEvent->key() == Qt::Key_Return) { + triggerSearch(); + keyEvent->accept(); + return true; + } + } + return QWidget::eventFilter(watched, event); +} + +TechnicPage::~TechnicPage() +{ + delete ui; +} + +bool TechnicPage::shouldDisplay() const +{ + return true; +} + +void TechnicPage::openedImpl() +{ + suggestCurrent(); + triggerSearch(); +} + +void TechnicPage::triggerSearch() { + model->searchWithTerm(ui->searchEdit->text()); +} + +void TechnicPage::onSelectionChanged(QModelIndex first, QModelIndex second) +{ + if(!first.isValid()) + { + if(isOpened) + { + dialog->setSuggestedPack(); + } + //ui->frame->clear(); + return; + } + + current = model->data(first, Qt::UserRole).value<Technic::Modpack>(); + suggestCurrent(); +} + +void TechnicPage::suggestCurrent() +{ + if (!isOpened) + { + return; + } + if (current.broken) + { + dialog->setSuggestedPack(); + return; + } + + QString editedLogoName = "technic_" + current.logoName.section(".", 0, 0); + model->getLogo(current.logoName, current.logoUrl, [this, editedLogoName](QString logo) + { + dialog->setSuggestedIconFromFile(logo, editedLogoName); + }); + + if (current.metadataLoaded) + { + metadataLoaded(); + return; + } + + NetJob *netJob = new NetJob(QString("Technic::PackMeta(%1)").arg(current.name)); + std::shared_ptr<QByteArray> response = std::make_shared<QByteArray>(); + QString slug = current.slug; + netJob->addNetAction(Net::Download::makeByteArray(QString("https://api.technicpack.net/modpack/%1?build=multimc").arg(slug), response.get())); + QObject::connect(netJob, &NetJob::succeeded, this, [this, response, slug] + { + if (current.slug != slug) + { + return; + } + QJsonParseError parse_error; + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + QJsonObject obj = doc.object(); + if(parse_error.error != QJsonParseError::NoError) + { + qWarning() << "Error while parsing JSON response from Technic at " << parse_error.offset << " reason: " << parse_error.errorString(); + qWarning() << *response; + return; + } + if (!obj.contains("url")) + { + qWarning() << "Json doesn't contain an url key"; + return; + } + QJsonValueRef url = obj["url"]; + if (url.isString()) + { + current.url = url.toString(); + } + else + { + if (!obj.contains("solder")) + { + qWarning() << "Json doesn't contain a valid url or solder key"; + return; + } + QJsonValueRef solderUrl = obj["solder"]; + if (solderUrl.isString()) + { + current.url = solderUrl.toString(); + current.isSolder = true; + } + else + { + qWarning() << "Json doesn't contain a valid url or solder key"; + return; + } + } + + current.minecraftVersion = Json::ensureString(obj, "minecraft", QString(), "__placeholder__"); + current.websiteUrl = Json::ensureString(obj, "platformUrl", QString(), "__placeholder__"); + current.author = Json::ensureString(obj, "user", QString(), "__placeholder__"); + current.description = Json::ensureString(obj, "description", QString(), "__placeholder__"); + current.metadataLoaded = true; + metadataLoaded(); + }); + netJob->start(APPLICATION->network()); +} + +// expects current.metadataLoaded to be true +void TechnicPage::metadataLoaded() +{ + QString text = ""; + QString name = current.name; + + if (current.websiteUrl.isEmpty()) + // This allows injecting HTML here. + text = name; + else + // URL not properly escaped for inclusion in HTML. The name allows for injecting HTML. + text = "<a href=\"" + current.websiteUrl + "\">" + name + "</a>"; + if (!current.author.isEmpty()) { + // This allows injecting HTML here + text += tr(" by ") + current.author; + } + + ui->frame->setModText(text); + ui->frame->setModDescription(current.description); + if (!current.isSolder) + { + dialog->setSuggestedPack(current.name, new Technic::SingleZipPackInstallTask(current.url, current.minecraftVersion)); + } + else + { + while (current.url.endsWith('/')) current.url.chop(1); + dialog->setSuggestedPack(current.name, new Technic::SolderPackInstallTask(APPLICATION->network(), current.url + "/modpack/" + current.slug, current.minecraftVersion)); + } +} diff --git a/launcher/ui/pages/modplatform/technic/TechnicPage.h b/launcher/ui/pages/modplatform/technic/TechnicPage.h new file mode 100644 index 00000000..21695dd0 --- /dev/null +++ b/launcher/ui/pages/modplatform/technic/TechnicPage.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 <QWidget> + +#include "ui/pages/BasePage.h" +#include <Application.h> +#include "tasks/Task.h" +#include "TechnicData.h" + +namespace Ui +{ +class TechnicPage; +} + +class NewInstanceDialog; + +namespace Technic { + class ListModel; +} + +class TechnicPage : public QWidget, public BasePage +{ + Q_OBJECT + +public: + explicit TechnicPage(NewInstanceDialog* dialog, QWidget *parent = 0); + virtual ~TechnicPage(); + virtual QString displayName() const override + { + return tr("Technic"); + } + virtual QIcon icon() const override + { + return APPLICATION->getThemedIcon("technic"); + } + virtual QString id() const override + { + return "technic"; + } + virtual QString helpPage() const override + { + return "Technic-platform"; + } + virtual bool shouldDisplay() const override; + + void openedImpl() override; + + bool eventFilter(QObject* watched, QEvent* event) override; + +private: + void suggestCurrent(); + void metadataLoaded(); + +private slots: + void triggerSearch(); + void onSelectionChanged(QModelIndex first, QModelIndex second); + +private: + Ui::TechnicPage *ui = nullptr; + NewInstanceDialog* dialog = nullptr; + Technic::ListModel* model = nullptr; + Technic::Modpack current; +}; diff --git a/launcher/ui/pages/modplatform/technic/TechnicPage.ui b/launcher/ui/pages/modplatform/technic/TechnicPage.ui new file mode 100644 index 00000000..dde685d9 --- /dev/null +++ b/launcher/ui/pages/modplatform/technic/TechnicPage.ui @@ -0,0 +1,95 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>TechnicPage</class> + <widget class="QWidget" name="TechnicPage"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>546</width> + <height>405</height> + </rect> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QWidget" name="widget" native="true"> + <layout class="QHBoxLayout" name="horizontalLayout"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QLineEdit" name="searchEdit"> + <property name="placeholderText"> + <string>Search and filter ...</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="searchButton"> + <property name="text"> + <string>Search</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QListView" name="packView"> + <property name="horizontalScrollBarPolicy"> + <enum>Qt::ScrollBarAlwaysOff</enum> + </property> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + <property name="iconSize"> + <size> + <width>48</width> + <height>48</height> + </size> + </property> + </widget> + </item> + <item> + <widget class="MCModInfoFrame" name="frame"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Minimum"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="frameShape"> + <enum>QFrame::StyledPanel</enum> + </property> + <property name="frameShadow"> + <enum>QFrame::Raised</enum> + </property> + </widget> + </item> + </layout> + </widget> + <customwidgets> + <customwidget> + <class>MCModInfoFrame</class> + <extends>QFrame</extends> + <header>ui/widgets/MCModInfoFrame.h</header> + <container>1</container> + </customwidget> + </customwidgets> + <tabstops> + <tabstop>searchEdit</tabstop> + <tabstop>searchButton</tabstop> + <tabstop>packView</tabstop> + </tabstops> + <resources/> + <connections/> +</ui> |