diff --git a/CMakeLists.txt b/CMakeLists.txt index 8c2b2db6..01436234 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -325,10 +325,10 @@ set(EDITOR_SOURCES set(EDITOR_QML_UI src/ui/common/CheckBox.qml src/ui/common/ComboBox.qml + src/ui/common/PickerData.qml src/ui/common/RadioButton.qml src/ui/common/SpinBox.qml src/ui/common/TextField.qml - src/ui/MainWindow.qml src/ui/database/ActorPage.qml src/ui/database/AttributePage.qml src/ui/database/DatabaseEntryListPage.qml @@ -337,23 +337,46 @@ set(EDITOR_QML_UI src/ui/database/DatabaseWindow.qml src/ui/database/ItemPage.qml src/ui/database/SkillPage.qml + src/ui/database/SystemPage.qml src/ui/database/VocabularyPage.qml + src/ui/MainWindow.qml + src/ui/picker/CharSetPicker.qml + src/ui/picker/FaceSetPicker.qml + src/ui/picker/ImagePicker.qml + src/ui/picker/MusicPicker.qml + src/ui/picker/PickerBase.qml + src/ui/picker/SoundPicker.qml + src/ui/viewer/CharSetViewer.qml + src/ui/viewer/FaceSetViewer.qml + src/ui/viewer/FileViewerBase.qml + src/ui/viewer/ImageViewer.qml + src/ui/viewer/MusicViewer.qml + src/ui/viewer/SoundViewer.qml + src/ui/viewer/ViewerBase.qml ) set(EDITOR_QML_INTERFACE + src/qmlbinding/directory_model.cpp + src/qmlbinding/directory_model.h src/qmlbinding/json_internals/json_t_database.cpp src/qmlbinding/json_internals/json_t_impl.h src/qmlbinding/json_internals/json_t_map.cpp src/qmlbinding/json_internals/json_t_treemap.cpp - src/qmlbinding/json.h src/qmlbinding/json_list_view.cpp src/qmlbinding/json_list_view.h src/qmlbinding/json_t.h src/qmlbinding/json_view.cpp src/qmlbinding/json_view.h + src/qmlbinding/json.h src/qmlbinding/lcf_glaze.h src/qmlbinding/project_data_gadget.cpp src/qmlbinding/project_data_gadget.h + src/ui/picker/charset_painted_item.cpp + src/ui/picker/charset_painted_item.h + src/ui/picker/faceset_painted_item.cpp + src/ui/picker/faceset_painted_item.h + src/ui/picker/sprite_painted_item.cpp + src/ui/picker/sprite_painted_item.h ) # Dependencies @@ -423,6 +446,7 @@ target_include_directories(${EXE_NAME} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/src ${CMAKE_CURRENT_SOURCE_DIR}/src/qmlbinding # Required by QML build step + ${CMAKE_CURRENT_SOURCE_DIR}/src/ui/picker # QML painted items INTERFACE $ ) diff --git a/src/common/filefinder.cpp b/src/common/filefinder.cpp index 1c4ad443..5be52ad3 100644 --- a/src/common/filefinder.cpp +++ b/src/common/filefinder.cpp @@ -19,10 +19,10 @@ #include "defines.h" #include "filefinder.h" -QString FileFinder::Find(const QDir& dir, const QString& filename, FileType type, QDir::Filter filter) { - auto fn = [&](std::initializer_list exts) -> QString { - for (const std::string& ext: exts) { - QString file_to_find = filename + ToQString(ext); +QString FileFinder::Find(const QDir& dir, const QString& filename, FileFinder::FileType type, QDir::Filter filter) { + auto fn = [&](QStringList exts) -> QString { + for (const QString& ext: exts) { + QString file_to_find = filename + ext; const auto& list = dir.entryList(filter); for (const QString& item: list) { if (item.compare(file_to_find, Qt::CaseInsensitive) == 0) { @@ -33,23 +33,7 @@ QString FileFinder::Find(const QDir& dir, const QString& filename, FileType type return nullptr; }; - switch (type) { - case FileType::Default: - return fn({""}); - case FileType::Image: - return fn({ ".bmp", ".png", ".xyz"}); - case FileType::Sound: - return fn({".opus", ".oga", ".ogg", ".wav", ".mp3"}); - case FileType::Music: - return fn({".opus", ".oga", ".ogg", ".wav", ".mid", ".midi", ".mp3"}); - case FileType::Video: - return fn({".webm", ".mp4", ".avi"}); - case FileType::Font: - return fn({".ttf", ".ttc", ".otf", ".fon"}); - } - - assert(false); - return nullptr; + return fn(GetFiltersForType(type)); } QString FileFinder::Find(const QDir& baseDir, const QString& subDir, const QString& filename, FileType type, QDir::Filter filter) { @@ -77,3 +61,25 @@ bool FileFinder::IsEasyRpgProject(const QDir& directory) { QString FileFinder::CombinePath(const QString& path1, const QString& path2) { return path1 + QDir::separator() + path2; } + +QStringList FileFinder::GetFiltersForType(FileType type) { + switch (type) { + case FileType::Default: + return {""}; + case FileType::Image: + return {".bmp", ".png", ".xyz"}; + case FileType::Sound: + return {".opus", ".oga", ".ogg", ".wav", ".mp3"}; + case FileType::Music: + return {".opus", ".oga", ".ogg", ".wav", ".mid", ".midi", ".mp3"}; + case FileType::Video: + return {".webm", ".mp4", ".avi"}; + case FileType::Font: + return {".ttf", ".ttc", ".otf", ".fon"}; + default: + assert(false); + break; + } + + return {}; +} diff --git a/src/common/filefinder.h b/src/common/filefinder.h index a56af668..2711cc9b 100644 --- a/src/common/filefinder.h +++ b/src/common/filefinder.h @@ -47,5 +47,7 @@ namespace FileFinder { bool IsEasyRpgProject(const QDir& directory); QString CombinePath(const QString& path1, const QString& path2); + + QStringList GetFiltersForType(FileType type); }; diff --git a/src/main.cpp b/src/main.cpp index 0af50b17..c8b83606 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -43,7 +43,7 @@ int main(int argc, char *argv[]) { // Kirigami only loads a custom style when using a static build // Lets wait for upstream to improve this #if defined(KIRIGAMI_STATIC) - // Default to org.kde.breeze style (from qqc2-breeze style) + // Default to org.kde.breeze style (from qqc2-breeze style) if (qEnvironmentVariableIsEmpty("QT_QUICK_CONTROLS_STYLE")) { const char* fstyle = "KIRIGAMI_FORCE_STYLE"; if (qEnvironmentVariableIsEmpty(fstyle) || qEnvironmentVariableIntValue(fstyle) > 0) { @@ -75,7 +75,7 @@ int main(int argc, char *argv[]) { #endif // setup qml engine - auto engine = core().qmlEngine(); + //auto engine = core().qmlEngine(); /*engine->loadFromModule("org.easyrpg.editor", "MainWindow"); if (engine->rootObjects().isEmpty()) { diff --git a/src/model/actor.cpp b/src/model/actor.cpp index 64c10b26..46e890e9 100644 --- a/src/model/actor.cpp +++ b/src/model/actor.cpp @@ -84,8 +84,8 @@ QAbstractItemModel* ActorModel::CreateEquipmentFilter(lcf::rpg::Item::Type type, auto filter = new SortFilterProxyModelIdFilter(indices, parent); if (!parent) { - QQmlEngine::setObjectOwnership(filter, QQmlEngine::JavaScriptOwnership); - } + QQmlEngine::setObjectOwnership(filter, QQmlEngine::JavaScriptOwnership); + } return filter; } diff --git a/src/qmlbinding/directory_model.cpp b/src/qmlbinding/directory_model.cpp new file mode 100644 index 00000000..856de253 --- /dev/null +++ b/src/qmlbinding/directory_model.cpp @@ -0,0 +1,118 @@ +/* + * This file is part of EasyRPG Editor. + * + * EasyRPG Editor 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, either version 3 of the License, or + * (at your option) any later version. + * + * EasyRPG Editor 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 EasyRPG Editor. If not, see . + */ + +#include "directory_model.h" +#include "common/filefinder.h" + +DirectoryModel::DirectoryModel(QObject* parent) : QAbstractListModel(parent) {} + +int DirectoryModel::rowCount(const QModelIndex& parent) const { + Q_UNUSED(parent) + return m_fileList.count(); +} + +QVariant DirectoryModel::data(const QModelIndex& index, int role) const { + if (!index.isValid() || index.row() < 0 || + index.row() >= m_fileList.count()) + return QVariant(); + + const QString& filename = m_fileList.at(index.row()); + + switch (role) { + case FileNameRole: + return filename; + case FullPathRole: + return QString("%1/%2").arg(m_path).arg(filename); + case BaseNameRole: + return filename.left(filename.lastIndexOf(".")); + case Qt::DisplayRole: + return filename; + default: + return QVariant(); + } +} + +QHash DirectoryModel::roleNames() const { + QHash roles; + roles[FileNameRole] = "fileName"; + roles[FullPathRole] = "fullPath"; + roles[BaseNameRole] = "baseName"; + return roles; +} + +QString DirectoryModel::path() const { return m_path; } + +void DirectoryModel::setPath(const QString& path) { + if (m_path == path) + return; + + m_path = path; + refreshModel(); + emit pathChanged(); +} + +FileFinder::FileType DirectoryModel::fileType() const { return m_fileType; } + +void DirectoryModel::setFileType(FileFinder::FileType fileType) { + if (m_fileType == fileType) + return; + + m_fileType = fileType; + refreshModel(); + emit fileTypeChanged(); +} + +void DirectoryModel::refreshModel() { + beginResetModel(); + + m_fileList.clear(); + + if (!m_path.isEmpty()) { + QDir dir(m_path); + if (dir.exists()) { + auto entries = dir.entryList(QDir::Files | QDir::NoDotAndDotDot, QDir::Name); + + // Apply filters based on file type + QStringList filters = FileFinder::GetFiltersForType(m_fileType); + + for (const QString& entry : entries) { + if (matchesFilter(entry, filters)) { + m_fileList.append(entry); + } + } + } + } + + endResetModel(); + emit countChanged(); +} + +bool DirectoryModel::matchesFilter(const QString& fileName, + const QStringList& filters) const { + if (filters.isEmpty() || filters.contains("")) + return true; + + QString fileLower = fileName.toLower(); + + for (const QString& filter : filters) { + if (fileLower.endsWith(filter.toLower())) { + return true; + } + } + + return false; +} diff --git a/src/qmlbinding/directory_model.h b/src/qmlbinding/directory_model.h new file mode 100644 index 00000000..f17aed4b --- /dev/null +++ b/src/qmlbinding/directory_model.h @@ -0,0 +1,70 @@ +/* + * This file is part of EasyRPG Editor. + * + * EasyRPG Editor 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, either version 3 of the License, or + * (at your option) any later version. + * + * EasyRPG Editor 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 EasyRPG Editor. If not, see . + */ + +#pragma once + +#include "common/filefinder.h" +#include +#include +#include +#include +#include + +class DirectoryModel : public QAbstractListModel { + Q_OBJECT + QML_ELEMENT + Q_PROPERTY(QString path READ path WRITE setPath NOTIFY pathChanged) + Q_PROPERTY(int count READ rowCount NOTIFY countChanged) + Q_PROPERTY(FileFinder::FileType fileType READ fileType WRITE setFileType NOTIFY + fileTypeChanged) + +public: + enum Roles { + FileNameRole = Qt::UserRole + 1, + FullPathRole, + BaseNameRole + }; + + explicit DirectoryModel(QObject* parent = nullptr); + + // QAbstractItemModel interface + int rowCount(const QModelIndex& parent = QModelIndex()) const override; + QVariant data(const QModelIndex& index, + int role = Qt::DisplayRole) const override; + Q_INVOKABLE QHash roleNames() const override; + + // Property accessors + QString path() const; + void setPath(const QString& path); + + FileFinder::FileType fileType() const; + void setFileType(FileFinder::FileType fileType); + +signals: + void pathChanged(); + void countChanged(); + void fileTypeChanged(); + +private: + void refreshModel(); + bool matchesFilter(const QString& fileName, + const QStringList& filters) const; + + QString m_path; + FileFinder::FileType m_fileType = FileFinder::FileType::Default; + QList m_fileList; +}; diff --git a/src/qmlbinding/json.h b/src/qmlbinding/json.h index 0f0f0fb9..81c01c88 100644 --- a/src/qmlbinding/json.h +++ b/src/qmlbinding/json.h @@ -83,4 +83,7 @@ class Json : public QObject { * @return JSON string */ Q_INVOKABLE virtual QString toJson(QString jsonPtr) const = 0; + +signals: + void valueChanged(QString jsonPtr); }; diff --git a/src/qmlbinding/json_internals/json_t_impl.h b/src/qmlbinding/json_internals/json_t_impl.h index d1a2a62c..4675cddb 100644 --- a/src/qmlbinding/json_internals/json_t_impl.h +++ b/src/qmlbinding/json_internals/json_t_impl.h @@ -35,12 +35,16 @@ JsonT::JsonT(QObject* parent) : Json(parent) {} template QString JsonT::str(QString jsonPtr) const { auto res = glz::get(*m_data, jsonPtr.toStdString()); - if (res) { return ToQString(res.value()); } - qDebug() << "Json::str: Not pointing to DBString: " << jsonPtr; + auto res2 = glz::get(*m_data, jsonPtr.toStdString()); + if (res2) { + return ToQString(res2.value()); + } + + qDebug() << "Json::str: Not pointing to String: " << jsonPtr; return "!BAD POINTER!"; } @@ -85,26 +89,40 @@ bool JsonT::boolean(QString jsonPtr) const { template void JsonT::set(QString jsonPtr, const QVariant& value) { + bool ok = true; + std::string ptr = jsonPtr.toStdString(); + switch (value.typeId()) { case QMetaType::Int: - glz::set(m_data, jsonPtr.toStdString(), value.toInt()); + ok = glz::set(m_data, ptr, value.toInt()); break; case QMetaType::Double: - glz::set(m_data, jsonPtr.toStdString(), value.toDouble()); + ok = glz::set(m_data, ptr, value.toDouble()); break; case QMetaType::QString: { lcf::DBString s = ToDBString(value.value()); - glz::set(m_data, jsonPtr.toStdString(), s); + ok = glz::set(m_data, ptr, s); + + if (!ok) { + std::string s2 = value.value().toStdString(); + ok = glz::set(m_data, ptr, s2); + } break; } case QMetaType::Bool: { - glz::set(m_data, jsonPtr.toStdString(), value.toBool()); + ok = glz::set(m_data, ptr, value.toBool()); break; } default: - qDebug() << "Json::set: Type unsupported: " << value.typeName(); + qDebug() << QString("Json::set: Type %1 unsupported for path %2").arg(value.typeName()).arg(jsonPtr); break; } + + if (!ok) { + qDebug() << QString("Json::set: Assignment of type %1 failed for path %2").arg(value.typeName()).arg(jsonPtr); + } else { + emit valueChanged(jsonPtr); + } } template diff --git a/src/qmlbinding/json_list_view.cpp b/src/qmlbinding/json_list_view.cpp index 369ec121..924afd02 100644 --- a/src/qmlbinding/json_list_view.cpp +++ b/src/qmlbinding/json_list_view.cpp @@ -18,13 +18,32 @@ #include "json_list_view.h" QHash JsonListView::roleNames() const { - QHash roles; + QHash roles; roles[Qt::DisplayRole] = "text"; - roles[NameRole] = "name"; - roles[TitleRole] = "title"; + roles[NameRole] = "name"; + roles[TitleRole] = "title"; roles[IdRole] = "value"; roles[IndexRole] = "index"; - return roles; + return roles; +} + +void JsonListView::onValueChanged(QString jsonPtr) { + // No filtering required as the JsonView we listen on already does this + // ptr looks like this: /INDEX/VALUE/ + // extract the index and notify a row change + + int slash_idx = jsonPtr.indexOf('/', 1); + if (slash_idx != -1) { + bool ok; + int row = jsonPtr.mid(1, slash_idx - 1).toInt(&ok); + if (ok) { + if (hasFallback()) { + row++; + } + QModelIndex idx = index(row, 0); + emit QAbstractListModel::dataChanged(idx, idx); + } + } } QString JsonListView::str(QString jsonPtr) const { diff --git a/src/qmlbinding/json_list_view.h b/src/qmlbinding/json_list_view.h index 5970297e..8503e8f3 100644 --- a/src/qmlbinding/json_list_view.h +++ b/src/qmlbinding/json_list_view.h @@ -65,6 +65,9 @@ class JsonListView : public QAbstractListModel { return !m_fallbackString.isEmpty(); } +public slots: + void onValueChanged(QString jsonPtr); + signals: void dataChanged(); void fallbackValueChanged(); @@ -82,6 +85,10 @@ class JsonListViewT : public JsonListView { explicit JsonListViewT(QObject* parent, std::vector* data, JsonView* view) : JsonListView(parent), m_data(data) { m_view = view; + assert(view); + + // Listen to all changes in the attached JsonView object + connect(view, &JsonView::valueChanged, this, &JsonListView::onValueChanged); } // QAbstractListModel overrides @@ -97,12 +104,12 @@ class JsonListViewT : public JsonListView { }; template -inline int JsonListViewT::rowCount(const QModelIndex &parent) const { +inline int JsonListViewT::rowCount(const QModelIndex& /*parent*/) const { return m_data->size() + (hasFallback() ? 1 : 0); } template -inline QVariant JsonListViewT::data(const QModelIndex &index, int role) const { +inline QVariant JsonListViewT::data(const QModelIndex& index, int role) const { if (!index.isValid() || index.row() >= rowCount(index)) { return QVariant(); } @@ -164,7 +171,7 @@ inline QVariant JsonListViewT::data(const QModelIndex &index, int role) } template -inline bool JsonListViewT::insertRows(int row, int count, const QModelIndex &parent) { +inline bool JsonListViewT::insertRows(int row, int /*count*/, const QModelIndex& /*parent*/) { if (row < 0 || row > rowCount()) { return false; } diff --git a/src/qmlbinding/json_view.cpp b/src/qmlbinding/json_view.cpp index 57984429..a7efd879 100644 --- a/src/qmlbinding/json_view.cpp +++ b/src/qmlbinding/json_view.cpp @@ -48,32 +48,42 @@ namespace { } } +JsonView::JsonView(QObject* parent) : QObject(parent) { + assert(parent); + auto json_parent = qobject_cast(parent); + assert(json_parent); + + // Listen to all changes in the parent Json object + // Filtering of irrelevant changes happens in the slot + connect(json_parent, &Json::valueChanged, this, &JsonView::onValueChanged); +} + QString JsonView::str(QString jsonPtr) const { - return static_cast(parent())->str(makePath(m_pathPrefix, jsonPtr)); + return qobject_cast(parent())->str(makePath(m_pathPrefix, jsonPtr)); } int JsonView::num(QString jsonPtr) const { - return static_cast(parent())->num(makePath(m_pathPrefix, jsonPtr)); + return qobject_cast(parent())->num(makePath(m_pathPrefix, jsonPtr)); } bool JsonView::boolean(QString jsonPtr) const { - return static_cast(parent())->boolean(makePath(m_pathPrefix, jsonPtr)); + return qobject_cast(parent())->boolean(makePath(m_pathPrefix, jsonPtr)); } void JsonView::set(QString jsonPtr, const QVariant& value) { - static_cast(parent())->set(makePath(m_pathPrefix, jsonPtr), value); + qobject_cast(parent())->set(makePath(m_pathPrefix, jsonPtr), value); } JsonView* JsonView::subtree(QString jsonPtr) { - return qvariant_cast(static_cast(parent())->subtree(makePath(m_pathPrefix, jsonPtr))); + return qvariant_cast(qobject_cast(parent())->subtree(makePath(m_pathPrefix, jsonPtr))); } JsonListView* JsonView::list(QString jsonPtr) { - return qvariant_cast(static_cast(parent())->list(makePath(m_pathPrefix, jsonPtr))); + return qvariant_cast(qobject_cast(parent())->list(makePath(m_pathPrefix, jsonPtr))); } QString JsonView::toJson(QString jsonPtr) const { - return static_cast(parent())->toJson(makePath(m_pathPrefix, jsonPtr)); + return qobject_cast(parent())->toJson(makePath(m_pathPrefix, jsonPtr)); } QString JsonView::pathPrefix() const { @@ -95,3 +105,11 @@ void JsonView::setPathPrefix(QString prefix) { m_pathPrefix = prefix; } + +void JsonView::onValueChanged(QString jsonPtr) { + if (jsonPtr.startsWith(pathPrefix() + "/")) { + // Pointer only contains the path relative to the view + QString trimmedPtr = jsonPtr.mid(pathPrefix().length()); + emit valueChanged(trimmedPtr); + } +} diff --git a/src/qmlbinding/json_view.h b/src/qmlbinding/json_view.h index 96405960..9c03ef28 100644 --- a/src/qmlbinding/json_view.h +++ b/src/qmlbinding/json_view.h @@ -34,7 +34,7 @@ class JsonView : public QObject { Q_PROPERTY(QString pathPrefix READ pathPrefix WRITE setPathPrefix NOTIFY pathPrefixChanged) public: - explicit JsonView(QObject* parent = nullptr) : QObject(parent) {} + explicit JsonView(QObject* parent = nullptr); // documentation: see json.h @@ -93,6 +93,10 @@ class JsonView : public QObject { signals: void pathPrefixChanged(); + void valueChanged(QString jsonPtr); + +public slots: + void onValueChanged(QString jsonPtr); private: QString m_pathPrefix; diff --git a/src/qmlbinding/project_data_gadget.cpp b/src/qmlbinding/project_data_gadget.cpp index 930a3720..c367fc6b 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 "common/image_loader.h" #include "model/actor.h" #include "model/project.h" #include "json_view.h" @@ -25,24 +26,24 @@ ProjectDataGadget::ProjectDataGadget(QObject* parent) : QObject(parent) { } void ProjectDataGadget::setProjectData(ProjectData* data) { - if (m_data == data) return; + if (m_data == data) return; - m_data = data; + m_data = data; - if (m_data) { - m_database_json.setData(&m_data->database()); - m_treemap_json.setData(&m_data->treeMap()); - } + if (m_data) { + m_database_json.setData(&m_data->database()); + m_treemap_json.setData(&m_data->treeMap()); + } - emit projectDataChanged(); + emit projectDataChanged(); } JsonView* ProjectDataGadget::database() { - return m_data ? qvariant_cast(m_database_json.subtree("/")) : nullptr; + return m_data ? qvariant_cast(m_database_json.subtree("/")) : nullptr; } JsonView* ProjectDataGadget::treeMap() { - return m_data ? qvariant_cast(m_treemap_json.subtree("/")) : nullptr; + return m_data ? qvariant_cast(m_treemap_json.subtree("/")) : nullptr; } QString ProjectDataGadget::findFile(const QString& filename, FileFinder::FileType type) const { @@ -65,6 +66,15 @@ QString ProjectDataGadget::findDirectory(const QString& baseDir, const QString& return m_data->project().findDirectory(baseDir, dir); } +QPixmap ProjectDataGadget::loadImage(const QString& dir, const QString& filename) const { + QString file = findFile(dir, filename, FileFinder::FileType::Image); + if (file.isEmpty()) { + return {}; + } + + return ImageLoader::Load(file); +} + ActorModel ProjectDataGadget::actorModel(int actor_index) { return ActorModel(*m_data, m_data->database().actors[actor_index]); } diff --git a/src/qmlbinding/project_data_gadget.h b/src/qmlbinding/project_data_gadget.h index 3c585a4d..9e3db09d 100644 --- a/src/qmlbinding/project_data_gadget.h +++ b/src/qmlbinding/project_data_gadget.h @@ -22,6 +22,7 @@ #include "model/project_data.h" #include #include +#include class ActorModel; class JsonView; @@ -37,25 +38,27 @@ class ProjectDataGadget : public QObject Q_PROPERTY(QString projectPath READ projectPath) public: - explicit ProjectDataGadget(QObject* parent = nullptr); + explicit ProjectDataGadget(QObject* parent = nullptr); - void setProjectData(ProjectData* data); + void setProjectData(ProjectData* data); - Q_INVOKABLE JsonView* database(); - Q_INVOKABLE JsonView* treeMap(); + Q_INVOKABLE JsonView* database(); + Q_INVOKABLE JsonView* treeMap(); - Q_INVOKABLE QString findFile(const QString& filename, FileFinder::FileType type = FileFinder::FileType::Default) const; - Q_INVOKABLE QString findFile(const QString& dir, const QString& filename, FileFinder::FileType type = FileFinder::FileType::Default) const; - Q_INVOKABLE QString findFileOrDefault(const QString& filename); - Q_INVOKABLE QString findDirectory(const QString& dir) const; - Q_INVOKABLE QString findDirectory(const QString& baseDir, const QString& dir) const; + Q_INVOKABLE QString findFile(const QString& filename, FileFinder::FileType type = FileFinder::FileType::Default) const; + Q_INVOKABLE QString findFile(const QString& dir, const QString& filename, FileFinder::FileType type = FileFinder::FileType::Default) const; + Q_INVOKABLE QString findFileOrDefault(const QString& filename); + Q_INVOKABLE QString findDirectory(const QString& dir) const; + Q_INVOKABLE QString findDirectory(const QString& baseDir, const QString& dir) const; + + Q_INVOKABLE QPixmap loadImage(const QString& dir, const QString& filename) const; Q_INVOKABLE ActorModel actorModel(int actor_index); QString projectPath() const; signals: - void projectDataChanged(); + void projectDataChanged(); private: ProjectData* m_data = nullptr; diff --git a/src/ui/common/CheckBox.qml b/src/ui/common/CheckBox.qml index 34ffa7e9..90acc147 100644 --- a/src/ui/common/CheckBox.qml +++ b/src/ui/common/CheckBox.qml @@ -11,10 +11,13 @@ Controls.CheckBox { property string key property Ez.JsonView jsonData + onKeyChanged: onDataChanged() + onJsonDataChanged: onDataChanged() + onToggled: { - if (jsonData !== null && key !== "") { - jsonData.set(key, checked); - } + if (jsonData !== null && key !== "") { + jsonData.set(key, checked); + } } Component.onCompleted: { @@ -22,10 +25,10 @@ Controls.CheckBox { } function onDataChanged() { - if (jsonData !== null && key !== "") { - checked = jsonData.boolean(key); - } else { - checked = false; - } + if (jsonData !== null && key !== "") { + checked = jsonData.boolean(key); + } else { + checked = false; + } } } diff --git a/src/ui/common/ComboBox.qml b/src/ui/common/ComboBox.qml index 8fb908e8..8028ffc0 100644 --- a/src/ui/common/ComboBox.qml +++ b/src/ui/common/ComboBox.qml @@ -9,10 +9,15 @@ Controls.ComboBox { id: root property string key - property var jsonData + property Ez.JsonView jsonData + + onKeyChanged: onDataChanged() + onJsonDataChanged: onDataChanged() + onModelChanged: onDataChanged() + onCountChanged: onDataChanged() textRole: "text" // In a JsonListView this is "ID: NAME", e.g. "0001: Aina" - valueRole: "value" // In a JsonListView is the "ID" + valueRole: "value" // In a JsonListView this is the "ID" onActivated: { if (jsonData !== null && key !== "") { @@ -27,13 +32,13 @@ Controls.ComboBox { function onDataChanged() { if (jsonData !== null && key !== "") { currentIndex = indexOfValue(jsonData.num(key)) - if (currentIndex === -1) { + if (currentIndex === -1 && root.model) { console.log(`ComboBox: ${jsonData.num(key)} not found for ${key}`); let isProxy = (root.model.sourceModel !== undefined); let model = isProxy ? root.model.sourceModel : root.model; - if (model.fallbackString !== "") { + if (model && model.fallbackString !== "") { currentIndex = indexOfValue(model.fallbackValue); } } diff --git a/src/ui/common/PickerData.qml b/src/ui/common/PickerData.qml new file mode 100644 index 00000000..6c1970d3 --- /dev/null +++ b/src/ui/common/PickerData.qml @@ -0,0 +1,42 @@ +import QtQuick + +QtObject { + id: root + + property string filename + property int index: 0 + property bool transparent: false + + property int fadein: 0 + property int volume: 100 + property int tempo: 100 + property int balance: 50 + + function fromMusic(jsonData) { + fromSound(jsonData) + fadein = jsonData.num("fadein") + return root + } + + function fromSound(jsonData) { + filename = jsonData.str("name") + volume = jsonData.num("volume") + tempo = jsonData.num("tempo") + balance = jsonData.num("balance") + return root + } + + function toMusic(jsonData) { + toSound(jsonData) + jsonData.set("fadein", fadein) + return root + } + + function toSound(jsonData) { + jsonData.set("name", filename) + jsonData.set("volume", volume) + jsonData.set("tempo", tempo) + jsonData.set("balance", balance) + return root + } +} diff --git a/src/ui/common/RadioButton.qml b/src/ui/common/RadioButton.qml index 806f4692..9be5bab9 100644 --- a/src/ui/common/RadioButton.qml +++ b/src/ui/common/RadioButton.qml @@ -13,6 +13,9 @@ Controls.RadioButton { property int value + onKeyChanged: onDataChanged() + onJsonDataChanged: onDataChanged() + onCheckedChanged: { if (checked && jsonData !== null && key !== "") { jsonData.set(key, value) diff --git a/src/ui/common/SpinBox.qml b/src/ui/common/SpinBox.qml index d71c316a..132a0d58 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 + onKeyChanged: updateData() + onJsonDataChanged: updateData() + property string prefix: "" property string suffix: "" @@ -21,11 +24,15 @@ Controls.SpinBox { } Component.onCompleted: { - onDataChanged() + updateData() } - function onDataChanged() { - value = jsonData.num(key) + function updateData() { + if (jsonData !== null && key !== "") { + value = jsonData.num(key) + } else { + value = 0; + } } textFromValue: function(value, locale) { diff --git a/src/ui/common/TextField.qml b/src/ui/common/TextField.qml index b0c60e15..9f29aece 100644 --- a/src/ui/common/TextField.qml +++ b/src/ui/common/TextField.qml @@ -11,19 +11,27 @@ Controls.TextField { property string key property Ez.JsonView jsonData - onTextChanged: { - //console.log("Text changed to:", key, jsonData, text) - if (jsonData !== null && key !== "") { - jsonData.set(key, text) - } + onKeyChanged: updateData() + onJsonDataChanged: updateData() + + onTextEdited: { + if (jsonData !== null && key !== "") { + jsonData.set(key, text) + } } Component.onCompleted: { - onDataChanged() + updateData() } - function onDataChanged() { - //console.log("data changed", jsonData.str(key)) - text = jsonData.str(key) + function updateData() { + if (jsonData !== null && key !== "") { + let jsonText = jsonData.str(key); + if (text !== jsonText) { + text = jsonText; + } + } else { + text = ""; + } } } diff --git a/src/ui/common/operand_widget.cpp b/src/ui/common/operand_widget.cpp index 9013686a..fd397231 100644 --- a/src/ui/common/operand_widget.cpp +++ b/src/ui/common/operand_widget.cpp @@ -171,13 +171,13 @@ void TimerOperandWidget::attach(EventCommandBaseWidget& base_widget, ProjectData base_widget.connectParameterHandler(m_comboVar, idx_value, op == 1); connect(m_spinSec, qOverload(&QSpinBox::valueChanged), this, - [=] (int new_value) { + [=, this] (int new_value) { int seconds = m_spinMin->value() * 60 + new_value; parameterChanged(m_operation.value, seconds); }); connect(m_spinMin, qOverload(&QSpinBox::valueChanged), this, - [=] (int new_value) { + [=, this] (int new_value) { int seconds = new_value * 60 + m_spinSec->value(); parameterChanged(m_operation.value, seconds); }); diff --git a/src/ui/database/ActorPage.qml b/src/ui/database/ActorPage.qml index 8c0eaa58..c7ca14a4 100644 --- a/src/ui/database/ActorPage.qml +++ b/src/ui/database/ActorPage.qml @@ -21,19 +21,41 @@ DatabaseEntryPage { Models.ListElement { key: "accessory_id"; label: "Accessory:"; type: 5 } } + /*property Ez.PickerData charsetPickerData: Ez.PickerData { + index: charsetViewer.characterIndex + Component.onCompleted: filename = charsetViewer.filename + } + + property Component charsetPickerComponent: Ez.ImagePicker { + onAccepted: { + charsetViewer.filename = pickerData.filename + charsetViewer.characterIndex = pickerData.index + root.jsonData.set("character_name", pickerData.filename) + root.jsonData.set("character_index", pickerData.index) + } + }*/ + Kirigami.FormLayout { - anchors.fill: parent + id: form1 + Layout.fillWidth: true + + Kirigami.Separator { + Kirigami.FormData.isSection: true + Kirigami.FormData.label: "General" + } Ez.TextField { jsonData: root.jsonData key: "name" Kirigami.FormData.label: "Name:" + Layout.fillWidth: true } Ez.TextField { jsonData: root.jsonData key: "title" Kirigami.FormData.label: "Title:" + Layout.fillWidth: true } RowLayout { @@ -78,6 +100,57 @@ DatabaseEntryPage { } } + Kirigami.Separator { + Kirigami.FormData.isSection: true + Kirigami.FormData.label: "Graphics" + } + + Ez.FaceSetViewer { + id: faceViewer + Kirigami.FormData.label: "Face:" + + filename: root.jsonData.str("face_name") + cellIndex: root.jsonData.num("face_index") + + pickerData: Ez.PickerData { + index: faceViewer.cellIndex + Component.onCompleted: filename = faceViewer.filename + } + + onAccepted: { + faceViewer.filename = pickerData.filename + faceViewer.cellIndex = pickerData.index + root.jsonData.set("face_name", pickerData.filename) + root.jsonData.set("face_index", pickerData.index) + } + } + + Ez.CharSetViewer { + id: charViewer + Kirigami.FormData.label: "Character:" + + spin: true + walk: true + filename: root.jsonData.str("character_name") + cellIndex: root.jsonData.num("character_index") + transparent: root.jsonData.boolean("transparent") + + pickerData: Ez.PickerData { + index: charViewer.cellIndex + transparent: charViewer.transparent + Component.onCompleted: filename = charViewer.filename + } + + onAccepted: { + charViewer.filename = pickerData.filename + charViewer.cellIndex = pickerData.index + charViewer.transparent = pickerData.transparent + root.jsonData.set("character_name", pickerData.filename) + root.jsonData.set("character_index", pickerData.index) + root.jsonData.set("transparent", pickerData.transparent) + } + } + Kirigami.Separator { Kirigami.FormData.isSection: true Kirigami.FormData.label: "Equipment" @@ -89,7 +162,7 @@ DatabaseEntryPage { Ez.ComboBox { readonly property var repdata: equipmentRepeater.model.get(index) - + Layout.fillWidth: true jsonData: root.jsonData key: "initial_equipment/" + repdata.key Kirigami.FormData.label: repdata.label diff --git a/src/ui/database/DatabaseEntryListPage.qml b/src/ui/database/DatabaseEntryListPage.qml index 6161fdea..874d6f0a 100644 --- a/src/ui/database/DatabaseEntryListPage.qml +++ b/src/ui/database/DatabaseEntryListPage.qml @@ -26,7 +26,7 @@ Kirigami.ScrollablePage { ListView { id: entryList - model: root.jsonData + model: root.jsonData onCurrentIndexChanged: { root.selectEntry(currentIndex) @@ -36,7 +36,7 @@ Kirigami.ScrollablePage { required property int index required property string name - width: ListView.view.width + width: ListView.view.width text: (index+1).toString().padStart(4, '0') + ": " + name highlighted: entryList.currentIndex === index @@ -54,18 +54,15 @@ Kirigami.ScrollablePage { var pageStack = applicationWindow().pageStack - // Reuse the already loaded Ui when item type (key) stays the same - if (pageStack.lastItem.key !== root.key) { - while (pageStack.depth > 2) { - pageStack.pop(); - } - } - - if (pageStack.depth <= 2) { + // Reuse the already loaded Ui + if (pageStack.depth > 2) { + pageStack.lastItem.jsonData = jsonData.subtree(index); + pageStack.lastItem.objIndex = index; + } else { //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 f5523e74..1e3f1d30 100644 --- a/src/ui/database/DatabaseEntryPage.qml +++ b/src/ui/database/DatabaseEntryPage.qml @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later import QtQuick +import QtQuick.Layouts import org.kde.kirigami as Kirigami import org.easyrpg.editor as Ez @@ -21,9 +22,27 @@ Kirigami.ScrollablePage { /** When the object comes from a list, contains the index. Otherwise -1. */ property int objIndex: -1 + // Update the Page title when the name changes + Connections { + target: jsonData + function onValueChanged(jsonPtr) { + if (objIndex >= 0 && jsonPtr == "/name") { + root.title = jsonData.str("name"); + } + } + } + + onJsonDataChanged: { + if (objIndex >= 0) { + root.title = jsonData.str("name"); + } + } + // TODO: Not used. Just for testing property bool showActions: false + Layout.fillWidth: true + actions: [ Kirigami.Action { text: "Cancel" diff --git a/src/ui/database/DatabasePage.qml b/src/ui/database/DatabasePage.qml index 4d21926f..09afcd43 100644 --- a/src/ui/database/DatabasePage.qml +++ b/src/ui/database/DatabasePage.qml @@ -18,6 +18,8 @@ Kirigami.ScrollablePage { /** Database of the current project */ property Ez.JsonView jsonData + title: "Database" + /** * name: Text shown to the user * key: JSON pointer to access the list items of this category @@ -46,11 +48,17 @@ Kirigami.ScrollablePage { key: "attributes" targetPage: "AttributePage.qml" } + ListElement { + name: "System" + key: "system" + targetPage: "SystemPage.qml" + single: true + } ListElement { name: "Vocabulary" key: "terms" - single: true targetPage: "VocabularyPage.qml" + single: true } } @@ -88,12 +96,14 @@ Kirigami.ScrollablePage { if (item.single === true) { pageStack.push(Qt.resolvedUrl(item.targetPage), { - "jsonData": jsonData.subtree(item.key) + "jsonData": jsonData.subtree(item.key), + "title": item.name }) } else { pageStack.push(Qt.resolvedUrl("DatabaseEntryListPage.qml"), { "jsonData": jsonData.list(item.key), - "targetPage": item.targetPage + "targetPage": item.targetPage, + "title": item.name }) } } diff --git a/src/ui/database/DatabaseWindow.qml b/src/ui/database/DatabaseWindow.qml index 68b3e6d7..9cf8f150 100644 --- a/src/ui/database/DatabaseWindow.qml +++ b/src/ui/database/DatabaseWindow.qml @@ -16,12 +16,12 @@ import org.easyrpg.editor as Ez Kirigami.ApplicationWindow { id: root - width: 1024 - height: 600 + width: Kirigami.Units.gridUnit * 74 + height: Kirigami.Units.gridUnit * 41 title: "EasyRPG Editor - Database" - pageStack.defaultColumnWidth: 200 + pageStack.defaultColumnWidth: Kirigami.Units.gridUnit * 11 /** Database of the current project */ property Ez.JsonView jsonData: Ez.ProjectData.database() diff --git a/src/ui/database/ItemPage.qml b/src/ui/database/ItemPage.qml index cdb87885..6106a21f 100644 --- a/src/ui/database/ItemPage.qml +++ b/src/ui/database/ItemPage.qml @@ -15,16 +15,16 @@ DatabaseEntryPage { 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:" + } } } diff --git a/src/ui/database/SystemPage.qml b/src/ui/database/SystemPage.qml new file mode 100644 index 00000000..4e3a2088 --- /dev/null +++ b/src/ui/database/SystemPage.qml @@ -0,0 +1,254 @@ +// SPDX-FileCopyrightText: EasyRPG Editor Authors +// SPDX-License-Identifier: GPL-3.0-or-later + +import Qt.labs.folderlistmodel 2.11 +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 + + component JsonImageViewer : Ez.ImageViewer { + id: imageViewer + + Layout.fillWidth: true + + property string key + filename: root.jsonData.str(key) + + pickerData: Ez.PickerData { + Component.onCompleted: filename = imageViewer.filename + } + onAccepted: { + imageViewer.filename = pickerData.filename + jsonData.set(key, pickerData.filename) + } + } + + component JsonMusicViewer : Ez.MusicViewer { + property string key + readonly property var musicData: root.jsonData.subtree(key) + pickerData: Ez.PickerData { + Component.onCompleted: fromMusic(musicData) + } + onAccepted: pickerData.toMusic(musicData) + } + + component JsonSoundViewer : Ez.SoundViewer { + property string key + readonly property var soundData: root.jsonData.subtree(key) + pickerData: Ez.PickerData { + Component.onCompleted: fromSound(soundData) + } + onAccepted: pickerData.toSound(soundData) + } + + component JsonCharSetViewer : Ez.CharSetViewer { + id: charViewer + property string nameKey + property string indexKey + + spin: true + walk: true + + filename: root.jsonData.str(nameKey) + cellIndex: root.jsonData.num(indexKey) + + pickerData: Ez.PickerData { + index: charViewer.cellIndex + Component.onCompleted: filename = charViewer.filename + } + + onAccepted: { + charViewer.filename = pickerData.filename + charViewer.cellIndex = pickerData.index + root.jsonData.set(nameKey, pickerData.filename) + root.jsonData.set(indexKey, pickerData.index) + } + } + + Kirigami.FormLayout { + anchors.fill: parent + + Kirigami.Separator { + Kirigami.FormData.isSection: true + Kirigami.FormData.label: "Title" + } + + JsonImageViewer { + directory: "Title" + key: "title_name" + } + + Kirigami.Separator { + Kirigami.FormData.isSection: true + Kirigami.FormData.label: "Game Over" + } + + JsonImageViewer { + directory: "GameOver" + key: "gameover_name" + } + + Kirigami.Separator { + Kirigami.FormData.isSection: true + Kirigami.FormData.label: "System" + } + + JsonImageViewer { + directory: "System" + key: "system_name" + } + + Kirigami.Separator { + Kirigami.FormData.isSection: true + Kirigami.FormData.label: "System 2" + } + + JsonImageViewer { + directory: "System 2" + key: "system2_name" + } + + Kirigami.Separator { + Kirigami.FormData.isSection: true + Kirigami.FormData.label: "Music" + } + + JsonMusicViewer { + key: "title_music" + Kirigami.FormData.label: "Title:" + } + + JsonMusicViewer { + key: "gameover_music" + Kirigami.FormData.label: "Game Over:" + } + + JsonMusicViewer { + key: "inn_music" + Kirigami.FormData.label: "Inn:" + } + + JsonMusicViewer { + key: "boat_music" + Kirigami.FormData.label: "Boat:" + } + + JsonMusicViewer { + key: "ship_music" + Kirigami.FormData.label: "Ship:" + } + + JsonMusicViewer { + key: "airship_music" + Kirigami.FormData.label: "Airship:" + } + + JsonMusicViewer { + key: "battle_music" + Kirigami.FormData.label: "Battle:" + } + + JsonMusicViewer { + key: "battle_end_music" + Kirigami.FormData.label: "Battle End:" + } + + Kirigami.Separator { + Kirigami.FormData.isSection: true + Kirigami.FormData.label: "Sound" + } + + + JsonSoundViewer { + key: "cursor_se" + Kirigami.FormData.label: "Cursor:" + } + + JsonSoundViewer { + key: "decision_se" + Kirigami.FormData.label: "Decision:" + } + + JsonSoundViewer { + key: "cancel_se" + Kirigami.FormData.label: "Cancel:" + } + + JsonSoundViewer { + key: "buzzer_se" + Kirigami.FormData.label: "Buzzer:" + } + + JsonSoundViewer { + key: "battle_se" + Kirigami.FormData.label: "Battle:" + } + + JsonSoundViewer { + key: "escape_se" + Kirigami.FormData.label: "Escape:" + } + + JsonSoundViewer { + key: "enemy_attack_se" + Kirigami.FormData.label: "Enemy Attack:" + } + + JsonSoundViewer { + key: "enemy_damaged_se" + Kirigami.FormData.label: "Enemy Damaged:" + } + + JsonSoundViewer { + key: "actor_damaged_se" + Kirigami.FormData.label: "Actor Damaged:" + } + + JsonSoundViewer { + key: "dodge_se" + Kirigami.FormData.label: "Dodge:" + } + + JsonSoundViewer { + key: "enemy_death_se" + Kirigami.FormData.label: "Enemy Death:" + } + + JsonSoundViewer { + key: "item_se" + Kirigami.FormData.label: "Item:" + } + + Kirigami.Separator { + Kirigami.FormData.isSection: true + Kirigami.FormData.label: "Vehicles" + } + + JsonCharSetViewer { + Kirigami.FormData.label: "Boat:" + nameKey: "boat_name" + indexKey: "boat_index" + showTransparency: false + } + + JsonCharSetViewer { + Kirigami.FormData.label: "Ship:" + nameKey: "ship_name" + indexKey: "ship_index" + showTransparency: false + } + + JsonCharSetViewer { + Kirigami.FormData.label: "Airship:" + nameKey: "airship_name" + indexKey: "airship_index" + showTransparency: false + } + } +} diff --git a/src/ui/database/actor_widget.cpp b/src/ui/database/actor_widget.cpp index 9a18f118..b562ebda 100644 --- a/src/ui/database/actor_widget.cpp +++ b/src/ui/database/actor_widget.cpp @@ -345,7 +345,7 @@ void ActorWidget::on_listStatusRanks_clicked() { } int index = ui->listStatusRanks->currentRow(); - if (m_current->state_ranks.size() <= index) { + if (static_cast(m_current->state_ranks.size()) <= index) { for (int i = m_current->state_ranks.size(); i <= index; i++) { m_current->state_ranks.push_back(2); } @@ -355,7 +355,7 @@ void ActorWidget::on_listStatusRanks_clicked() { m_current->state_ranks[index] = rank; ui->listStatusRanks->item(index)->setIcon(QIcon(QString(":/ranks/rank%1").arg(rank))); - if (index == m_current->state_ranks.size() - 1 && rank == 2) { + if (index == static_cast(m_current->state_ranks.size()) - 1 && rank == 2) { m_current->state_ranks.pop_back(); for (int i = m_current->state_ranks.size() - 1; i >= 0; i--) { if (m_current->state_ranks[i] == 2) { @@ -373,7 +373,7 @@ void ActorWidget::on_listAttributeRanks_clicked() { } int index = ui->listAttributeRanks->currentRow(); - if (m_current->attribute_ranks.size() <= index) { + if (static_cast(m_current->attribute_ranks.size()) <= index) { for (int i = m_current->attribute_ranks.size(); i <= index; i++) { m_current->attribute_ranks.push_back(2); } @@ -383,7 +383,7 @@ void ActorWidget::on_listAttributeRanks_clicked() { m_current->attribute_ranks[index] = rank; ui->listAttributeRanks->item(index)->setIcon(QIcon(QString(":/ranks/rank%1").arg(rank))); - if (index == m_current->attribute_ranks.size() - 1 && rank == 2) { + if (index == static_cast(m_current->attribute_ranks.size()) - 1 && rank == 2) { m_current->attribute_ranks.pop_back(); for (int i = m_current->attribute_ranks.size() - 1; i >= 0; i--) { if (m_current->attribute_ranks[i] == 2) { diff --git a/src/ui/database/attribute_widget.cpp b/src/ui/database/attribute_widget.cpp index 1fc69b35..f29ab1dc 100644 --- a/src/ui/database/attribute_widget.cpp +++ b/src/ui/database/attribute_widget.cpp @@ -69,8 +69,6 @@ void AttributeWidget::on_currentAttributeChanged(lcf::rpg::Attribute *attribute) } m_current = attribute; - auto& database = m_project.database(); - LcfWidgetBinding::bind(ui->lineName, attribute->name); LcfWidgetBinding::bind(m_buttonGroupType, attribute->type); LcfWidgetBinding::bind(ui->spinA, attribute->a_rate); diff --git a/src/ui/database/common_event_widget.cpp b/src/ui/database/common_event_widget.cpp index 64808b3d..36ea6b77 100644 --- a/src/ui/database/common_event_widget.cpp +++ b/src/ui/database/common_event_widget.cpp @@ -56,7 +56,7 @@ void CommonEventWidget::setData(lcf::rpg::CommonEvent* common_event) { updateComboSwitchEnabled(); } -void CommonEventWidget::on_comboTrigger_currentIndexChanged(int index) { +void CommonEventWidget::on_comboTrigger_currentIndexChanged(int /*index*/) { updateComboSwitchEnabled(); } diff --git a/src/ui/event/event_command_base_widget.cpp b/src/ui/event/event_command_base_widget.cpp index 8a520242..7984dca8 100644 --- a/src/ui/event/event_command_base_widget.cpp +++ b/src/ui/event/event_command_base_widget.cpp @@ -39,7 +39,7 @@ void EventCommandBaseWidget::setData(lcf::rpg::EventCommand* cmd) { auto idx = widget->objectName().indexOf("_argX"); if (idx != -1) { connect(widget, &QLineEdit::textEdited, this, - [=] (auto text) { + [=, this] (auto text) { cmd->string = lcf::DBString(ToDBString(text)); stringParameterChanged(text); }); @@ -150,7 +150,7 @@ void EventCommandBaseWidget::connectParameterHandler(QButtonGroup* group, int in Q_ASSERT_X(button, "connectParameterHandler", "No AbstractButton with this parameter value"); connect(group, QOverload::of(&QButtonGroup::buttonToggled), this, - [=](QAbstractButton*, bool checked) { + [=, this](QAbstractButton*, bool checked) { if (checked) { int id = group->checkedId(); @@ -169,7 +169,7 @@ void EventCommandBaseWidget::connectParameterHandler(RpgComboBoxBase* combo, int resizeCommandList(index); connect(combo->comboBox(), QOverload::of(&QComboBox::currentIndexChanged), this, - [=](int selected_index){ + [=, this](int selected_index){ m_cmd->parameters[index] = selected_index + 1; emit parameterChanged(index, selected_index + 1); }); @@ -183,7 +183,7 @@ void EventCommandBaseWidget::connectParameterHandler(QSpinBox *spin, int index, resizeCommandList(index); connect(spin, qOverload(&QSpinBox::valueChanged), this, - [=] (int new_value) { + [=, this] (int new_value) { m_cmd->parameters[index] = new_value; emit parameterChanged(index, new_value); }); @@ -196,10 +196,10 @@ void EventCommandBaseWidget::connectParameterHandler(QSpinBox *spin, int index, void EventCommandBaseWidget::connectParameterHandler(QCheckBox* check, int index, bool set_value) { resizeCommandList(index); - connect(check, qOverload(&QCheckBox::stateChanged), this, - [=] (int new_value) { - m_cmd->parameters[index] = new_value; - emit parameterChanged(index, new_value); + connect(check, &QCheckBox::checkStateChanged, this, + [=, this] (Qt::CheckState state) { + m_cmd->parameters[index] = (state == Qt::Checked ? 1 : 0); + emit parameterChanged(index, (state == Qt::Checked ? 1 : 0)); }); if (set_value) { @@ -211,7 +211,7 @@ void EventCommandBaseWidget::connectParameterHandler(QSlider* slider, int index, resizeCommandList(index); connect(slider, qOverload(&QSlider::valueChanged), this, - [=] (int new_value) { + [=, this] (int new_value) { m_cmd->parameters[index] = new_value; emit parameterChanged(index, new_value); }); diff --git a/src/ui/main_window.cpp b/src/ui/main_window.cpp index d968ebbe..ae78a18f 100644 --- a/src/ui/main_window.cpp +++ b/src/ui/main_window.cpp @@ -354,8 +354,8 @@ void MainWindow::ImportProject(const QDir& src_dir, QDir& target_dir, bool conve if (convert_xyz && info.dir().dirName() != MUSIC && info.dir().dirName() != SOUND) { QFile file(dest_file); - file.open(QIODevice::ReadOnly); - if (file.read(4) == "XYZ1") + bool success = file.open(QIODevice::ReadOnly); + if (success && file.read(4) == "XYZ1") { QString conv_path = target_dir.path() + "/" + info.dir().dirName() + "/" + info.completeBaseName() + ".png"; if (convertXYZtoPNG(file, conv_path)) @@ -494,9 +494,9 @@ void MainWindow::on_actionDatabaseNew_triggered() { auto engine = core().qmlEngine(); // Inject ProjectData into the QML - auto* projectGadget = engine->singletonInstance( - "org.easyrpg.editor", "ProjectData" - ); + auto* projectGadget = engine->singletonInstance( + "org.easyrpg.editor", "ProjectData" + ); projectGadget->setProjectData(&core().project()->projectData()); @@ -686,7 +686,7 @@ bool MainWindow::removeDir(const QString & dirName, const QString &root) QMessageBox::warning(this, tr("An error ocurred"), QString(tr("Could't delete %1")).arg(info.absoluteFilePath()), - QMessageBox::Ok, 0); + QMessageBox::Ok); return false; } } @@ -817,7 +817,7 @@ void MainWindow::on_actionProjectOpen_triggered() m_settings.setValue(DEFAULT_DIR_KEY,dlg.getDefaultDir()); } -void MainWindow::on_actionJukebox_triggered(bool disconnect) +void MainWindow::on_actionJukebox_triggered(bool) { } diff --git a/src/ui/other/open_project_dialog.cpp b/src/ui/other/open_project_dialog.cpp index 19532bd4..adefa051 100644 --- a/src/ui/other/open_project_dialog.cpp +++ b/src/ui/other/open_project_dialog.cpp @@ -112,7 +112,7 @@ bool OpenProjectDialog::removeDir(const QString & dirName) QMessageBox::warning(this, tr("An error ocurred"), QString(tr("Could't delete %1")).arg(info.absoluteFilePath()), - QMessageBox::Ok, 0); + QMessageBox::Ok); return false; } } diff --git a/src/ui/picker/CharSetPicker.qml b/src/ui/picker/CharSetPicker.qml new file mode 100644 index 00000000..53205bcb --- /dev/null +++ b/src/ui/picker/CharSetPicker.qml @@ -0,0 +1,94 @@ +// SPDX-FileCopyrightText: EasyRPG Editor Authors +// SPDX-License-Identifier: GPL-3.0-or-later + +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls as Controls +import org.kde.kirigami as Kirigami +import org.easyrpg.editor as Ez + +PickerBase { + id: root + + directory: "CharSet" + property bool showTransparency: true + + Ez.CharSetPaintedItem { + id: charsetPainted + pickerMode: true + projectData: Ez.ProjectData + filename: root.filename + cellIndex: root.pickerData.index + transparent: root.pickerData.transparent + onCellIndexChanged: { + root.pickerData.index = cellIndex + } + Layout.fillWidth: true + implicitWidth: 192 + implicitHeight: 192 + } + + Kirigami.Separator { + visible: root.showTransparency + Kirigami.FormData.isSection: true + Kirigami.FormData.label: "Options" + } + + Controls.CheckBox { + visible: root.showTransparency + text: "Transparent" + checked: root.pickerData.transparent + onClicked: root.pickerData.transparent = checked + } + + Kirigami.Separator { + Kirigami.FormData.isSection: true + Kirigami.FormData.label: "Facing (Preview Only)" + } + + ColumnLayout { + Controls.RadioButton { + text: "Up" + checked: charsetPainted.facing === Ez.CharSetPaintedItem.Direction_up + onClicked: charsetPainted.facing = Ez.CharSetPaintedItem.Direction_up + } + Controls.RadioButton { + text: "Right" + checked: charsetPainted.facing === Ez.CharSetPaintedItem.Direction_right + onClicked: charsetPainted.facing = Ez.CharSetPaintedItem.Direction_right + } + Controls.RadioButton { + text: "Down" + checked: charsetPainted.facing === Ez.CharSetPaintedItem.Direction_down + onClicked: charsetPainted.facing = Ez.CharSetPaintedItem.Direction_down + } + Controls.RadioButton { + text: "Left" + checked: charsetPainted.facing === Ez.CharSetPaintedItem.Direction_left + onClicked: charsetPainted.facing = Ez.CharSetPaintedItem.Direction_left + } + } + + Kirigami.Separator { + Kirigami.FormData.isSection: true + Kirigami.FormData.label: "Pattern (Preview Only)" + } + + ColumnLayout { + Controls.RadioButton { + text: "Left" + checked: charsetPainted.frame === Ez.CharSetPaintedItem.Frame_left + onClicked: charsetPainted.frame = Ez.CharSetPaintedItem.Frame_left + } + Controls.RadioButton { + text: "Middle" + checked: charsetPainted.frame === Ez.CharSetPaintedItem.Frame_middle + onClicked: charsetPainted.frame = Ez.CharSetPaintedItem.Frame_middle + } + Controls.RadioButton { + text: "Right" + checked: charsetPainted.frame === Ez.CharSetPaintedItem.Frame_right + onClicked: charsetPainted.frame = Ez.CharSetPaintedItem.Frame_right + } + } +} diff --git a/src/ui/picker/FaceSetPicker.qml b/src/ui/picker/FaceSetPicker.qml new file mode 100644 index 00000000..7c3dfc32 --- /dev/null +++ b/src/ui/picker/FaceSetPicker.qml @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: EasyRPG Editor Authors +// SPDX-License-Identifier: GPL-3.0-or-later + +import QtQuick +import QtQuick.Layouts +import org.kde.kirigami as Kirigami +import org.easyrpg.editor as Ez + +PickerBase { + id: root + + directory: "FaceSet" + + Ez.FaceSetPaintedItem { + id: facesetGrid + pickerMode: true + projectData: Ez.ProjectData + filename: root.filename + cellIndex: root.pickerData.index + onCellIndexChanged: { + root.pickerData.index = cellIndex + } + Layout.fillWidth: true + implicitWidth: 192 + implicitHeight: 192 + } +} diff --git a/src/ui/picker/ImagePicker.qml b/src/ui/picker/ImagePicker.qml new file mode 100644 index 00000000..7c382b1e --- /dev/null +++ b/src/ui/picker/ImagePicker.qml @@ -0,0 +1,22 @@ +// SPDX-FileCopyrightText: EasyRPG Editor Authors +// SPDX-License-Identifier: GPL-3.0-or-later + +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls as Controls +import org.kde.kirigami as Kirigami +import org.easyrpg.editor as Ez + +PickerBase { + id: root + + Ez.SpritePaintedItem { + id: sprite + + directory: root.directory + filename: root.filename + projectData: Ez.ProjectData + + Layout.fillWidth: true + } +} diff --git a/src/ui/picker/MusicPicker.qml b/src/ui/picker/MusicPicker.qml new file mode 100644 index 00000000..c6af630a --- /dev/null +++ b/src/ui/picker/MusicPicker.qml @@ -0,0 +1,45 @@ +// SPDX-FileCopyrightText: EasyRPG Editor Authors +// SPDX-License-Identifier: GPL-3.0-or-later + +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls as Controls +import org.kde.kirigami as Kirigami +import org.easyrpg.editor as Ez + +PickerBase { + id: root + + directory: "Music" + + Controls.SpinBox { + id: fadeinInput + value: root.pickerData.fadein + Kirigami.FormData.label: "Fade In:" + } + + Controls.SpinBox { + id: volumeInput + value: root.pickerData.volume + Kirigami.FormData.label: "Volume:" + } + + Controls.SpinBox { + id: tempoInput + value: root.pickerData.tempo + Kirigami.FormData.label: "Pitch:" + } + + Controls.SpinBox { + id: balanceInput + value: root.pickerData.balance + Kirigami.FormData.label: "Balance:" + } + + onAccepted: { + root.pickerData.fadein = fadeinInput.value + root.pickerData.volume = volumeInput.value + root.pickerData.tempo = tempoInput.value + root.pickerData.balance = balanceInput.value + } +} diff --git a/src/ui/picker/PickerBase.qml b/src/ui/picker/PickerBase.qml new file mode 100644 index 00000000..54341c0d --- /dev/null +++ b/src/ui/picker/PickerBase.qml @@ -0,0 +1,95 @@ +// SPDX-FileCopyrightText: EasyRPG Editor Authors +// SPDX-License-Identifier: GPL-3.0-or-later + +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls as Controls +import org.kde.kirigami as Kirigami +import org.easyrpg.editor as Ez + +Kirigami.Page { + id: root + + required property Ez.PickerData pickerData + + readonly property alias filename: nameInput.text + property string directory + + default property alias formContent: formLayout.data + + /** Signal emitted when confirming */ + signal accepted() + + actions: [ + Kirigami.Action { + text: "Cancel" + icon.name: "cancel" + onTriggered: { + Kirigami.PageStack.closeDialog() + } + }, + Kirigami.Action { + text: "Select" + icon.name: "confirm" + onTriggered: { + root.pickerData.filename = nameInput.text + root.accepted() + Kirigami.PageStack.closeDialog() + } + } + ] + + RowLayout { + anchors.fill: parent + + Controls.ScrollView { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.preferredWidth: parent.width * 0.25 + + background: Rectangle { + //color: Kirigami.Theme.viewBackgroundColor + } + + ListView { + id: dirList + clip: true + + model: Ez.DirectoryModel { + path: Ez.ProjectData.findDirectory(root.directory) + } + + delegate: Controls.ItemDelegate { + width: ListView.view.width + text: baseName + + highlighted: baseName === nameInput.text + + action: Controls.Action { + onTriggered: { + nameInput.text = baseName + } + } + + Component.onCompleted: { + if (highlighted) { + dirList.positionViewAtIndex(index, ListView.Center) + } + } + } + } + } + + Kirigami.FormLayout { + id: formLayout + Layout.alignment: Qt.AlignTop + + Controls.TextField { + id: nameInput + text: root.pickerData.filename + Kirigami.FormData.label: "Name:" + visible: false + } + } + } +} diff --git a/src/ui/picker/SoundPicker.qml b/src/ui/picker/SoundPicker.qml new file mode 100644 index 00000000..0b4ca09e --- /dev/null +++ b/src/ui/picker/SoundPicker.qml @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: EasyRPG Editor Authors +// SPDX-License-Identifier: GPL-3.0-or-later + +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls as Controls +import org.kde.kirigami as Kirigami +import org.easyrpg.editor as Ez + +PickerBase { + id: root + + directory: "Sound" + + Controls.SpinBox { + id: volumeInput + value: root.pickerData.volume + Kirigami.FormData.label: "Volume:" + } + + Controls.SpinBox { + id: tempoInput + value: root.pickerData.tempo + Kirigami.FormData.label: "Pitch:" + } + + Controls.SpinBox { + id: balanceInput + value: root.pickerData.balance + Kirigami.FormData.label: "Balance:" + } + + onAccepted: { + root.pickerData.volume = volumeInput.value + root.pickerData.tempo = tempoInput.value + root.pickerData.balance = balanceInput.value + } +} diff --git a/src/ui/picker/charset_painted_item.cpp b/src/ui/picker/charset_painted_item.cpp new file mode 100644 index 00000000..49861963 --- /dev/null +++ b/src/ui/picker/charset_painted_item.cpp @@ -0,0 +1,142 @@ +/* + * This file is part of EasyRPG Editor. + * + * EasyRPG Editor 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, either version 3 of the License, or + * (at your option) any later version. + * + * EasyRPG Editor 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 EasyRPG Editor. If not, see . + */ + +#include "charset_painted_item.h" + +CharSetPaintedItem::CharSetPaintedItem(QQuickItem *parent) : SpritePaintedItem(parent) { + m_directory = QStringLiteral("CharSet"); + m_cellWidth = 24; + m_cellHeight = 32; + m_skipCellsX = 2; // block width is 3 cells, so skip 2 + m_skipCellsY = 3; // block height is 4 cells, so skip 3 + setCellOffsetX(m_frame * m_cellWidth); + setCellOffsetY(m_facing * m_cellHeight); + + // Animation timer ~30 fps + m_timer.setInterval(1000 / 30); + QObject::connect(&m_timer, &QTimer::timeout, this, &CharSetPaintedItem::tick); +} + +int CharSetPaintedItem::facing() const { + return m_facing; +} + +void CharSetPaintedItem::setFacing(int facing) { + if (m_facing == facing) { + return; + } + m_facing = facing; + setCellOffsetY(m_facing * m_cellHeight); + emit facingChanged(); + update(); +} + +int CharSetPaintedItem::frame() const { + return m_frame; +} + +void CharSetPaintedItem::setFrame(int frame) { + if (m_frame == frame) { + return; + } + m_frame = frame; + setCellOffsetX(m_frame * m_cellWidth); + emit frameChanged(); + update(); +} + +bool CharSetPaintedItem::spin() const { + return m_spin; +} + +void CharSetPaintedItem::setSpin(bool spin) { + if (m_spin == spin) { + return; + } + m_spin = spin; + emit spinChanged(); + + if (m_spin || m_walk) { + m_timer.start(); + } else { + m_timer.stop(); + } +} + +bool CharSetPaintedItem::walk() const { + return m_walk; +} + +void CharSetPaintedItem::setWalk(bool walk) { + if (m_walk == walk) { + return; + } + m_walk = walk; + emit walkChanged(); + + if (m_spin || m_walk) { + m_timer.start(); + } else { + m_timer.stop(); + } +} + +QRectF CharSetPaintedItem::sourceRect() const { + if (pickerMode()) { + return SpritePaintedItem::sourceRect(); + } + + const int blockSizeX = 72; + const int blockSizeY = 128; + const int colsPerRow = 4; + + const int blockX = (m_cellIndex % colsPerRow) * blockSizeX; + const int blockY = (m_cellIndex / colsPerRow) * blockSizeY; + + const int x = blockX + m_frame * m_cellWidth; + const int y = blockY + m_facing * m_cellHeight; + + return QRectF(x, y, m_cellWidth, m_cellHeight); +} + +void CharSetPaintedItem::tick() { + static const int walkPatterns[4] = {Frame_middle, Frame_right, Frame_middle, Frame_left}; + + frame_count++; + + if (frame_count == 90) { + frame_count = 0; + if (m_spin) { + m_facing++; + if (m_facing > Direction_left) { + m_facing = Direction_up; + } + emit facingChanged(); + } + } + + if (m_walk) { + m_frame = walkPatterns[m_pattern]; + emit frameChanged(); + + if (frame_count % 6 == 0) { + m_pattern = (m_pattern + 1) % 4; + } + } + + update(); +} diff --git a/src/ui/picker/charset_painted_item.h b/src/ui/picker/charset_painted_item.h new file mode 100644 index 00000000..44219712 --- /dev/null +++ b/src/ui/picker/charset_painted_item.h @@ -0,0 +1,86 @@ +/* + * This file is part of EasyRPG Editor. + * + * EasyRPG Editor 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, either version 3 of the License, or + * (at your option) any later version. + * + * EasyRPG Editor 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 EasyRPG Editor. If not, see . + */ + +#pragma once + +#include "sprite_painted_item.h" +#include + +/** + * Displays an animated character from a CharSet spritesheet. + */ +class CharSetPaintedItem : public SpritePaintedItem { + Q_OBJECT + QML_ELEMENT + Q_PROPERTY(int facing READ facing WRITE setFacing NOTIFY facingChanged) + Q_PROPERTY(int frame READ frame WRITE setFrame NOTIFY frameChanged) + Q_PROPERTY(bool spin READ spin WRITE setSpin NOTIFY spinChanged) + Q_PROPERTY(bool walk READ walk WRITE setWalk NOTIFY walkChanged) + +public: + enum Direction { + Direction_up = 0, + Direction_right = 1, + Direction_down = 2, + Direction_left = 3 + }; + Q_ENUM(Direction) + + enum Frame { + Frame_left = 0, + Frame_middle = 1, + Frame_right = 2 + }; + Q_ENUM(Frame) + + explicit CharSetPaintedItem(QQuickItem* parent = nullptr); + + int facing() const; + void setFacing(int facing); + + int frame() const; + void setFrame(int frame); + + bool spin() const; + void setSpin(bool spin); + + bool walk() const; + void setWalk(bool walk); + +signals: + void characterIndexChanged(); + void facingChanged(); + void frameChanged(); + void spinChanged(); + void walkChanged(); + +protected: + QRectF sourceRect() const override; + +private: + void tick(); + + QTimer m_timer; + + int m_facing = Direction_down; + int m_frame = Frame_middle; + bool m_spin = false; + bool m_walk = false; + + int m_pattern = 0; + int frame_count = 0; +}; diff --git a/src/ui/picker/faceset_painted_item.cpp b/src/ui/picker/faceset_painted_item.cpp new file mode 100644 index 00000000..a4cd3f06 --- /dev/null +++ b/src/ui/picker/faceset_painted_item.cpp @@ -0,0 +1,27 @@ +/* + * This file is part of EasyRPG Editor. + * + * EasyRPG Editor 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, either version 3 of the License, or + * (at your option) any later version. + * + * EasyRPG Editor 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 EasyRPG Editor. If not, see . + */ + +#include "faceset_painted_item.h" +#include +#include + +FaceSetPaintedItem::FaceSetPaintedItem(QQuickItem* parent) : SpritePaintedItem(parent) { + setDirectory(QStringLiteral("FaceSet")); + setCellWidth(48); + setCellHeight(48); +} + diff --git a/src/ui/picker/faceset_painted_item.h b/src/ui/picker/faceset_painted_item.h new file mode 100644 index 00000000..812b432c --- /dev/null +++ b/src/ui/picker/faceset_painted_item.h @@ -0,0 +1,33 @@ +/* + * This file is part of EasyRPG Editor. + * + * EasyRPG Editor 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, either version 3 of the License, or + * (at your option) any later version. + * + * EasyRPG Editor 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 EasyRPG Editor. If not, see . + */ + +#pragma once + +#include "sprite_painted_item.h" + +/** + * Displays or picks faces from a FaceSet spritesheet. + */ +class FaceSetPaintedItem : public SpritePaintedItem { + Q_OBJECT + QML_ELEMENT + +public: + explicit FaceSetPaintedItem(QQuickItem* parent = nullptr); + +protected: +}; diff --git a/src/ui/picker/sprite_painted_item.cpp b/src/ui/picker/sprite_painted_item.cpp new file mode 100644 index 00000000..5f4f7d85 --- /dev/null +++ b/src/ui/picker/sprite_painted_item.cpp @@ -0,0 +1,341 @@ +/* + * This file is part of EasyRPG Editor. + * + * EasyRPG Editor 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, either version 3 of the License, or + * (at your option) any later version. + * + * EasyRPG Editor 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 EasyRPG Editor. If not, see . + */ + +#include "sprite_painted_item.h" +#include + +SpritePaintedItem::SpritePaintedItem(QQuickItem *parent) : QQuickPaintedItem(parent) { + setAcceptedMouseButtons(Qt::LeftButton); +} + +QString SpritePaintedItem::filename() const { + return m_filename; +} + +void SpritePaintedItem::setFilename(const QString& filename) { + if (m_filename == filename) { + return; + } + m_filename = filename; + m_image = {}; + emit filenameChanged(); + reload(); +} + +QString SpritePaintedItem::directory() const { + return m_directory; +} + +void SpritePaintedItem::setDirectory(const QString& directory) { + if (m_directory == directory) { + return; + } + m_directory = directory; + m_image = {}; + emit directoryChanged(); + reload(); +} + +ProjectDataGadget* SpritePaintedItem::projectData() const { return m_projectData; } + +void SpritePaintedItem::setProjectData(ProjectDataGadget* projectData) { + m_projectData = projectData; + m_image = {}; + emit projectDataChanged(); + reload(); +} + +bool SpritePaintedItem::pickerMode() const { + return m_pickerMode; +} + +void SpritePaintedItem::setPickerMode(bool pickerMode) { + if (m_pickerMode == pickerMode) { + return; + } + m_pickerMode = pickerMode; + emit pickerModeChanged(); + update(); +} + +int SpritePaintedItem::cellIndex() const { + return m_cellIndex; +} + +void SpritePaintedItem::setCellIndex(int cellIndex) { + if (m_cellIndex == cellIndex) { + return; + } + m_cellIndex = cellIndex; + emit cellIndexChanged(); + update(); +} + +int SpritePaintedItem::cellWidth() const { + return m_cellWidth; +} + +void SpritePaintedItem::setCellWidth(int cellWidth) { + if (m_cellWidth == cellWidth) { + return; + } + m_cellWidth = cellWidth; + emit cellWidthChanged(); + update(); +} + +int SpritePaintedItem::cellHeight() const { + return m_cellHeight; +} + +void SpritePaintedItem::setCellHeight(int cellHeight) { + if (m_cellHeight == cellHeight) { + return; + } + m_cellHeight = cellHeight; + emit cellHeightChanged(); + update(); +} + +bool SpritePaintedItem::transparent() const { + return m_transparent; +} + +void SpritePaintedItem::setTransparent(bool transparent) { + if (m_transparent == transparent) { + return; + } + m_transparent = transparent; + emit transparentChanged(); + update(); +} + +int SpritePaintedItem::cellOffsetX() const { + return m_cellOffsetX; +} + +void SpritePaintedItem::setCellOffsetX(int cellOffsetX) { + if (m_cellOffsetX == cellOffsetX) { + return; + } + m_cellOffsetX = cellOffsetX; + emit cellOffsetXChanged(); + update(); +} + +int SpritePaintedItem::cellOffsetY() const { + return m_cellOffsetY; +} + +void SpritePaintedItem::setCellOffsetY(int cellOffsetY) { + if (m_cellOffsetY == cellOffsetY) { + return; + } + m_cellOffsetY = cellOffsetY; + emit cellOffsetYChanged(); + update(); +} + +int SpritePaintedItem::skipCellsX() const { + return m_skipCellsX; +} + +void SpritePaintedItem::setSkipCellsX(int skipCellsX) { + if (m_skipCellsX == skipCellsX) { + return; + } + m_skipCellsX = skipCellsX; + emit skipCellsXChanged(); + update(); +} + +int SpritePaintedItem::skipCellsY() const { + return m_skipCellsY; +} + +void SpritePaintedItem::setSkipCellsY(int skipCellsY) { + if (m_skipCellsY == skipCellsY) { + return; + } + m_skipCellsY = skipCellsY; + emit skipCellsYChanged(); + update(); +} + +int SpritePaintedItem::gridColumns() const { + return m_gridColumns; +} + +void SpritePaintedItem::setGridColumns(int gridColumns) { + if (m_gridColumns == gridColumns) { + return; + } + m_gridColumns = gridColumns; + emit gridColumnsChanged(); + update(); +} + +QRectF SpritePaintedItem::sourceRect() const { + if (m_cellWidth <= 0 || m_cellHeight <= 0) { + return m_image.rect(); + } + const int pitchX = m_cellWidth * (m_skipCellsX + 1); + const int pitchY = m_cellHeight * (m_skipCellsY + 1); + const int col = m_cellIndex % m_gridColumns; + const int row = m_cellIndex / m_gridColumns; + return QRectF(col * pitchX + m_cellOffsetX, row * pitchY + m_cellOffsetY, m_cellWidth, m_cellHeight); +} + +void SpritePaintedItem::paint(QPainter* painter) { + if (m_image.isNull()) { + return; + } + + if (m_transparent) { + painter->setOpacity(0.5); + } + + if (m_pickerMode) { + if (m_cellWidth <= 0 || m_cellHeight <= 0) { + const QSize targetSize(qMax(1, int(width())), qMax(1, int(height()))); + const QPixmap scaled = m_image.scaled(targetSize, Qt::KeepAspectRatio, Qt::FastTransformation); + painter->drawPixmap(QRectF(0, 0, scaled.width(), scaled.height()), scaled, scaled.rect()); + } else { + // Picker mode: draw only the non-skipped cells sequentially + const int pitchX = m_cellWidth * (m_skipCellsX + 1); + const int pitchY = m_cellHeight * (m_skipCellsY + 1); + const int cols = qMax(1, m_image.width() / pitchX); + const int rows = qMax(1, m_image.height() / pitchY); + const int packedWidth = cols * m_cellWidth; + const int packedHeight = rows * m_cellHeight; + + const QSize targetSize(qMax(1, int(width())), qMax(1, int(height()))); + const QSize scaledSize = QSize(packedWidth, packedHeight).scaled(targetSize, Qt::KeepAspectRatio); + + const qreal scaleX = static_cast(scaledSize.width()) / packedWidth; + const qreal scaleY = static_cast(scaledSize.height()) / packedHeight; + + for (int row = 0; row < rows; ++row) { + for (int col = 0; col < cols; ++col) { + const QRectF srcRect(col * pitchX + m_cellOffsetX, row * pitchY + m_cellOffsetY, m_cellWidth, m_cellHeight); + const QRectF destRect(col * m_cellWidth * scaleX, row * m_cellHeight * scaleY, m_cellWidth * scaleX, m_cellHeight * scaleY); + painter->drawPixmap(destRect, m_image, srcRect); + } + } + + // Draw selection rectangle + const int selCol = m_cellIndex % m_gridColumns; + const int selRow = m_cellIndex / m_gridColumns; + const QRectF selRect( + selCol * m_cellWidth * scaleX, + selRow * m_cellHeight * scaleY, + m_cellWidth * scaleX, + m_cellHeight * scaleY + ); + + if (m_transparent) { + painter->setOpacity(1.0); + } + + QPen selPen(Qt::white); + selPen.setWidth(2); + selPen.setCosmetic(true); + + painter->setPen(selPen); + painter->setBrush(Qt::NoBrush); + painter->drawRect(selRect); + } + } else { + // Viewer mode: Draw the selected cell from the original image + // Scaled to fill the widget + const QRectF src = sourceRect(); + painter->drawPixmap(QRectF(0, 0, width(), height()), m_image, src); + } + + if (m_transparent && !m_pickerMode) { + painter->setOpacity(1.0); + } +} + +void SpritePaintedItem::mousePressEvent(QMouseEvent* event) { + if (m_image.isNull()) { + return; + } + + if (!m_pickerMode) { + return; + } + + if (m_cellWidth <= 0 || m_cellHeight <= 0) { + return; + } + + const int pitchX = m_cellWidth * (m_skipCellsX + 1); + const int pitchY = m_cellHeight * (m_skipCellsY + 1); + const int cols = qMax(1, m_image.width() / pitchX); + const int rows = qMax(1, m_image.height() / pitchY); + const int packedWidth = cols * m_cellWidth; + const int packedHeight = rows * m_cellHeight; + + const QSize targetSize(qMax(1, int(width())), qMax(1, int(height()))); + const QSize scaledSize = QSize(packedWidth, packedHeight).scaled(targetSize, Qt::KeepAspectRatio); + + const qreal scaleX = static_cast(scaledSize.width()) / packedWidth; + const qreal scaleY = static_cast(scaledSize.height()) / packedHeight; + + const int col = static_cast(event->pos().x() / (m_cellWidth * scaleX)); + const int row = static_cast(event->pos().y() / (m_cellHeight * scaleY)); + + if (col >= 0 && col < cols && row >= 0 && row < rows) { + const int index = row * m_gridColumns + col; + setCellIndex(index); + } +} + +void SpritePaintedItem::reload() { + if (!projectData()) { + return; + } + + if (m_image.isNull()) { + m_image = projectData()->loadImage(m_directory, m_filename); + } + + if (!m_image.isNull()) { + if (pickerMode()) { + if (m_cellWidth > 0 && m_cellHeight > 0) { + const int pitchX = m_cellWidth * (m_skipCellsX + 1); + const int pitchY = m_cellHeight * (m_skipCellsY + 1); + const int cols = qMax(1, m_image.width() / pitchX); + const int rows = qMax(1, m_image.height() / pitchY); + setImplicitSize(cols * m_cellWidth, rows * m_cellHeight); + } else { + setImplicitSize(m_image.width(), m_image.height()); + } + } else { + if (m_cellWidth > 0 && m_cellHeight > 0) { + setImplicitSize(m_cellWidth, m_cellHeight); + } else { + setImplicitSize(m_image.width(), m_image.height()); + } + } + } else { + setImplicitSize(m_cellWidth, m_cellHeight); + } + + update(); +} diff --git a/src/ui/picker/sprite_painted_item.h b/src/ui/picker/sprite_painted_item.h new file mode 100644 index 00000000..d6b633fd --- /dev/null +++ b/src/ui/picker/sprite_painted_item.h @@ -0,0 +1,128 @@ +/* + * This file is part of EasyRPG Editor. + * + * EasyRPG Editor 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, either version 3 of the License, or + * (at your option) any later version. + * + * EasyRPG Editor 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 EasyRPG Editor. If not, see . + */ + +#pragma once + +#include +#include +#include "project_data_gadget.h" + +/** + * Base class for spritesheet based painted items. + * + * The picker mode decides whether it displays a single item (false) or is used + * in a picker context (true). + */ +class SpritePaintedItem : public QQuickPaintedItem { + Q_OBJECT + QML_ELEMENT + Q_PROPERTY(QString filename READ filename WRITE setFilename NOTIFY filenameChanged) + Q_PROPERTY(QString directory READ directory WRITE setDirectory NOTIFY directoryChanged) + Q_PROPERTY(ProjectDataGadget* projectData READ projectData WRITE setProjectData NOTIFY projectDataChanged) + Q_PROPERTY(bool pickerMode READ pickerMode WRITE setPickerMode NOTIFY pickerModeChanged) + Q_PROPERTY(int cellIndex READ cellIndex WRITE setCellIndex NOTIFY cellIndexChanged) + Q_PROPERTY(int cellWidth READ cellWidth WRITE setCellWidth NOTIFY cellWidthChanged) + Q_PROPERTY(int cellHeight READ cellHeight WRITE setCellHeight NOTIFY cellHeightChanged) + Q_PROPERTY(bool transparent READ transparent WRITE setTransparent NOTIFY transparentChanged) + Q_PROPERTY(int skipCellsX READ skipCellsX WRITE setSkipCellsX NOTIFY skipCellsXChanged) + Q_PROPERTY(int skipCellsY READ skipCellsY WRITE setSkipCellsY NOTIFY skipCellsYChanged) + Q_PROPERTY(int gridColumns READ gridColumns WRITE setGridColumns NOTIFY gridColumnsChanged) + +public: + explicit SpritePaintedItem(QQuickItem* parent = nullptr); + + QString filename() const; + void setFilename(const QString& filename); + + QString directory() const; + void setDirectory(const QString& directory); + + ProjectDataGadget* projectData() const; + void setProjectData(ProjectDataGadget* projectData); + + bool pickerMode() const; + void setPickerMode(bool pickerMode); + + int cellIndex() const; + void setCellIndex(int cellIndex); + + int cellWidth() const; + void setCellWidth(int cellWidth); + + int cellHeight() const; + void setCellHeight(int cellHeight); + + bool transparent() const; + void setTransparent(bool transparent); + + int cellOffsetX() const; + void setCellOffsetX(int cellOffsetX); + + int cellOffsetY() const; + void setCellOffsetY(int cellOffsetY); + + int skipCellsX() const; + void setSkipCellsX(int skipCellsX); + + int skipCellsY() const; + void setSkipCellsY(int skipCellsY); + + int gridColumns() const; + void setGridColumns(int gridColumns); + + void paint(QPainter* painter) override; + +signals: + void filenameChanged(); + void directoryChanged(); + void projectDataChanged(); + void pickerModeChanged(); + void cellIndexChanged(); + void cellWidthChanged(); + void cellHeightChanged(); + void transparentChanged(); + void cellOffsetXChanged(); + void cellOffsetYChanged(); + void skipCellsXChanged(); + void skipCellsYChanged(); + void gridColumnsChanged(); + +protected: + /** Compute the source rectangle in image coordinates for the current cell. */ + virtual QRectF sourceRect() const; + + void mousePressEvent(QMouseEvent *event) override; + + virtual void reload(); + + QString m_filename; + QString m_directory; + ProjectDataGadget* m_projectData = nullptr; + bool m_pickerMode = false; + bool m_transparent = false; + QPixmap m_image; + + int m_cellIndex = 0; + int m_cellWidth = 0; + int m_cellHeight = 0; + int m_cellOffsetX = 0; + int m_cellOffsetY = 0; + int m_skipCellsX = 0; + int m_skipCellsY = 0; + int m_gridColumns = 4; +}; + diff --git a/src/ui/viewer/CharSetViewer.qml b/src/ui/viewer/CharSetViewer.qml new file mode 100644 index 00000000..0a659a4b --- /dev/null +++ b/src/ui/viewer/CharSetViewer.qml @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: EasyRPG Editor Authors +// SPDX-License-Identifier: GPL-3.0-or-later + +import QtQuick +import org.kde.kirigami as Kirigami +import org.easyrpg.editor as Ez + +ViewerBase { + id: root + + implicitWidth: 48 + implicitHeight: 64 + + pickerComponent: Ez.CharSetPicker { + onAccepted: root.accepted() + } + + property alias filename: paintedItem.filename + property alias cellIndex: paintedItem.cellIndex + property alias spin: paintedItem.spin + property alias walk: paintedItem.walk + property alias transparent: paintedItem.transparent + property bool showTransparency: true + + pickerProperties: { "showTransparency": root.showTransparency } + + Ez.CharSetPaintedItem { + id: paintedItem + + anchors.fill: parent + + projectData: Ez.ProjectData + } +} diff --git a/src/ui/viewer/FaceSetViewer.qml b/src/ui/viewer/FaceSetViewer.qml new file mode 100644 index 00000000..4824dfe5 --- /dev/null +++ b/src/ui/viewer/FaceSetViewer.qml @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: EasyRPG Editor Authors +// SPDX-License-Identifier: GPL-3.0-or-later + +import QtQuick +import QtQuick.Layouts +import org.kde.kirigami as Kirigami +import org.easyrpg.editor as Ez + +ViewerBase { + id: root + + implicitWidth: 48 + implicitHeight: 48 + + pickerComponent: Ez.FaceSetPicker { + onAccepted: root.accepted() + } + + property alias filename: paintedItem.filename + property alias cellIndex: paintedItem.cellIndex + + Ez.FaceSetPaintedItem { + id: paintedItem + + anchors.fill: parent + + projectData: Ez.ProjectData + } +} diff --git a/src/ui/viewer/FileViewerBase.qml b/src/ui/viewer/FileViewerBase.qml new file mode 100644 index 00000000..d8fba7ec --- /dev/null +++ b/src/ui/viewer/FileViewerBase.qml @@ -0,0 +1,42 @@ +// SPDX-FileCopyrightText: EasyRPG Editor Authors +// SPDX-License-Identifier: GPL-3.0-or-later + +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls as Controls +import org.kde.kirigami as Kirigami +import org.easyrpg.editor as Ez + +RowLayout { + id: root + + property alias value: component.text + + property Ez.PickerData pickerData + + property Component pickerComponent + + /** Signal emitted when confirming in the picker dialog */ + signal accepted() + + Controls.TextField { + id: component + text: pickerData.filename + onTextEdited: { + pickerData.filename = text + root.accepted() + } + } + + Controls.Button { + //icon.name: "document-open" + text: "..." + + onClicked: { + applicationWindow().pageStack.pushDialogLayer(root.pickerComponent, { + pickerData: root.pickerData + }); + } + } +} + diff --git a/src/ui/viewer/ImageViewer.qml b/src/ui/viewer/ImageViewer.qml new file mode 100644 index 00000000..ce98ac0e --- /dev/null +++ b/src/ui/viewer/ImageViewer.qml @@ -0,0 +1,47 @@ +// SPDX-FileCopyrightText: EasyRPG Editor Authors +// SPDX-License-Identifier: GPL-3.0-or-later + +import QtQuick +import QtQuick.Dialogs +import QtQuick.Controls +import org.kde.kirigami as Kirigami +import org.easyrpg.editor as Ez + +Item { + id: root + + implicitWidth: sprite.implicitWidth + implicitHeight: sprite.implicitHeight + + property Ez.PickerData pickerData + + property Component pickerComponent: Ez.ImagePicker { + onAccepted: root.accepted() + } + + /** Signal emitted when confirming in the picker dialog */ + signal accepted() + + property alias directory: sprite.directory + property alias filename: sprite.filename + + Ez.SpritePaintedItem { + id: sprite + + anchors.fill: parent + + projectData: Ez.ProjectData + + MouseArea { + id: mouseArea + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + applicationWindow().pageStack.pushDialogLayer(root.pickerComponent, { + pickerData: root.pickerData, + directory: sprite.directory + }); + } + } + } +} diff --git a/src/ui/viewer/MusicViewer.qml b/src/ui/viewer/MusicViewer.qml new file mode 100644 index 00000000..cce7f50c --- /dev/null +++ b/src/ui/viewer/MusicViewer.qml @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: EasyRPG Editor Authors +// SPDX-License-Identifier: GPL-3.0-or-later + +import QtQuick +import org.easyrpg.editor as Ez + +FileViewerBase { + id: root + + pickerComponent: Component { + Ez.MusicPicker { + onAccepted: root.accepted() + } + } +} + diff --git a/src/ui/viewer/SoundViewer.qml b/src/ui/viewer/SoundViewer.qml new file mode 100644 index 00000000..ad96c0a5 --- /dev/null +++ b/src/ui/viewer/SoundViewer.qml @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: EasyRPG Editor Authors +// SPDX-License-Identifier: GPL-3.0-or-later + +import QtQuick +import org.easyrpg.editor as Ez + +FileViewerBase { + id: root + + pickerComponent: Component { + Ez.SoundPicker { + onAccepted: root.accepted() + } + } +} + diff --git a/src/ui/viewer/ViewerBase.qml b/src/ui/viewer/ViewerBase.qml new file mode 100644 index 00000000..521e8274 --- /dev/null +++ b/src/ui/viewer/ViewerBase.qml @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: EasyRPG Editor Authors +// SPDX-License-Identifier: GPL-3.0-or-later + +import QtQuick +import org.kde.kirigami as Kirigami +import org.easyrpg.editor as Ez + +Item { + id: root + + property Ez.PickerData pickerData + property Component pickerComponent + property var pickerProperties: ({}) + + /** Signal emitted when confirming in the picker dialog */ + signal accepted() + + default property alias viewerContent: container.data + + Item { + id: container + anchors.fill: parent + } + + MouseArea { + id: mouseArea + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + let props = Object.assign({ pickerData: root.pickerData }, root.pickerProperties); + applicationWindow().pageStack.pushDialogLayer(root.pickerComponent, props); + } + } +}