From e8ca7e8a9e3a905c3c519364c2156d64fee2ef72 Mon Sep 17 00:00:00 2001 From: maelstrom Date: Sun, 13 Apr 2025 15:15:23 +0200 Subject: [PATCH] feat(editor): new properties widget --- core/src/objects/base/member.h | 11 +++ core/src/objects/part.cpp | 9 ++- editor/CMakeLists.txt | 4 +- editor/mainwindow.ui | 2 +- editor/panes/propertiesmodel.cpp | 118 --------------------------- editor/panes/propertiesmodel.h | 31 ------- editor/panes/propertiesview.cpp | 135 +++++++++++++++++++++++++++++-- editor/panes/propertiesview.h | 7 +- 8 files changed, 153 insertions(+), 164 deletions(-) delete mode 100644 editor/panes/propertiesmodel.cpp delete mode 100644 editor/panes/propertiesmodel.h diff --git a/core/src/objects/base/member.h b/core/src/objects/base/member.h index 1c93316..6800478 100644 --- a/core/src/objects/base/member.h +++ b/core/src/objects/base/member.h @@ -49,12 +49,23 @@ enum PropertyFlags { PROP_NOSAVE = 1 << 1, // Do not serialize }; +enum PropertyCategory { + PROP_CATEGORY_DATA, + PROP_CATEGORY_APPEARENCE, + PROP_CATEGORY_BEHAVIOR, + PROP_CATEGORY_PART, + PROP_CATEGORY_SURFACE, +}; + +const int PROPERTY_CATEGORY_MAX = PROP_CATEGORY_SURFACE; + struct PropertyMeta { void* backingField; const Data::TypeInfo* type; FieldCodec codec; std::optional> updateCallback; PropertyFlags flags; + PropertyCategory category = PROP_CATEGORY_DATA; }; typedef std::variant MemberMeta; diff --git a/core/src/objects/part.cpp b/core/src/objects/part.cpp index 8f0fc4b..bee17db 100644 --- a/core/src/objects/part.cpp +++ b/core/src/objects/part.cpp @@ -70,11 +70,13 @@ Part::Part(PartConstructParams params): Instance(&TYPE), cframe(Data::CFrame(par .backingField = &anchored, .type = &Data::Bool::TYPE, .codec = fieldCodecOf(), - .updateCallback = memberFunctionOf(&Part::onUpdated, this) + .updateCallback = memberFunctionOf(&Part::onUpdated, this), + .category = PROP_CATEGORY_BEHAVIOR, }}, { "Locked", { .backingField = &locked, .type = &Data::Bool::TYPE, .codec = fieldCodecOf(), + .category = PROP_CATEGORY_BEHAVIOR, }}, { "Position", { .backingField = &cframe, .type = &Vector3::TYPE, @@ -96,15 +98,18 @@ Part::Part(PartConstructParams params): Instance(&TYPE), cframe(Data::CFrame(par .backingField = &size, .type = &Vector3::TYPE, .codec = fieldCodecOf(), - .updateCallback = memberFunctionOf(&Part::onUpdated, this) + .updateCallback = memberFunctionOf(&Part::onUpdated, this), + .category = PROP_CATEGORY_PART, }}, { "Color", { .backingField = &color, .type = &Data::Color3::TYPE, .codec = fieldCodecOf(), + .category = PROP_CATEGORY_APPEARENCE, }}, { "Transparency", { .backingField = &transparency, .type = &Data::Float::TYPE, .codec = fieldCodecOf(), + .category = PROP_CATEGORY_APPEARENCE, }} } }); diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 0df4337..2af17a7 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -30,8 +30,6 @@ set(PROJECT_SOURCES panes/explorermodel.cpp panes/propertiesview.h panes/propertiesview.cpp - panes/propertiesmodel.h - panes/propertiesmodel.cpp ${TS_FILES} ) @@ -64,7 +62,7 @@ else() endif() target_include_directories(editor PUBLIC "../core/src" "../include") -target_link_libraries(editor PRIVATE openblocks Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::Multimedia) +target_link_libraries(editor PRIVATE openblocks Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::WidgetsPrivate Qt${QT_VERSION_MAJOR}::Multimedia) # Qt6 does not include QOpenGLWidgets as part of Widgets base anymore, so # we have to include it manually diff --git a/editor/mainwindow.ui b/editor/mainwindow.ui index 04b431f..76029e9 100644 --- a/editor/mainwindow.ui +++ b/editor/mainwindow.ui @@ -40,7 +40,7 @@ 0 0 - 935 + 1027 30 diff --git a/editor/panes/propertiesmodel.cpp b/editor/panes/propertiesmodel.cpp deleted file mode 100644 index 73947cf..0000000 --- a/editor/panes/propertiesmodel.cpp +++ /dev/null @@ -1,118 +0,0 @@ -#include "propertiesmodel.h" -#include "datatypes/base.h" -#include "datatypes/cframe.h" -#include "objects/base/member.h" -#include "qnamespace.h" - -PropertiesModel::PropertiesModel(InstanceRef selectedItem, QWidget *parent) - : QAbstractItemModel(parent) - , selectedItem(selectedItem) { - this->propertiesList.reserve(selectedItem->GetProperties().size()); - for (std::string name : selectedItem->GetProperties()) { - PropertyMeta meta = selectedItem->GetPropertyMeta(name).value(); - // Don't show CFrames in properties - if (meta.type == &Data::CFrame::TYPE) continue; - - this->propertiesList.push_back(name); - } -} - -PropertiesModel::~PropertiesModel() = default; - - -QVariant PropertiesModel::data(const QModelIndex &index, int role) const { - if (!index.isValid()) - return {}; - - std::string propertyName = propertiesList[index.row()]; - PropertyMeta meta = selectedItem->GetPropertyMeta(propertyName).value(); - - switch (role) { - case Qt::EditRole: - case Qt::DisplayRole: - if (index.column() == 0) - return QString::fromStdString(propertyName); - else if (index.column() == 1 && meta.type != &Data::Bool::TYPE) { - return QString::fromStdString(selectedItem->GetPropertyValue(propertyName).value().ToString()); - } - return {}; - case Qt::CheckStateRole: - if (index.column() == 0) return {}; - else if (index.column() == 1 && meta.type == &Data::Bool::TYPE) - return selectedItem->GetPropertyValue(propertyName)->get() ? Qt::Checked : Qt::Unchecked; - return {}; - // case Qt::DecorationRole: - // return iconOf(item->GetClass()); - } - - return {}; -} - -bool PropertiesModel::setData(const QModelIndex &index, const QVariant &value, int role) { - if (index.column() != 1) return false; - - std::string propertyName = propertiesList[index.row()]; - PropertyMeta meta = selectedItem->GetPropertyMeta(propertyName).value(); - - switch (role) { - case Qt::EditRole: - if (!meta.type->fromString) - return false; - - selectedItem->SetPropertyValue(propertyName, meta.type->fromString(value.toString().toStdString())); - return true; - case Qt::CheckStateRole: - if (meta.type != &Data::Bool::TYPE) - return false; - - selectedItem->SetPropertyValue(propertyName, Data::Bool(value.toBool())); - return true; - } - - return false; -} - -Qt::ItemFlags PropertiesModel::flags(const QModelIndex &index) const { - if (!index.isValid()) - return Qt::NoItemFlags; - - if (index.column() == 0) - return Qt::ItemIsEnabled; - - std::string propertyName = propertiesList[index.row()]; - PropertyMeta meta = selectedItem->GetPropertyMeta(propertyName).value(); - - if (index.column() == 1) { - if (meta.type == &Data::Bool::TYPE) - return Qt::ItemIsEnabled | Qt::ItemIsUserCheckable; - else - return Qt::ItemIsEnabled | Qt::ItemIsEditable; - } - - return Qt::NoItemFlags; -}; - -QVariant PropertiesModel::headerData(int section, Qt::Orientation orientation, - int role) const { - return QString(""); -} - -QModelIndex PropertiesModel::index(int row, int column, - const QModelIndex &parent) const { - if (!hasIndex(row, column, parent)) - return {}; - - return createIndex(row, column); -} - -QModelIndex PropertiesModel::parent(const QModelIndex &index) const { - return {}; -} - -int PropertiesModel::rowCount(const QModelIndex &parent) const { - return !parent.isValid() ? this->propertiesList.size() : 0; -} - -int PropertiesModel::columnCount(const QModelIndex &parent) const { - return 2; -} \ No newline at end of file diff --git a/editor/panes/propertiesmodel.h b/editor/panes/propertiesmodel.h deleted file mode 100644 index 8c3aae9..0000000 --- a/editor/panes/propertiesmodel.h +++ /dev/null @@ -1,31 +0,0 @@ -#pragma once - -#include "objects/base/instance.h" -#include "qabstractitemmodel.h" -#include "qnamespace.h" -#include -#include - -class PropertiesModel : public QAbstractItemModel { - Q_OBJECT -public: - Q_DISABLE_COPY_MOVE(PropertiesModel) - - explicit PropertiesModel(InstanceRef selectedItem, QWidget *parent = nullptr); - ~PropertiesModel() override; - - QVariant data(const QModelIndex &index, int role) const override; - bool setData(const QModelIndex &index, const QVariant &value, int role) override; - Qt::ItemFlags flags(const QModelIndex &index) const override; - QVariant headerData(int section, Qt::Orientation orientation, - int role = Qt::DisplayRole) const override; - QModelIndex index(int row, int column, - const QModelIndex &parent = {}) const override; - QModelIndex parent(const QModelIndex &index) const override; - int rowCount(const QModelIndex &parent = {}) const override; - int columnCount(const QModelIndex &parent = {}) const override; - -private: - InstanceRef selectedItem; - std::vector propertiesList; -}; \ No newline at end of file diff --git a/editor/panes/propertiesview.cpp b/editor/panes/propertiesview.cpp index abe630d..28c915c 100644 --- a/editor/panes/propertiesview.cpp +++ b/editor/panes/propertiesview.cpp @@ -1,20 +1,141 @@ #include "propertiesview.h" +#include "datatypes/meta.h" +#include "objects/base/member.h" #include "propertiesmodel.h" #include "qaction.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class CustomItemDelegate : public QStyledItemDelegate { +public: + CustomItemDelegate(QObject* parent = nullptr) : QStyledItemDelegate(parent) {} + + void initStyleOption(QStyleOptionViewItem *option, const QModelIndex &index) const override { + // https://stackoverflow.com/a/76645757/16255372 + // https://stackoverflow.com/a/70078448/16255372 + + int indent = dynamic_cast(parent())->indentation(); + + QStyledItemDelegate::initStyleOption(option, index); + + if (index.parent().isValid()) { + option->rect.adjust(-indent, 0, -indent, 0); + } else { + option->state &= ~QStyle::State_Selected; + + QWidget* parentWidget = dynamic_cast(parent()); + option->backgroundBrush = parentWidget->palette().dark(); + } + }; +}; PropertiesView::PropertiesView(QWidget* parent): - QTreeView(parent) { - this->setStyleSheet(QString("QTreeView::branch { border: none; }")); + QTreeWidget(parent) { + + clear(); + setHeaderHidden(true); + setColumnCount(2); + setAlternatingRowColors(true); + setItemDelegate(new CustomItemDelegate(this)); } PropertiesView::~PropertiesView() { } +QStringList PROPERTY_CATEGORY_NAMES { + "Data", + "Appearence", + "Behavior", + "Part", + "Surface" +}; + +void PropertiesView::drawBranches(QPainter *painter, const QRect &rect, const QModelIndex &index) const { + // https://codebrowser.dev/qt5/qtbase/src/widgets/itemviews/qtreeview.cpp.html#312opt + Q_D(const QTreeView); + QStyleOptionViewItem opt = viewOptions(); + + const QTreeViewItem& viewItem = d->viewItems.at(d->current); + + // Taken from source code (above) + bool hoverRow = selectionBehavior() == QAbstractItemView::SelectRows + && opt.showDecorationSelected + && index.parent() == d->hover.parent() + && index.row() == d->hover.row(); + + // Un-indent branch + opt.rect = rect; + if (index.parent().isValid()) + opt.rect.adjust(0, 0, -indentation(), 0); + opt.state |= QStyle::State_Item; + if (viewItem.hasChildren) + opt.state |= QStyle::State_Children; + if (viewItem.expanded) + opt.state |= QStyle::State_Open; + if (viewItem.hasMoreSiblings || viewItem.parentItem > -1 && d->viewItems.at(viewItem.parentItem).hasMoreSiblings) + opt.state |= QStyle::State_Sibling; + + opt.state.setFlag(QStyle::State_MouseOver, hoverRow || d->current == d->hoverBranch); + + // Draw background for headings + if (!index.parent().isValid()) + painter->fillRect(opt.rect, palette().dark()); + + style()->drawPrimitive(QStyle::PE_IndicatorBranch, &opt, painter, this); +} + void PropertiesView::setSelected(std::optional instance) { - if (instance.has_value()) { - this->setModel(new PropertiesModel(instance.value())); - } else { - if (this->model()) delete this->model(); - this->setModel(nullptr); + clear(); + if (!instance) return; + InstanceRef inst = instance.value(); + + std::map propertyCategories; + + for (int i = 0; i <= PROPERTY_CATEGORY_MAX; i++) { + QTreeWidgetItem* item = new QTreeWidgetItem; + + QBrush brush; + brush.setColor(QPalette::Midlight); + brush.setStyle(Qt::SolidPattern); + + item->setData(0, Qt::DisplayRole, PROPERTY_CATEGORY_NAMES[i]); + item->setFirstColumnSpanned(true); + + propertyCategories[(PropertyCategory)i] = item; + addTopLevelItem(item); } + + std::vector properties = inst->GetProperties(); + + for (std::string property : properties) { + PropertyMeta meta = inst->GetPropertyMeta(property).value(); + Data::Variant currentValue = inst->GetPropertyValue(property).value(); + + QTreeWidgetItem* item = new QTreeWidgetItem; + item->setData(0, Qt::DisplayRole, QString::fromStdString(property)); + item->setData(1, Qt::DisplayRole, QString::fromStdString(currentValue.ToString())); + + propertyCategories[meta.category]->addChild(item); + propertyCategories[meta.category]->setExpanded(true); + } + + // Remove child-less categories + for (int i = 0; i <= PROPERTY_CATEGORY_MAX; i++) { + if (propertyCategories[(PropertyCategory)i]->childCount() > 0) continue; + int idx = indexOfTopLevelItem(propertyCategories[(PropertyCategory)i]); + delete takeTopLevelItem(idx); + } + + resizeColumnToContents(0); } \ No newline at end of file diff --git a/editor/panes/propertiesview.h b/editor/panes/propertiesview.h index d934ae4..8748cea 100644 --- a/editor/panes/propertiesview.h +++ b/editor/panes/propertiesview.h @@ -1,11 +1,14 @@ #pragma once +#include #include "objects/base/instance.h" -#include "qtreeview.h" class Ui_MainWindow; -class PropertiesView : public QTreeView { +class PropertiesView : public QTreeWidget { + Q_DECLARE_PRIVATE(QTreeView) +protected: + void drawBranches(QPainter *painter, const QRect &rect, const QModelIndex &index) const override; public: PropertiesView(QWidget* parent = nullptr); ~PropertiesView() override;