From 17d8ad968958f300f7fb994d092bfcbb32ad9802 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thorbj=C3=B8rn=20Lindeijer?= Date: Fri, 30 Aug 2024 17:14:30 +0200 Subject: [PATCH] Implemented "Map Size" and "Tile Size" properties * Map size is read-only but features a "Resize Map" button that triggers the resize dialog. * Both map size and tile size are implemented based on the new "ValueTypeEditorFactory" which selects an editor based on the value type. This functionality is moved out of the VariantEditor. * Introduced AbstractProperty, ValueProperty and QObjectProperty in an attempt to provide some convenience on top of the Property interface. I'm not entirely convinced it was a good idea to put the "createEditor" function on the Property. It makes it easy to do custom widgets for a property, but annoying to have it use a type-based editor. --- src/tiled/propertieswidget.cpp | 117 ++++++- src/tiled/propertieswidget.h | 4 + src/tiled/varianteditor.cpp | 561 ++++++++++++++++++++------------- src/tiled/varianteditor.h | 170 ++++++++-- 4 files changed, 593 insertions(+), 259 deletions(-) diff --git a/src/tiled/propertieswidget.cpp b/src/tiled/propertieswidget.cpp index f8ac584c89..0e7a882c01 100644 --- a/src/tiled/propertieswidget.cpp +++ b/src/tiled/propertieswidget.cpp @@ -37,6 +37,7 @@ #include #include #include +#include #include #include #include @@ -47,6 +48,7 @@ PropertiesWidget::PropertiesWidget(QWidget *parent) : QWidget{parent} , mDocument(nullptr) , mPropertyBrowser(new VariantEditor(this)) + , mDefaultEditorFactory(std::make_unique()) { mActionAddProperty = new QAction(this); mActionAddProperty->setEnabled(false); @@ -145,6 +147,8 @@ static bool anyObjectHasProperty(const QList &objects, const QString &n class MapOrientationProperty : public EnumProperty { + Q_OBJECT + public: MapOrientationProperty(MapDocument *mapDocument) : EnumProperty(tr("Orientation")) @@ -183,6 +187,98 @@ class MapOrientationProperty : public EnumProperty MapDocument *mMapDocument; }; +class MapSizeProperty : public AbstractProperty +{ + Q_OBJECT + +public: + MapSizeProperty(MapDocument *mapDocument, EditorFactory *editorFactory) + : AbstractProperty(tr("Map Size"), editorFactory) + , mMapDocument(mapDocument) + { + connect(mMapDocument, &MapDocument::mapChanged, + this, &Property::valueChanged); + } + + QVariant value() const override { return mMapDocument->map()->size(); } + void setValue(const QVariant &) override {}; + + QWidget *createEditor(QWidget *parent) override + { + auto widget = new QWidget(parent); + auto layout = new QVBoxLayout(widget); + auto valueEdit = AbstractProperty::createEditor(widget); + auto resizeButton = new QPushButton(tr("Resize Map"), widget); + + valueEdit->setEnabled(false); + layout->setContentsMargins(QMargins()); + layout->addWidget(valueEdit); + layout->addWidget(resizeButton, 0, Qt::AlignLeft); + + connect(resizeButton, &QPushButton::clicked, [] { + ActionManager::action("ResizeMap")->trigger(); + }); + + return widget; + } + +private: + MapDocument *mMapDocument; +}; + +class TileSizeProperty : public AbstractProperty +{ + Q_OBJECT + +public: + TileSizeProperty(MapDocument *mapDocument, EditorFactory *editorFactory) + : AbstractProperty(tr("Tile Size"), editorFactory) + , mMapDocument(mapDocument) + { + connect(mMapDocument, &Document::changed, + this, &TileSizeProperty::onChanged); + } + + QVariant value() const override + { + return mMapDocument->map()->tileSize(); + } + + void setValue(const QVariant &value) override + { + auto oldSize = mMapDocument->map()->tileSize(); + auto newSize = value.toSize(); + + if (oldSize.width() != newSize.width()) { + auto command = new ChangeMapProperty(mMapDocument, + Map::TileWidthProperty, + newSize.width()); + mMapDocument->undoStack()->push(command); + } + + if (oldSize.height() != newSize.height()) { + auto command = new ChangeMapProperty(mMapDocument, + Map::TileHeightProperty, + newSize.height()); + mMapDocument->undoStack()->push(command); + } + }; + +private: + void onChanged(const ChangeEvent &event) + { + if (event.type != ChangeEvent::MapChanged) + return; + + const auto property = static_cast(event).property; + if (property == Map::TileWidthProperty || property == Map::TileHeightProperty) + emit valueChanged(); + } + + MapDocument *mMapDocument; +}; + + void PropertiesWidget::currentObjectChanged(Object *object) { // mPropertyBrowser->setObject(object); @@ -195,12 +291,22 @@ void PropertiesWidget::currentObjectChanged(Object *object) break; case Object::MapType: { Map *map = static_cast(object); - mPropertyBrowser->addHeader(tr("Map")); - mPropertyBrowser->addProperty(new MapOrientationProperty(static_cast(mDocument))); + auto mapDocument = static_cast(mDocument); - auto sizeProperty = new VariantProperty(tr("Map Size"), map->size()); - sizeProperty->setEnabled(false); - mPropertyBrowser->addProperty(sizeProperty); + mPropertyBrowser->addHeader(tr("Map")); + mPropertyBrowser->addProperty(new MapOrientationProperty(mapDocument)); + mPropertyBrowser->addProperty(new MapSizeProperty(mapDocument, mDefaultEditorFactory.get())); + mPropertyBrowser->addProperty(new TileSizeProperty(mapDocument, mDefaultEditorFactory.get())); + // todo: infinite + // todo: hex side length + // todo: stagger axis + // todo: stagger index + // todo: parallax origin + // todo: layer data format + // todo: chunk size + // todo: tile render order + // todo: compression level + // todo: background color break; } @@ -608,3 +714,4 @@ void PropertiesWidget::retranslateUi() } // namespace Tiled #include "moc_propertieswidget.cpp" +#include "propertieswidget.moc" diff --git a/src/tiled/propertieswidget.h b/src/tiled/propertieswidget.h index dc1e2cefe0..f9f5db095e 100644 --- a/src/tiled/propertieswidget.h +++ b/src/tiled/propertieswidget.h @@ -22,11 +22,14 @@ #include +#include + namespace Tiled { class Object; class Document; +class ValueTypeEditorFactory; class VariantEditor; /** @@ -75,6 +78,7 @@ public slots: Document *mDocument; VariantEditor *mPropertyBrowser; + std::unique_ptr mDefaultEditorFactory; QAction *mActionAddProperty; QAction *mActionRemoveProperty; QAction *mActionRenameProperty; diff --git a/src/tiled/varianteditor.cpp b/src/tiled/varianteditor.cpp index 6a644d3715..60d2290417 100644 --- a/src/tiled/varianteditor.cpp +++ b/src/tiled/varianteditor.cpp @@ -22,23 +22,35 @@ #include "colorbutton.h" #include "compression.h" #include "map.h" -#include "tiled.h" #include "utils.h" #include #include #include -#include #include #include #include #include +#include #include #include #include namespace Tiled { +AbstractProperty::AbstractProperty(const QString &name, + EditorFactory *editorFactory, + QObject *parent) + : Property(name, parent) + , m_editorFactory(editorFactory) +{} + +QWidget *AbstractProperty::createEditor(QWidget *parent) +{ + return m_editorFactory->createEditor(this, parent); +} + + class SpinBox : public QSpinBox { Q_OBJECT @@ -201,8 +213,9 @@ class LineEditLabel : public ElidingLabel class StringEditorFactory : public EditorFactory { public: - QWidget *createEditor(const QVariant &value, QWidget *parent) override + QWidget *createEditor(Property *property, QWidget *parent) override { + auto value = property->value(); auto editor = new QLineEdit(parent); editor->setText(value.toString()); return editor; @@ -212,8 +225,9 @@ class StringEditorFactory : public EditorFactory class IntEditorFactory : public EditorFactory { public: - QWidget *createEditor(const QVariant &value, QWidget *parent) override + QWidget *createEditor(Property *property, QWidget *parent) override { + auto value = property->value(); auto editor = new SpinBox(parent); editor->setValue(value.toInt()); return editor; @@ -223,8 +237,9 @@ class IntEditorFactory : public EditorFactory class FloatEditorFactory : public EditorFactory { public: - QWidget *createEditor(const QVariant &value, QWidget *parent) override + QWidget *createEditor(Property *property, QWidget *parent) override { + auto value = property->value(); auto editor = new DoubleSpinBox(parent); editor->setValue(value.toDouble()); return editor; @@ -234,8 +249,9 @@ class FloatEditorFactory : public EditorFactory class BoolEditorFactory : public EditorFactory { public: - QWidget *createEditor(const QVariant &value, QWidget *parent) override + QWidget *createEditor(Property *property, QWidget *parent) override { + auto value = property->value(); auto editor = new QCheckBox(parent); bool checked = value.toBool(); editor->setChecked(checked); @@ -252,8 +268,9 @@ class BoolEditorFactory : public EditorFactory class PointEditorFactory : public EditorFactory { public: - QWidget *createEditor(const QVariant &value, QWidget *parent) override + QWidget *createEditor(Property *property, QWidget *parent) override { + auto value = property->value(); auto editor = new QWidget(parent); auto horizontalLayout = new QHBoxLayout(editor); horizontalLayout->setContentsMargins(QMargins()); @@ -282,8 +299,9 @@ class PointEditorFactory : public EditorFactory class PointFEditorFactory : public EditorFactory { public: - QWidget *createEditor(const QVariant &value, QWidget *parent) override + QWidget *createEditor(Property *property, QWidget *parent) override { + auto value = property->value(); auto editor = new QWidget(parent); auto horizontalLayout = new QHBoxLayout(editor); horizontalLayout->setContentsMargins(QMargins()); @@ -309,94 +327,29 @@ class PointFEditorFactory : public EditorFactory } }; -class SizeEditor : public QWidget +/** + * A widget for editing a QSize value. + */ +class SizeEdit : public QWidget { + Q_OBJECT + Q_PROPERTY(QSize value READ value WRITE setValue NOTIFY valueChanged FINAL) + public: - SizeEditor(QWidget *parent = nullptr) - : QWidget(parent) - , m_widthLabel(new QLabel(QStringLiteral("W"), this)) - , m_heightLabel(new QLabel(QStringLiteral("H"), this)) - , m_widthSpinBox(new SpinBox(this)) - , m_heightSpinBox(new SpinBox(this)) - { - m_widthLabel->setAlignment(Qt::AlignCenter); - m_heightLabel->setAlignment(Qt::AlignCenter); - - auto layout = new QGridLayout(this); - layout->setContentsMargins(QMargins()); - layout->setColumnStretch(1, 1); - layout->setColumnStretch(3, 1); - layout->setSpacing(Utils::dpiScaled(3)); - - const int horizontalMargin = Utils::dpiScaled(3); - m_widthLabel->setContentsMargins(horizontalMargin, 0, horizontalMargin, 0); - m_heightLabel->setContentsMargins(horizontalMargin, 0, horizontalMargin, 0); - - layout->addWidget(m_widthLabel, 0, 0); - layout->addWidget(m_widthSpinBox, 0, 1); - layout->addWidget(m_heightLabel, 0, 2); - layout->addWidget(m_heightSpinBox, 0, 3); - } + SizeEdit(QWidget *parent = nullptr); - void setValue(const QSize &size) - { - m_widthSpinBox->setValue(size.width()); - m_heightSpinBox->setValue(size.height()); - } + void setValue(const QSize &size); + QSize value() const; - QSize value() const - { - return QSize(m_widthSpinBox->value(), m_heightSpinBox->value()); - } +signals: + void valueChanged(); private: - void resizeEvent(QResizeEvent *event) override - { - QWidget::resizeEvent(event); - - const auto direction = event->size().width() < minimumHorizontalWidth() - ? Qt::Vertical : Qt::Horizontal; - - if (m_direction != direction) { - m_direction = direction; - - auto layout = qobject_cast(this->layout()); - - // Remove all widgets from layout, without deleting them - layout->removeWidget(m_widthLabel); - layout->removeWidget(m_widthSpinBox); - layout->removeWidget(m_heightLabel); - layout->removeWidget(m_heightSpinBox); - - if (direction == Qt::Horizontal) { - layout->addWidget(m_widthLabel, 0, 0); - layout->addWidget(m_widthSpinBox, 0, 1); - layout->addWidget(m_heightLabel, 0, 2); - layout->addWidget(m_heightSpinBox, 0, 3); - layout->setColumnStretch(3, 1); - } else { - layout->addWidget(m_widthLabel, 0, 0); - layout->addWidget(m_widthSpinBox, 0, 1); - layout->addWidget(m_heightLabel, 1, 0); - layout->addWidget(m_heightSpinBox, 1, 1); - layout->setColumnStretch(3, 0); - } - - // this avoids flickering when the layout changes - layout->activate(); - } - } + void resizeEvent(QResizeEvent *event) override; - int minimumHorizontalWidth() const - { - return m_widthLabel->minimumSizeHint().width() + - m_widthSpinBox->minimumSizeHint().width() + - m_heightLabel->minimumSizeHint().width() + - m_heightSpinBox->minimumSizeHint().width() + - layout()->spacing() * 3; - } + int minimumHorizontalWidth() const; - Qt::Orientation m_direction = Qt::Horizontal; + Qt::Orientation m_orientation = Qt::Horizontal; QLabel *m_widthLabel; QLabel *m_heightLabel; SpinBox *m_widthSpinBox; @@ -406,10 +359,21 @@ class SizeEditor : public QWidget class SizeEditorFactory : public EditorFactory { public: - QWidget *createEditor(const QVariant &value, QWidget *parent) override + QWidget *createEditor(Property *property, QWidget *parent) override { - auto editor = new SizeEditor(parent); - editor->setValue(value.toSize()); + auto editor = new SizeEdit(parent); + auto syncEditor = [property, editor]() { + const QSignalBlocker blocker(editor); + editor->setValue(property->value().toSize()); + }; + syncEditor(); + + QObject::connect(property, &Property::valueChanged, editor, syncEditor); + QObject::connect(editor, &SizeEdit::valueChanged, property, + [property, editor]() { + property->setValue(editor->value()); + }); + return editor; } }; @@ -417,8 +381,9 @@ class SizeEditorFactory : public EditorFactory class RectFEditorFactory : public EditorFactory { public: - QWidget *createEditor(const QVariant &value, QWidget *parent) override + QWidget *createEditor(Property *property, QWidget *parent) override { + auto value = property->value(); auto editor = new QWidget(parent); auto gridLayout = new QGridLayout(editor); gridLayout->setContentsMargins(QMargins()); @@ -467,38 +432,25 @@ class RectFEditorFactory : public EditorFactory class ColorEditorFactory : public EditorFactory { public: - QWidget *createEditor(const QVariant &value, QWidget *parent) override + QWidget *createEditor(Property *property, QWidget *parent) override { + auto value = property->value(); auto editor = new ColorButton(parent); editor->setColor(value.value()); return editor; } }; -class EnumEditorFactory : public EditorFactory -{ -public: - QWidget *createEditor(const QVariant &value, QWidget *parent) override - { - return nullptr; - } - - void setEnumNames(const QStringList &enumNames) - { - m_enumNamesModel.setStringList(enumNames); - } - - void setEnumValues(const QList &enumValues) - { - m_enumValues = enumValues; - } -private: - QStringListModel m_enumNamesModel; - QList m_enumValues; -}; +ValueProperty::ValueProperty(const QString &name, + const QVariant &value, + EditorFactory *editorFactory, + QObject *parent) + : AbstractProperty(name, editorFactory, parent) + , m_value(value) +{} -void VariantProperty::setValue(const QVariant &value) +void ValueProperty::setValue(const QVariant &value) { if (m_value != value) { m_value = value; @@ -506,41 +458,23 @@ void VariantProperty::setValue(const QVariant &value) } } -QWidget *VariantProperty::createEditor(QWidget *parent) -{ - switch (m_value.userType()) { - case QMetaType::QSize: - return SizeEditorFactory().createEditor(m_value, parent); - default: - break; - } - return nullptr; -} +EnumProperty::EnumProperty(const QString &name, + const QStringList &enumNames, + const QList &enumValues, + QObject *parent) + : AbstractProperty(name, &m_editorFactory, parent) + , m_editorFactory(enumNames, enumValues) +{} -QWidget *EnumProperty::createEditor(QWidget *parent) +void EnumProperty::setEnumNames(const QStringList &enumNames) { - auto editor = new QComboBox(parent); - // This allows the combo box to shrink horizontally. - editor->setSizeAdjustPolicy(QComboBox::AdjustToMinimumContentsLengthWithIcon); - editor->setModel(&m_enumNamesModel); - - auto syncEditor = [editor, this]() { - const QSignalBlocker blocker(editor); - if (m_enumValues.isEmpty()) - editor->setCurrentIndex(value().toInt()); - else - editor->setCurrentIndex(m_enumValues.indexOf(value().toInt())); - }; - syncEditor(); - - connect(this, &Property::valueChanged, editor, syncEditor); - connect(editor, qOverload(&QComboBox::currentIndexChanged), this, - [this](int index) { - setValue(m_enumValues.isEmpty() ? index : m_enumValues.at(index)); - }); + m_editorFactory.setEnumNames(enumNames); +} - return editor; +void EnumProperty::setEnumValues(const QList &enumValues) +{ + m_editorFactory.setEnumValues(enumValues); } @@ -567,59 +501,35 @@ VariantEditor::VariantEditor(QWidget *parent) m_gridLayout->setColumnMinimumWidth(MiddleSpacing, Utils::dpiScaled(2)); m_gridLayout->setColumnMinimumWidth(RightSpacing, Utils::dpiScaled(3)); - registerEditorFactory(QMetaType::QString, std::make_unique()); - registerEditorFactory(QMetaType::Int, std::make_unique()); - registerEditorFactory(QMetaType::Double, std::make_unique()); - registerEditorFactory(QMetaType::Bool, std::make_unique()); - registerEditorFactory(QMetaType::QPoint, std::make_unique()); - registerEditorFactory(QMetaType::QPointF, std::make_unique()); - registerEditorFactory(QMetaType::QSize, std::make_unique()); - registerEditorFactory(QMetaType::QRectF, std::make_unique()); - registerEditorFactory(QMetaType::QColor, std::make_unique()); - - auto alignmentEditorFactory = std::make_unique(); - alignmentEditorFactory->setEnumNames({ - tr("Unspecified"), - tr("Top Left"), - tr("Top"), - tr("Top Right"), - tr("Left"), - tr("Center"), - tr("Right"), - tr("Bottom Left"), - tr("Bottom"), - tr("Bottom Right"), - }); - registerEditorFactory(qMetaTypeId(), std::move(alignmentEditorFactory)); - - auto orientationEditorFactory = std::make_unique(); - orientationEditorFactory->setEnumNames({ - tr("Orthogonal"), - tr("Isometric"), - tr("Isometric (Staggered)"), - tr("Hexagonal (Staggered)"), - }); - orientationEditorFactory->setEnumValues({ - Map::Orthogonal, - Map::Isometric, - Map::Staggered, - Map::Hexagonal, - }); - registerEditorFactory(qMetaTypeId(), std::move(orientationEditorFactory)); - - auto staggerAxisEditorFactory = std::make_unique(); - staggerAxisEditorFactory->setEnumNames({ - tr("X"), - tr("Y"), - }); - registerEditorFactory(qMetaTypeId(), std::move(staggerAxisEditorFactory)); - - auto staggerIndexEditorFactory = std::make_unique(); - staggerIndexEditorFactory->setEnumNames({ - tr("Odd"), - tr("Even"), - }); - registerEditorFactory(qMetaTypeId(), std::move(staggerIndexEditorFactory)); + // auto alignmentEditorFactory = std::make_unique(); + // alignmentEditorFactory->setEnumNames({ + // tr("Unspecified"), + // tr("Top Left"), + // tr("Top"), + // tr("Top Right"), + // tr("Left"), + // tr("Center"), + // tr("Right"), + // tr("Bottom Left"), + // tr("Bottom"), + // tr("Bottom Right"), + // }); + // registerEditorFactory(qMetaTypeId(), std::move(alignmentEditorFactory)); + + + // auto staggerAxisEditorFactory = std::make_unique(); + // staggerAxisEditorFactory->setEnumNames({ + // tr("X"), + // tr("Y"), + // }); + // registerEditorFactory(qMetaTypeId(), std::move(staggerAxisEditorFactory)); + + // auto staggerIndexEditorFactory = std::make_unique(); + // staggerIndexEditorFactory->setEnumNames({ + // tr("Odd"), + // tr("Even"), + // }); + // registerEditorFactory(qMetaTypeId(), std::move(staggerIndexEditorFactory)); QStringList layerFormatNames = { QCoreApplication::translate("PreferencesDialog", "XML (deprecated)"), @@ -642,19 +552,19 @@ VariantEditor::VariantEditor(QWidget *parent) layerFormatNames.append(QCoreApplication::translate("PreferencesDialog", "CSV")); layerFormatValues.append(Map::CSV); - auto layerFormatEditorFactory = std::make_unique(); - layerFormatEditorFactory->setEnumNames(layerFormatNames); - layerFormatEditorFactory->setEnumValues(layerFormatValues); - registerEditorFactory(qMetaTypeId(), std::move(layerFormatEditorFactory)); + // auto layerFormatEditorFactory = std::make_unique(); + // layerFormatEditorFactory->setEnumNames(layerFormatNames); + // layerFormatEditorFactory->setEnumValues(layerFormatValues); + // registerEditorFactory(qMetaTypeId(), std::move(layerFormatEditorFactory)); - auto renderOrderEditorFactory = std::make_unique(); - renderOrderEditorFactory->setEnumNames({ - tr("Right Down"), - tr("Right Up"), - tr("Left Down"), - tr("Left Up"), - }); - registerEditorFactory(qMetaTypeId(), std::move(renderOrderEditorFactory)); + // auto renderOrderEditorFactory = std::make_unique(); + // renderOrderEditorFactory->setEnumNames({ + // tr("Right Down"), + // tr("Right Up"), + // tr("Left Down"), + // tr("Left Up"), + // }); + // registerEditorFactory(qMetaTypeId(), std::move(renderOrderEditorFactory)); // setValue(QVariantMap { // { QStringLiteral("Name"), QVariant(QLatin1String("Hello")) }, @@ -698,11 +608,6 @@ VariantEditor::VariantEditor(QWidget *parent) // addHeader(tr("Custom Properties")); } -void VariantEditor::registerEditorFactory(int type, std::unique_ptr factory) -{ - m_factories[type] = std::move(factory); -} - void VariantEditor::clear() { QLayoutItem *item; @@ -785,13 +690,7 @@ void VariantEditor::addValue(const QVariant &value) QWidget *VariantEditor::createEditor(Property *property) { - const auto editor = property->createEditor(m_widget); - // const auto value = property->value(); - // const int type = value.userType(); - // auto factory = m_factories.find(type); - // if (factory != m_factories.end()) { - // const auto editor = factory->second->createEditor(value, m_widget); - if (editor) { + if (const auto editor = property->createEditor(m_widget)) { editor->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Fixed); return editor; } else { @@ -800,6 +699,222 @@ QWidget *VariantEditor::createEditor(Property *property) return nullptr; } +SizeEdit::SizeEdit(QWidget *parent) + : QWidget(parent) + , m_widthLabel(new QLabel(QStringLiteral("W"), this)) + , m_heightLabel(new QLabel(QStringLiteral("H"), this)) + , m_widthSpinBox(new SpinBox(this)) + , m_heightSpinBox(new SpinBox(this)) +{ + m_widthLabel->setAlignment(Qt::AlignCenter); + m_heightLabel->setAlignment(Qt::AlignCenter); + + auto layout = new QGridLayout(this); + layout->setContentsMargins(QMargins()); + layout->setColumnStretch(1, 1); + layout->setColumnStretch(3, 1); + layout->setSpacing(Utils::dpiScaled(3)); + + const int horizontalMargin = Utils::dpiScaled(3); + m_widthLabel->setContentsMargins(horizontalMargin, 0, horizontalMargin, 0); + m_heightLabel->setContentsMargins(horizontalMargin, 0, horizontalMargin, 0); + + layout->addWidget(m_widthLabel, 0, 0); + layout->addWidget(m_widthSpinBox, 0, 1); + layout->addWidget(m_heightLabel, 0, 2); + layout->addWidget(m_heightSpinBox, 0, 3); + + connect(m_widthSpinBox, qOverload(&QSpinBox::valueChanged), this, &SizeEdit::valueChanged); + connect(m_heightSpinBox, qOverload(&QSpinBox::valueChanged), this, &SizeEdit::valueChanged); +} + +void SizeEdit::setValue(const QSize &size) +{ + m_widthSpinBox->setValue(size.width()); + m_heightSpinBox->setValue(size.height()); +} + +QSize SizeEdit::value() const +{ + return QSize(m_widthSpinBox->value(), m_heightSpinBox->value()); +} + +void SizeEdit::resizeEvent(QResizeEvent *event) +{ + QWidget::resizeEvent(event); + + const auto orientation = event->size().width() < minimumHorizontalWidth() + ? Qt::Vertical : Qt::Horizontal; + + if (m_orientation != orientation) { + m_orientation = orientation; + + auto layout = qobject_cast(this->layout()); + + // Remove all widgets from layout, without deleting them + layout->removeWidget(m_widthLabel); + layout->removeWidget(m_widthSpinBox); + layout->removeWidget(m_heightLabel); + layout->removeWidget(m_heightSpinBox); + + if (orientation == Qt::Horizontal) { + layout->addWidget(m_widthLabel, 0, 0); + layout->addWidget(m_widthSpinBox, 0, 1); + layout->addWidget(m_heightLabel, 0, 2); + layout->addWidget(m_heightSpinBox, 0, 3); + layout->setColumnStretch(3, 1); + } else { + layout->addWidget(m_widthLabel, 0, 0); + layout->addWidget(m_widthSpinBox, 0, 1); + layout->addWidget(m_heightLabel, 1, 0); + layout->addWidget(m_heightSpinBox, 1, 1); + layout->setColumnStretch(3, 0); + } + + // this avoids flickering when the layout changes + layout->activate(); + } +} + +int SizeEdit::minimumHorizontalWidth() const +{ + return m_widthLabel->minimumSizeHint().width() + + m_widthSpinBox->minimumSizeHint().width() + + m_heightLabel->minimumSizeHint().width() + + m_heightSpinBox->minimumSizeHint().width() + + layout()->spacing() * 3; +} + + +EnumEditorFactory::EnumEditorFactory(const QStringList &enumNames, + const QList &enumValues) + : m_enumNamesModel(enumNames) + , m_enumValues(enumValues) +{} + +void EnumEditorFactory::setEnumNames(const QStringList &enumNames) +{ + m_enumNamesModel.setStringList(enumNames); +} + +void EnumEditorFactory::setEnumValues(const QList &enumValues) +{ + m_enumValues = enumValues; +} + +QWidget *EnumEditorFactory::createEditor(Property *property, QWidget *parent) +{ + auto editor = new QComboBox(parent); + // This allows the combo box to shrink horizontally. + editor->setSizeAdjustPolicy(QComboBox::AdjustToMinimumContentsLengthWithIcon); + editor->setModel(&m_enumNamesModel); + + auto syncEditor = [property, editor, this]() { + const QSignalBlocker blocker(editor); + if (m_enumValues.isEmpty()) + editor->setCurrentIndex(property->value().toInt()); + else + editor->setCurrentIndex(m_enumValues.indexOf(property->value().toInt())); + }; + syncEditor(); + + QObject::connect(property, &Property::valueChanged, editor, syncEditor); + QObject::connect(editor, qOverload(&QComboBox::currentIndexChanged), property, + [property, this](int index) { + property->setValue(m_enumValues.isEmpty() ? index : m_enumValues.at(index)); + }); + + return editor; +} + + +ValueTypeEditorFactory::ValueTypeEditorFactory() +{ + // Register some useful default editor factories + registerEditorFactory(QMetaType::Bool, std::make_unique()); + registerEditorFactory(QMetaType::Double, std::make_unique()); + registerEditorFactory(QMetaType::Int, std::make_unique()); + registerEditorFactory(QMetaType::QColor, std::make_unique()); + registerEditorFactory(QMetaType::QPoint, std::make_unique()); + registerEditorFactory(QMetaType::QPointF, std::make_unique()); + registerEditorFactory(QMetaType::QRectF, std::make_unique()); + registerEditorFactory(QMetaType::QSize, std::make_unique()); + registerEditorFactory(QMetaType::QString, std::make_unique()); +} + +void ValueTypeEditorFactory::registerEditorFactory(int type, std::unique_ptr factory) +{ + m_factories[type] = std::move(factory); +} + +QObjectProperty *ValueTypeEditorFactory::createQObjectProperty(QObject *qObject, + const char *name, + const QString &displayName) +{ + auto metaObject = qObject->metaObject(); + auto propertyIndex = metaObject->indexOfProperty(name); + if (propertyIndex < 0) + return nullptr; + + return new QObjectProperty(qObject, + metaObject->property(propertyIndex), + displayName.isEmpty() ? QString::fromUtf8(name) + : displayName, + this); +} + +ValueProperty *ValueTypeEditorFactory::createProperty(const QString &name, const QVariant &value) +{ + const int type = value.userType(); + auto factory = m_factories.find(type); + if (factory != m_factories.end()) + return new ValueProperty(name, value, factory->second.get()); + return nullptr; +} + +QWidget *ValueTypeEditorFactory::createEditor(Property *property, QWidget *parent) +{ + const auto value = property->value(); + const int type = value.userType(); + auto factory = m_factories.find(type); + if (factory != m_factories.end()) + return factory->second->createEditor(property, parent); + return nullptr; +} + + +QObjectProperty::QObjectProperty(QObject *object, + QMetaProperty property, + const QString &displayName, + EditorFactory *editorFactory, + QObject *parent) + : AbstractProperty(displayName, editorFactory, parent) + , m_object(object) + , m_property(property) +{ + // If the property has a notify signal, forward it to valueChanged + auto notify = property.notifySignal(); + if (notify.isValid()) { + auto valuePropertyIndex = metaObject()->indexOfProperty("value"); + auto valueProperty = metaObject()->property(valuePropertyIndex); + auto valueChanged = valueProperty.notifySignal(); + + connect(m_object, notify, this, valueChanged); + } + + setEnabled(m_property.isWritable()); +} + +QVariant QObjectProperty::value() const +{ + return m_property.read(m_object); +} + +void QObjectProperty::setValue(const QVariant &value) +{ + m_property.write(m_object, value); +} + } // namespace Tiled #include "moc_varianteditor.cpp" diff --git a/src/tiled/varianteditor.h b/src/tiled/varianteditor.h index 8fe48c725a..2057629da6 100644 --- a/src/tiled/varianteditor.h +++ b/src/tiled/varianteditor.h @@ -21,6 +21,7 @@ #pragma once #include +#include #include #include #include @@ -34,6 +35,9 @@ class QGridLayout; namespace Tiled { +/** + * A property represents a named value that can create its own edit widget. + */ class Property : public QObject { Q_OBJECT @@ -47,7 +51,7 @@ class Property : public QObject , m_name(name) {} - QString name() const { return m_name; } + const QString &name() const { return m_name; } bool isEnabled() const { return m_enabled; } void setEnabled(bool enabled) @@ -72,61 +76,168 @@ class Property : public QObject bool m_enabled = true; }; -class VariantProperty : public Property +/** + * An editor factory is responsible for creating an editor widget for a given + * property. It can be used to share the configuration of editor widgets + * between different properties. + */ +class EditorFactory +{ + Q_DECLARE_TR_FUNCTIONS(EditorFactory) + +public: + virtual QWidget *createEditor(Property *property, QWidget *parent) = 0; +}; + +/** + * An editor factory that creates a combo box for enum properties. + */ +class EnumEditorFactory : public EditorFactory +{ +public: + EnumEditorFactory(const QStringList &enumNames = {}, + const QList &enumValues = {}); + + void setEnumNames(const QStringList &enumNames); + void setEnumValues(const QList &enumValues); + + QWidget *createEditor(Property *property, QWidget *parent) override; + +private: + QStringListModel m_enumNamesModel; + QList m_enumValues; +}; + +/** + * A property that uses an editor factory to create its editor, but does not + * store a value itself. + * + * The property does not take ownership of the editor factory. + */ +class AbstractProperty : public Property { Q_OBJECT public: - VariantProperty(const QString &name, const QVariant &value, QObject *parent = nullptr) - : Property(name, parent) - , m_value(value) - { - } + AbstractProperty(const QString &name, + EditorFactory *editorFactory, + QObject *parent = nullptr); + + QWidget *createEditor(QWidget *parent) override; + +private: + EditorFactory *m_editorFactory; +}; + +/** + * A property that stores a value of a given type and uses an editor factory to + * create its editor. + * + * The property does not take ownership of the editor factory. + */ +class ValueProperty : public AbstractProperty +{ + Q_OBJECT + +public: + ValueProperty(const QString &name, + const QVariant &value, + EditorFactory *editorFactory, + QObject *parent = nullptr); QVariant value() const override { return m_value; } void setValue(const QVariant &value) override; - QWidget *createEditor(QWidget *parent) override; - private: QVariant m_value; }; -class EnumProperty : public Property +/** + * A property that wraps a value of a QObject property and uses an editor + * factory to create its editor. + * + * The property does not take ownership of the editor factory. + */ +class QObjectProperty : public AbstractProperty { Q_OBJECT public: - EnumProperty(const QString &name, QObject *parent = nullptr) - : Property(name, parent) - {} + QObjectProperty(QObject *object, + QMetaProperty property, + const QString &displayName, + EditorFactory *editorFactory, + QObject *parent = nullptr); - QWidget *createEditor(QWidget *parent) override; + QVariant value() const override; + void setValue(const QVariant &value) override; - void setEnumNames(const QStringList &enumNames) - { - m_enumNamesModel.setStringList(enumNames); - } +private: + QObject *m_object; + QMetaProperty m_property; +}; - void setEnumValues(const QList &enumValues) - { - m_enumValues = enumValues; - } +/** + * An editor factory that selects the appropriate editor factory based on the + * type of the property value. + * + * todo: rename to VariantEditorFactory when the old one is removed + */ +class ValueTypeEditorFactory : public EditorFactory +{ +public: + ValueTypeEditorFactory(); + + /** + * Register an editor factory for a given type. + * + * When there is already an editor factory registered for the given type, + * it will be replaced. + */ + void registerEditorFactory(int type, std::unique_ptr factory); + + /** + * Creates a property that wraps a QObject property and will use the editor + * factory registered for the type of the value. + */ + QObjectProperty *createQObjectProperty(QObject *qObject, + const char *name, + const QString &displayName = {}); + + /** + * Creates a property with the given name and value. The property will use + * the editor factory registered for the type of the value. + */ + ValueProperty *createProperty(const QString &name, const QVariant &value); + + QWidget *createEditor(Property *property, QWidget *parent) override; private: - QStringListModel m_enumNamesModel; - QList m_enumValues; + std::unordered_map> m_factories; }; -class EditorFactory +/** + * A property that wraps an enum value and uses an editor factory to create + * its editor. + */ +class EnumProperty : public AbstractProperty { - Q_DECLARE_TR_FUNCTIONS(EditorFactory) + Q_OBJECT public: - virtual QWidget *createEditor(const QVariant &value, - QWidget *parent) = 0; + EnumProperty(const QString &name, + const QStringList &enumNames = {}, + const QList &enumValues = {}, + QObject *parent = nullptr); + + void setEnumNames(const QStringList &enumNames); + void setEnumValues(const QList &enumValues); + +private: + EnumEditorFactory m_editorFactory; }; + class VariantEditor : public QScrollArea { Q_OBJECT @@ -134,8 +245,6 @@ class VariantEditor : public QScrollArea public: VariantEditor(QWidget *parent = nullptr); - void registerEditorFactory(int type, std::unique_ptr factory); - void clear(); void addHeader(const QString &text); void addSeparator(); @@ -157,7 +266,6 @@ class VariantEditor : public QScrollArea QWidget *m_widget; QGridLayout *m_gridLayout; int m_rowIndex = 0; - std::unordered_map> m_factories; }; } // namespace Tiled