From b8a2c4a0b81ec08f68de0aca8d25036267c910b9 Mon Sep 17 00:00:00 2001 From: Ghabry Date: Tue, 21 Apr 2026 20:33:03 +0200 Subject: [PATCH 01/10] clangd: Remove problematic compiler flags and raise warning level --- .clangd | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .clangd diff --git a/.clangd b/.clangd new file mode 100644 index 0000000..4cb930b --- /dev/null +++ b/.clangd @@ -0,0 +1,3 @@ +CompileFlags: + Add: [-W3] + Remove: [-fsanitize*, -mno-direct-extern-access] From 1456b625558b15aaeb299758568f8762ca4239a5 Mon Sep 17 00:00:00 2001 From: Ghabry Date: Tue, 21 Apr 2026 20:33:39 +0200 Subject: [PATCH 02/10] JSON: silence warning that the object cannot be created through QML --- src/qmlbinding/json.h | 1 + 1 file changed, 1 insertion(+) diff --git a/src/qmlbinding/json.h b/src/qmlbinding/json.h index 625ca09..0f0f0fb 100644 --- a/src/qmlbinding/json.h +++ b/src/qmlbinding/json.h @@ -29,6 +29,7 @@ class Json : public QObject { Q_OBJECT QML_ELEMENT + QML_UNCREATABLE("Json is an abstract base class") public: explicit Json(QObject* parent = nullptr) : QObject(parent) {} From ddff391c32a79afcf28945ca14d7008dff3a8fbf Mon Sep 17 00:00:00 2001 From: Ghabry Date: Tue, 21 Apr 2026 20:34:10 +0200 Subject: [PATCH 03/10] Database: Fix corrupted title and enc strings --- src/model/project.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/model/project.cpp b/src/model/project.cpp index f617e72..b8e0786 100644 --- a/src/model/project.cpp +++ b/src/model/project.cpp @@ -69,11 +69,11 @@ std::shared_ptr Project::load(const QDir& dir) { if (!cfg.isNull()) { lcf::INIReader ini(cfg.toStdString()); - auto title = ini.GetString("RPG_RT", GAMETITLE, tr("Untitled").toStdString()); + std::string title = std::string(ini.GetString("RPG_RT", GAMETITLE, tr("Untitled").toStdString())); if (project_type == FileFinder::ProjectType::Legacy) { // Check for game encoding - auto enc = ini.GetString("EasyRPG", "Encoding", ""); + std::string enc = std::string(ini.GetString("EasyRPG", "Encoding", "")); if (enc.empty()) { // Only use the title for encoding detection // This is called for all games in the "Open Project" list From a146d8f163407dfe488c7e871730f82175459e2e Mon Sep 17 00:00:00 2001 From: Ghabry Date: Tue, 21 Apr 2026 21:11:48 +0200 Subject: [PATCH 04/10] Add Attribute Page and RadioButton Something simple to get started --- CMakeLists.txt | 4 +- src/ui/common/RadioButton.qml | 29 +++++++++++++ src/ui/database/AttributePage.qml | 68 +++++++++++++++++++++++++++++++ src/ui/database/DatabasePage.qml | 5 +++ 4 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 src/ui/common/RadioButton.qml create mode 100644 src/ui/database/AttributePage.qml diff --git a/CMakeLists.txt b/CMakeLists.txt index bf23f41..4b33dae 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -324,14 +324,16 @@ set(EDITOR_SOURCES set(EDITOR_QML_UI src/ui/common/CheckBox.qml + src/ui/common/RadioButton.qml src/ui/common/SpinBox.qml src/ui/common/TextField.qml src/ui/MainWindow.qml - src/ui/database/DatabaseWindow.qml src/ui/database/ActorPage.qml + src/ui/database/AttributePage.qml src/ui/database/DatabaseEntryListPage.qml src/ui/database/DatabaseEntryPage.qml src/ui/database/DatabasePage.qml + src/ui/database/DatabaseWindow.qml src/ui/database/ItemPage.qml src/ui/database/SkillPage.qml src/ui/database/VocabularyPage.qml diff --git a/src/ui/common/RadioButton.qml b/src/ui/common/RadioButton.qml new file mode 100644 index 0000000..806f469 --- /dev/null +++ b/src/ui/common/RadioButton.qml @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: EasyRPG Editor Authors +// SPDX-License-Identifier: GPL-3.0-or-later + +import QtQuick +import QtQuick.Controls as Controls +import org.easyrpg.editor as Ez + +Controls.RadioButton { + id: root + + property string key + property Ez.JsonView jsonData + + property int value + + onCheckedChanged: { + if (checked && jsonData !== null && key !== "") { + jsonData.set(key, value) + } + } + + Component.onCompleted: { + onDataChanged() + } + + function onDataChanged() { + checked = (jsonData.num(key) === value) + } +} diff --git a/src/ui/database/AttributePage.qml b/src/ui/database/AttributePage.qml new file mode 100644 index 0000000..8f79390 --- /dev/null +++ b/src/ui/database/AttributePage.qml @@ -0,0 +1,68 @@ +// SPDX-FileCopyrightText: EasyRPG Editor Authors +// SPDX-License-Identifier: GPL-3.0-or-later + +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls as Controls +import QtQuick.Dialogs as Dialogs +import QtQml.Models as Models +import org.kde.kirigami as Kirigami +import org.easyrpg.editor as Ez + +DatabaseEntryPage { + id: root + + Models.ListModel { + id: rateModel + Models.ListElement { key: "a_rate"; label: "A Rate:" } + Models.ListElement { key: "b_rate"; label: "B Rate:" } + Models.ListElement { key: "c_rate"; label: "C Rate:" } + Models.ListElement { key: "d_rate"; label: "D Rate:" } + Models.ListElement { key: "e_rate"; label: "E Rate:" } + } + + Kirigami.FormLayout { + anchors.fill: parent + + Ez.TextField { + jsonData: root.jsonData + key: "name" + Kirigami.FormData.label: "Name:" + } + + ColumnLayout { + Kirigami.FormData.label: "Attribute Type:" + Kirigami.FormData.buddyFor: radio_physical + Ez.RadioButton { + id: radio_physical + jsonData: root.jsonData + key: "type" + text: "Physical" + value: 0 + } + Ez.RadioButton { + jsonData: root.jsonData + key: "type" + text: "Magical" + value: 1 + } + } + + Kirigami.Separator { + Kirigami.FormData.isSection: true + Kirigami.FormData.label: "Damage Multipliers" + } + + Repeater { + model: rateModel + + Ez.SpinBox { + jsonData: root.jsonData + key: model.key + Kirigami.FormData.label: model.label + from: -9999 + to: 9999 + } + } + } +} diff --git a/src/ui/database/DatabasePage.qml b/src/ui/database/DatabasePage.qml index 7fca014..4d21926 100644 --- a/src/ui/database/DatabasePage.qml +++ b/src/ui/database/DatabasePage.qml @@ -41,6 +41,11 @@ Kirigami.ScrollablePage { key: "skills" targetPage: "SkillPage.qml" } + ListElement { + name: "Attributes" + key: "attributes" + targetPage: "AttributePage.qml" + } ListElement { name: "Vocabulary" key: "terms" From 434cdb4a720a882f5fe741349db005a41eae14ab Mon Sep 17 00:00:00 2001 From: Ghabry Date: Tue, 21 Apr 2026 21:23:15 +0200 Subject: [PATCH 05/10] SpinBox: Support prefix/suffix --- src/ui/common/SpinBox.qml | 19 +++++++++++++++++++ src/ui/database/AttributePage.qml | 1 + 2 files changed, 20 insertions(+) diff --git a/src/ui/common/SpinBox.qml b/src/ui/common/SpinBox.qml index 14b834b..d71c316 100644 --- a/src/ui/common/SpinBox.qml +++ b/src/ui/common/SpinBox.qml @@ -11,6 +11,9 @@ Controls.SpinBox { property string key property Ez.JsonView jsonData + property string prefix: "" + property string suffix: "" + onValueChanged: { if (jsonData !== null && key !== "") { jsonData.set(key, value) @@ -24,4 +27,20 @@ Controls.SpinBox { function onDataChanged() { value = jsonData.num(key) } + + textFromValue: function(value, locale) { + return prefix + Number(value).toLocaleString(locale, 'f', 0) + suffix; + } + + valueFromText: function(text, locale) { + if (prefix !== "" && text.startsWith(prefix)) { + text = text.substring(prefix.length); + } + + if (suffix !== "" && text.endsWith(suffix)) { + text = text.substring(0, text.length - suffix.length); + } + + return Number.fromLocaleString(locale, text); + } } diff --git a/src/ui/database/AttributePage.qml b/src/ui/database/AttributePage.qml index 8f79390..edf7c23 100644 --- a/src/ui/database/AttributePage.qml +++ b/src/ui/database/AttributePage.qml @@ -62,6 +62,7 @@ DatabaseEntryPage { Kirigami.FormData.label: model.label from: -9999 to: 9999 + suffix: "%" } } } From 5aa8fb3972578bc2ab59ac27ce682c2575495192 Mon Sep 17 00:00:00 2001 From: Ghabry Date: Tue, 21 Apr 2026 23:50:52 +0200 Subject: [PATCH 06/10] Add ComboBox type and implement fallback logic to add a "None" element if necessary. --- CMakeLists.txt | 1 + src/qmlbinding/json_internals/json_t_impl.h | 22 ++++++++--- src/qmlbinding/json_list_view.cpp | 4 +- src/qmlbinding/json_list_view.h | 42 +++++++++++++++----- src/ui/common/ComboBox.qml | 44 +++++++++++++++++++++ src/ui/database/ActorPage.qml | 14 +++++++ src/ui/database/DatabaseEntryListPage.qml | 8 ++-- 7 files changed, 115 insertions(+), 20 deletions(-) create mode 100644 src/ui/common/ComboBox.qml diff --git a/CMakeLists.txt b/CMakeLists.txt index 4b33dae..8c2b2db 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -324,6 +324,7 @@ set(EDITOR_SOURCES set(EDITOR_QML_UI src/ui/common/CheckBox.qml + src/ui/common/ComboBox.qml src/ui/common/RadioButton.qml src/ui/common/SpinBox.qml src/ui/common/TextField.qml diff --git a/src/qmlbinding/json_internals/json_t_impl.h b/src/qmlbinding/json_internals/json_t_impl.h index bfd53a7..d827303 100644 --- a/src/qmlbinding/json_internals/json_t_impl.h +++ b/src/qmlbinding/json_internals/json_t_impl.h @@ -47,10 +47,22 @@ QString JsonT::str(QString jsonPtr) const { template int JsonT::num(QString jsonPtr) const { - auto res = glz::get(*m_data, jsonPtr.toStdString()); + std::string ptr = jsonPtr.toStdString(); - if (res) { - return res.value(); + if (auto res = glz::get(*m_data, ptr)) { + return res.value().get(); + } + + if (auto res = glz::get(*m_data, ptr)) { + return static_cast(res.value().get()); + } + + if (auto res = glz::get(*m_data, ptr)) { + return static_cast(res.value().get()); + } + + if (auto res = glz::get(*m_data, ptr)) { + return static_cast(res.value().get()); } qDebug() << "Json::num: Not pointing to int: " << jsonPtr; @@ -87,14 +99,14 @@ void JsonT::set(QString jsonPtr, const QVariant& value) { break; } default: - assert(false); + qDebug() << "Json::set: Type unsupported: " << value.typeName(); break; } } template concept IsVector = requires { - typename T::value_type; + typename T::value_type; } && std::is_class_v; template diff --git a/src/qmlbinding/json_list_view.cpp b/src/qmlbinding/json_list_view.cpp index 8b5cfbe..cbd1f7d 100644 --- a/src/qmlbinding/json_list_view.cpp +++ b/src/qmlbinding/json_list_view.cpp @@ -22,8 +22,8 @@ QHash JsonListView::roleNames() const { roles[Qt::DisplayRole] = "display"; roles[NameRole] = "name"; roles[TitleRole] = "title"; - roles[IdRole] = "index"; - roles[IndexRole] = "listindex"; + roles[IdRole] = "identifier"; + roles[IndexRole] = "index"; return roles; } diff --git a/src/qmlbinding/json_list_view.h b/src/qmlbinding/json_list_view.h index 71f57e5..5970297 100644 --- a/src/qmlbinding/json_list_view.h +++ b/src/qmlbinding/json_list_view.h @@ -28,6 +28,8 @@ class JsonListView : public QAbstractListModel { Q_OBJECT QML_ELEMENT + Q_PROPERTY(int fallbackValue MEMBER m_fallbackValue NOTIFY fallbackValueChanged) + Q_PROPERTY(QString fallbackString MEMBER m_fallbackString NOTIFY fallbackStringChanged) public: enum RoleNames { @@ -59,11 +61,19 @@ class JsonListView : public QAbstractListModel { JsonView* view() const { return m_view; } void setView(JsonView* view) { m_view = view; } + bool hasFallback() const { + return !m_fallbackString.isEmpty(); + } + signals: void dataChanged(); + void fallbackValueChanged(); + void fallbackStringChanged(); protected: JsonView* m_view = nullptr; + int m_fallbackValue = 0; + QString m_fallbackString; }; template @@ -82,14 +92,13 @@ class JsonListViewT : public JsonListView { std::vector* data() const { return m_data; } void setData(std::vector* data) { m_data = data; } - private: std::vector* m_data = nullptr; }; template inline int JsonListViewT::rowCount(const QModelIndex &parent) const { - return m_data->size(); + return m_data->size() + (hasFallback() ? 1 : 0); } template @@ -98,7 +107,6 @@ inline QVariant JsonListViewT::data(const QModelIndex &index, int role) return QVariant(); } - QVariant value; std::string role_str; switch (role) { @@ -116,27 +124,43 @@ inline QVariant JsonListViewT::data(const QModelIndex &index, int role) case Qt::DisplayRole: { auto id = data(index, IdRole); auto name = data(index, NameRole); - auto s = QString("%1: %2").arg(id.toInt(), 4, u'0').arg(name.toString()); - qDebug() << s; + auto s = QString("%1: %2").arg(id.toInt(), 4, 10, u'0').arg(name.toString()); return QVariant::fromValue(s); } default: return QVariant(); } - role_str = std::format("/{}/{}", index.row(), role_str); + if (hasFallback() && index.row() == 0) { + switch (role) { + case NameRole: + return QVariant::fromValue(m_fallbackString); + case IdRole: + return QVariant::fromValue(m_fallbackValue); + default: + return {}; + } + } + + // Shift row back in case of fallback + int row = index.row(); + if (hasFallback()) { + --row; + } + + role_str = std::format("/{}/{}", row, role_str); auto res = glz::get(*m_data, role_str); if (res.has_value()) { - value = QVariant::fromValue(ToQString(res.value())); + return QVariant::fromValue(ToQString(res.value())); } auto res2 = glz::get(*m_data, role_str); if (res2.has_value()) { - value = QVariant::fromValue(res2.value()); + return QVariant::fromValue(res2.value().get()); } - return value; + return {}; } template diff --git a/src/ui/common/ComboBox.qml b/src/ui/common/ComboBox.qml new file mode 100644 index 0000000..a12d6b6 --- /dev/null +++ b/src/ui/common/ComboBox.qml @@ -0,0 +1,44 @@ +// SPDX-FileCopyrightText: EasyRPG Editor Authors +// SPDX-License-Identifier: GPL-3.0-or-later + +import QtQuick +import QtQuick.Controls as Controls +import org.easyrpg.editor as Ez + +Controls.ComboBox { + id: root + + property string key + property Ez.JsonView jsonData + + textRole: "display" + valueRole: "identifier" + + property string fallbackString: "" + + onActivated: { + if (jsonData !== null && key !== "") { + jsonData.set(key, currentValue) + } + } + + Component.onCompleted: { + onDataChanged() + } + + function onDataChanged() { + if (jsonData !== null && key !== "") { + currentIndex = indexOfValue(jsonData.num(key)) + if (currentIndex === -1) { + console.log("Warning: Value " + jsonData.num(key) + " not found in ComboBox model for key " + key) + } + } + } + + onModelChanged: applyFallbackString() + onFallbackStringChanged: applyFallbackString() + + function applyFallbackString() { + root.model.fallbackString = root.fallbackString + } +} diff --git a/src/ui/database/ActorPage.qml b/src/ui/database/ActorPage.qml index 1b1e1ee..e63f4a0 100644 --- a/src/ui/database/ActorPage.qml +++ b/src/ui/database/ActorPage.qml @@ -68,5 +68,19 @@ DatabaseEntryPage { enabled: critical_hit_cb.checked } } + + + Kirigami.Separator { + Kirigami.FormData.isSection: true + Kirigami.FormData.label: "Equipment" + } + + Ez.ComboBox { + jsonData: root.jsonData + key: "initial_equipment/weapon_id" + Kirigami.FormData.label: "Weapon Type:" + model: Ez.ProjectData.database().list("items") + fallbackString: "(None)" + } } } diff --git a/src/ui/database/DatabaseEntryListPage.qml b/src/ui/database/DatabaseEntryListPage.qml index 9b0b983..c376bd8 100644 --- a/src/ui/database/DatabaseEntryListPage.qml +++ b/src/ui/database/DatabaseEntryListPage.qml @@ -33,17 +33,17 @@ Kirigami.ScrollablePage { } delegate: Controls.ItemDelegate { - required property int listindex + required property int index required property string name width: ListView.view.width - text: (listindex+1).toString().padStart(4, '0') + ": " + name + text: (index+1).toString().padStart(4, '0') + ": " + name - highlighted: entryList.currentIndex === listindex + highlighted: entryList.currentIndex === index action: Controls.Action { onTriggered: { - entryList.currentIndex = listindex + entryList.currentIndex = index } } } From 7b196accf287f8ab0c623c9f4784b767e5898e1e Mon Sep 17 00:00:00 2001 From: Ghabry Date: Wed, 22 Apr 2026 01:42:04 +0200 Subject: [PATCH 07/10] Add Equipment selection to the Actor This requires filtering the Equipment list. We already had a filter model for this. The change was just a bit more involved because the ActorModel must be a value type to be usable as a Q_GADGET. --- src/common/lcf_widget_binding.h | 2 +- src/common/sortfilter_proxy_models.cpp | 3 +- src/model/actor.cpp | 21 +++++++------- src/model/actor.h | 11 +++++--- src/model/enemy.cpp | 6 +++- src/model/rpg_base.cpp | 2 +- src/model/rpg_base.h | 12 ++++---- src/qmlbinding/project_data_gadget.cpp | 8 ++++-- src/qmlbinding/project_data_gadget.h | 3 ++ src/ui/common/ComboBox.qml | 18 ++++++------ src/ui/common/rpg_model.h | 3 +- src/ui/common/system_color_combobox.cpp | 1 + src/ui/database/ActorPage.qml | 34 ++++++++++++++++++----- src/ui/database/DatabaseEntryListPage.qml | 3 +- src/ui/database/DatabaseEntryPage.qml | 3 ++ src/ui/database/actor_widget.cpp | 2 +- 16 files changed, 86 insertions(+), 46 deletions(-) diff --git a/src/common/lcf_widget_binding.h b/src/common/lcf_widget_binding.h index 0921e80..bda5ada 100644 --- a/src/common/lcf_widget_binding.h +++ b/src/common/lcf_widget_binding.h @@ -79,7 +79,7 @@ namespace LcfWidgetBinding { data.obj() = static_cast(checkBox->isChecked()); }; - QWidget::connect(checkBox, &QCheckBox::stateChanged, parent, callback); + QWidget::connect(checkBox, &QCheckBox::checkStateChanged, parent, callback); } template diff --git a/src/common/sortfilter_proxy_models.cpp b/src/common/sortfilter_proxy_models.cpp index b094ba6..5ebb099 100644 --- a/src/common/sortfilter_proxy_models.cpp +++ b/src/common/sortfilter_proxy_models.cpp @@ -16,6 +16,7 @@ */ #include "sortfilter_proxy_models.h" +#include "json_list_view.h" SortFilterProxyModelIdFilter::SortFilterProxyModelIdFilter(const std::vector& indices) : QSortFilterProxyModel() { @@ -25,7 +26,7 @@ SortFilterProxyModelIdFilter::SortFilterProxyModelIdFilter(const std::vectorindex(sourceRow, 0, sourceParent); - int val = sourceModel()->data(index, Qt::UserRole).toInt(); + int val = sourceModel()->data(index, JsonListView::IdRole).toInt(); return std::binary_search(this->indices.begin(), this->indices.end(), val); } diff --git a/src/model/actor.cpp b/src/model/actor.cpp index 5282ff6..08af790 100644 --- a/src/model/actor.cpp +++ b/src/model/actor.cpp @@ -17,24 +17,25 @@ #include "actor.h" #include +#include "common/image_loader.h" #include "ui/database/actor_widget.h" #include "common/dbstring.h" #include "common/sortfilter_proxy_models.h" ActorModel::ActorModel(ProjectData& project, lcf::rpg::Actor& data) : - RpgBase(project), m_data(data) { + RpgBase(project), m_data(&data) { } lcf::rpg::Actor& ActorModel::data() { - return m_data; + return *m_data; } QPixmap ActorModel::preview() { - QString path = m_project.project().findFile("FaceSet", ToQString(m_data.face_name), FileFinder::FileType::Image); + QString path = m_project->project().findFile("FaceSet", ToQString(m_data->face_name), FileFinder::FileType::Image); if (!path.isEmpty()) { QPixmap faceSet = ImageLoader::Load(path); - int x = (m_data.face_index % 4) * 48; - int y = (m_data.face_index / 4) * 48; + int x = (m_data->face_index % 4) * 48; + int y = (m_data->face_index / 4) * 48; return faceSet.copy(x, y, 48, 48); } else { @@ -46,11 +47,11 @@ QPixmap ActorModel::preview() { } const lcf::rpg::Actor& ActorModel::data() const { - return m_data; + return *m_data; } bool ActorModel::IsItemUsable(const lcf::rpg::Item& item) const { - int query_idx = m_data.ID - 1; + int query_idx = m_data->ID - 1; auto* query_set = &item.actor_set; /*TODO if (Player::IsRPG2k3() && Data::system.equipment_setting == lcf::rpg::System::EquipmentSetting_class) { auto* cls = GetClass(); @@ -69,10 +70,10 @@ bool ActorModel::IsItemUsable(const lcf::rpg::Item& item) const { return (*query_set)[query_idx]; } -QSortFilterProxyModel* ActorModel::CreateEquipmentFilter(lcf::rpg::Item::Type type) { - std::vector indices; +QAbstractItemModel* ActorModel::CreateEquipmentFilter(lcf::rpg::Item::Type type) { + std::vector indices = {0}; // Include (None) - for (const auto& item : m_project.database().items) { + for (const auto& item : m_project->database().items) { if (item.type != type || !IsItemUsable(item)) { continue; } diff --git a/src/model/actor.h b/src/model/actor.h index aeac910..cb4d989 100644 --- a/src/model/actor.h +++ b/src/model/actor.h @@ -20,17 +20,20 @@ #include #include #include -#include "project.h" #include "rpg_base.h" -class QSortFilterProxyModel; +class QAbstractItemModel; +class ProjectData; /** * A thin wrapper around lcf::rpg::Actor */ class ActorModel : public RpgBase { + Q_GADGET + public: + ActorModel() = default; ActorModel(ProjectData& project, lcf::rpg::Actor& data); bool IsItemUsable(const lcf::rpg::Item& item) const; @@ -41,7 +44,7 @@ class ActorModel : public RpgBase * @param type Equipment type * @return QSortFilterProxyModel */ - QSortFilterProxyModel* CreateEquipmentFilter(lcf::rpg::Item::Type type); + Q_INVOKABLE QAbstractItemModel* CreateEquipmentFilter(lcf::rpg::Item::Type type); lcf::rpg::Actor& data(); @@ -54,6 +57,6 @@ class ActorModel : public RpgBase QPixmap preview() override; private: - lcf::rpg::Actor& m_data; + lcf::rpg::Actor* m_data = nullptr; }; diff --git a/src/model/enemy.cpp b/src/model/enemy.cpp index a29d528..e3cde94 100644 --- a/src/model/enemy.cpp +++ b/src/model/enemy.cpp @@ -16,6 +16,10 @@ */ #include "enemy.h" +#include "common/filefinder.h" +#include "common/image_loader.h" +#include "model/project.h" +#include "model/project_data.h" #include "ui/database/enemy_widget.h" #include "common/dbstring.h" @@ -29,7 +33,7 @@ lcf::rpg::Enemy& EnemyModel::data() { } QPixmap EnemyModel::preview() { - QPixmap monster = ImageLoader::Load(m_project.project().findFile("Monster", ToQString(data().battler_name), FileFinder::FileType::Image)); + QPixmap monster = ImageLoader::Load(m_project->project().findFile("Monster", ToQString(data().battler_name), FileFinder::FileType::Image)); if (!monster) { return QPixmap(48, 48); } diff --git a/src/model/rpg_base.cpp b/src/model/rpg_base.cpp index e281c33..87e2d89 100644 --- a/src/model/rpg_base.cpp +++ b/src/model/rpg_base.cpp @@ -18,6 +18,6 @@ #include "rpg_base.h" -RpgBase::RpgBase(ProjectData &project) : m_project(project) { +RpgBase::RpgBase(ProjectData &project) : m_project(&project) { } diff --git a/src/model/rpg_base.h b/src/model/rpg_base.h index 41060e5..bc42e56 100644 --- a/src/model/rpg_base.h +++ b/src/model/rpg_base.h @@ -19,12 +19,14 @@ #include #include -#include "common/image_loader.h" -#include "project.h" -#include "core.h" + +class ProjectData; class RpgBase { + Q_GADGET + public: + RpgBase() = default; explicit RpgBase(ProjectData& project); virtual QPixmap preview() { @@ -32,9 +34,9 @@ class RpgBase { } ProjectData& project() const { - return m_project; + return *m_project; } protected: - ProjectData& m_project; + ProjectData* m_project = nullptr; }; diff --git a/src/qmlbinding/project_data_gadget.cpp b/src/qmlbinding/project_data_gadget.cpp index 6f02d74..930a372 100644 --- a/src/qmlbinding/project_data_gadget.cpp +++ b/src/qmlbinding/project_data_gadget.cpp @@ -16,6 +16,7 @@ */ #include "project_data_gadget.h" +#include "model/actor.h" #include "model/project.h" #include "json_view.h" @@ -37,9 +38,6 @@ void ProjectDataGadget::setProjectData(ProjectData* data) { } JsonView* ProjectDataGadget::database() { - auto res = m_database_json.subtree("/"); - qDebug() << "database() " << m_data << " " << res; - return m_data ? qvariant_cast(m_database_json.subtree("/")) : nullptr; } @@ -67,6 +65,10 @@ QString ProjectDataGadget::findDirectory(const QString& baseDir, const QString& return m_data->project().findDirectory(baseDir, dir); } +ActorModel ProjectDataGadget::actorModel(int actor_index) { + return ActorModel(*m_data, m_data->database().actors[actor_index]); +} + QString ProjectDataGadget::projectPath() const { return m_data->project().projectDir().absolutePath(); } diff --git a/src/qmlbinding/project_data_gadget.h b/src/qmlbinding/project_data_gadget.h index 1993d4c..3c585a4 100644 --- a/src/qmlbinding/project_data_gadget.h +++ b/src/qmlbinding/project_data_gadget.h @@ -23,6 +23,7 @@ #include #include +class ActorModel; class JsonView; /** @@ -49,6 +50,8 @@ class ProjectDataGadget : public QObject Q_INVOKABLE QString findDirectory(const QString& dir) const; Q_INVOKABLE QString findDirectory(const QString& baseDir, const QString& dir) const; + Q_INVOKABLE ActorModel actorModel(int actor_index); + QString projectPath() const; signals: diff --git a/src/ui/common/ComboBox.qml b/src/ui/common/ComboBox.qml index a12d6b6..69fbc63 100644 --- a/src/ui/common/ComboBox.qml +++ b/src/ui/common/ComboBox.qml @@ -14,8 +14,6 @@ Controls.ComboBox { textRole: "display" valueRole: "identifier" - property string fallbackString: "" - onActivated: { if (jsonData !== null && key !== "") { jsonData.set(key, currentValue) @@ -30,15 +28,15 @@ Controls.ComboBox { if (jsonData !== null && key !== "") { currentIndex = indexOfValue(jsonData.num(key)) if (currentIndex === -1) { - console.log("Warning: Value " + jsonData.num(key) + " not found in ComboBox model for key " + key) - } - } - } + console.log(`ComboBox: ${jsonData.num(key)} not found for ${key} ${root.model.fallbackString}`); - onModelChanged: applyFallbackString() - onFallbackStringChanged: applyFallbackString() + let isProxy = (root.model.sourceModel !== undefined); + let model = isProxy ? root.model.sourceModel : root.model; - function applyFallbackString() { - root.model.fallbackString = root.fallbackString + if (model.fallbackString !== "") { + currentIndex = indexOfValue(model.fallbackValue); + } + } + } } } diff --git a/src/ui/common/rpg_model.h b/src/ui/common/rpg_model.h index 1dbf5b8..82938f9 100644 --- a/src/ui/common/rpg_model.h +++ b/src/ui/common/rpg_model.h @@ -26,6 +26,7 @@ #include "common/dbstring.h" #include "common/filefinder.h" #include "common/image_loader.h" +#include "json_list_view.h" #include "model/rpg_reflect.h" template @@ -62,7 +63,7 @@ QVariant RpgModel::data(const QModelIndex &index, int role) const { return QString("%1: %2").arg(data.ID, 4, 10, QChar('0')).arg(ToQString(data.name)); } else if (role == Qt::DecorationRole) { return typename RpgReflect::model_type(m_project, m_data[index.row()]).preview(); - } else if (role == Qt::UserRole) { + } else if (role == Qt::UserRole || role == JsonListView::IdRole) { return m_data[index.row()].ID; } else if (role == ModelDataObject) { return QVariant::fromValue(&m_data[index.row()]); diff --git a/src/ui/common/system_color_combobox.cpp b/src/ui/common/system_color_combobox.cpp index 2c537b0..ccfa638 100644 --- a/src/ui/common/system_color_combobox.cpp +++ b/src/ui/common/system_color_combobox.cpp @@ -16,6 +16,7 @@ */ #include "system_color_combobox.h" +#include "common/image_loader.h" SystemColorComboBox::SystemColorComboBox(QWidget *parent) : QComboBox(parent) {} diff --git a/src/ui/database/ActorPage.qml b/src/ui/database/ActorPage.qml index e63f4a0..8c0eaa5 100644 --- a/src/ui/database/ActorPage.qml +++ b/src/ui/database/ActorPage.qml @@ -12,6 +12,15 @@ import org.easyrpg.editor as Ez DatabaseEntryPage { id: root + Models.ListModel { + id: equipmentModel + Models.ListElement { key: "weapon_id"; label: "Weapon:"; type: 1 } + Models.ListElement { key: "shield_id"; label: "Shield:"; type: 2 } + Models.ListElement { key: "armor_id"; label: "Armor:"; type: 3 } + Models.ListElement { key: "helmet_id"; label: "Helmet:"; type: 4 } + Models.ListElement { key: "accessory_id"; label: "Accessory:"; type: 5 } + } + Kirigami.FormLayout { anchors.fill: parent @@ -69,18 +78,29 @@ DatabaseEntryPage { } } - Kirigami.Separator { Kirigami.FormData.isSection: true Kirigami.FormData.label: "Equipment" } - Ez.ComboBox { - jsonData: root.jsonData - key: "initial_equipment/weapon_id" - Kirigami.FormData.label: "Weapon Type:" - model: Ez.ProjectData.database().list("items") - fallbackString: "(None)" + Repeater { + id: equipmentRepeater + model: equipmentModel + + Ez.ComboBox { + readonly property var repdata: equipmentRepeater.model.get(index) + + jsonData: root.jsonData + key: "initial_equipment/" + repdata.key + Kirigami.FormData.label: repdata.label + model: { + let filter = Ez.ProjectData.actorModel(root.objIndex).CreateEquipmentFilter(repdata.type); + let list = Ez.ProjectData.database().list("items"); + list.fallbackString = "(None)"; + filter.sourceModel = list; + return filter; + } + } } } } diff --git a/src/ui/database/DatabaseEntryListPage.qml b/src/ui/database/DatabaseEntryListPage.qml index c376bd8..6161fde 100644 --- a/src/ui/database/DatabaseEntryListPage.qml +++ b/src/ui/database/DatabaseEntryListPage.qml @@ -65,7 +65,8 @@ Kirigami.ScrollablePage { //console.log("Pushing:", root.targetPage); pageStack.push(Qt.resolvedUrl(root.targetPage), { // Use the index passed to the function - jsonData: jsonData.subtree("/" + index) + jsonData: jsonData.subtree("/" + index), + objIndex: index }); } } diff --git a/src/ui/database/DatabaseEntryPage.qml b/src/ui/database/DatabaseEntryPage.qml index ba98ae0..f5523e7 100644 --- a/src/ui/database/DatabaseEntryPage.qml +++ b/src/ui/database/DatabaseEntryPage.qml @@ -18,6 +18,9 @@ Kirigami.ScrollablePage { Kirigami.ColumnView.fillWidth: true Kirigami.ColumnView.reservedSpace: applicationWindow().pageStack.defaultColumnWidth * (applicationWindow().pageStack.depth - 1) + /** When the object comes from a list, contains the index. Otherwise -1. */ + property int objIndex: -1 + // TODO: Not used. Just for testing property bool showActions: false diff --git a/src/ui/database/actor_widget.cpp b/src/ui/database/actor_widget.cpp index 0a7e1e1..4225153 100644 --- a/src/ui/database/actor_widget.cpp +++ b/src/ui/database/actor_widget.cpp @@ -241,7 +241,7 @@ void ActorWidget::on_currentActorChanged(lcf::rpg::Actor *actor) auto equipFilter = [&](auto& cbox, auto type) { SignalBlocker s(cbox->comboBox()); - cbox->setFilter(ActorModel(m_project, *m_current).CreateEquipmentFilter(type)); + cbox->setFilter(static_cast(ActorModel(m_project, *m_current).CreateEquipmentFilter(type))); }; equipFilter(ui->comboInitialWeapon, lcf::rpg::Item::Type_weapon); From 66d8807406db306d7039e5fbcbe04cf5e4a0d994 Mon Sep 17 00:00:00 2001 From: Ghabry Date: Wed, 22 Apr 2026 02:07:53 +0200 Subject: [PATCH 08/10] Actor Model: Fix memory leak of the ProxyModel --- src/common/sortfilter_proxy_models.cpp | 4 ++-- src/common/sortfilter_proxy_models.h | 4 +++- src/model/actor.cpp | 10 ++++++++-- src/model/actor.h | 2 +- src/ui/database/actor_widget.cpp | 2 +- 5 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/common/sortfilter_proxy_models.cpp b/src/common/sortfilter_proxy_models.cpp index 5ebb099..6d2e975 100644 --- a/src/common/sortfilter_proxy_models.cpp +++ b/src/common/sortfilter_proxy_models.cpp @@ -18,8 +18,8 @@ #include "sortfilter_proxy_models.h" #include "json_list_view.h" -SortFilterProxyModelIdFilter::SortFilterProxyModelIdFilter(const std::vector& indices) : - QSortFilterProxyModel() { +SortFilterProxyModelIdFilter::SortFilterProxyModelIdFilter(const std::vector& indices, QObject* parent) : + QSortFilterProxyModel(parent) { this->indices = indices; std::sort(this->indices.begin(), this->indices.end()); } diff --git a/src/common/sortfilter_proxy_models.h b/src/common/sortfilter_proxy_models.h index e5b840a..79ded91 100644 --- a/src/common/sortfilter_proxy_models.h +++ b/src/common/sortfilter_proxy_models.h @@ -28,8 +28,10 @@ * Filters by a list of Lcf IDs */ class SortFilterProxyModelIdFilter : public QSortFilterProxyModel { + Q_OBJECT + public: - SortFilterProxyModelIdFilter(const std::vector& indices); + SortFilterProxyModelIdFilter(const std::vector& indices, QObject* parent); bool filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const override; private: diff --git a/src/model/actor.cpp b/src/model/actor.cpp index 08af790..64c10b2 100644 --- a/src/model/actor.cpp +++ b/src/model/actor.cpp @@ -70,7 +70,7 @@ bool ActorModel::IsItemUsable(const lcf::rpg::Item& item) const { return (*query_set)[query_idx]; } -QAbstractItemModel* ActorModel::CreateEquipmentFilter(lcf::rpg::Item::Type type) { +QAbstractItemModel* ActorModel::CreateEquipmentFilter(lcf::rpg::Item::Type type, QObject* parent) { std::vector indices = {0}; // Include (None) for (const auto& item : m_project->database().items) { @@ -81,5 +81,11 @@ QAbstractItemModel* ActorModel::CreateEquipmentFilter(lcf::rpg::Item::Type type) indices.push_back(item.ID); } - return new SortFilterProxyModelIdFilter(indices); + auto filter = new SortFilterProxyModelIdFilter(indices, parent); + + if (!parent) { + QQmlEngine::setObjectOwnership(filter, QQmlEngine::JavaScriptOwnership); + } + + return filter; } diff --git a/src/model/actor.h b/src/model/actor.h index cb4d989..37cc3c6 100644 --- a/src/model/actor.h +++ b/src/model/actor.h @@ -44,7 +44,7 @@ class ActorModel : public RpgBase * @param type Equipment type * @return QSortFilterProxyModel */ - Q_INVOKABLE QAbstractItemModel* CreateEquipmentFilter(lcf::rpg::Item::Type type); + Q_INVOKABLE QAbstractItemModel* CreateEquipmentFilter(lcf::rpg::Item::Type type, QObject* parent = nullptr); lcf::rpg::Actor& data(); diff --git a/src/ui/database/actor_widget.cpp b/src/ui/database/actor_widget.cpp index 4225153..9a18f11 100644 --- a/src/ui/database/actor_widget.cpp +++ b/src/ui/database/actor_widget.cpp @@ -241,7 +241,7 @@ void ActorWidget::on_currentActorChanged(lcf::rpg::Actor *actor) auto equipFilter = [&](auto& cbox, auto type) { SignalBlocker s(cbox->comboBox()); - cbox->setFilter(static_cast(ActorModel(m_project, *m_current).CreateEquipmentFilter(type))); + cbox->setFilter(static_cast(ActorModel(m_project, *m_current).CreateEquipmentFilter(type, this))); }; equipFilter(ui->comboInitialWeapon, lcf::rpg::Item::Type_weapon); From 3410fd0a78d3405dcdd3715dae85238445bff210 Mon Sep 17 00:00:00 2001 From: Ghabry Date: Wed, 22 Apr 2026 16:43:11 +0200 Subject: [PATCH 09/10] Make our ComboBox also work for other types of models --- src/qmlbinding/json_internals/json_t_impl.h | 3 ++ src/qmlbinding/json_list_view.cpp | 4 +-- src/ui/common/ComboBox.qml | 8 ++--- src/ui/database/SkillPage.qml | 37 +++++++++++++++------ 4 files changed, 36 insertions(+), 16 deletions(-) diff --git a/src/qmlbinding/json_internals/json_t_impl.h b/src/qmlbinding/json_internals/json_t_impl.h index d827303..d1a2a62 100644 --- a/src/qmlbinding/json_internals/json_t_impl.h +++ b/src/qmlbinding/json_internals/json_t_impl.h @@ -89,6 +89,9 @@ void JsonT::set(QString jsonPtr, const QVariant& value) { case QMetaType::Int: glz::set(m_data, jsonPtr.toStdString(), value.toInt()); break; + case QMetaType::Double: + glz::set(m_data, jsonPtr.toStdString(), value.toDouble()); + break; case QMetaType::QString: { lcf::DBString s = ToDBString(value.value()); glz::set(m_data, jsonPtr.toStdString(), s); diff --git a/src/qmlbinding/json_list_view.cpp b/src/qmlbinding/json_list_view.cpp index cbd1f7d..369ec12 100644 --- a/src/qmlbinding/json_list_view.cpp +++ b/src/qmlbinding/json_list_view.cpp @@ -19,10 +19,10 @@ QHash JsonListView::roleNames() const { QHash roles; - roles[Qt::DisplayRole] = "display"; + roles[Qt::DisplayRole] = "text"; roles[NameRole] = "name"; roles[TitleRole] = "title"; - roles[IdRole] = "identifier"; + roles[IdRole] = "value"; roles[IndexRole] = "index"; return roles; } diff --git a/src/ui/common/ComboBox.qml b/src/ui/common/ComboBox.qml index 69fbc63..8fb908e 100644 --- a/src/ui/common/ComboBox.qml +++ b/src/ui/common/ComboBox.qml @@ -9,10 +9,10 @@ Controls.ComboBox { id: root property string key - property Ez.JsonView jsonData + property var jsonData - textRole: "display" - valueRole: "identifier" + textRole: "text" // In a JsonListView this is "ID: NAME", e.g. "0001: Aina" + valueRole: "value" // In a JsonListView is the "ID" onActivated: { if (jsonData !== null && key !== "") { @@ -28,7 +28,7 @@ Controls.ComboBox { if (jsonData !== null && key !== "") { currentIndex = indexOfValue(jsonData.num(key)) if (currentIndex === -1) { - console.log(`ComboBox: ${jsonData.num(key)} not found for ${key} ${root.model.fallbackString}`); + console.log(`ComboBox: ${jsonData.num(key)} not found for ${key}`); let isProxy = (root.model.sourceModel !== undefined); let model = isProxy ? root.model.sourceModel : root.model; diff --git a/src/ui/database/SkillPage.qml b/src/ui/database/SkillPage.qml index 632da10..63b9f09 100644 --- a/src/ui/database/SkillPage.qml +++ b/src/ui/database/SkillPage.qml @@ -4,25 +4,42 @@ import QtQuick import QtQuick.Layouts import QtQuick.Controls as Controls +import QtQml.Models as Models import org.kde.kirigami as Kirigami import org.easyrpg.editor as Ez DatabaseEntryPage { id: root + Models.ListModel { + id: typeModel + Models.ListElement { value: 0; text: "Normal" } + Models.ListElement { value: 1; text: "Teleport" } + Models.ListElement { value: 2; text: "Escape" } + Models.ListElement { value: 3; text: "Switch" } + } + Kirigami.FormLayout { anchors.fill: parent - Ez.TextField { - jsonData: root.jsonData - key: "name" - Kirigami.FormData.label: "Name:" - } + Ez.TextField { + jsonData: root.jsonData + key: "name" + Kirigami.FormData.label: "Name:" + } + + Ez.TextField { + jsonData: root.jsonData + key: "description" + Kirigami.FormData.label: "Description:" + } - Ez.TextField { - jsonData: root.jsonData - key: "description" - Kirigami.FormData.label: "Description:" - } + Ez.ComboBox { + id: cbType + jsonData: root.jsonData + key: "type" + Kirigami.FormData.label: "Type: " + model: typeModel + } } } From 054d3f639276102fd0fb349ab71aa2eddb8098be Mon Sep 17 00:00:00 2001 From: Ghabry Date: Wed, 22 Apr 2026 17:02:40 +0200 Subject: [PATCH 10/10] CheckBox: Use checked instead of checkState which is only for Tristate --- src/ui/common/CheckBox.qml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/ui/common/CheckBox.qml b/src/ui/common/CheckBox.qml index 8d040c7..34ffa7e 100644 --- a/src/ui/common/CheckBox.qml +++ b/src/ui/common/CheckBox.qml @@ -12,9 +12,8 @@ Controls.CheckBox { property Ez.JsonView jsonData onToggled: { - //console.log("Text changed to:", text) if (jsonData !== null && key !== "") { - jsonData.set(key, checkState); + jsonData.set(key, checked); } } @@ -23,6 +22,10 @@ Controls.CheckBox { } function onDataChanged() { - checkState = (jsonData.boolean(key)); + if (jsonData !== null && key !== "") { + checked = jsonData.boolean(key); + } else { + checked = false; + } } }