NOISSUE continue reshuffling the codebase

This commit is contained in:
Petr Mrázek
2021-11-22 03:55:16 +01:00
parent 5040231f8d
commit b258eac215
244 changed files with 516 additions and 768 deletions

View File

@ -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();
}

View File

@ -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;
};

View File

@ -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>

View File

@ -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();
}

View File

@ -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;
};

View File

@ -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&amp;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&amp;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&amp;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&amp;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>

View File

@ -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();
}

View File

@ -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;
};

View File

@ -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>&lt;html&gt;&lt;body&gt;&lt;h1&gt;Upgrade is required&lt;/h1&gt;&lt;p&gt;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.&lt;/p&gt;&lt;p&gt;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.&lt;/p&gt;&lt;p&gt;Please report any issues on our &lt;a href=&quot;https://github.com/MultiMC/Launcher/issues&quot;&gt;github issues page&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;There is also a &lt;a href=&quot;https://discord.gg/GtPmv93&quot;&gt;discord channel for testing here&lt;/a&gt;.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</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>

View File

@ -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();
}
}

View File

@ -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;
};

View File

@ -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>&amp;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>

View File

@ -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 &current, 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);
}

View File

@ -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 &current, 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;
};

View File

@ -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>&amp;Add</string>
</property>
<property name="toolTip">
<string>Add mods</string>
</property>
</action>
<action name="actionRemove">
<property name="text">
<string>&amp;Remove</string>
</property>
<property name="toolTip">
<string>Remove selected mods</string>
</property>
</action>
<action name="actionEnable">
<property name="text">
<string>&amp;Enable</string>
</property>
<property name="toolTip">
<string>Enable selected mods</string>
</property>
</action>
<action name="actionDisable">
<property name="text">
<string>&amp;Disable</string>
</property>
<property name="toolTip">
<string>Disable selected mods</string>
</property>
</action>
<action name="actionView_configs">
<property name="text">
<string>View &amp;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 &amp;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>

View File

@ -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;
}

View File

@ -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;
};

View File

@ -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>

View File

@ -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();
}
}

View File

@ -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;
};

View File

@ -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>&amp;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>

View File

@ -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");
}
};

View File

@ -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"

View File

@ -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;
};

View File

@ -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>

View File

@ -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 &current, 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"

View File

@ -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 &current, 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;
};

View File

@ -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>&amp;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&amp;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>

View File

@ -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;
}
};

View File

@ -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");
}
};

View File

@ -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 &current, 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 &current, 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"

View File

@ -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 &current, const QModelIndex &previous);
private slots:
void updateRunningStatus(bool running);
void onGameUpdateError(QString error);
void packageCurrent(const QModelIndex &current, const QModelIndex &previous);
void showContextMenu(const QPoint &pos);
void onFilterTextChanged(const QString & newContents);
};

View File

@ -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>

View File

@ -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 &current, 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"

View File

@ -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 &current, const QModelIndex &previous);
void mceditState(LoggedProcess::State state);
void ShowContextMenu(const QPoint &pos);
};

View File

@ -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>