From 511076d3ba194da9f71092b311b5503365c090aa Mon Sep 17 00:00:00 2001 From: Tayou Date: Mon, 27 Feb 2023 17:30:30 +0100 Subject: [PATCH] cleaned up and improved GameOptions Model & Page - added array support - cleaned up logic - ran clang-format - added description & default value columns - added basic editing support (bools only) - no saving Co-authored-by: TheLastRar Signed-off-by: Tayou --- .../minecraft/gameoptions/GameOptions.cpp | 529 +++++++++++++----- launcher/minecraft/gameoptions/GameOptions.h | 117 ++-- .../ui/pages/instance/GameOptionsPage.cpp | 170 +++--- launcher/ui/pages/instance/GameOptionsPage.h | 5 + launcher/ui/pages/instance/GameOptionsPage.ui | 2 +- 5 files changed, 587 insertions(+), 236 deletions(-) diff --git a/launcher/minecraft/gameoptions/GameOptions.cpp b/launcher/minecraft/gameoptions/GameOptions.cpp index 443525ae4..4fc179c55 100644 --- a/launcher/minecraft/gameoptions/GameOptions.cpp +++ b/launcher/minecraft/gameoptions/GameOptions.cpp @@ -1,129 +1,400 @@ -#include "GameOptions.h" -#include -#include -#include "FileSystem.h" - -namespace { -bool load(const QString& path, std::vector& contents, int& version) -{ - contents.clear(); - QFile file(path); - if (!file.open(QFile::ReadOnly)) { - qWarning() << "Failed to read options file."; - return false; - } - version = 0; - while (!file.atEnd()) { - auto line = file.readLine(); - if (line.endsWith('\n')) { - line.chop(1); - } - auto separatorIndex = line.indexOf(':'); - if (separatorIndex == -1) { - continue; - } - auto key = QString::fromUtf8(line.data(), separatorIndex); - auto value = QString::fromUtf8(line.data() + separatorIndex + 1, line.size() - 1 - separatorIndex); - qDebug() << "!!" << key << "!!"; - if (key == "version") { - version = value.toInt(); - continue; - } - contents.emplace_back(GameOptionItem{ key, value }); - } - qDebug() << "Loaded" << path << "with version:" << version; - return true; -} -bool save(const QString& path, std::vector& mapping, int version) -{ - QSaveFile out(path); - if (!out.open(QIODevice::WriteOnly)) { - return false; - } - if (version != 0) { - QString versionLine = QString("version:%1\n").arg(version); - out.write(versionLine.toUtf8()); - } - auto iter = mapping.begin(); - while (iter != mapping.end()) { - out.write(iter->key.toUtf8()); - out.write(":"); - out.write(iter->value.toUtf8()); - out.write("\n"); - iter++; - } - return out.commit(); -} -} // namespace - -GameOptions::GameOptions(const QString& path) : path(path) -{ - reload(); -} - -QVariant GameOptions::headerData(int section, Qt::Orientation orientation, int role) const -{ - if (role != Qt::DisplayRole) { - return QAbstractListModel::headerData(section, orientation, role); - } - switch (section) { - case 0: - return tr("Key"); - case 1: - return tr("Value"); - default: - return QVariant(); - } -} - -QVariant GameOptions::data(const QModelIndex& index, int role) const -{ - if (!index.isValid()) - return QVariant(); - - int row = index.row(); - int column = index.column(); - - if (row < 0 || row >= int(contents.size())) - return QVariant(); - - switch (role) { - case Qt::DisplayRole: - if (column == 0) { - return contents[row].key; - } else { - return contents[row].value; - } - default: - return QVariant(); - } - return QVariant(); -} - -int GameOptions::rowCount(const QModelIndex&) const -{ - return contents.size(); -} - -int GameOptions::columnCount(const QModelIndex&) const -{ - return 2; -} - -bool GameOptions::isLoaded() const -{ - return loaded; -} - -bool GameOptions::reload() -{ - beginResetModel(); - loaded = load(path, contents, version); - endResetModel(); - return loaded; -} - -bool GameOptions::save() -{ - return ::save(path, contents, version); -} +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2023 Tayou + * Copyright (C) 2023 TheLastRar + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include "FileSystem.h" + +#include "FileSystem.h" +#include "GameOptions.h" + +static Qt::CheckState boolToState(bool b) +{ + return b ? Qt::Checked : Qt::Unchecked; +}; + +namespace { +bool load(const QString& path, std::vector& contents, int& version) +{ + contents.clear(); + QFile file(path); + if (!file.open(QFile::ReadOnly)) { + qWarning() << "Failed to read options file."; + return false; + } + + version = 0; + while (!file.atEnd()) { + // This should be handled by toml++ or some other toml parser rather than this manual parsing + QString line = QString::fromUtf8(file.readLine()); + if (line.endsWith('\n')) { + line.chop(1); + } + if (line.endsWith('\r')) { + line.chop(1); + } + GameOptionItem item = GameOptionItem(); + + auto parts = line.split(':'); + + item.key = parts[0]; + item.value = parts[1]; + item.type = OptionType::String; + qDebug() << "Reading Game Options Key:" << item.key; + + if (item.key == "version") { + version = item.value.toInt(); + continue; + }; + + bool isInt = false; + bool isFloat = false; + item.intValue = item.value.toInt(&isInt); + item.floatValue = item.value.toFloat(&isFloat); + if (isInt) { + item.type = OptionType::Int; + // qDebug() << "The value" << value << "is a int"; + } else if (isFloat) { + item.type = OptionType::Float; + // qDebug() << "The value" << value << "is a float"; + } else if (item.value == "true" || item.value == "false") { + item.boolValue = item.value == "true" ? true : false; + item.type = OptionType::Bool; + qDebug() << "The value" << item.value << "is a bool"; + } else if (item.value.endsWith("]") && item.value.startsWith("[")) { + qDebug() << "The value" << item.value << "is an array"; + for (QString part : item.value.mid(1, item.value.size() - 2).split(",")) { + GameOptionChildItem child{ part, static_cast(contents.size()) }; + qDebug() << "Array has entry" << part; + item.children.append(child); + } + } + contents.emplace_back(item); + } + qDebug() << "Loaded" << path << "with version:" << version; + return true; +} +bool save(const QString& path, std::vector& mapping, int version) +{ + QSaveFile out(path); + if (!out.open(QIODevice::WriteOnly)) { + return false; + } + if (version != 0) { + QString versionLine = QString("version:%1\n").arg(version); + out.write(versionLine.toUtf8()); + } + auto iter = mapping.begin(); + while (iter != mapping.end()) { + out.write(iter->key.toUtf8()); + out.write(":"); + out.write(iter->value.toUtf8()); + out.write("\n"); + iter++; + } + return out.commit(); +} +} // namespace + +GameOptions::GameOptions(const QString& path) : path(path) +{ + reload(); +} + +QVariant GameOptions::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (role != Qt::DisplayRole) { + return QAbstractItemModel::headerData(section, orientation, role); + } + switch (section) { + case 0: + return tr("Key"); + case 1: + return tr("Description"); + case 2: + return tr("Value"); + case 3: + return tr("Default Value"); + default: + return QVariant(); + } +} +bool GameOptions::setData(const QModelIndex& index, const QVariant& value, int role) +{ + auto row = index.row(); + auto column = (Column)index.column(); + if (column == Column::Value) { + switch (contents[row].type) { + case OptionType::String: { + contents[row].value = value.toString(); + } + case OptionType::Int: { + contents[row].intValue = value.toInt(); + } + case OptionType::Bool: { + contents[row].boolValue = value.toBool(); + } + case OptionType::Float: { + contents[row].floatValue = value.toFloat(); + } + } + } + + return true; +} + +Qt::ItemFlags GameOptions::flags(const QModelIndex& index) const +{ + Qt::ItemFlags flags = QAbstractItemModel::flags(index); + + if (!index.isValid()) + return flags; + + Column column = (Column)index.column(); + + if (column == Column::Key) { + return flags; + } + + if (index.parent().isValid()) { + return flags; + } + + if (contents[index.row()].children.count() > 0) { + return flags; + } + + flags = flags | Qt::ItemFlag::ItemIsEditable; + if (column == Column::Value || column == Column::DefaultValue) { + flags = flags | Qt::ItemFlag::ItemIsUserCheckable; + } + if (column == Column::DefaultValue) { + flags = flags & ~Qt::ItemFlag::ItemIsEnabled; + } + + return flags; +} + +QVariant GameOptions::data(const QModelIndex& index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + int row = index.row(); + Column column = (Column)index.column(); + + if (row < 0 || row >= int(contents.size())) + return QVariant(); + + if (index.parent().isValid()) { + switch (role) { + case Qt::DisplayRole: { + if (column == Column::Value) { + GameOptionChildItem* item = static_cast(index.internalPointer()); + return item->value; + } else { + return QVariant(); + } + } + default: { + return QVariant(); + } + } + } + + switch (role) { + case Qt::DisplayRole: { + switch (column) { + case Column::Key: { + return contents[row].key; + } + case Column::Description: { + return "Description goes here!"; + } + case Column::Value: { + switch (contents[row].type) { + case OptionType::String: { + return contents[row].value; + } + case OptionType::Int: { + return contents[row].intValue; + } + case OptionType::Bool: { + return contents[row].boolValue; + } + case OptionType::Float: { + return contents[row].floatValue; + } + case OptionType::KeyBind: { + return contents[row].value; + } + default: { + return QVariant(); + } + } + } + case Column::DefaultValue: { + switch (contents[row].type) { + case OptionType::String: { + return contents[row].value; + } + case OptionType::Int: { + return contents[row].intValue; + } + case OptionType::Bool: { + return contents[row].boolValue; + } + case OptionType::Float: { + return contents[row].floatValue; + } + case OptionType::KeyBind: { + return contents[row].value; + } + default: { + return QVariant(); + } + } + } + default: { + return QVariant(); + } + } + } + case Qt::CheckStateRole: { + switch (column) { + case Column::Value: { + if (contents[row].type == OptionType::Bool) { + return boolToState(contents[row].boolValue); + } else { + return QVariant(); + } + } + case Column::DefaultValue: { + return boolToState(contents[row].boolValue); + } else { + return QVariant(); + } + } + default: { + return QVariant(); + } + } + } + default: { + return QVariant(); + } + } + return QVariant(); +} + +QModelIndex GameOptions::index(int row, int column, const QModelIndex& parent) const +{ + if (!hasIndex(row, column, parent)) + return QModelIndex(); + + if (parent.isValid()) { + if (parent.parent().isValid()) + return QModelIndex(); + + GameOptionItem* item = static_cast(parent.internalPointer()); + return createIndex(row, column, &item->children[row]); + } else { + return createIndex(row, column, &contents[row]); + } +} + +QModelIndex GameOptions::parent(const QModelIndex& index) const +{ + if (!index.isValid()) + return QModelIndex(); + + const void* childItem = index.internalPointer(); + + // Determine where childItem points to + if (childItem >= &contents[0] && childItem <= &contents.back()) { + // Parent is root/contents + return QModelIndex(); + } else { + GameOptionChildItem* child = static_cast(index.internalPointer()); + return createIndex(child->parentRow, 0, &contents[child->parentRow]); + } +} + +int GameOptions::rowCount(const QModelIndex& parent) const +{ + if (!parent.isValid()) { + return static_cast(contents.size()); + } else { + if (parent.column() > 0) + return 0; + + // Our tree model is only one layer deep + // If we have parent, we can't go deeper + if (parent.parent().isValid()) + return 0; + + GameOptionItem* item = static_cast(parent.internalPointer()); + return item->children.count(); + } +} + +int GameOptions::columnCount(const QModelIndex& parent) const +{ + // Our tree model is only one layer deep + // If we have parent, we can't go deeper + if (parent.parent().isValid()) + return 0; + + return 4; +} + +bool GameOptions::isLoaded() const +{ + return loaded; +} + +bool GameOptions::reload() +{ + beginResetModel(); + loaded = load(path, contents, version); + endResetModel(); + return loaded; +} + +bool GameOptions::save() +{ + return ::save(path, contents, version); +} diff --git a/launcher/minecraft/gameoptions/GameOptions.h b/launcher/minecraft/gameoptions/GameOptions.h index ae031efb2..fb366b729 100644 --- a/launcher/minecraft/gameoptions/GameOptions.h +++ b/launcher/minecraft/gameoptions/GameOptions.h @@ -1,32 +1,85 @@ -#pragma once - -#include -#include -#include - -struct GameOptionItem { - QString key; - QString value; -}; - -class GameOptions : public QAbstractListModel { - Q_OBJECT - public: - explicit GameOptions(const QString& path); - virtual ~GameOptions() = default; - - int rowCount(const QModelIndex& parent = QModelIndex()) const override; - int columnCount(const QModelIndex& parent) const override; - QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; - QVariant headerData(int section, Qt::Orientation orientation, int role) const override; - - bool isLoaded() const; - bool reload(); - bool save(); - - private: - std::vector contents; - bool loaded = false; - QString path; - int version = 0; -}; +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2023 Tayou + * Copyright (C) 2023 TheLastRar + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once + +#include +#include +#include + +enum class OptionType { String, Int, Float, Bool, KeyBind }; + +struct GameOptionChildItem { + QString value; + int parentRow; +}; + +struct GameOptionItem { + QString key; + bool boolValue; + int intValue; + float floatValue; + QString value; + OptionType type; + QList children; +}; + +class GameOptions : public QAbstractItemModel { + Q_OBJECT + public: + enum class Column { Key, Description, Value, DefaultValue }; + explicit GameOptions(const QString& path); + virtual ~GameOptions() = default; + + int rowCount(const QModelIndex& parent = QModelIndex()) const override; + int columnCount(const QModelIndex& parent) const override; + Qt::ItemFlags flags(const QModelIndex& index) const override; + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; + QVariant headerData(int section, Qt::Orientation orientation, int role) const override; + QModelIndex index(int row, int column, const QModelIndex& parent = QModelIndex()) const override; + QModelIndex parent(const QModelIndex& index) const override; + + bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole) override; + + bool isLoaded() const; + bool reload(); + bool save(); + + private: + std::vector contents; + bool loaded = false; + QString path; + int version = 0; +}; diff --git a/launcher/ui/pages/instance/GameOptionsPage.cpp b/launcher/ui/pages/instance/GameOptionsPage.cpp index 8db392b1d..71cf3d8f0 100644 --- a/launcher/ui/pages/instance/GameOptionsPage.cpp +++ b/launcher/ui/pages/instance/GameOptionsPage.cpp @@ -1,74 +1,96 @@ -// SPDX-License-Identifier: GPL-3.0-only -/* - * Prism Launcher - Minecraft Launcher - * Copyright (c) 2022 Jamie Mansfield - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - * This file incorporates work covered by the following copyright and - * permission notice: - * - * Copyright 2013-2021 MultiMC Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "GameOptionsPage.h" -#include "minecraft/MinecraftInstance.h" -#include "minecraft/gameoptions/GameOptions.h" -#include "ui_GameOptionsPage.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(); -} - -void GameOptionsPage::retranslate() -{ - ui->retranslateUi(this); -} +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield + * Copyright (C) 2023 Tayou + * Copyright (C) 2023 TheLastRar + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "GameOptionsPage.h" +#include "minecraft/MinecraftInstance.h" +#include "minecraft/gameoptions/GameOptions.h" +#include "ui_GameOptionsPage.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(); + head->setDefaultSectionSize(250); + if (head->count()) { + for (int i = 1; i < head->count(); i++) { + head->setSectionResizeMode(i, QHeaderView::Stretch); + } + head->setSectionResizeMode(head->count() -1, QHeaderView::Stretch); + } + connect(ui->optionsView, &QTreeView::doubleClicked, this, &GameOptionsPage::OptionDoubleClicked); +} + +GameOptionsPage::~GameOptionsPage() +{ + // m_model->save(); +} + +void GameOptionsPage::openedImpl() +{ + // m_model->observe(); +} + +void GameOptionsPage::closedImpl() +{ + // m_model->unobserve(); +} + +void GameOptionsPage::retranslate() +{ + ui->retranslateUi(this); +} + +// QTreeView's double click checks if the cell clicked on has children +// but a typical tree model would only have children in the first column +// Workaround this by calling expand ourself +void GameOptionsPage::OptionDoubleClicked(const QModelIndex& index) +{ + if (!index.isValid() || index.column() == 0) + return; + + const QModelIndex firstColumn = ui->optionsView->model()->index(index.row(), 0, index.parent()); + if (!ui->optionsView->model()->hasChildren(firstColumn)) + return; + + if (ui->optionsView->isExpanded(firstColumn)) + ui->optionsView->collapse(firstColumn); + else + ui->optionsView->expand(firstColumn); +} diff --git a/launcher/ui/pages/instance/GameOptionsPage.h b/launcher/ui/pages/instance/GameOptionsPage.h index a132843e7..dedd09ef0 100644 --- a/launcher/ui/pages/instance/GameOptionsPage.h +++ b/launcher/ui/pages/instance/GameOptionsPage.h @@ -2,6 +2,8 @@ /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield + * Copyright (C) 2023 Tayou + * Copyright (C) 2023 TheLastRar * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -67,4 +69,7 @@ class GameOptionsPage : public QWidget, public BasePage { private: // data Ui::GameOptionsPage* ui = nullptr; std::shared_ptr m_model; + + private Q_SLOTS: + void OptionDoubleClicked(const QModelIndex& index); }; diff --git a/launcher/ui/pages/instance/GameOptionsPage.ui b/launcher/ui/pages/instance/GameOptionsPage.ui index f0a5ce0ee..6f9d170ce 100644 --- a/launcher/ui/pages/instance/GameOptionsPage.ui +++ b/launcher/ui/pages/instance/GameOptionsPage.ui @@ -66,7 +66,7 @@ - false + true false