diff --git a/AUTHORS b/AUTHORS index 9d7b5369b6..6ff7cbddb7 100644 --- a/AUTHORS +++ b/AUTHORS @@ -362,6 +362,12 @@ Icons from the Elementary icon theme (GPLv3) * src/tiled/images/32/dialog-error.png * src/tiled/images/32/dialog-warning.png +Icons from the GNOME project (CC0 1.0 Universal) +* src/tiled/resources/images/scalable/text-bold-symbolic.svg +* src/tiled/resources/images/scalable/text-italic-symbolic.svg +* src/tiled/resources/images/scalable/text-underline-symbolic.svg +* src/tiled/resources/images/scalable/text-strikethrough-symbolic.svg + Tilesets: diff --git a/src/libtiled/imagelayer.cpp b/src/libtiled/imagelayer.cpp index 9086d87c8a..ab1169115c 100644 --- a/src/libtiled/imagelayer.cpp +++ b/src/libtiled/imagelayer.cpp @@ -104,8 +104,7 @@ ImageLayer *ImageLayer::initializeClone(ImageLayer *clone) const clone->mImageSource = mImageSource; clone->mTransparentColor = mTransparentColor; clone->mImage = mImage; - clone->mRepeatX = mRepeatX; - clone->mRepeatY = mRepeatY; + clone->mRepetition = mRepetition; return clone; } diff --git a/src/libtiled/imagelayer.h b/src/libtiled/imagelayer.h index 2a63df2071..faf00a4c6b 100644 --- a/src/libtiled/imagelayer.h +++ b/src/libtiled/imagelayer.h @@ -46,6 +46,19 @@ namespace Tiled { class TILEDSHARED_EXPORT ImageLayer : public Layer { public: + enum Repetition { + /** + * Makes the image repeat along the X axis. + */ + RepeatX = 0x1, + + /** + * Makes the image repeat along the Y axis. + */ + RepeatY = 0x2, + }; + Q_DECLARE_FLAGS(RepetitionFlags, Repetition) + ImageLayer(const QString &name, int x, int y); ~ImageLayer() override; @@ -114,25 +127,13 @@ class TILEDSHARED_EXPORT ImageLayer : public Layer */ bool isEmpty() const override; - /** - * Returns true if the image of this layer repeats along the X axis. - */ - bool repeatX() const { return mRepeatX; } - - /** - * Returns true if the image of this layer repeats along the Y axis. - */ - bool repeatY() const { return mRepeatY; } - - /** - * Sets whether the image of this layer repeats along the X axis. - */ - void setRepeatX(bool repeatX) { mRepeatX = repeatX; } + bool repeatX() const { return mRepetition & RepeatX; } + bool repeatY() const { return mRepetition & RepeatY; } + RepetitionFlags repetition() const { return mRepetition; } - /** - * Sets whether the image of this layer repeats along the Y axis. - */ - void setRepeatY(bool repeatY) { mRepeatY = repeatY; } + void setRepeatX(bool repeatX) { mRepetition.setFlag(RepeatX, repeatX); } + void setRepeatY(bool repeatY) { mRepetition.setFlag(RepeatY, repeatY); } + void setRepetition(RepetitionFlags repetition) { mRepetition = repetition; } ImageLayer *clone() const override; @@ -143,8 +144,10 @@ class TILEDSHARED_EXPORT ImageLayer : public Layer QUrl mImageSource; QColor mTransparentColor; QPixmap mImage; - bool mRepeatX = false; - bool mRepeatY = false; + RepetitionFlags mRepetition; }; } // namespace Tiled + +Q_DECLARE_METATYPE(Tiled::ImageLayer::RepetitionFlags) +Q_DECLARE_OPERATORS_FOR_FLAGS(Tiled::ImageLayer::RepetitionFlags) diff --git a/src/libtiled/map.h b/src/libtiled/map.h index 0abee3233e..b7e58117fd 100644 --- a/src/libtiled/map.h +++ b/src/libtiled/map.h @@ -791,3 +791,5 @@ Q_DECLARE_METATYPE(Tiled::Map*) Q_DECLARE_METATYPE(Tiled::Map::Orientation) Q_DECLARE_METATYPE(Tiled::Map::LayerDataFormat) Q_DECLARE_METATYPE(Tiled::Map::RenderOrder) +Q_DECLARE_METATYPE(Tiled::Map::StaggerAxis) +Q_DECLARE_METATYPE(Tiled::Map::StaggerIndex) diff --git a/src/libtiled/objectgroup.h b/src/libtiled/objectgroup.h index 07765ebdbd..3abacd4a7e 100644 --- a/src/libtiled/objectgroup.h +++ b/src/libtiled/objectgroup.h @@ -238,3 +238,4 @@ TILEDSHARED_EXPORT ObjectGroup::DrawOrder drawOrderFromString(const QString &); } // namespace Tiled Q_DECLARE_METATYPE(Tiled::ObjectGroup*) +Q_DECLARE_METATYPE(Tiled::ObjectGroup::DrawOrder) diff --git a/src/libtiled/tileset.h b/src/libtiled/tileset.h index c7b5b01156..934adfdbfe 100644 --- a/src/libtiled/tileset.h +++ b/src/libtiled/tileset.h @@ -739,5 +739,9 @@ inline void Tileset::setTransformationFlags(TransformationFlags flags) Q_DECLARE_METATYPE(Tiled::Tileset*) Q_DECLARE_METATYPE(Tiled::SharedTileset) +Q_DECLARE_METATYPE(Tiled::Tileset::Orientation) +Q_DECLARE_METATYPE(Tiled::Tileset::TileRenderSize) +Q_DECLARE_METATYPE(Tiled::Tileset::FillMode) +Q_DECLARE_METATYPE(Tiled::Tileset::TransformationFlags) Q_DECLARE_OPERATORS_FOR_FLAGS(Tiled::Tileset::TransformationFlags) diff --git a/src/libtiled/wangset.h b/src/libtiled/wangset.h index 2955762c0a..c2df394b53 100644 --- a/src/libtiled/wangset.h +++ b/src/libtiled/wangset.h @@ -424,5 +424,6 @@ TILEDSHARED_EXPORT WangSet::Type wangSetTypeFromString(const QString &); } // namespace Tiled -Q_DECLARE_METATYPE(Tiled::WangSet*) Q_DECLARE_METATYPE(Tiled::WangId) +Q_DECLARE_METATYPE(Tiled::WangSet*) +Q_DECLARE_METATYPE(Tiled::WangSet::Type) diff --git a/src/tiled/changeevents.h b/src/tiled/changeevents.h index c5e4061f27..313bb36dfa 100644 --- a/src/tiled/changeevents.h +++ b/src/tiled/changeevents.h @@ -61,6 +61,7 @@ class ChangeEvent WangSetRemoved, WangSetChanged, WangColorAboutToBeRemoved, + WangColorChanged, } type; protected: @@ -164,7 +165,7 @@ class TileLayerChangeEvent : public LayerChangeEvent class ImageLayerChangeEvent : public LayerChangeEvent { public: - enum TileLayerProperty { + enum ImageLayerProperty { TransparentColorProperty = 1 << 7, ImageSourceProperty = 1 << 8, RepeatProperty = 1 << 9, @@ -277,17 +278,20 @@ class WangSetChangeEvent : public ChangeEvent { public: enum WangSetProperty { - TypeProperty = 1 << 0, + NameProperty, + TypeProperty, + ImageProperty, + ColorCountProperty, }; - WangSetChangeEvent(WangSet *wangSet, int properties) + WangSetChangeEvent(WangSet *wangSet, WangSetProperty property) : ChangeEvent(WangSetChanged) , wangSet(wangSet) - , properties(properties) + , property(property) {} WangSet *wangSet; - int properties; + WangSetProperty property; }; class WangColorEvent : public ChangeEvent @@ -303,4 +307,24 @@ class WangColorEvent : public ChangeEvent int color; }; +class WangColorChangeEvent : public ChangeEvent +{ +public: + enum WangColorProperty { + NameProperty, + ColorProperty, + ImageProperty, + ProbabilityProperty, + }; + + WangColorChangeEvent(WangColor *wangColor, WangColorProperty property) + : ChangeEvent(WangColorChanged) + , wangColor(wangColor) + , property(property) + {} + + WangColor *wangColor; + WangColorProperty property; +}; + } // namespace Tiled diff --git a/src/tiled/changeproperties.cpp b/src/tiled/changeproperties.cpp index 613c3022ec..9b36a3a479 100644 --- a/src/tiled/changeproperties.cpp +++ b/src/tiled/changeproperties.cpp @@ -34,7 +34,7 @@ ChangeClassName::ChangeClassName(Document *document, QUndoCommand *parent) : ChangeValue(document, objects, className, parent) { - setText(QCoreApplication::translate("Undo Commands", "Change Type")); + setText(QCoreApplication::translate("Undo Commands", "Change Class")); } void ChangeClassName::undo() diff --git a/src/tiled/colorbutton.cpp b/src/tiled/colorbutton.cpp index 85d39307c9..25c1d3751e 100644 --- a/src/tiled/colorbutton.cpp +++ b/src/tiled/colorbutton.cpp @@ -40,7 +40,7 @@ ColorButton::ColorButton(QWidget *parent) void ColorButton::setColor(const QColor &color) { - if (mColor == color || !color.isValid()) + if (mColor == color) return; mColor = color; @@ -75,7 +75,9 @@ void ColorButton::pickColor() void ColorButton::updateIcon() { + // todo: fix gray icon in disabled state (consider using opacity, and not using an icon at all) setIcon(Utils::colorIcon(mColor, iconSize())); + setText(mColor.isValid() ? QString() : tr("Unset")); } #include "moc_colorbutton.cpp" diff --git a/src/tiled/document.cpp b/src/tiled/document.cpp index 7c546cd1e1..51c3fe7d4c 100644 --- a/src/tiled/document.cpp +++ b/src/tiled/document.cpp @@ -145,6 +145,7 @@ void Document::setCurrentObject(Object *object, Document *owningDocument) emit currentObjectSet(object); emit currentObjectChanged(object); + emit currentObjectsChanged(); } /** diff --git a/src/tiled/document.h b/src/tiled/document.h index a99b8049ac..62c2088bfb 100644 --- a/src/tiled/document.h +++ b/src/tiled/document.h @@ -139,6 +139,7 @@ class Document : public QObject, void currentObjectSet(Object *object); void currentObjectChanged(Object *object); + void currentObjectsChanged(); /** * Makes the Properties window visible and take focus. diff --git a/src/tiled/fileedit.cpp b/src/tiled/fileedit.cpp index e6804e999b..5e1b09e266 100644 --- a/src/tiled/fileedit.cpp +++ b/src/tiled/fileedit.cpp @@ -45,7 +45,7 @@ FileEdit::FileEdit(QWidget *parent) mOkTextColor = mLineEdit->palette().color(QPalette::Active, QPalette::Text); QToolButton *button = new QToolButton(this); - button->setText(QLatin1String("...")); + button->setText(QStringLiteral("…")); button->setAutoRaise(true); button->setToolTip(tr("Choose")); layout->addWidget(mLineEdit); diff --git a/src/tiled/layerdock.cpp b/src/tiled/layerdock.cpp index 4810d0367d..f6dd6b3abc 100644 --- a/src/tiled/layerdock.cpp +++ b/src/tiled/layerdock.cpp @@ -29,7 +29,6 @@ #include "map.h" #include "mapdocument.h" #include "mapdocumentactionhandler.h" -#include "objectgroup.h" #include "reversingproxymodel.h" #include "utils.h" #include "iconcheckdelegate.h" @@ -39,10 +38,8 @@ #include #include #include -#include #include #include -#include #include #include #include @@ -54,8 +51,6 @@ using namespace Tiled; LayerDock::LayerDock(QWidget *parent): QDockWidget(parent), - mOpacityLabel(new QLabel), - mOpacitySlider(new QSlider(Qt::Horizontal)), mLayerView(new LayerView) { setObjectName(QLatin1String("layerDock")); @@ -64,13 +59,6 @@ LayerDock::LayerDock(QWidget *parent): QVBoxLayout *layout = new QVBoxLayout(widget); layout->setContentsMargins(0, 0, 0, 0); - QHBoxLayout *opacityLayout = new QHBoxLayout; - mOpacitySlider->setRange(0, 100); - mOpacitySlider->setEnabled(false); - opacityLayout->addWidget(mOpacityLabel); - opacityLayout->addWidget(mOpacitySlider); - mOpacityLabel->setBuddy(mOpacitySlider); - MapDocumentActionHandler *handler = MapDocumentActionHandler::instance(); QMenu *newLayerMenu = handler->createNewLayerMenu(this); @@ -99,20 +87,12 @@ LayerDock::LayerDock(QWidget *parent): buttonContainer->addWidget(spacerWidget); buttonContainer->addAction(ActionManager::action("HighlightCurrentLayer")); - QVBoxLayout *listAndToolBar = new QVBoxLayout; - listAndToolBar->setSpacing(0); - listAndToolBar->addWidget(mLayerView); - listAndToolBar->addWidget(buttonContainer); - - layout->addLayout(opacityLayout); - layout->addLayout(listAndToolBar); + layout->setSpacing(0); + layout->addWidget(mLayerView); + layout->addWidget(buttonContainer); setWidget(widget); retranslateUi(); - - connect(mOpacitySlider, &QAbstractSlider::valueChanged, - this, &LayerDock::sliderValueChanged); - updateOpacitySlider(); } void LayerDock::setMapDocument(MapDocument *mapDocument) @@ -126,10 +106,6 @@ void LayerDock::setMapDocument(MapDocument *mapDocument) mMapDocument = mapDocument; if (mMapDocument) { - connect(mMapDocument, &MapDocument::changed, - this, &LayerDock::documentChanged); - connect(mMapDocument, &MapDocument::currentLayerChanged, - this, &LayerDock::updateOpacitySlider); connect(mMapDocument, &MapDocument::editLayerNameRequested, this, &LayerDock::editLayerName); } @@ -145,8 +121,6 @@ void LayerDock::setMapDocument(MapDocument *mapDocument) mLayerView->header()->resizeSection(1, iconSectionWidth); mLayerView->header()->resizeSection(2, iconSectionWidth); } - - updateOpacitySlider(); } void LayerDock::changeEvent(QEvent *e) @@ -162,40 +136,6 @@ void LayerDock::changeEvent(QEvent *e) } } -void LayerDock::updateOpacitySlider() -{ - const bool enabled = mMapDocument && - mMapDocument->currentLayer() != nullptr; - - mOpacitySlider->setEnabled(enabled); - mOpacityLabel->setEnabled(enabled); - - QScopedValueRollback updating(mUpdatingSlider, true); - if (enabled) { - qreal opacity = mMapDocument->currentLayer()->opacity(); - mOpacitySlider->setValue(qRound(opacity * 100)); - } else { - mOpacitySlider->setValue(100); - } -} - -void LayerDock::documentChanged(const ChangeEvent &change) -{ - switch (change.type) { - case ChangeEvent::LayerChanged: { - auto &layerChange = static_cast(change); - - // Don't update the slider when we're the ones changing the layer opacity - if ((layerChange.properties & LayerChangeEvent::OpacityProperty) && !mChangingLayerOpacity) - if (layerChange.layer == mMapDocument->currentLayer()) - updateOpacitySlider(); - break; - } - default: - break; - } -} - void LayerDock::editLayerName() { if (!isVisible()) @@ -208,33 +148,9 @@ void LayerDock::editLayerName() mLayerView->editLayerModelIndex(layerModel->index(currentLayer)); } -void LayerDock::sliderValueChanged(int opacity) -{ - if (!mMapDocument) - return; - - // When the slider changes value just because we're updating it, it - // shouldn't try to set the layer opacity. - if (mUpdatingSlider) - return; - - const auto layer = mMapDocument->currentLayer(); - if (!layer) - return; - - if (static_cast(layer->opacity() * 100) != opacity) { - LayerModel *layerModel = mMapDocument->layerModel(); - QScopedValueRollback updating(mChangingLayerOpacity, true); - layerModel->setData(layerModel->index(layer), - qreal(opacity) / 100, - LayerModel::OpacityRole); - } -} - void LayerDock::retranslateUi() { setWindowTitle(tr("Layers")); - mOpacityLabel->setText(tr("Opacity:")); mNewLayerButton->setToolTip(tr("New Layer")); } diff --git a/src/tiled/layerdock.h b/src/tiled/layerdock.h index 41e286c530..2b1692750d 100644 --- a/src/tiled/layerdock.h +++ b/src/tiled/layerdock.h @@ -28,9 +28,7 @@ #include class QAbstractProxyModel; -class QLabel; class QModelIndex; -class QUndoStack; namespace Tiled { @@ -58,20 +56,13 @@ class LayerDock : public QDockWidget void changeEvent(QEvent *e) override; private: - void updateOpacitySlider(); - void documentChanged(const ChangeEvent &change); void editLayerName(); - void sliderValueChanged(int opacity); void retranslateUi(); - QLabel *mOpacityLabel; - QSlider *mOpacitySlider; QToolButton *mNewLayerButton; LayerView *mLayerView; MapDocument *mMapDocument = nullptr; - bool mUpdatingSlider = false; - bool mChangingLayerOpacity = false; }; /** diff --git a/src/tiled/libtilededitor.qbs b/src/tiled/libtilededitor.qbs index 9e576776b4..8b4ca0cf35 100644 --- a/src/tiled/libtilededitor.qbs +++ b/src/tiled/libtilededitor.qbs @@ -407,6 +407,8 @@ DynamicLibrary { "propertieswidget.h", "propertybrowser.cpp", "propertybrowser.h", + "propertyeditorwidgets.cpp", + "propertyeditorwidgets.h", "propertytypeseditor.cpp", "propertytypeseditor.h", "propertytypeseditor.ui", @@ -553,6 +555,8 @@ DynamicLibrary { "undodock.h", "utils.cpp", "utils.h", + "varianteditor.cpp", + "varianteditor.h", "varianteditorfactory.cpp", "varianteditorfactory.h", "variantpropertymanager.cpp", diff --git a/src/tiled/mapdocument.cpp b/src/tiled/mapdocument.cpp index 06ba3b6d1b..64da85f709 100644 --- a/src/tiled/mapdocument.cpp +++ b/src/tiled/mapdocument.cpp @@ -326,6 +326,9 @@ void MapDocument::setSelectedLayers(const QList &layers) mSelectedLayers = layers; emit selectedLayersChanged(); + + if (currentObject() && currentObject()->typeId() == Object::LayerType) + emit currentObjectsChanged(); } void MapDocument::switchCurrentLayer(Layer *layer) @@ -1277,8 +1280,10 @@ void MapDocument::setSelectedObjects(const QList &selectedObjects) // Make sure the current object is one of the selected ones if (!selectedObjects.isEmpty()) { if (currentObject() && currentObject()->typeId() == Object::MapObjectType) { - if (selectedObjects.contains(static_cast(currentObject()))) + if (selectedObjects.contains(static_cast(currentObject()))) { + emit currentObjectsChanged(); return; + } } setCurrentObject(selectedObjects.first()); @@ -1730,8 +1735,11 @@ void MapDocument::deselectObjects(const QList &objects) removedAboutToBeSelectedObjects += mAboutToBeSelectedObjects.removeAll(object); } - if (removedSelectedObjects > 0) + if (removedSelectedObjects > 0) { emit selectedObjectsChanged(); + if (mCurrentObject && mCurrentObject->typeId() == Object::MapObjectType) + emit currentObjectsChanged(); + } if (removedAboutToBeSelectedObjects > 0) emit aboutToBeSelectedObjectsChanged(mAboutToBeSelectedObjects); } diff --git a/src/tiled/propertieswidget.cpp b/src/tiled/propertieswidget.cpp index e118088c2f..ba11667291 100644 --- a/src/tiled/propertieswidget.cpp +++ b/src/tiled/propertieswidget.cpp @@ -22,32 +22,499 @@ #include "actionmanager.h" #include "addpropertydialog.h" +#include "changeimagelayerproperty.h" +#include "changelayer.h" +#include "changemapobject.h" +#include "changemapproperty.h" +#include "changeobjectgroupproperties.h" #include "changeproperties.h" +#include "changetile.h" +#include "changetileimagesource.h" +#include "changewangcolordata.h" +#include "changewangsetdata.h" #include "clipboardmanager.h" +#include "compression.h" #include "mapdocument.h" -#include "mapobject.h" +#include "objectgroup.h" +#include "objectrefedit.h" +#include "objecttemplate.h" +#include "preferences.h" #include "propertybrowser.h" +#include "tilesetchanges.h" +#include "tilesetdocument.h" +#include "tilesetparametersedit.h" #include "utils.h" +#include "varianteditor.h" #include "variantpropertymanager.h" +#include "wangoverlay.h" #include +#include +#include #include #include #include #include #include #include +#include +#include #include +#include #include #include namespace Tiled { +template<> EnumData enumData() +{ + return {{ + QCoreApplication::translate("Alignment", "Unspecified"), + QCoreApplication::translate("Alignment", "Top Left"), + QCoreApplication::translate("Alignment", "Top"), + QCoreApplication::translate("Alignment", "Top Right"), + QCoreApplication::translate("Alignment", "Left"), + QCoreApplication::translate("Alignment", "Center"), + QCoreApplication::translate("Alignment", "Right"), + QCoreApplication::translate("Alignment", "Bottom Left"), + QCoreApplication::translate("Alignment", "Bottom"), + QCoreApplication::translate("Alignment", "Bottom Right") + }}; +} + +template<> EnumData enumData() +{ + // We leave out the "Unknown" orientation, because it shouldn't occur here + return {{ + QCoreApplication::translate("Tiled::NewMapDialog", "Orthogonal"), + QCoreApplication::translate("Tiled::NewMapDialog", "Isometric"), + QCoreApplication::translate("Tiled::NewMapDialog", "Isometric (Staggered)"), + QCoreApplication::translate("Tiled::NewMapDialog", "Hexagonal (Staggered)") + }, { + Map::Orthogonal, + Map::Isometric, + Map::Staggered, + Map::Hexagonal, + }}; +} + +template<> EnumData enumData() +{ + return {{ + QCoreApplication::translate("StaggerAxis", "X"), + QCoreApplication::translate("StaggerAxis", "Y") + }}; +} + +template<> EnumData enumData() +{ + return {{ + QCoreApplication::translate("StaggerIndex", "Odd"), + QCoreApplication::translate("StaggerIndex", "Even") + }}; +} + +template<> EnumData enumData() +{ + return {{ + QCoreApplication::translate("RenderOrder", "Right Down"), + QCoreApplication::translate("RenderOrder", "Right Up"), + QCoreApplication::translate("RenderOrder", "Left Down"), + QCoreApplication::translate("RenderOrder", "Left Up") + }}; +} + +template<> EnumData enumData() +{ + QStringList names { + QCoreApplication::translate("PreferencesDialog", "XML (deprecated)"), + QCoreApplication::translate("PreferencesDialog", "Base64 (uncompressed)"), + QCoreApplication::translate("PreferencesDialog", "Base64 (gzip compressed)"), + QCoreApplication::translate("PreferencesDialog", "Base64 (zlib compressed)"), + }; + QList values { + Map::XML, + Map::Base64, + Map::Base64Gzip, + Map::Base64Zlib, + }; + + if (compressionSupported(Zstandard)) { + names.append(QCoreApplication::translate("PreferencesDialog", "Base64 (Zstandard compressed)")); + values.append(Map::Base64Zstandard); + } + + names.append(QCoreApplication::translate("PreferencesDialog", "CSV")); + values.append(Map::CSV); + + return { names, values }; +} + +template<> EnumData enumData() +{ + return {{ + QCoreApplication::translate("Tileset", "Orthogonal"), + QCoreApplication::translate("Tileset", "Isometric"), + }}; +} + +template<> EnumData enumData() +{ + return {{ + QCoreApplication::translate("Tileset", "Tile Size"), + QCoreApplication::translate("Tileset", "Map Grid Size"), + }}; +} + +template<> EnumData enumData() +{ + return {{ + QCoreApplication::translate("Tileset", "Stretch"), + QCoreApplication::translate("Tileset", "Preserve Aspect Ratio"), + }}; +} + +template<> EnumData enumData() +{ + return {{ + QCoreApplication::translate("ObjectGroup", "Top Down"), + QCoreApplication::translate("ObjectGroup", "Index Order"), + }}; +} + +template<> EnumData enumData() +{ + const QStringList names { + QCoreApplication::translate("WangSet", "Corner"), + QCoreApplication::translate("WangSet", "Edge"), + QCoreApplication::translate("WangSet", "Mixed"), + }; + + QMap icons; + icons.insert(WangSet::Corner, wangSetIcon(WangSet::Corner)); + icons.insert(WangSet::Edge, wangSetIcon(WangSet::Edge)); + icons.insert(WangSet::Mixed, wangSetIcon(WangSet::Mixed)); + + return { names, {}, icons }; +} + + +class ObjectRefProperty : public PropertyTemplate +{ + Q_OBJECT + +public: + using PropertyTemplate::PropertyTemplate; + + QWidget *createEditor(QWidget *parent) override + { + auto editor = new ObjectRefEdit(parent); + auto syncEditor = [this, editor] { + const QSignalBlocker blocker(editor); + editor->setValue(value()); + }; + syncEditor(); + connect(this, &Property::valueChanged, editor, syncEditor); + connect(editor, &ObjectRefEdit::valueChanged, + this, [this](const DisplayObjectRef &value) { + setValue(value); + }); + return editor; + } +}; + + +class ImageLayerRepeatProperty : public PropertyTemplate +{ + Q_OBJECT + +public: + using PropertyTemplate::PropertyTemplate; + + QWidget *createEditor(QWidget *parent) override + { + auto editor = new QWidget(parent); + auto layout = new QHBoxLayout(editor); + auto repeatX = new QCheckBox(tr("X"), editor); + auto repeatY = new QCheckBox(tr("Y"), editor); + layout->setContentsMargins(QMargins()); + layout->addWidget(repeatX); + layout->addWidget(repeatY); + + auto syncEditor = [=] { + const QSignalBlocker xBlocker(repeatX); + const QSignalBlocker yBlocker(repeatY); + const auto v = value(); + repeatX->setChecked(v & ImageLayer::RepeatX); + repeatY->setChecked(v & ImageLayer::RepeatY); + }; + auto syncProperty = [=] { + ImageLayer::RepetitionFlags v; + if (repeatX->isChecked()) + v |= ImageLayer::RepeatX; + if (repeatY->isChecked()) + v |= ImageLayer::RepeatY; + setValue(v); + }; + + syncEditor(); + + connect(this, &Property::valueChanged, editor, syncEditor); + connect(repeatX, &QCheckBox::toggled, this, syncProperty); + connect(repeatY, &QCheckBox::toggled, this, syncProperty); + return editor; + } +}; + + +class TransformationFlagsProperty : public PropertyTemplate +{ + Q_OBJECT + +public: + using PropertyTemplate::PropertyTemplate; + + QWidget *createEditor(QWidget *parent) override + { + QIcon flipHorizontalIcon(QLatin1String(":images/24/flip-horizontal.png")); + QIcon flipVerticalIcon(QLatin1String(":images/24/flip-vertical.png")); + QIcon rotateRightIcon(QLatin1String(":images/24/rotate-right.png")); + + flipHorizontalIcon.addFile(QLatin1String(":images/32/flip-horizontal.png")); + flipVerticalIcon.addFile(QLatin1String(":images/32/flip-vertical.png")); + rotateRightIcon.addFile(QLatin1String(":images/32/rotate-right.png")); + + auto editor = new QWidget(parent); + + auto flipHorizontally = new QToolButton(editor); + flipHorizontally->setToolTip(tr("Flip Horizontally")); + flipHorizontally->setIcon(flipHorizontalIcon); + flipHorizontally->setCheckable(true); + + auto flipVertically = new QToolButton(editor); + flipVertically->setToolTip(tr("Flip Vertically")); + flipVertically->setIcon(flipVerticalIcon); + flipVertically->setCheckable(true); + + auto rotate = new QToolButton(editor); + rotate->setToolTip(tr("Rotate")); + rotate->setIcon(rotateRightIcon); + rotate->setCheckable(true); + + auto preferUntransformed = new QCheckBox(tr("Prefer Untransformed"), editor); + + auto horizontalLayout = new QHBoxLayout; + horizontalLayout->addWidget(flipHorizontally); + horizontalLayout->addWidget(flipVertically); + horizontalLayout->addWidget(rotate); + horizontalLayout->addStretch(); + + auto verticalLayout = new QVBoxLayout(editor); + verticalLayout->setContentsMargins(QMargins()); + verticalLayout->setSpacing(Utils::dpiScaled(4)); + verticalLayout->addLayout(horizontalLayout); + verticalLayout->addWidget(preferUntransformed); + + auto syncEditor = [=] { + const QSignalBlocker horizontalBlocker(flipHorizontally); + const QSignalBlocker verticalBlocker(flipVertically); + const QSignalBlocker rotateBlocker(rotate); + const QSignalBlocker preferUntransformedBlocker(preferUntransformed); + const auto v = value(); + flipHorizontally->setChecked(v & Tileset::AllowFlipHorizontally); + flipVertically->setChecked(v & Tileset::AllowFlipVertically); + rotate->setChecked(v & Tileset::AllowRotate); + preferUntransformed->setChecked(v & Tileset::PreferUntransformed); + }; + auto syncProperty = [=] { + Tileset::TransformationFlags v; + if (flipHorizontally->isChecked()) + v |= Tileset::AllowFlipHorizontally; + if (flipVertically->isChecked()) + v |= Tileset::AllowFlipVertically; + if (rotate->isChecked()) + v |= Tileset::AllowRotate; + if (preferUntransformed->isChecked()) + v |= Tileset::PreferUntransformed; + setValue(v); + }; + + syncEditor(); + + connect(this, &Property::valueChanged, editor, syncEditor); + connect(flipHorizontally, &QAbstractButton::toggled, this, syncProperty); + connect(flipVertically, &QAbstractButton::toggled, this, syncProperty); + connect(rotate, &QAbstractButton::toggled, this, syncProperty); + connect(preferUntransformed, &QAbstractButton::toggled, this, syncProperty); + return editor; + } +}; + + +class TilesetImageProperty : public GroupProperty +{ + Q_OBJECT + +public: + TilesetImageProperty(TilesetDocument *tilesetDocument, QObject *parent) + : GroupProperty(tr("Tileset Image"), parent) + , mTilesetDocument(tilesetDocument) + {} + + DisplayMode displayMode() const override { return DisplayMode::Default; } + + QWidget *createEditor(QWidget *parent) override + { + auto editor = new TilesetParametersEdit(parent); + editor->setTilesetDocument(mTilesetDocument); + return editor; + } + +private: + TilesetDocument *mTilesetDocument; +}; + +static bool propertyValueAffected(Object *currentObject, + Object *changedObject, + const QString &propertyName) +{ + if (currentObject == changedObject) + return true; + + // Changed property may be inherited + if (currentObject && currentObject->typeId() == Object::MapObjectType && changedObject->typeId() == Object::TileType) { + auto tile = static_cast(currentObject)->cell().tile(); + if (tile == changedObject && !currentObject->hasProperty(propertyName)) + return true; + } + + return false; +} + +static bool objectPropertiesRelevant(Document *document, Object *object) +{ + auto currentObject = document->currentObject(); + if (!currentObject) + return false; + + if (currentObject == object) + return true; + + if (currentObject->typeId() == Object::MapObjectType) + if (static_cast(currentObject)->cell().tile() == object) + return true; + + if (document->currentObjects().contains(object)) + return true; + + return false; +} + + +class VariantMapProperty : public GroupProperty +{ + Q_OBJECT + +public: + VariantMapProperty(const QString &name, QObject *parent = nullptr) + : GroupProperty(name, parent) + { + } + + void setValue(const QVariantMap &value); + const QVariantMap &value() const { return mValue; } + +signals: + void memberValueChanged(const QStringList &path, const QVariant &value); + +protected: + Document *mDocument = nullptr; + +private: + Property *createProperty(const QStringList &path, + std::function get, + std::function set); + + void createClassMembers(const QStringList &path, + GroupProperty *groupProperty, + const ClassPropertyType &classType, + std::function get); + + void updateModifiedRecursively(Property *property, const QVariant &value); + void emitValueChangedRecursively(Property *property); + + QVariantMap mValue; + QHash mPropertyMap; +}; + + +class CustomProperties : public VariantMapProperty +{ + Q_OBJECT + +public: + CustomProperties(QObject *parent = nullptr) + : VariantMapProperty(tr("Custom Properties"), parent) + { + connect(this, &VariantMapProperty::memberValueChanged, + this, &CustomProperties::setPropertyValue); + } + + void setDocument(Document *document); + +private: + // todo: optimize further + void propertyAdded(Object *object, const QString &) { + if (!objectPropertiesRelevant(mDocument, object)) + return; + refresh(); + } + void propertyRemoved(Object *object, const QString &) { + if (mUpdating) + return; + if (!objectPropertiesRelevant(mDocument, object)) + return; + refresh(); + } + void propertyChanged(Object *object, const QString &name) { + if (mUpdating) + return; + if (!propertyValueAffected(mDocument->currentObject(), object, name)) + return; + refresh(); + } + void propertiesChanged(Object *object) { + if (!objectPropertiesRelevant(mDocument, object)) + return; + refresh(); + } + + void refresh(); + + void setPropertyValue(const QStringList &path, const QVariant &value); + + bool mUpdating = false; +}; + + PropertiesWidget::PropertiesWidget(QWidget *parent) : QWidget{parent} - , mDocument(nullptr) - , mPropertyBrowser(new PropertyBrowser) + , mCustomProperties(new CustomProperties) + , mScrollArea(new QScrollArea(this)) { + auto scrollWidget = new QWidget(mScrollArea); + scrollWidget->setBackgroundRole(QPalette::AlternateBase); + scrollWidget->setMinimumWidth(Utils::dpiScaled(120)); + + auto verticalLayout = new QVBoxLayout(scrollWidget); + mPropertyBrowser = new VariantEditor(scrollWidget); + verticalLayout->addWidget(mPropertyBrowser); + verticalLayout->addStretch(); + verticalLayout->setContentsMargins(0, 0, 0, Utils::dpiScaled(4)); + + mScrollArea->setWidget(scrollWidget); + mScrollArea->setWidgetResizable(true); + mActionAddProperty = new QAction(this); mActionAddProperty->setEnabled(false); mActionAddProperty->setIcon(QIcon(QLatin1String(":/images/16/add.png"))); @@ -82,15 +549,15 @@ PropertiesWidget::PropertiesWidget(QWidget *parent) QVBoxLayout *layout = new QVBoxLayout(this); layout->setContentsMargins(0, 0, 0, 0); layout->setSpacing(0); - layout->addWidget(mPropertyBrowser); + layout->addWidget(mScrollArea); layout->addWidget(toolBar); setLayout(layout); mPropertyBrowser->setContextMenuPolicy(Qt::CustomContextMenu); connect(mPropertyBrowser, &PropertyBrowser::customContextMenuRequested, this, &PropertiesWidget::showContextMenu); - connect(mPropertyBrowser, &PropertyBrowser::selectedItemsChanged, - this, &PropertiesWidget::updateActions); + // connect(mPropertyBrowser, &PropertyBrowser::selectedItemsChanged, + // this, &PropertiesWidget::updateActions); retranslateUi(); } @@ -110,7 +577,8 @@ void PropertiesWidget::setDocument(Document *document) mDocument->disconnect(this); mDocument = document; - mPropertyBrowser->setDocument(document); + // mPropertyBrowser->setDocument(document); + mCustomProperties->setDocument(document); if (document) { connect(document, &Document::currentObjectChanged, @@ -131,7 +599,7 @@ void PropertiesWidget::setDocument(Document *document) void PropertiesWidget::selectCustomProperty(const QString &name) { - mPropertyBrowser->selectCustomProperty(name); + // mPropertyBrowser->selectCustomProperty(name); } static bool anyObjectHasProperty(const QList &objects, const QString &name) @@ -143,9 +611,1612 @@ static bool anyObjectHasProperty(const QList &objects, const QString &n return false; } +static QStringList classNamesFor(const Object &object) +{ + QStringList names; + for (const auto type : Object::propertyTypes()) + if (type->isClass()) + if (static_cast(type)->isClassFor(object)) + names.append(type->name); + return names; +} + +class ClassNameProperty : public StringProperty +{ + Q_OBJECT + +public: + ClassNameProperty(Document *document, Object *object, QObject *parent = nullptr) + : StringProperty(tr("Class"), + [this] { return mObject->className(); }, + [this] (const QString &value) { + QUndoStack *undoStack = mDocument->undoStack(); + undoStack->push(new ChangeClassName(mDocument, + mDocument->currentObjects(), + value)); + }, + parent) + , mDocument(document) + , mObject(object) + { + updatePlaceholderText(); + + connect(mDocument, &Document::changed, + this, &ClassNameProperty::onChanged); + } + + QWidget *createEditor(QWidget *parent) override + { + auto editor = new QComboBox(parent); + editor->setEditable(true); + editor->lineEdit()->setPlaceholderText(placeholderText()); + editor->addItems(classNamesFor(*mObject)); + auto syncEditor = [this, editor] { + const QSignalBlocker blocker(editor); + editor->setCurrentText(value()); + }; + syncEditor(); + connect(this, &Property::valueChanged, editor, syncEditor); + connect(this, &StringProperty::placeholderTextChanged, + editor->lineEdit(), &QLineEdit::setPlaceholderText); + connect(editor, &QComboBox::currentTextChanged, this, &StringProperty::setValue); + connect(Preferences::instance(), &Preferences::propertyTypesChanged, + editor, [=] { + const QSignalBlocker blocker(editor); + editor->clear(); + editor->addItems(classNamesFor(*mObject)); + syncEditor(); + }); + return editor; + } + +private: + void onChanged(const ChangeEvent &event) + { + if (event.type != ChangeEvent::ObjectsChanged) + return; + + const auto objectsEvent = static_cast(event); + if (!objectsEvent.objects.contains(mObject)) + return; + + if (objectsEvent.properties & ObjectsChangeEvent::ClassProperty) { + updatePlaceholderText(); + emit valueChanged(); + } + } + + void updatePlaceholderText() + { + if (mObject->typeId() == Object::MapObjectType && mObject->className().isEmpty()) + setPlaceholderText(static_cast(mObject)->effectiveClassName()); + else + setPlaceholderText(QString()); + } + + Document *mDocument; + Object *mObject; +}; + + +class MapSizeProperty : public SizeProperty +{ + Q_OBJECT + +public: + MapSizeProperty(MapDocument *mapDocument, + QObject *parent = nullptr) + : SizeProperty(tr("Map Size"), + [this]{ return mMapDocument->map()->size(); }, {}, + parent) + , mMapDocument(mapDocument) + { + connect(mMapDocument, &MapDocument::mapChanged, + this, &Property::valueChanged); + } + + QWidget *createEditor(QWidget *parent) override + { + auto widget = new QWidget(parent); + auto layout = new QVBoxLayout(widget); + auto valueEdit = SizeProperty::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 ObjectProperties : public QObject +{ + Q_OBJECT + +public: + ObjectProperties(Document *document, Object *object, QObject *parent = nullptr) + : QObject(parent) + , mDocument(document) + , mObject(object) + { + mClassProperty = new ClassNameProperty(document, object, this); + } + + virtual void populateEditor(VariantEditor *) + { + // nothing added here due to property grouping + } + +protected: + void push(QUndoCommand *command) + { + mDocument->undoStack()->push(command); + } + + Document *mDocument; + Property *mClassProperty; + Object *mObject; +}; + + +class MapProperties : public ObjectProperties +{ + Q_OBJECT + +public: + MapProperties(MapDocument *document, QObject *parent = nullptr) + : ObjectProperties(document, document->map(), parent) + { + mOrientationProperty = new EnumProperty( + tr("Orientation"), + [this] { + return map()->orientation(); + }, + [this](Map::Orientation value) { + push(new ChangeMapProperty(mapDocument(), value)); + }); + + mSizeProperty = new MapSizeProperty(mapDocument(), this); + + mTileSizeProperty = new SizeProperty( + tr("Tile Size"), + [this] { + return mapDocument()->map()->tileSize(); + }, + [this](const QSize &newSize) { + const auto oldSize = mapDocument()->map()->tileSize(); + + if (oldSize.width() != newSize.width()) { + push(new ChangeMapProperty(mapDocument(), + Map::TileWidthProperty, + newSize.width())); + } + + if (oldSize.height() != newSize.height()) { + push(new ChangeMapProperty(mapDocument(), + Map::TileHeightProperty, + newSize.height())); + } + }, + this); + mTileSizeProperty->setMinimum(1); + mTileSizeProperty->setSuffix(tr(" px")); + + mInfiniteProperty = new BoolProperty( + tr("Infinite"), + [this] { + return map()->infinite(); + }, + [this](const bool &value) { + push(new ChangeMapProperty(mapDocument(), + Map::InfiniteProperty, + value ? 1 : 0)); + }); + + mHexSideLengthProperty = new IntProperty( + tr("Hex Side Length"), + [this] { + return map()->hexSideLength(); + }, + [this](const QVariant &value) { + push(new ChangeMapProperty(mapDocument(), + Map::HexSideLengthProperty, + value.toInt())); + }); + mHexSideLengthProperty->setSuffix(tr(" px")); + + mStaggerAxisProperty = new EnumProperty( + tr("Stagger Axis"), + [this] { + return map()->staggerAxis(); + }, + [this](Map::StaggerAxis value) { + push(new ChangeMapProperty(mapDocument(), value)); + }); + + mStaggerIndexProperty = new EnumProperty( + tr("Stagger Index"), + [this] { + return map()->staggerIndex(); + }, + [this](Map::StaggerIndex value) { + push(new ChangeMapProperty(mapDocument(), value)); + }); + + mParallaxOriginProperty = new PointFProperty( + tr("Parallax Origin"), + [this] { + return map()->parallaxOrigin(); + }, + [this](const QPointF &value) { + push(new ChangeMapProperty(mapDocument(), value)); + }); + + mLayerDataFormatProperty = new EnumProperty( + tr("Layer Data Format"), + [this] { + return map()->layerDataFormat(); + }, + [this](Map::LayerDataFormat value) { + push(new ChangeMapProperty(mapDocument(), value)); + }); + + mChunkSizeProperty = new SizeProperty( + tr("Output Chunk Size"), + [this] { + return map()->chunkSize(); + }, + [this](const QSize &value) { + push(new ChangeMapProperty(mapDocument(), value)); + }); + mChunkSizeProperty->setMinimum(CHUNK_SIZE_MIN); + + mRenderOrderProperty = new EnumProperty( + tr("Tile Render Order"), + [this] { + return map()->renderOrder(); + }, + [this](Map::RenderOrder value) { + push(new ChangeMapProperty(mapDocument(), value)); + }); + + mCompressionLevelProperty = new IntProperty( + tr("Compression Level"), + [this] { + return map()->compressionLevel(); + }, + [this](const int &value) { + push(new ChangeMapProperty(mapDocument(), value)); + }); + + mBackgroundColorProperty = new ColorProperty( + tr("Background Color"), + [this] { + return map()->backgroundColor(); + }, + [this](const QColor &value) { + push(new ChangeMapProperty(mapDocument(), value)); + }); + + mMapProperties = new GroupProperty(tr("Map")); + mMapProperties->addProperty(mClassProperty); + mMapProperties->addSeparator(); + mMapProperties->addProperty(mOrientationProperty); + mMapProperties->addProperty(mSizeProperty); + mMapProperties->addProperty(mInfiniteProperty); + mMapProperties->addProperty(mTileSizeProperty); + mMapProperties->addProperty(mHexSideLengthProperty); + mMapProperties->addProperty(mStaggerAxisProperty); + mMapProperties->addProperty(mStaggerIndexProperty); + mMapProperties->addSeparator(); + mMapProperties->addProperty(mParallaxOriginProperty); + mMapProperties->addSeparator(); + mMapProperties->addProperty(mLayerDataFormatProperty); + mMapProperties->addProperty(mChunkSizeProperty); + mMapProperties->addProperty(mCompressionLevelProperty); + mMapProperties->addSeparator(); + mMapProperties->addProperty(mRenderOrderProperty); + mMapProperties->addProperty(mBackgroundColorProperty); + + updateEnabledState(); + connect(document, &Document::changed, + this, &MapProperties::onChanged); + } + + void populateEditor(VariantEditor *editor) override + { + editor->addProperty(mMapProperties); + } + +private: + void onChanged(const ChangeEvent &event) + { + if (event.type != ChangeEvent::MapChanged) + return; + + const auto property = static_cast(event).property; + switch (property) { + case Map::TileWidthProperty: + case Map::TileHeightProperty: + emit mTileSizeProperty->valueChanged(); + break; + case Map::InfiniteProperty: + emit mInfiniteProperty->valueChanged(); + break; + case Map::HexSideLengthProperty: + emit mHexSideLengthProperty->valueChanged(); + break; + case Map::StaggerAxisProperty: + emit mStaggerAxisProperty->valueChanged(); + break; + case Map::StaggerIndexProperty: + emit mStaggerIndexProperty->valueChanged(); + break; + case Map::ParallaxOriginProperty: + emit mParallaxOriginProperty->valueChanged(); + break; + case Map::OrientationProperty: + emit mOrientationProperty->valueChanged(); + break; + case Map::RenderOrderProperty: + emit mRenderOrderProperty->valueChanged(); + break; + case Map::BackgroundColorProperty: + emit mBackgroundColorProperty->valueChanged(); + break; + case Map::LayerDataFormatProperty: + emit mLayerDataFormatProperty->valueChanged(); + break; + case Map::CompressionLevelProperty: + emit mCompressionLevelProperty->valueChanged(); + break; + case Map::ChunkSizeProperty: + emit mChunkSizeProperty->valueChanged(); + break; + } + + updateEnabledState(); + } + + void updateEnabledState() + { + const auto orientation = map()->orientation(); + const bool stagger = orientation == Map::Staggered || orientation == Map::Hexagonal; + + mHexSideLengthProperty->setEnabled(orientation == Map::Hexagonal); + mStaggerAxisProperty->setEnabled(stagger); + mStaggerIndexProperty->setEnabled(stagger); + mRenderOrderProperty->setEnabled(orientation == Map::Orthogonal); + mChunkSizeProperty->setEnabled(map()->infinite()); + + switch (map()->layerDataFormat()) { + case Map::XML: + case Map::Base64: + case Map::CSV: + mCompressionLevelProperty->setEnabled(false); + break; + case Map::Base64Gzip: + case Map::Base64Zlib: + case Map::Base64Zstandard: + mCompressionLevelProperty->setEnabled(true); + break; + } + } + + MapDocument *mapDocument() const + { + return static_cast(mDocument); + } + + Map *map() const + { + return mapDocument()->map(); + } + + GroupProperty *mMapProperties; + Property *mOrientationProperty; + Property *mSizeProperty; + SizeProperty *mTileSizeProperty; + Property *mInfiniteProperty; + IntProperty *mHexSideLengthProperty; + Property *mStaggerAxisProperty; + Property *mStaggerIndexProperty; + Property *mParallaxOriginProperty; + Property *mLayerDataFormatProperty; + SizeProperty *mChunkSizeProperty; + Property *mRenderOrderProperty; + Property *mCompressionLevelProperty; + Property *mBackgroundColorProperty; +}; + +class LayerProperties : public ObjectProperties +{ + Q_OBJECT + +public: + LayerProperties(MapDocument *document, Layer *object, QObject *parent = nullptr) + : ObjectProperties(document, object, parent) + { + // todo: would be nicer to avoid the SpinBox and use a custom widget + // might also be nice to embed this in the header instead of using a property + mIdProperty = new IntProperty( + tr("ID"), + [this] { return layer()->id(); }); + mIdProperty->setEnabled(false); + + // todo: the below should be able to apply to all selected layers + + mNameProperty = new StringProperty( + tr("Name"), + [this] { return layer()->name(); }, + [this](const QString &value) { + push(new SetLayerName(mapDocument(), { layer() }, value)); + }); + + mVisibleProperty = new BoolProperty( + tr("Visible"), + [this] { return layer()->isVisible(); }, + [this](const bool &value) { + push(new SetLayerVisible(mapDocument(), { layer() }, value)); + }); + + mLockedProperty = new BoolProperty( + tr("Locked"), + [this] { return layer()->isLocked(); }, + [this](const bool &value) { + push(new SetLayerLocked(mapDocument(), { layer() }, value)); + }); + + mOpacityProperty = new IntProperty( + tr("Opacity"), + [this] { return qRound(layer()->opacity() * 100); }, + [this](const int &value) { + push(new SetLayerOpacity(mapDocument(), { layer() }, qreal(value) / 100)); + }); + mOpacityProperty->setRange(0, 100); + mOpacityProperty->setSuffix(tr("%")); + mOpacityProperty->setSliderEnabled(true); + + mTintColorProperty = new ColorProperty( + tr("Tint Color"), + [this] { return layer()->tintColor(); }, + [this](const QColor &value) { + push(new SetLayerTintColor(mapDocument(), { layer() }, value)); + }); + + mOffsetProperty = new PointFProperty( + tr("Offset"), + [this] { return layer()->offset(); }, + [this](const QPointF &value) { + push(new SetLayerOffset(mapDocument(), { layer() }, value)); + }); + + mParallaxFactorProperty = new PointFProperty( + tr("Parallax Factor"), + [this] { return layer()->parallaxFactor(); }, + [this](const QPointF &value) { + push(new SetLayerParallaxFactor(mapDocument(), { layer() }, value)); + }); + mParallaxFactorProperty->setSingleStep(0.1); + + mLayerProperties = new GroupProperty(tr("Layer")); + mLayerProperties->addProperty(mIdProperty); + mLayerProperties->addProperty(mNameProperty); + mLayerProperties->addProperty(mClassProperty); + mLayerProperties->addSeparator(); + mLayerProperties->addProperty(mVisibleProperty); + mLayerProperties->addProperty(mLockedProperty); + mLayerProperties->addProperty(mOpacityProperty); + mLayerProperties->addProperty(mTintColorProperty); + mLayerProperties->addProperty(mOffsetProperty); + mLayerProperties->addProperty(mParallaxFactorProperty); + + connect(document, &Document::changed, + this, &LayerProperties::onChanged); + } + + void populateEditor(VariantEditor *editor) override + { + editor->addProperty(mLayerProperties); + } + +protected: + virtual void onChanged(const ChangeEvent &event) + { + if (event.type != ChangeEvent::LayerChanged) + return; + + const auto &layerChange = static_cast(event); + if (layerChange.layer != layer()) + return; + + if (layerChange.properties & LayerChangeEvent::VisibleProperty) + emit mVisibleProperty->valueChanged(); + if (layerChange.properties & LayerChangeEvent::LockedProperty) + emit mLockedProperty->valueChanged(); + if (layerChange.properties & LayerChangeEvent::OpacityProperty) + emit mOpacityProperty->valueChanged(); + if (layerChange.properties & LayerChangeEvent::TintColorProperty) + emit mTintColorProperty->valueChanged(); + if (layerChange.properties & LayerChangeEvent::OffsetProperty) + emit mOffsetProperty->valueChanged(); + if (layerChange.properties & LayerChangeEvent::ParallaxFactorProperty) + emit mParallaxFactorProperty->valueChanged(); + } + + MapDocument *mapDocument() const + { + return static_cast(mDocument); + } + + Layer *layer() const + { + return static_cast(mObject); + } + + GroupProperty *mLayerProperties; + Property *mIdProperty; + Property *mNameProperty; + Property *mVisibleProperty; + Property *mLockedProperty; + IntProperty *mOpacityProperty; + Property *mTintColorProperty; + Property *mOffsetProperty; + PointFProperty *mParallaxFactorProperty; +}; + +class ImageLayerProperties : public LayerProperties +{ + Q_OBJECT + +public: + ImageLayerProperties(MapDocument *document, ImageLayer *object, QObject *parent = nullptr) + : LayerProperties(document, object, parent) + { + mImageProperty = new UrlProperty( + tr("Image Source"), + [this] { return imageLayer()->imageSource(); }, + [this](const QUrl &value) { + push(new ChangeImageLayerImageSource(mapDocument(), { imageLayer() }, value)); + }); + mImageProperty->setFilter(Utils::readableImageFormatsFilter()); + + mTransparentColorProperty = new ColorProperty( + tr("Transparent Color"), + [this] { return imageLayer()->transparentColor(); }, + [this](const QColor &value) { + push(new ChangeImageLayerTransparentColor(mapDocument(), { imageLayer() }, value)); + }); + + mRepeatProperty = new ImageLayerRepeatProperty( + tr("Repeat"), + [this] { return imageLayer()->repetition(); }, + [this](const ImageLayer::RepetitionFlags &value) { + const bool repeatX = value & ImageLayer::RepeatX; + const bool repeatY = value & ImageLayer::RepeatY; + if (repeatX != imageLayer()->repeatX()) + push(new ChangeImageLayerRepeatX(mapDocument(), { imageLayer() }, repeatX)); + if (repeatY != imageLayer()->repeatY()) + push(new ChangeImageLayerRepeatY(mapDocument(), { imageLayer() }, repeatY)); + }); + + mImageLayerProperties = new GroupProperty(tr("Image Layer")); + mImageLayerProperties->addProperty(mImageProperty); + mImageLayerProperties->addProperty(mTransparentColorProperty); + mImageLayerProperties->addSeparator(); + mImageLayerProperties->addProperty(mRepeatProperty); + } + + void populateEditor(VariantEditor *editor) override + { + LayerProperties::populateEditor(editor); + editor->addProperty(mImageLayerProperties); + } + +private: + void onChanged(const ChangeEvent &event) override + { + LayerProperties::onChanged(event); + + if (event.type != ChangeEvent::ImageLayerChanged) + return; + + const auto &layerChange = static_cast(event); + if (layerChange.layer != layer()) + return; + + if (layerChange.properties & ImageLayerChangeEvent::ImageSourceProperty) + emit mImageProperty->valueChanged(); + if (layerChange.properties & ImageLayerChangeEvent::TransparentColorProperty) + emit mTransparentColorProperty->valueChanged(); + if (layerChange.properties & ImageLayerChangeEvent::RepeatProperty) { + emit mRepeatProperty->valueChanged(); + } + } + + ImageLayer *imageLayer() const + { + return static_cast(mObject); + } + + GroupProperty *mImageLayerProperties; + UrlProperty *mImageProperty; + Property *mTransparentColorProperty; + Property *mRepeatProperty; +}; + +class ObjectGroupProperties : public LayerProperties +{ + Q_OBJECT + +public: + ObjectGroupProperties(MapDocument *document, ObjectGroup *object, QObject *parent = nullptr) + : LayerProperties(document, object, parent) + { + mColorProperty = new ColorProperty( + tr("Color"), + [this] { return objectGroup()->color(); }, + [this](const QColor &value) { + push(new ChangeObjectGroupColor(mapDocument(), { objectGroup() }, value)); + }); + + mDrawOrderProperty = new EnumProperty( + tr("Draw Order"), + [this] { return objectGroup()->drawOrder(); }, + [this](ObjectGroup::DrawOrder value) { + push(new ChangeObjectGroupDrawOrder(mapDocument(), { objectGroup() }, value)); + }); + + mObjectGroupProperties = new GroupProperty(tr("Object Layer")); + mObjectGroupProperties->addProperty(mColorProperty); + mObjectGroupProperties->addProperty(mDrawOrderProperty); + } + + void populateEditor(VariantEditor *editor) override + { + LayerProperties::populateEditor(editor); + editor->addProperty(mObjectGroupProperties); + } + +private: + void onChanged(const ChangeEvent &event) override + { + LayerProperties::onChanged(event); + + if (event.type != ChangeEvent::ObjectGroupChanged) + return; + + const auto &layerChange = static_cast(event); + if (layerChange.objectGroup != objectGroup()) + return; + + if (layerChange.properties & ObjectGroupChangeEvent::ColorProperty) + emit mColorProperty->valueChanged(); + if (layerChange.properties & ObjectGroupChangeEvent::DrawOrderProperty) + emit mDrawOrderProperty->valueChanged(); + } + + ObjectGroup *objectGroup() const + { + return static_cast(mObject); + } + + GroupProperty *mObjectGroupProperties; + Property *mColorProperty; + Property *mDrawOrderProperty; +}; + +class TilesetProperties : public ObjectProperties +{ + Q_OBJECT + +public: + TilesetProperties(TilesetDocument *document, QObject *parent = nullptr) + : ObjectProperties(document, document->tileset().data(), parent) + { + mNameProperty = new StringProperty( + tr("Name"), + [this] { + return tilesetDocument()->tileset()->name(); + }, + [this](const QString &value) { + push(new RenameTileset(tilesetDocument(), value)); + }); + + mObjectAlignmentProperty = new EnumProperty( + tr("Object Alignment"), + [this] { + return tileset()->objectAlignment(); + }, + [this](Alignment value) { + push(new ChangeTilesetObjectAlignment(tilesetDocument(), value)); + }); + + mTileOffsetProperty = new PointProperty( + tr("Drawing Offset"), + [this] { + return tileset()->tileOffset(); + }, + [this](const QPoint &value) { + push(new ChangeTilesetTileOffset(tilesetDocument(), value)); + }); + mTileOffsetProperty->setSuffix(tr(" px")); + + mTileRenderSizeProperty = new EnumProperty( + tr("Tile Render Size"), + [this] { + return tileset()->tileRenderSize(); + }, + [this](Tileset::TileRenderSize value) { + push(new ChangeTilesetTileRenderSize(tilesetDocument(), value)); + }); + + mFillModeProperty = new EnumProperty( + tr("Fill Mode"), + [this] { + return tileset()->fillMode(); + }, + [this](Tileset::FillMode value) { + push(new ChangeTilesetFillMode(tilesetDocument(), value)); + }); + + mBackgroundColorProperty = new ColorProperty( + tr("Background Color"), + [this] { + return tileset()->backgroundColor(); + }, + [this](const QColor &value) { + push(new ChangeTilesetBackgroundColor(tilesetDocument(), value)); + }); + + mOrientationProperty = new EnumProperty( + tr("Orientation"), + [this] { + return tileset()->orientation(); + }, + [this](Tileset::Orientation value) { + push(new ChangeTilesetOrientation(tilesetDocument(), value)); + }); + + mGridSizeProperty = new SizeProperty( + tr("Grid Size"), + [this] { + return tileset()->gridSize(); + }, + [this](const QSize &value) { + push(new ChangeTilesetGridSize(tilesetDocument(), value)); + }); + mGridSizeProperty->setMinimum(1); + mGridSizeProperty->setSuffix(tr(" px")); + + mColumnCountProperty = new IntProperty( + tr("Columns"), + [this] { + return tileset()->columnCount(); + }, + [this](const int &value) { + push(new ChangeTilesetColumnCount(tilesetDocument(), value)); + }); + mColumnCountProperty->setMinimum(1); + + mAllowedTransformationsProperty = new TransformationFlagsProperty( + tr("Allowed Transformations"), + [this] { + return tileset()->transformationFlags(); + }, + [this](const Tileset::TransformationFlags &value) { + push(new ChangeTilesetTransformationFlags(tilesetDocument(), value)); + }); + + // todo: image file name doesn't update in the TilesetParametersEdit + mTilesetImageProperty = new TilesetImageProperty(document, this); + + mImageProperty = new UrlProperty( + tr("Image"), + [this] { return tileset()->imageSource(); }); + + mTransparentColorProperty = new ColorProperty( + tr("Transparent Color"), + [this] { return tileset()->transparentColor(); }); + + mTileSizeProperty = new SizeProperty( + tr("Tile Size"), + [this] { return tileset()->tileSize(); }); + + mMarginProperty = new IntProperty( + tr("Margin"), + [this] { return tileset()->margin(); }); + + mTileSpacingProperty = new IntProperty( + tr("Spacing"), + [this] { return tileset()->tileSpacing(); }); + + mTileSizeProperty->setSuffix(tr(" px")); + mMarginProperty->setSuffix(tr(" px")); + mTileSpacingProperty->setSuffix(tr(" px")); + + mImageProperty->setEnabled(false); + mTransparentColorProperty->setEnabled(false); + mTileSizeProperty->setEnabled(false); + mMarginProperty->setEnabled(false); + mTileSpacingProperty->setEnabled(false); + + mTilesetImageProperty->addProperty(mImageProperty); + mTilesetImageProperty->addProperty(mTransparentColorProperty); + mTilesetImageProperty->addProperty(mTileSizeProperty); + mTilesetImageProperty->addProperty(mMarginProperty); + mTilesetImageProperty->addProperty(mTileSpacingProperty); + + mTilesetProperties = new GroupProperty(tr("Tileset")); + mTilesetProperties->addProperty(mNameProperty); + mTilesetProperties->addProperty(mClassProperty); + mTilesetProperties->addSeparator(); + mTilesetProperties->addProperty(mObjectAlignmentProperty); + mTilesetProperties->addProperty(mTileOffsetProperty); + mTilesetProperties->addProperty(mTileRenderSizeProperty); + mTilesetProperties->addProperty(mFillModeProperty); + mTilesetProperties->addProperty(mBackgroundColorProperty); + mTilesetProperties->addProperty(mOrientationProperty); + mTilesetProperties->addProperty(mGridSizeProperty); + mTilesetProperties->addProperty(mColumnCountProperty); + mTilesetProperties->addProperty(mAllowedTransformationsProperty); + + if (!tileset()->isCollection()) + mTilesetProperties->addProperty(mTilesetImageProperty); + + updateEnabledState(); + connect(tilesetDocument(), &Document::changed, + this, &TilesetProperties::onChanged); + + connect(tilesetDocument(), &TilesetDocument::tilesetNameChanged, + mNameProperty, &Property::valueChanged); + connect(tilesetDocument(), &TilesetDocument::tilesetTileOffsetChanged, + mTileOffsetProperty, &Property::valueChanged); + connect(tilesetDocument(), &TilesetDocument::tilesetObjectAlignmentChanged, + mObjectAlignmentProperty, &Property::valueChanged); + connect(tilesetDocument(), &TilesetDocument::tilesetChanged, + this, &TilesetProperties::onTilesetChanged); + } + + void populateEditor(VariantEditor *editor) + { + editor->addProperty(mTilesetProperties); + } + +private: + void onChanged(const ChangeEvent &event) + { + if (event.type != ChangeEvent::TilesetChanged) + return; + + const auto property = static_cast(event).property; + switch (property) { + case Tileset::FillModeProperty: + emit mFillModeProperty->valueChanged(); + break; + case Tileset::TileRenderSizeProperty: + emit mTileRenderSizeProperty->valueChanged(); + break; + } + } + + void onTilesetChanged(Tileset *) + { + // the following properties have no specific change events + emit mBackgroundColorProperty->valueChanged(); + emit mOrientationProperty->valueChanged(); + emit mGridSizeProperty->valueChanged(); + emit mColumnCountProperty->valueChanged(); + emit mAllowedTransformationsProperty->valueChanged(); + emit mImageProperty->valueChanged(); + emit mTransparentColorProperty->valueChanged(); + emit mTileSizeProperty->valueChanged(); + emit mMarginProperty->valueChanged(); + emit mTileSpacingProperty->valueChanged(); + } + + void updateEnabledState() + { + const bool collection = tileset()->isCollection(); + mColumnCountProperty->setEnabled(collection); + } + + TilesetDocument *tilesetDocument() const + { + return static_cast(mDocument); + } + + Tileset *tileset() const + { + return tilesetDocument()->tileset().data(); + } + + GroupProperty *mTilesetProperties; + Property *mNameProperty; + Property *mObjectAlignmentProperty; + PointProperty *mTileOffsetProperty; + Property *mTileRenderSizeProperty; + Property *mFillModeProperty; + Property *mBackgroundColorProperty; + Property *mOrientationProperty; + SizeProperty *mGridSizeProperty; + IntProperty *mColumnCountProperty; + Property *mAllowedTransformationsProperty; + GroupProperty *mTilesetImageProperty; + Property *mImageProperty; + Property *mTransparentColorProperty; + SizeProperty *mTileSizeProperty; + IntProperty *mMarginProperty; + IntProperty *mTileSpacingProperty; +}; + +class MapObjectProperties : public ObjectProperties +{ + Q_OBJECT + +public: + MapObjectProperties(MapDocument *document, MapObject *object, QObject *parent = nullptr) + : ObjectProperties(document, object, parent) + { + mIdProperty = new IntProperty( + tr("ID"), + [this] { return mapObject()->id(); }); + mIdProperty->setEnabled(false); + + mTemplateProperty = new UrlProperty( + tr("Template"), + [this] { + if (auto objectTemplate = mapObject()->objectTemplate()) + return QUrl::fromLocalFile(objectTemplate->fileName()); + return QUrl(); + }); + mTemplateProperty->setEnabled(false); + + mNameProperty = new StringProperty( + tr("Name"), + [this] { + return mapObject()->name(); + }, + [this](const QString &value) { + changeMapObject(MapObject::NameProperty, value); + }); + + mVisibleProperty = new BoolProperty( + tr("Visible"), + [this] { + return mapObject()->isVisible(); + }, + [this](const bool &value) { + changeMapObject(MapObject::VisibleProperty, value); + }); + + mPositionProperty = new PointFProperty( + tr("Position"), + [this] { + return mapObject()->position(); + }, + [this](const QPointF &value) { + changeMapObject(MapObject::PositionProperty, value); + }); + + mSizeProperty = new SizeFProperty( + tr("Size"), + [this] { + return mapObject()->size(); + }, + [this](const QSizeF &value) { + changeMapObject(MapObject::SizeProperty, value); + }); + + mRotationProperty = new FloatProperty( + tr("Rotation"), + [this] { + return mapObject()->rotation(); + }, + [this](const qreal &value) { + changeMapObject(MapObject::RotationProperty, value); + }, + this); + mRotationProperty->setSuffix(QStringLiteral("°")); + + // todo: make this a custom widget with "Horizontal" and "Vertical" checkboxes + mFlippingProperty = new IntProperty( + tr("Flipping"), + [this] { + return mapObject()->cell().flags(); + }, + [this](const int &value) { + const int flippingFlags = value; + + MapObjectCell mapObjectCell; + mapObjectCell.object = mapObject(); + mapObjectCell.cell = mapObject()->cell(); + mapObjectCell.cell.setFlippedHorizontally(flippingFlags & 1); + mapObjectCell.cell.setFlippedVertically(flippingFlags & 2); + + auto command = new ChangeMapObjectCells(mDocument, { mapObjectCell }); + + command->setText(QCoreApplication::translate("Undo Commands", + "Flip %n Object(s)", + nullptr, + mapDocument()->selectedObjects().size())); + push(command); + }); + + mTextProperty = new MultilineStringProperty( + tr("Text"), + [this] { + return mapObject()->textData().text; + }, + [this](const QString &value) { + changeMapObject(MapObject::TextProperty, value); + }); + + mTextAlignmentProperty = new QtAlignmentProperty( + tr("Alignment"), + [this] { + return mapObject()->textData().alignment; + }, + [this](const Qt::Alignment &value) { + changeMapObject(MapObject::TextAlignmentProperty, + QVariant::fromValue(value)); + }); + + mTextFontProperty = new FontProperty( + tr("Font"), + [this] { + return mapObject()->textData().font; + }, + [this](const QFont &value) { + changeMapObject(MapObject::TextFontProperty, value); + }); + + mTextWordWrapProperty = new BoolProperty( + tr("Word Wrap"), + [this] { + return mapObject()->textData().wordWrap; + }, + [this](const bool &value) { + changeMapObject(MapObject::TextWordWrapProperty, value); + }); + + mTextColorProperty = new ColorProperty( + tr("Text Color"), + [this] { + return mapObject()->textData().color; + }, + [this](const QColor &value) { + changeMapObject(MapObject::TextColorProperty, value); + }); + + mObjectProperties = new GroupProperty(tr("Object")); + mObjectProperties->addProperty(mIdProperty); + mObjectProperties->addProperty(mTemplateProperty); + mObjectProperties->addProperty(mNameProperty); + mObjectProperties->addProperty(mClassProperty); + mObjectProperties->addSeparator(); + + if (mapDocument()->allowHidingObjects()) + mObjectProperties->addProperty(mVisibleProperty); + + mObjectProperties->addProperty(mPositionProperty); + + if (mapObject()->hasDimensions()) + mObjectProperties->addProperty(mSizeProperty); + + if (mapObject()->canRotate()) + mObjectProperties->addProperty(mRotationProperty); + + if (mapObject()->isTileObject()) { + mObjectProperties->addSeparator(); + mObjectProperties->addProperty(mFlippingProperty); + } + + if (mapObject()->shape() == MapObject::Text) { + mObjectProperties->addSeparator(); + mObjectProperties->addProperty(mTextProperty); + mObjectProperties->addProperty(mTextAlignmentProperty); + mObjectProperties->addProperty(mTextFontProperty); + mObjectProperties->addProperty(mTextWordWrapProperty); + mObjectProperties->addProperty(mTextColorProperty); + } + + connect(document, &Document::changed, + this, &MapObjectProperties::onChanged); + + updateEnabledState(); + } + + void populateEditor(VariantEditor *editor) override + { + editor->addProperty(mObjectProperties); + } + +private: + void onChanged(const ChangeEvent &event) + { + if (event.type != ChangeEvent::MapObjectsChanged) + return; + + const auto &change = static_cast(event); + if (!change.mapObjects.contains(mapObject())) + return; + + if (change.properties & MapObject::NameProperty) + emit mNameProperty->valueChanged(); + if (change.properties & MapObject::VisibleProperty) + emit mVisibleProperty->valueChanged(); + if (change.properties & MapObject::PositionProperty) + emit mPositionProperty->valueChanged(); + if (change.properties & MapObject::SizeProperty) + emit mSizeProperty->valueChanged(); + if (change.properties & MapObject::RotationProperty) + emit mRotationProperty->valueChanged(); + if (change.properties & MapObject::CellProperty) + emit mFlippingProperty->valueChanged(); + if (change.properties & MapObject::TextProperty) + emit mTextProperty->valueChanged(); + if (change.properties & MapObject::TextFontProperty) + emit mTextFontProperty->valueChanged(); + if (change.properties & MapObject::TextAlignmentProperty) + emit mTextAlignmentProperty->valueChanged(); + if (change.properties & MapObject::TextWordWrapProperty) + emit mTextWordWrapProperty->valueChanged(); + if (change.properties & MapObject::TextColorProperty) + emit mTextColorProperty->valueChanged(); + } + + void updateEnabledState() + { + mVisibleProperty->setEnabled(mapDocument()->allowHidingObjects()); + mSizeProperty->setEnabled(mapObject()->hasDimensions()); + mRotationProperty->setEnabled(mapObject()->canRotate()); + mFlippingProperty->setEnabled(mapObject()->isTileObject()); + + const bool isText = mapObject()->shape() == MapObject::Text; + mTextProperty->setEnabled(isText); + mTextAlignmentProperty->setEnabled(isText); + mTextFontProperty->setEnabled(isText); + mTextWordWrapProperty->setEnabled(isText); + mTextColorProperty->setEnabled(isText); + } + + MapDocument *mapDocument() const + { + return static_cast(mDocument); + } + + MapObject *mapObject() const + { + return static_cast(mObject); + } + + void changeMapObject(MapObject::Property property, const QVariant &value) + { + push(new ChangeMapObject(mapDocument(), mapObject(), property, value)); + } + + GroupProperty *mObjectProperties; + Property *mIdProperty; + Property *mTemplateProperty; + Property *mNameProperty; + Property *mVisibleProperty; + Property *mPositionProperty; + Property *mSizeProperty; + FloatProperty *mRotationProperty; + + // for tile objects + Property *mFlippingProperty; + + // for text objects + Property *mTextProperty; + Property *mTextAlignmentProperty; + Property *mTextFontProperty; + Property *mTextWordWrapProperty; + Property *mTextColorProperty; +}; + +class TileProperties : public ObjectProperties +{ + Q_OBJECT + +public: + TileProperties(Document *document, Tile *object, QObject *parent = nullptr) + : ObjectProperties(document, object, parent) + { + mIdProperty = new IntProperty( + tr("ID"), + [this] { return tile()->id(); }); + mIdProperty->setEnabled(false); + + mImageProperty = new UrlProperty( + tr("Image"), + [this] { return tile()->imageSource(); }, + [this](const QUrl &value) { + push(new ChangeTileImageSource(tilesetDocument(), + tile(), + value)); + }); + mImageProperty->setFilter(Utils::readableImageFormatsFilter()); + + mRectangleProperty = new RectProperty( + tr("Rectangle"), + [this] { return tile()->imageRect(); }, + [this](const QRect &value) { + push(new ChangeTileImageRect(tilesetDocument(), + { tile() }, + { value })); + }); + mRectangleProperty->setConstraint(object->image().rect()); + + mProbabilityProperty = new FloatProperty( + tr("Probability"), + [this] { return tile()->probability(); }, + [this](const double &value) { + push(new ChangeTileProbability(tilesetDocument(), + tilesetDocument()->selectedTiles(), + value)); + }); + mProbabilityProperty->setToolTip(tr("Relative chance this tile will be picked")); + mProbabilityProperty->setMinimum(0.0); + + mTileProperties = new GroupProperty(tr("Tile")); + mTileProperties->addProperty(mIdProperty); + mTileProperties->addProperty(mClassProperty); + mTileProperties->addSeparator(); + + if (!tile()->imageSource().isEmpty()) + mTileProperties->addProperty(mImageProperty); + + mTileProperties->addProperty(mRectangleProperty); + mTileProperties->addProperty(mProbabilityProperty); + + // annoying... maybe we should somehow always have the relevant TilesetDocument + if (auto tilesetDocument = qobject_cast(document)) { + connect(tilesetDocument, &TilesetDocument::tileImageSourceChanged, + this, &TileProperties::tileImageSourceChanged); + + connect(tilesetDocument, &TilesetDocument::tileProbabilityChanged, + this, &TileProperties::tileProbabilityChanged); + } else if (auto mapDocument = qobject_cast(document)) { + connect(mapDocument, &MapDocument::tileImageSourceChanged, + this, &TileProperties::tileImageSourceChanged); + + connect(mapDocument, &MapDocument::tileProbabilityChanged, + this, &TileProperties::tileProbabilityChanged); + } + + updateEnabledState(); + } + + void populateEditor(VariantEditor *editor) override + { + editor->addProperty(mTileProperties); + } + +private: + void tileImageSourceChanged(Tile *tile) + { + if (tile != this->tile()) + return; + mRectangleProperty->setConstraint(tile->image().rect()); + emit mImageProperty->valueChanged(); + emit mRectangleProperty->valueChanged(); + } + + void tileProbabilityChanged(Tile *tile) + { + if (tile != this->tile()) + return; + emit mProbabilityProperty->valueChanged(); + } + + void updateEnabledState() + { + const bool hasTilesetDocument = tilesetDocument(); + const auto isCollection = tile()->tileset()->isCollection(); + mClassProperty->setEnabled(hasTilesetDocument); + mImageProperty->setEnabled(hasTilesetDocument && isCollection); + mRectangleProperty->setEnabled(hasTilesetDocument && isCollection); + mProbabilityProperty->setEnabled(hasTilesetDocument); + } + + TilesetDocument *tilesetDocument() const + { + return qobject_cast(mDocument); + } + + Tile *tile() const + { + return static_cast(mObject); + } + + GroupProperty *mTileProperties; + Property *mIdProperty; + UrlProperty *mImageProperty; + RectProperty *mRectangleProperty; + FloatProperty *mProbabilityProperty; +}; + +class WangSetProperties : public ObjectProperties +{ + Q_OBJECT + +public: + WangSetProperties(Document *document, WangSet *object, + QObject *parent = nullptr) + : ObjectProperties(document, object, parent) + { + mNameProperty = new StringProperty( + tr("Name"), + [this] { return wangSet()->name(); }, + [this](const QString &value) { + push(new RenameWangSet(tilesetDocument(), wangSet(), value)); + }); + + mTypeProperty = new EnumProperty( + tr("Type"), + [this] { return wangSet()->type(); }, + [this](WangSet::Type value) { + push(new ChangeWangSetType(tilesetDocument(), wangSet(), value)); + }); + + mColorCountProperty = new IntProperty( + tr("Color Count"), + [this] { return wangSet()->colorCount(); }, + [this](const int &value) { + push(new ChangeWangSetColorCount(tilesetDocument(), + wangSet(), + value)); + }); + mColorCountProperty->setRange(0, WangId::MAX_COLOR_COUNT); + + mWangSetProperties = new GroupProperty(tr("Terrain Set")); + mWangSetProperties->addProperty(mNameProperty); + mWangSetProperties->addProperty(mClassProperty); + mWangSetProperties->addSeparator(); + mWangSetProperties->addProperty(mTypeProperty); + mWangSetProperties->addProperty(mColorCountProperty); + + connect(document, &Document::changed, + this, &WangSetProperties::onChanged); + + updateEnabledState(); + } + + void populateEditor(VariantEditor *editor) override + { + editor->addProperty(mWangSetProperties); + } + +private: + void onChanged(const ChangeEvent &event) + { + if (event.type != ChangeEvent::WangSetChanged) + return; + + const auto &wangSetChange = static_cast(event); + if (wangSetChange.wangSet != wangSet()) + return; + + switch (wangSetChange.property) { + case WangSetChangeEvent::NameProperty: + emit mNameProperty->valueChanged(); + break; + case WangSetChangeEvent::TypeProperty: + emit mTypeProperty->valueChanged(); + break; + case WangSetChangeEvent::ImageProperty: + break; + case WangSetChangeEvent::ColorCountProperty: + emit mColorCountProperty->valueChanged(); + break; + } + } + + void updateEnabledState() + { + const bool hasTilesetDocument = tilesetDocument(); + mNameProperty->setEnabled(hasTilesetDocument); + mTypeProperty->setEnabled(hasTilesetDocument); + mColorCountProperty->setEnabled(hasTilesetDocument); + } + + TilesetDocument *tilesetDocument() const + { + return qobject_cast(mDocument); + } + + WangSet *wangSet() + { + return static_cast(mObject); + } + + GroupProperty *mWangSetProperties; + Property *mNameProperty; + Property *mTypeProperty; + IntProperty *mColorCountProperty; +}; + +class WangColorProperties : public ObjectProperties +{ + Q_OBJECT + +public: + WangColorProperties(Document *document, WangColor *object, + QObject *parent = nullptr) + : ObjectProperties(document, object, parent) + { + mNameProperty = new StringProperty( + tr("Name"), + [this] { return wangColor()->name(); }, + [this](const QString &value) { + push(new ChangeWangColorName(tilesetDocument(), wangColor(), value)); + }); + + mColorProperty = new ColorProperty( + tr("Color"), + [this] { return wangColor()->color(); }, + [this](const QColor &value) { + push(new ChangeWangColorColor(tilesetDocument(), wangColor(), value)); + }); + + mProbabilityProperty = new FloatProperty( + tr("Probability"), + [this] { return wangColor()->probability(); }, + [this](const double &value) { + push(new ChangeWangColorProbability(tilesetDocument(), wangColor(), value)); + }); + mProbabilityProperty->setMinimum(0.01); + + mWangColorProperties = new GroupProperty(tr("Terrain")); + mWangColorProperties->addProperty(mNameProperty); + mWangColorProperties->addProperty(mClassProperty); + mWangColorProperties->addSeparator(); + mWangColorProperties->addProperty(mColorProperty); + mWangColorProperties->addProperty(mProbabilityProperty); + + connect(document, &Document::changed, + this, &WangColorProperties::onChanged); + + updateEnabledState(); + } + + void populateEditor(VariantEditor *editor) override + { + editor->addProperty(mWangColorProperties); + } + +private: + void onChanged(const ChangeEvent &event) + { + if (event.type != ChangeEvent::WangColorChanged) + return; + + const auto &wangColorChange = static_cast(event); + if (wangColorChange.wangColor != wangColor()) + return; + + switch (wangColorChange.property) { + case WangColorChangeEvent::NameProperty: + emit mNameProperty->valueChanged(); + break; + case WangColorChangeEvent::ColorProperty: + emit mColorProperty->valueChanged(); + break; + case WangColorChangeEvent::ImageProperty: + break; + case WangColorChangeEvent::ProbabilityProperty: + emit mProbabilityProperty->valueChanged(); + break; + } + } + + void updateEnabledState() + { + const bool hasTilesetDocument = tilesetDocument(); + mNameProperty->setEnabled(hasTilesetDocument); + mClassProperty->setEnabled(hasTilesetDocument); + mColorProperty->setEnabled(hasTilesetDocument); + mProbabilityProperty->setEnabled(hasTilesetDocument); + } + + TilesetDocument *tilesetDocument() const + { + return qobject_cast(mDocument); + } + + WangColor *wangColor() + { + return static_cast(mObject); + } + + GroupProperty *mWangColorProperties; + Property *mNameProperty; + Property *mColorProperty; + FloatProperty *mProbabilityProperty; +}; + + void PropertiesWidget::currentObjectChanged(Object *object) { - mPropertyBrowser->setObject(object); + mPropertyBrowser->clear(); + + delete mPropertiesObject; + mPropertiesObject = nullptr; + + if (object) { + switch (object->typeId()) { + case Object::LayerType: { + auto mapDocument = static_cast(mDocument); + + switch (static_cast(object)->layerType()) { + case Layer::ImageLayerType: + mPropertiesObject = new ImageLayerProperties(mapDocument, + static_cast(object), + this); + break; + case Layer::ObjectGroupType: + mPropertiesObject = new ObjectGroupProperties(mapDocument, + static_cast(object), + this); + break; + case Layer::TileLayerType: + case Layer::GroupLayerType: + mPropertiesObject = new LayerProperties(mapDocument, + static_cast(object), + this); + break; + } + break; + } + case Object::MapObjectType: + mPropertiesObject = new MapObjectProperties(static_cast(mDocument), + static_cast(object), this); + break; + case Object::MapType: + mPropertiesObject = new MapProperties(static_cast(mDocument), + this); + break; + case Object::TilesetType: + mPropertiesObject = new TilesetProperties(static_cast(mDocument), + this); + break; + case Object::TileType: + mPropertiesObject = new TileProperties(mDocument, + static_cast(object), + this); + break; + case Object::WangSetType: + mPropertiesObject = new WangSetProperties(mDocument, + static_cast(object), + this); + break; + case Object::WangColorType: + mPropertiesObject = new WangColorProperties(mDocument, + static_cast(object), + this); + break; + case Object::ProjectType: + case Object::WorldType: + // these types are currently not handled by the Properties dock + break; + } + } + + if (mPropertiesObject) { + mPropertiesObject->populateEditor(mPropertyBrowser); + mPropertyBrowser->addProperty(mCustomProperties); + } bool editingTileset = mDocument && mDocument->type() == Document::TilesetDocumentType; bool isTileset = object && object->isPartOfTileset(); @@ -155,8 +2226,231 @@ void PropertiesWidget::currentObjectChanged(Object *object) mActionAddProperty->setEnabled(enabled); } + +void VariantMapProperty::setValue(const QVariantMap &value) +{ + mValue = value; + + clear(); + + QMapIterator it(mValue); + while (it.hasNext()) { + it.next(); + const QString &name = it.key(); + + auto get = [=] { + return mValue.value(name); + }; + auto set = [=] (const QVariant &value) { + mValue.insert(name, value); + emit memberValueChanged({ name }, value); + emit valueChanged(); + }; + + if (auto property = createProperty({ name }, std::move(get), std::move(set))) { + property->setActions(Property::Remove); + + updateModifiedRecursively(property, it.value()); + + addProperty(property); + mPropertyMap.insert(name, property); + + connect(property, &Property::removeRequested, this, [=] { + mValue.remove(name); + + if (auto property = mPropertyMap.take(name)) + deleteProperty(property); + + emit memberValueChanged({ name }, QVariant()); + emit valueChanged(); + }); + } + } + + emit valueChanged(); +} + +Property *VariantMapProperty::createProperty(const QStringList &path, + std::function get, + std::function set) +{ + const auto value = get(); + const auto type = value.userType(); + const auto &name = path.last(); + + if (type == filePathTypeId()) { + auto getUrl = [get = std::move(get)] { return get().value().url; }; + auto setUrl = [set = std::move(set)] (const QUrl &value) { + set(QVariant::fromValue(FilePath { value })); + }; + return new UrlProperty(name, std::move(getUrl), std::move(setUrl)); + } + + if (type == objectRefTypeId()) { + auto getObjectRef = [get = std::move(get), this] { + return DisplayObjectRef(get().value(), + static_cast(mDocument)); + }; + auto setObjectRef = [set = std::move(set)](const DisplayObjectRef &value) { + set(QVariant::fromValue(value.ref)); + }; + return new ObjectRefProperty(name, std::move(getObjectRef), std::move(setObjectRef)); + } + + if (type == propertyValueId()) { + const auto propertyValue = value.value(); + if (auto propertyType = propertyValue.type()) { + switch (propertyType->type) { + case PropertyType::PT_Invalid: + break; + case PropertyType::PT_Class: { + auto classType = static_cast(*propertyType); + + auto groupProperty = new GroupProperty(name); + groupProperty->setHeader(false); + + createClassMembers(path, groupProperty, classType, std::move(get)); + + return groupProperty; + } + case PropertyType::PT_Enum: { + auto enumProperty = new BaseEnumProperty( + name, + [get = std::move(get)] { return get().value().value.toInt(); }, + [set = std::move(set), propertyType](int value) { + set(propertyType->wrap(value)); + }); + + auto enumType = static_cast(*propertyType); + enumProperty->setEnumData(enumType.values); + enumProperty->setFlags(enumType.valuesAsFlags); + + return enumProperty; + } + } + } + } + + return createVariantProperty(name, std::move(get), std::move(set)); +} + +void VariantMapProperty::createClassMembers(const QStringList &path, + GroupProperty *groupProperty, + const ClassPropertyType &classType, + std::function get) +{ + // Create a sub-property for each member + QMapIterator it(classType.members); + while (it.hasNext()) { + it.next(); + const QString &name = it.key(); + + auto childPath = path; + childPath.append(name); + + auto getMember = [=] { + auto def = classType.members.value(name); + return get().value().value.toMap().value(name, def); + }; + auto setMember = [=] (const QVariant &value) { + if (setPropertyMemberValue(mValue, childPath, value)) { + const auto &topLevelName = childPath.first(); + updateModifiedRecursively(mPropertyMap.value(topLevelName), + mValue.value(topLevelName)); + + emit memberValueChanged(childPath, value); + emit valueChanged(); + } + }; + + if (auto childProperty = createProperty(childPath, std::move(getMember), setMember)) { + childProperty->setActions(Property::Reset); + groupProperty->addProperty(childProperty); + + connect(childProperty, &Property::resetRequested, this, [=] { + setMember(QVariant()); + emitValueChangedRecursively(childProperty); + }); + } + } +} + +void VariantMapProperty::updateModifiedRecursively(Property *property, + const QVariant &value) +{ + auto groupProperty = dynamic_cast(property); + if (!groupProperty) + return; + + const QVariantMap classValue = value.value().value.toMap(); + + for (auto subProperty : groupProperty->subProperties()) { + const auto &name = subProperty->name(); + const bool isModified = classValue.contains(name); + + if (subProperty->isModified() != isModified || subProperty->isModified()) { + subProperty->setModified(isModified); + updateModifiedRecursively(subProperty, classValue.value(name)); + } + } +} + +void VariantMapProperty::emitValueChangedRecursively(Property *property) +{ + emit property->valueChanged(); + + if (auto groupProperty = dynamic_cast(property)) + for (auto subProperty : groupProperty->subProperties()) + emitValueChangedRecursively(subProperty); +} + + +void CustomProperties::setDocument(Document *document) +{ + if (mDocument == document) + return; + + if (mDocument) + mDocument->disconnect(this); + + mDocument = document; + + if (document) { + connect(document, &Document::currentObjectsChanged, this, &CustomProperties::refresh); + + connect(document, &Document::propertyAdded, this, &CustomProperties::propertyAdded); + connect(document, &Document::propertyRemoved, this, &CustomProperties::propertyRemoved); + connect(document, &Document::propertyChanged, this, &CustomProperties::propertyChanged); + connect(document, &Document::propertiesChanged, this, &CustomProperties::propertiesChanged); + } + + refresh(); +} + +void CustomProperties::refresh() +{ + if (mDocument && mDocument->currentObject()) + setValue(mDocument->currentObject()->properties()); + else + setValue({}); +} + +void CustomProperties::setPropertyValue(const QStringList &path, const QVariant &value) +{ + const auto objects = mDocument->currentObjects(); + if (!objects.isEmpty()) { + QScopedValueRollback updating(mUpdating, true); + if (path.size() > 1 || value.isValid()) + mDocument->undoStack()->push(new SetProperty(mDocument, objects, path, value)); + else + mDocument->undoStack()->push(new RemoveProperty(mDocument, objects, path.first())); + } +} + + void PropertiesWidget::updateActions() { +#if 0 const QList items = mPropertyBrowser->selectedItems(); bool allCustomProperties = !items.isEmpty() && mPropertyBrowser->allCustomPropertyItems(items); bool editingTileset = mDocument && mDocument->type() == Document::TilesetDocumentType; @@ -176,6 +2470,7 @@ void PropertiesWidget::updateActions() mActionRemoveProperty->setEnabled(canModify); mActionRenameProperty->setEnabled(canModify && items.size() == 1); +#endif } void PropertiesWidget::cutProperties() @@ -186,6 +2481,7 @@ void PropertiesWidget::cutProperties() bool PropertiesWidget::copyProperties() { +#if 0 Object *object = mPropertyBrowser->object(); if (!object) return false; @@ -206,6 +2502,7 @@ bool PropertiesWidget::copyProperties() } ClipboardManager::instance()->setProperties(properties); +#endif return true; } @@ -269,11 +2566,12 @@ void PropertiesWidget::addProperty(const QString &name, const QVariant &value) name, value)); } - mPropertyBrowser->editCustomProperty(name); + // mPropertyBrowser->editCustomProperty(name); } void PropertiesWidget::removeProperties() { +#if 0 Object *object = mDocument->currentObject(); if (!object) return; @@ -299,10 +2597,12 @@ void PropertiesWidget::removeProperties() } undoStack->endMacro(); +#endif } void PropertiesWidget::renameProperty() { +#if 0 QtBrowserItem *item = mPropertyBrowser->currentItem(); if (!mPropertyBrowser->isCustomPropertyItem(item)) return; @@ -317,10 +2617,12 @@ void PropertiesWidget::renameProperty() dialog->setWindowTitle(QCoreApplication::translate("Tiled::PropertiesDock", "Rename Property")); connect(dialog, &QInputDialog::textValueSelected, this, &PropertiesWidget::renamePropertyTo); dialog->open(); +#endif } void PropertiesWidget::renamePropertyTo(const QString &name) { +#if 0 if (name.isEmpty()) return; @@ -334,10 +2636,12 @@ void PropertiesWidget::renamePropertyTo(const QString &name) QUndoStack *undoStack = mDocument->undoStack(); undoStack->push(new RenameProperty(mDocument, mDocument->currentObjects(), oldName, name)); +#endif } void PropertiesWidget::showContextMenu(const QPoint &pos) { +#if 0 const Object *object = mDocument->currentObject(); if (!object) return; @@ -474,6 +2778,7 @@ void PropertiesWidget::showContextMenu(const QPoint &pos) undoStack->endMacro(); } +#endif } bool PropertiesWidget::event(QEvent *event) @@ -529,3 +2834,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 022a520b2f..a38f10ddd4 100644 --- a/src/tiled/propertieswidget.h +++ b/src/tiled/propertieswidget.h @@ -22,12 +22,16 @@ #include +class QScrollArea; + namespace Tiled { class Object; +class CustomProperties; class Document; -class PropertyBrowser; +class ObjectProperties; +class VariantEditor; /** * The PropertiesWidget combines the PropertyBrowser with some controls that @@ -73,8 +77,11 @@ public slots: void retranslateUi(); - Document *mDocument; - PropertyBrowser *mPropertyBrowser; + Document *mDocument = nullptr; + ObjectProperties *mPropertiesObject = nullptr; + CustomProperties *mCustomProperties = nullptr; + QScrollArea *mScrollArea; + VariantEditor *mPropertyBrowser; QAction *mActionAddProperty; QAction *mActionRemoveProperty; QAction *mActionRenameProperty; diff --git a/src/tiled/propertyeditorwidgets.cpp b/src/tiled/propertyeditorwidgets.cpp new file mode 100644 index 0000000000..284248c9a2 --- /dev/null +++ b/src/tiled/propertyeditorwidgets.cpp @@ -0,0 +1,596 @@ +/* + * propertyeditorwidgets.cpp + * Copyright 2024, Thorbjørn Lindeijer + * + * This file is part of Tiled. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ + +#include "propertyeditorwidgets.h" + +#include "utils.h" + +#include +#include +#include +#include +#include +#include + +namespace Tiled { + +/** + * Strips a floating point number representation of redundant trailing zeros. + * Examples: + * + * 0.01000 -> 0.01 + * 3.000 -> 3.0 + */ +static QString removeRedundantTrialingZeros(const QString &text) +{ + const QString decimalPoint = QLocale::system().decimalPoint(); + const auto decimalPointIndex = text.lastIndexOf(decimalPoint); + if (decimalPointIndex < 0) // return if there is no decimal point + return text; + + const auto afterDecimalPoint = decimalPointIndex + decimalPoint.length(); + int redundantZeros = 0; + + for (int i = text.length() - 1; i > afterDecimalPoint && text.at(i) == QLatin1Char('0'); --i) + ++redundantZeros; + + return text.left(text.length() - redundantZeros); +} + + +SpinBox::SpinBox(QWidget *parent) + : QSpinBox(parent) +{ + // Allow the full range by default. + setRange(std::numeric_limits::lowest(), + std::numeric_limits::max()); + + // Don't respond to keyboard input immediately. + setKeyboardTracking(false); + + // Allow the widget to shrink horizontally. + setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); +} + +QSize SpinBox::minimumSizeHint() const +{ + // Don't adjust the horizontal size hint based on the maximum value. + auto hint = QSpinBox::minimumSizeHint(); + hint.setWidth(Utils::dpiScaled(50)); + return hint; +} + + +DoubleSpinBox::DoubleSpinBox(QWidget *parent) + : QDoubleSpinBox(parent) +{ + // Allow the full range by default. + setRange(std::numeric_limits::lowest(), + std::numeric_limits::max()); + + // Increase possible precision. + setDecimals(9); + + // Don't respond to keyboard input immediately. + setKeyboardTracking(false); + + // Allow the widget to shrink horizontally. + setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); +} + +QSize DoubleSpinBox::minimumSizeHint() const +{ + // Don't adjust the horizontal size hint based on the maximum value. + auto hint = QDoubleSpinBox::minimumSizeHint(); + hint.setWidth(Utils::dpiScaled(50)); + return hint; +} + +QString DoubleSpinBox::textFromValue(double val) const +{ + auto text = QDoubleSpinBox::textFromValue(val); + + // remove redundant trailing 0's in case of high precision + if (decimals() > 3) + return removeRedundantTrialingZeros(text); + + return text; +} + + +ResponsivePairswiseWidget::ResponsivePairswiseWidget(QWidget *parent) + : QWidget(parent) +{ + auto layout = new QGridLayout(this); + layout->setContentsMargins(QMargins()); + layout->setColumnStretch(1, 1); + layout->setSpacing(Utils::dpiScaled(4)); +} + +void ResponsivePairswiseWidget::setWidgetPairs(const QVector &widgetPairs) +{ + const int horizontalMargin = Utils::dpiScaled(3); + + for (auto &pair : widgetPairs) { + pair.label->setAlignment(Qt::AlignCenter); + pair.label->setContentsMargins(horizontalMargin, 0, horizontalMargin, 0); + } + + m_widgetPairs = widgetPairs; + + addWidgetsToLayout(); +} + +void ResponsivePairswiseWidget::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 = this->layout(); + + // Remove all widgets from layout, without deleting them + for (auto &pair : m_widgetPairs) { + layout->removeWidget(pair.label); + layout->removeWidget(pair.widget); + } + + addWidgetsToLayout(); + + // This avoids flickering when the layout changes + layout->activate(); + } +} + +void ResponsivePairswiseWidget::addWidgetsToLayout() +{ + auto layout = qobject_cast(this->layout()); + + const int maxColumns = m_orientation == Qt::Horizontal ? 4 : 2; + int row = 0; + int column = 0; + + for (auto &pair : m_widgetPairs) { + layout->addWidget(pair.label, row, column); + layout->addWidget(pair.widget, row, column + 1); + column += 2; + + if (column == maxColumns) { + column = 0; + ++row; + } + } + + layout->setColumnStretch(3, m_orientation == Qt::Horizontal ? 1 : 0); +} + +int ResponsivePairswiseWidget::minimumHorizontalWidth() const +{ + const int spacing = layout()->spacing(); + int sum = 0; + int minimum = 0; + int index = 0; + + for (auto &pair : m_widgetPairs) { + sum += (pair.label->minimumSizeHint().width() + + pair.widget->minimumSizeHint().width() + + spacing * 2); + + if (++index % 2 == 0) { + minimum = std::max(sum - spacing, minimum); + sum = 0; + } + } + + return minimum; +} + + +SizeEdit::SizeEdit(QWidget *parent) + : ResponsivePairswiseWidget(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)) +{ + setWidgetPairs({ + { m_widthLabel, m_widthSpinBox }, + { m_heightLabel, m_heightSpinBox }, + }); + + 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::setMinimum(int minimum) +{ + m_widthSpinBox->setMinimum(minimum); + m_heightSpinBox->setMinimum(minimum); +} + +void SizeEdit::setSuffix(const QString &suffix) +{ + m_widthSpinBox->setSuffix(suffix); + m_heightSpinBox->setSuffix(suffix); +} + + +SizeFEdit::SizeFEdit(QWidget *parent) + : ResponsivePairswiseWidget(parent) + , m_widthLabel(new QLabel(QStringLiteral("W"), this)) + , m_heightLabel(new QLabel(QStringLiteral("H"), this)) + , m_widthSpinBox(new DoubleSpinBox(this)) + , m_heightSpinBox(new DoubleSpinBox(this)) +{ + setWidgetPairs({ + { m_widthLabel, m_widthSpinBox }, + { m_heightLabel, m_heightSpinBox }, + }); + + connect(m_widthSpinBox, qOverload(&QDoubleSpinBox::valueChanged), this, &SizeFEdit::valueChanged); + connect(m_heightSpinBox, qOverload(&QDoubleSpinBox::valueChanged), this, &SizeFEdit::valueChanged); +} + +void SizeFEdit::setValue(const QSizeF &size) +{ + m_widthSpinBox->setValue(size.width()); + m_heightSpinBox->setValue(size.height()); +} + +QSizeF SizeFEdit::value() const +{ + return QSizeF(m_widthSpinBox->value(), + m_heightSpinBox->value()); +} + + +PointEdit::PointEdit(QWidget *parent) + : ResponsivePairswiseWidget(parent) + , m_xLabel(new QLabel(QStringLiteral("X"), this)) + , m_yLabel(new QLabel(QStringLiteral("Y"), this)) + , m_xSpinBox(new SpinBox(this)) + , m_ySpinBox(new SpinBox(this)) +{ + setWidgetPairs({ + { m_xLabel, m_xSpinBox }, + { m_yLabel, m_ySpinBox }, + }); + + connect(m_xSpinBox, qOverload(&QSpinBox::valueChanged), this, &PointEdit::valueChanged); + connect(m_ySpinBox, qOverload(&QSpinBox::valueChanged), this, &PointEdit::valueChanged); +} + +void PointEdit::setValue(const QPoint &point) +{ + m_xSpinBox->setValue(point.x()); + m_ySpinBox->setValue(point.y()); +} + +QPoint PointEdit::value() const +{ + return QPoint(m_xSpinBox->value(), + m_ySpinBox->value()); +} + +void PointEdit::setSuffix(const QString &suffix) +{ + m_xSpinBox->setSuffix(suffix); + m_ySpinBox->setSuffix(suffix); +} + + +PointFEdit::PointFEdit(QWidget *parent) + : ResponsivePairswiseWidget(parent) + , m_xLabel(new QLabel(QStringLiteral("X"), this)) + , m_yLabel(new QLabel(QStringLiteral("Y"), this)) + , m_xSpinBox(new DoubleSpinBox(this)) + , m_ySpinBox(new DoubleSpinBox(this)) +{ + setWidgetPairs({ + { m_xLabel, m_xSpinBox }, + { m_yLabel, m_ySpinBox }, + }); + + connect(m_xSpinBox, qOverload(&QDoubleSpinBox::valueChanged), this, &PointFEdit::valueChanged); + connect(m_ySpinBox, qOverload(&QDoubleSpinBox::valueChanged), this, &PointFEdit::valueChanged); +} + +void PointFEdit::setValue(const QPointF &point) +{ + m_xSpinBox->setValue(point.x()); + m_ySpinBox->setValue(point.y()); +} + +QPointF PointFEdit::value() const +{ + return QPointF(m_xSpinBox->value(), + m_ySpinBox->value()); +} + +void PointFEdit::setSingleStep(double step) +{ + m_xSpinBox->setSingleStep(step); + m_ySpinBox->setSingleStep(step); +} + + +RectEdit::RectEdit(QWidget *parent) + : ResponsivePairswiseWidget(parent) + , m_xLabel(new QLabel(QStringLiteral("X"), this)) + , m_yLabel(new QLabel(QStringLiteral("Y"), this)) + , m_widthLabel(new QLabel(QStringLiteral("W"), this)) + , m_heightLabel(new QLabel(QStringLiteral("H"), this)) + , m_xSpinBox(new SpinBox(this)) + , m_ySpinBox(new SpinBox(this)) + , m_widthSpinBox(new SpinBox(this)) + , m_heightSpinBox(new SpinBox(this)) +{ + setWidgetPairs({ + { m_xLabel, m_xSpinBox }, + { m_yLabel, m_ySpinBox }, + { m_widthLabel, m_widthSpinBox }, + { m_heightLabel, m_heightSpinBox }, + }); + + m_widthSpinBox->setMinimum(0); + m_heightSpinBox->setMinimum(0); + + connect(m_xSpinBox, qOverload(&QSpinBox::valueChanged), this, &RectEdit::valueChanged); + connect(m_ySpinBox, qOverload(&QSpinBox::valueChanged), this, &RectEdit::valueChanged); + connect(m_widthSpinBox, qOverload(&QSpinBox::valueChanged), this, &RectEdit::valueChanged); + connect(m_heightSpinBox, qOverload(&QSpinBox::valueChanged), this, &RectEdit::valueChanged); +} + +void RectEdit::setValue(const QRect &rect) +{ + m_xSpinBox->setValue(rect.x()); + m_ySpinBox->setValue(rect.y()); + m_widthSpinBox->setValue(rect.width()); + m_heightSpinBox->setValue(rect.height()); +} + +QRect RectEdit::value() const +{ + return QRect(m_xSpinBox->value(), + m_ySpinBox->value(), + m_widthSpinBox->value(), + m_heightSpinBox->value()); +} + +void RectEdit::setConstraint(const QRect &constraint) +{ + if (constraint.isNull()) { + m_xSpinBox->setRange(std::numeric_limits::lowest(), + std::numeric_limits::max()); + m_ySpinBox->setRange(std::numeric_limits::lowest(), + std::numeric_limits::max()); + m_widthSpinBox->setRange(0, std::numeric_limits::max()); + m_heightSpinBox->setRange(0, std::numeric_limits::max()); + } else { + m_xSpinBox->setRange(constraint.left(), constraint.right() + 1); + m_ySpinBox->setRange(constraint.top(), constraint.bottom() + 1); + m_widthSpinBox->setRange(0, constraint.width()); + m_heightSpinBox->setRange(0, constraint.height()); + } +} + + +RectFEdit::RectFEdit(QWidget *parent) + : ResponsivePairswiseWidget(parent) + , m_xLabel(new QLabel(QStringLiteral("X"), this)) + , m_yLabel(new QLabel(QStringLiteral("Y"), this)) + , m_widthLabel(new QLabel(QStringLiteral("W"), this)) + , m_heightLabel(new QLabel(QStringLiteral("H"), this)) + , m_xSpinBox(new DoubleSpinBox(this)) + , m_ySpinBox(new DoubleSpinBox(this)) + , m_widthSpinBox(new DoubleSpinBox(this)) + , m_heightSpinBox(new DoubleSpinBox(this)) +{ + setWidgetPairs({ + { m_xLabel, m_xSpinBox }, + { m_yLabel, m_ySpinBox }, + { m_widthLabel, m_widthSpinBox }, + { m_heightLabel, m_heightSpinBox }, + }); + + connect(m_xSpinBox, qOverload(&QDoubleSpinBox::valueChanged), this, &RectFEdit::valueChanged); + connect(m_ySpinBox, qOverload(&QDoubleSpinBox::valueChanged), this, &RectFEdit::valueChanged); + connect(m_widthSpinBox, qOverload(&QDoubleSpinBox::valueChanged), this, &RectFEdit::valueChanged); + connect(m_heightSpinBox, qOverload(&QDoubleSpinBox::valueChanged), this, &RectFEdit::valueChanged); +} + +void RectFEdit::setValue(const QRectF &rect) +{ + m_xSpinBox->setValue(rect.x()); + m_ySpinBox->setValue(rect.y()); + m_widthSpinBox->setValue(rect.width()); + m_heightSpinBox->setValue(rect.height()); +} + +QRectF RectFEdit::value() const +{ + return QRectF(m_xSpinBox->value(), + m_ySpinBox->value(), + m_widthSpinBox->value(), + m_heightSpinBox->value()); +} + + +ElidingLabel::ElidingLabel(QWidget *parent) + : ElidingLabel(QString(), parent) +{} + +ElidingLabel::ElidingLabel(const QString &text, QWidget *parent) + : QLabel(text, parent) +{ + setSizePolicy(QSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed)); +} + +QSize ElidingLabel::minimumSizeHint() const +{ + auto hint = QLabel::minimumSizeHint(); + hint.setWidth(std::min(hint.width(), Utils::dpiScaled(30))); + return hint; +} + +void ElidingLabel::paintEvent(QPaintEvent *) +{ + const int m = margin(); + const QRect cr = contentsRect().adjusted(m, m, -m, -m); + const Qt::LayoutDirection dir = text().isRightToLeft() ? Qt::RightToLeft : Qt::LeftToRight; + const int align = QStyle::visualAlignment(dir, alignment()); + const int flags = align | (dir == Qt::LeftToRight ? Qt::TextForceLeftToRight + : Qt::TextForceRightToLeft); + + QStyleOption opt; + opt.initFrom(this); + + const auto elidedText = opt.fontMetrics.elidedText(text(), Qt::ElideRight, cr.width()); + const bool isElided = elidedText != text(); + + if (isElided != m_isElided) { + m_isElided = isElided; + setToolTip(isElided ? text() : QString()); + } + + QStylePainter p(this); + p.drawItemText(cr, flags, opt.palette, isEnabled(), elidedText, foregroundRole()); +} + + +PropertyLabel::PropertyLabel(int level, QWidget *parent) + : ElidingLabel(parent) +{ + setMinimumWidth(Utils::dpiScaled(50)); + setLevel(level); +} + +void PropertyLabel::setLevel(int level) +{ + m_level = level; + + const int spacing = Utils::dpiScaled(3); + const int branchIndicatorWidth = Utils::dpiScaled(14); + setContentsMargins(spacing + branchIndicatorWidth * std::max(m_level, 1), + spacing, spacing, spacing); +} + +void PropertyLabel::setHeader(bool header) +{ + if (m_header == header) + return; + + m_header = header; + setBackgroundRole(header ? QPalette::Dark : QPalette::NoRole); + setForegroundRole(header ? QPalette::BrightText : QPalette::NoRole); + setAutoFillBackground(header); +} + +void PropertyLabel::setExpandable(bool expandable) +{ + if (m_expandable == expandable) + return; + + m_expandable = expandable; + update(); +} + +void PropertyLabel::setExpanded(bool expanded) +{ + if (m_expanded == expanded) + return; + + m_expanded = expanded; + update(); + emit toggled(m_expanded); +} + +void PropertyLabel::setModified(bool modified) +{ + auto f = font(); + f.setBold(modified); + setFont(f); +} + +void PropertyLabel::mousePressEvent(QMouseEvent *event) +{ + if (m_expandable && event->button() == Qt::LeftButton) { + setExpanded(!m_expanded); + return; + } + + ElidingLabel::mousePressEvent(event); +} + +void PropertyLabel::paintEvent(QPaintEvent *event) +{ + ElidingLabel::paintEvent(event); + + const int spacing = Utils::dpiScaled(3); + const int branchIndicatorWidth = Utils::dpiScaled(14); + + QStyleOption branchOption; + branchOption.initFrom(this); + branchOption.rect = QRect(branchIndicatorWidth * std::max(m_level - 1, 0), 0, + branchIndicatorWidth + spacing, height()); + if (m_expandable) + branchOption.state |= QStyle::State_Children; + if (m_expanded) + branchOption.state |= QStyle::State_Open; + + QStylePainter p(this); + p.drawPrimitive(QStyle::PE_IndicatorBranch, branchOption); + + if (m_header) { + const QColor color = static_cast(p.style()->styleHint(QStyle::SH_Table_GridLineColor, &branchOption)); + p.save(); + p.setPen(QPen(color)); + p.drawLine(0, height() - 1, width(), height() - 1); + p.restore(); + } +} + + +QSize PropertyLabel::sizeHint() const +{ + auto hint = ElidingLabel::sizeHint(); + hint.setHeight(m_lineEdit.sizeHint().height()); + return hint; +} + +} // namespace Tiled + +#include "moc_propertyeditorwidgets.cpp" diff --git a/src/tiled/propertyeditorwidgets.h b/src/tiled/propertyeditorwidgets.h new file mode 100644 index 0000000000..cf3330ccd1 --- /dev/null +++ b/src/tiled/propertyeditorwidgets.h @@ -0,0 +1,316 @@ +/* + * propertyeditorwidgets.h + * Copyright 2024, Thorbjørn Lindeijer + * + * This file is part of Tiled. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ + +#pragma once + +#include +#include +#include + +class QLabel; + +namespace Tiled { + +/** + * A spin box that allows the full range by default and shrinks horizontally. + * It also doesn't adjust the horizontal size hint based on the maximum value. + */ +class SpinBox : public QSpinBox +{ + Q_OBJECT + +public: + SpinBox(QWidget *parent = nullptr); + + QSize minimumSizeHint() const override; +}; + +/** + * A double spin box that allows the full range by default and shrinks + * horizontally. It also doesn't adjust the horizontal size hint based on the + * maximum value. + * + * The precision is increased to 9 decimal places. Redundant trailing 0's are + * removed. + */ +class DoubleSpinBox : public QDoubleSpinBox +{ + Q_OBJECT + +public: + DoubleSpinBox(QWidget *parent = nullptr); + + QSize minimumSizeHint() const override; + QString textFromValue(double val) const override; +}; + +/** + * A widget that shows label/widget pairs, wrapping them either two per row + * or each on their own row, depending on the available space. + */ +class ResponsivePairswiseWidget : public QWidget +{ + Q_OBJECT + +public: + struct WidgetPair { + QLabel *label; + QWidget *widget; + }; + + ResponsivePairswiseWidget(QWidget *parent = nullptr); + + void setWidgetPairs(const QVector &widgetPairs); + +protected: + void resizeEvent(QResizeEvent *event) override; + +private: + void addWidgetsToLayout(); + int minimumHorizontalWidth() const; + + Qt::Orientation m_orientation = Qt::Horizontal; + QVector m_widgetPairs; +}; + +/** + * A widget for editing a QSize value. + */ +class SizeEdit : public ResponsivePairswiseWidget +{ + Q_OBJECT + Q_PROPERTY(QSize value READ value WRITE setValue NOTIFY valueChanged FINAL) + +public: + SizeEdit(QWidget *parent = nullptr); + + void setValue(const QSize &size); + QSize value() const; + + void setMinimum(int minimum); + void setSuffix(const QString &suffix); + +signals: + void valueChanged(); + +private: + QLabel *m_widthLabel; + QLabel *m_heightLabel; + SpinBox *m_widthSpinBox; + SpinBox *m_heightSpinBox; +}; + +/** + * A widget for editing a QSizeF value. + */ +class SizeFEdit : public ResponsivePairswiseWidget +{ + Q_OBJECT + Q_PROPERTY(QSizeF value READ value WRITE setValue NOTIFY valueChanged FINAL) + +public: + SizeFEdit(QWidget *parent = nullptr); + + void setValue(const QSizeF &size); + QSizeF value() const; + +signals: + void valueChanged(); + +private: + QLabel *m_widthLabel; + QLabel *m_heightLabel; + DoubleSpinBox *m_widthSpinBox; + DoubleSpinBox *m_heightSpinBox; +}; + +/** + * A widget for editing a QPoint value. + */ +class PointEdit : public ResponsivePairswiseWidget +{ + Q_OBJECT + Q_PROPERTY(QPoint value READ value WRITE setValue NOTIFY valueChanged FINAL) + +public: + PointEdit(QWidget *parent = nullptr); + + void setValue(const QPoint &size); + QPoint value() const; + + void setSuffix(const QString &suffix); + +signals: + void valueChanged(); + +private: + QLabel *m_xLabel; + QLabel *m_yLabel; + SpinBox *m_xSpinBox; + SpinBox *m_ySpinBox; +}; + +/** + * A widget for editing a QPointF value. + */ +class PointFEdit : public ResponsivePairswiseWidget +{ + Q_OBJECT + Q_PROPERTY(QPointF value READ value WRITE setValue NOTIFY valueChanged FINAL) + +public: + PointFEdit(QWidget *parent = nullptr); + + void setValue(const QPointF &size); + QPointF value() const; + + void setSingleStep(double step); + +signals: + void valueChanged(); + +private: + QLabel *m_xLabel; + QLabel *m_yLabel; + DoubleSpinBox *m_xSpinBox; + DoubleSpinBox *m_ySpinBox; +}; + +/** + * A widget for editing a QRect value. + */ +class RectEdit : public ResponsivePairswiseWidget +{ + Q_OBJECT + Q_PROPERTY(QRect value READ value WRITE setValue NOTIFY valueChanged FINAL) + +public: + RectEdit(QWidget *parent = nullptr); + + void setValue(const QRect &size); + QRect value() const; + + void setConstraint(const QRect &constraint); + +signals: + void valueChanged(); + +private: + QLabel *m_xLabel; + QLabel *m_yLabel; + QLabel *m_widthLabel; + QLabel *m_heightLabel; + SpinBox *m_xSpinBox; + SpinBox *m_ySpinBox; + SpinBox *m_widthSpinBox; + SpinBox *m_heightSpinBox; +}; + +/** + * A widget for editing a QRectF value. + */ +class RectFEdit : public ResponsivePairswiseWidget +{ + Q_OBJECT + Q_PROPERTY(QRectF value READ value WRITE setValue NOTIFY valueChanged FINAL) + +public: + RectFEdit(QWidget *parent = nullptr); + + void setValue(const QRectF &size); + QRectF value() const; + +signals: + void valueChanged(); + +private: + QLabel *m_xLabel; + QLabel *m_yLabel; + QLabel *m_widthLabel; + QLabel *m_heightLabel; + DoubleSpinBox *m_xSpinBox; + DoubleSpinBox *m_ySpinBox; + DoubleSpinBox *m_widthSpinBox; + DoubleSpinBox *m_heightSpinBox; +}; + +/** + * A label that elides its text if there is not enough space. + * + * The elided text is shown as a tooltip. + */ +class ElidingLabel : public QLabel +{ + Q_OBJECT + +public: + explicit ElidingLabel(QWidget *parent = nullptr); + ElidingLabel(const QString &text, QWidget *parent = nullptr); + + QSize minimumSizeHint() const override; + +protected: + void paintEvent(QPaintEvent *) override; + +private: + bool m_isElided = false; +}; + +/** + * A property label widget, which can be a header or just be expandable. + */ +class PropertyLabel : public ElidingLabel +{ + Q_OBJECT + +public: + PropertyLabel(int level, QWidget *parent = nullptr); + + void setLevel(int level); + + void setHeader(bool header); + bool isHeader() const { return m_header; } + + void setExpandable(bool expandable); + bool isExpandable() const { return m_expandable; } + + void setExpanded(bool expanded); + bool isExpanded() const { return m_expanded; } + + void setModified(bool modified); + + QSize sizeHint() const override; + +signals: + void toggled(bool expanded); + +protected: + void mousePressEvent(QMouseEvent *event) override; + void paintEvent(QPaintEvent *) override; + +private: + QLineEdit m_lineEdit; + int m_level = 0; + bool m_header = false; + bool m_expandable = false; + bool m_expanded = false; +}; + +} // namespace Tiled diff --git a/src/tiled/resources/images/scalable/text-bold-symbolic.svg b/src/tiled/resources/images/scalable/text-bold-symbolic.svg new file mode 100644 index 0000000000..bdbddea8d2 --- /dev/null +++ b/src/tiled/resources/images/scalable/text-bold-symbolic.svg @@ -0,0 +1,2 @@ + + diff --git a/src/tiled/resources/images/scalable/text-italic-symbolic.svg b/src/tiled/resources/images/scalable/text-italic-symbolic.svg new file mode 100644 index 0000000000..9fa6e67de6 --- /dev/null +++ b/src/tiled/resources/images/scalable/text-italic-symbolic.svg @@ -0,0 +1,2 @@ + + diff --git a/src/tiled/resources/images/scalable/text-strikethrough-symbolic.svg b/src/tiled/resources/images/scalable/text-strikethrough-symbolic.svg new file mode 100644 index 0000000000..6df152de47 --- /dev/null +++ b/src/tiled/resources/images/scalable/text-strikethrough-symbolic.svg @@ -0,0 +1,2 @@ + + diff --git a/src/tiled/resources/images/scalable/text-underline-symbolic.svg b/src/tiled/resources/images/scalable/text-underline-symbolic.svg new file mode 100644 index 0000000000..ff520eb3c7 --- /dev/null +++ b/src/tiled/resources/images/scalable/text-underline-symbolic.svg @@ -0,0 +1,2 @@ + + diff --git a/src/tiled/scriptdialog.cpp b/src/tiled/scriptdialog.cpp index 0c8b64afb3..b92a993ac0 100644 --- a/src/tiled/scriptdialog.cpp +++ b/src/tiled/scriptdialog.cpp @@ -26,6 +26,7 @@ #include "mainwindow.h" #include "scriptimage.h" #include "scriptmanager.h" +#include "utils.h" #include #include @@ -41,8 +42,6 @@ #include #include -#include - static const int leftColumnStretch = 0; // stretch as much as we can so that the left column looks as close to zero width as possible when there is no content static const int rightColumnStretch = 1; @@ -51,18 +50,6 @@ namespace Tiled { QSet ScriptDialog::sDialogInstances; -static void deleteAllFromLayout(QLayout *layout) -{ - while (QLayoutItem *item = layout->takeAt(0)) { - delete item->widget(); - - if (QLayout *layout = item->layout()) - deleteAllFromLayout(layout); - - delete item; - } -} - ScriptImageWidget::ScriptImageWidget(Tiled::ScriptImage *image, QWidget *parent) : QLabel(parent) { @@ -144,7 +131,7 @@ void ScriptDialog::initializeLayout() void ScriptDialog::clear() { - deleteAllFromLayout(layout()); + Utils::deleteAllFromLayout(layout()); initializeLayout(); } diff --git a/src/tiled/textpropertyedit.cpp b/src/tiled/textpropertyedit.cpp index 147852cc22..85c131ebab 100644 --- a/src/tiled/textpropertyedit.cpp +++ b/src/tiled/textpropertyedit.cpp @@ -117,7 +117,7 @@ TextPropertyEdit::TextPropertyEdit(QWidget *parent) setFocusProxy(mLineEdit); QToolButton *button = new QToolButton(this); - button->setText(QStringLiteral("...")); + button->setText(QStringLiteral("…")); button->setAutoRaise(true); // Set a validator that replaces newline characters by literal "\\n". diff --git a/src/tiled/textpropertyedit.h b/src/tiled/textpropertyedit.h index 085a096ae7..a509ab38fc 100644 --- a/src/tiled/textpropertyedit.h +++ b/src/tiled/textpropertyedit.h @@ -38,12 +38,15 @@ QString escapeNewlines(const QString &string); class TextPropertyEdit : public QWidget { Q_OBJECT + Q_PROPERTY(QString text READ text WRITE setText NOTIFY textChanged FINAL) public: explicit TextPropertyEdit(QWidget *parent = nullptr); QString text() const; + QLineEdit *lineEdit() const { return mLineEdit; } + public slots: void setText(const QString &text); diff --git a/src/tiled/tilesetdocument.cpp b/src/tiled/tilesetdocument.cpp index f378489189..5f74d7bd77 100644 --- a/src/tiled/tilesetdocument.cpp +++ b/src/tiled/tilesetdocument.cpp @@ -371,6 +371,9 @@ void TilesetDocument::setSelectedTiles(const QList &selectedTiles) { mSelectedTiles = selectedTiles; emit selectedTilesChanged(); + + if (currentObject() && currentObject()->typeId() == Object::TileType) + emit currentObjectsChanged(); } QList TilesetDocument::currentObjects() const diff --git a/src/tiled/tilesetparametersedit.cpp b/src/tiled/tilesetparametersedit.cpp index a5138da7d0..82700fb74b 100644 --- a/src/tiled/tilesetparametersedit.cpp +++ b/src/tiled/tilesetparametersedit.cpp @@ -44,7 +44,7 @@ TilesetParametersEdit::TilesetParametersEdit(QWidget *parent) QToolButton *button = new QToolButton(this); button->setSizePolicy(QSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred)); - button->setText(tr("Edit...")); + button->setText(tr("Edit Tileset")); layout->addWidget(mLabel); layout->addWidget(button); diff --git a/src/tiled/tilesetview.cpp b/src/tiled/tilesetview.cpp index 9693a45d6a..ac15fce3c2 100644 --- a/src/tiled/tilesetview.cpp +++ b/src/tiled/tilesetview.cpp @@ -831,7 +831,7 @@ void TilesetView::onChange(const ChangeEvent &change) case ChangeEvent::WangSetChanged: { auto &wangSetChange = static_cast(change); if (mEditWangSet && wangSetChange.wangSet == mWangSet && - (wangSetChange.properties & WangSetChangeEvent::TypeProperty)) { + (wangSetChange.property == WangSetChangeEvent::TypeProperty)) { viewport()->update(); } break; diff --git a/src/tiled/tilesetwangsetmodel.cpp b/src/tiled/tilesetwangsetmodel.cpp index 5a6d6f5a6d..76ccbf1a8a 100644 --- a/src/tiled/tilesetwangsetmodel.cpp +++ b/src/tiled/tilesetwangsetmodel.cpp @@ -22,6 +22,7 @@ #include "changeevents.h" #include "changewangsetdata.h" +#include "mapdocument.h" #include "tile.h" #include "tileset.h" #include "tilesetdocument.h" @@ -159,13 +160,14 @@ void TilesetWangSetModel::setWangSetName(WangSet *wangSet, const QString &name) Q_ASSERT(wangSet->tileset() == mTilesetDocument->tileset().data()); wangSet->setName(name); emitWangSetChange(wangSet); + emitToTilesetAndMaps(WangSetChangeEvent(wangSet, WangSetChangeEvent::NameProperty)); } void TilesetWangSetModel::setWangSetType(WangSet *wangSet, WangSet::Type type) { Q_ASSERT(wangSet->tileset() == mTilesetDocument->tileset().data()); wangSet->setType(type); - emit mTilesetDocument->changed(WangSetChangeEvent(wangSet, WangSetChangeEvent::TypeProperty)); + emitToTilesetAndMaps(WangSetChangeEvent(wangSet, WangSetChangeEvent::TypeProperty)); } void TilesetWangSetModel::setWangSetColorCount(WangSet *wangSet, int value) @@ -173,6 +175,7 @@ void TilesetWangSetModel::setWangSetColorCount(WangSet *wangSet, int value) Q_ASSERT(wangSet->tileset() == mTilesetDocument->tileset().data()); wangSet->setColorCount(value); emitWangSetChange(wangSet); + emitToTilesetAndMaps(WangSetChangeEvent(wangSet, WangSetChangeEvent::ColorCountProperty)); } void TilesetWangSetModel::setWangSetImage(WangSet *wangSet, int tileId) @@ -187,6 +190,7 @@ void TilesetWangSetModel::insertWangColor(WangSet *wangSet, const QSharedPointer Q_ASSERT(wangSet->tileset() == mTilesetDocument->tileset().data()); wangSet->insertWangColor(wangColor); emitWangSetChange(wangSet); + emitToTilesetAndMaps(WangSetChangeEvent(wangSet, WangSetChangeEvent::ColorCountProperty)); } QSharedPointer TilesetWangSetModel::takeWangColorAt(WangSet *wangSet, int color) @@ -196,6 +200,7 @@ QSharedPointer TilesetWangSetModel::takeWangColorAt(WangSet *wangSet, auto wangColor = wangSet->takeWangColorAt(color); emit wangColorRemoved(wangColor.data()); emitWangSetChange(wangSet); + emitToTilesetAndMaps(WangSetChangeEvent(wangSet, WangSetChangeEvent::ColorCountProperty)); return wangColor; } @@ -206,6 +211,17 @@ void TilesetWangSetModel::emitWangSetChange(WangSet *wangSet) emit wangSetChanged(wangSet); } +void TilesetWangSetModel::emitToTilesetAndMaps(const ChangeEvent &event) +{ + emit mTilesetDocument->changed(event); + + // todo: this doesn't work reliably because it only reaches maps that use + // the tileset, whereas the Properties view can be showing stuff from any + // tileset. + for (MapDocument *mapDocument : mTilesetDocument->mapDocuments()) + emit mapDocument->changed(event); +} + void TilesetWangSetModel::documentChanged(const ChangeEvent &event) { switch (event.type) { diff --git a/src/tiled/tilesetwangsetmodel.h b/src/tiled/tilesetwangsetmodel.h index 3a61c2acd4..6675acb3be 100644 --- a/src/tiled/tilesetwangsetmodel.h +++ b/src/tiled/tilesetwangsetmodel.h @@ -98,6 +98,7 @@ class TilesetWangSetModel : public QAbstractListModel void documentChanged(const ChangeEvent &event); void emitWangSetChange(WangSet *wangSet); + void emitToTilesetAndMaps(const ChangeEvent &event); TilesetDocument *mTilesetDocument; }; diff --git a/src/tiled/utils.cpp b/src/tiled/utils.cpp index be6ae66ad6..a33c256869 100644 --- a/src/tiled/utils.cpp +++ b/src/tiled/utils.cpp @@ -41,6 +41,8 @@ #include #include #include +#include +#include #include #include #include @@ -310,6 +312,9 @@ QIcon themeIcon(const QString &name) QIcon colorIcon(const QColor &color, QSize size) { + if (!color.isValid()) + return QIcon(); + QPixmap pixmap(size); pixmap.fill(color); @@ -607,5 +612,20 @@ QString Error::jsonParseError(QJsonParseError error) } +/** + * Recursively deletes all items and their widgets from the given layout. + */ +void deleteAllFromLayout(QLayout *layout) +{ + while (QLayoutItem *item = layout->takeAt(0)) { + delete item->widget(); + + if (QLayout *layout = item->layout()) + deleteAllFromLayout(layout); + + delete item; + } +} + } // namespace Utils } // namespace Tiled diff --git a/src/tiled/utils.h b/src/tiled/utils.h index 3e73500b3a..e7c9721015 100644 --- a/src/tiled/utils.h +++ b/src/tiled/utils.h @@ -33,6 +33,7 @@ class QAction; class QKeyEvent; +class QLayout; class QMenu; namespace Tiled { @@ -111,5 +112,7 @@ namespace Error { QString jsonParseError(QJsonParseError error); } // namespace Error +void deleteAllFromLayout(QLayout *layout); + } // namespace Utils } // namespace Tiled diff --git a/src/tiled/varianteditor.cpp b/src/tiled/varianteditor.cpp new file mode 100644 index 0000000000..a73a3831e1 --- /dev/null +++ b/src/tiled/varianteditor.cpp @@ -0,0 +1,834 @@ +/* + * varianteditor.cpp + * Copyright 2024, Thorbjørn Lindeijer + * + * This file is part of Tiled. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ + +#include "varianteditor.h" + +#include "colorbutton.h" +#include "fileedit.h" +#include "textpropertyedit.h" +#include "utils.h" +#include "propertyeditorwidgets.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Tiled { + +void Property::setToolTip(const QString &toolTip) +{ + if (m_toolTip != toolTip) { + m_toolTip = toolTip; + emit toolTipChanged(toolTip); + } +} + +void Property::setEnabled(bool enabled) +{ + if (m_enabled != enabled) { + m_enabled = enabled; + emit enabledChanged(enabled); + } +} + +void Property::setModified(bool modified) +{ + if (m_modified != modified) { + m_modified = modified; + emit modifiedChanged(modified); + } +} + +void StringProperty::setPlaceholderText(const QString &placeholderText) +{ + if (m_placeholderText != placeholderText) { + m_placeholderText = placeholderText; + emit placeholderTextChanged(placeholderText); + } +} + +QWidget *StringProperty::createEditor(QWidget *parent) +{ + auto editor = new QLineEdit(parent); + editor->setPlaceholderText(m_placeholderText); + + auto syncEditor = [=] { + editor->setText(value()); + }; + syncEditor(); + + connect(this, &Property::valueChanged, editor, syncEditor); + connect(this, &StringProperty::placeholderTextChanged, editor, &QLineEdit::setPlaceholderText); + connect(editor, &QLineEdit::textEdited, this, &StringProperty::setValue); + + return editor; +} + +QWidget *MultilineStringProperty::createEditor(QWidget *parent) +{ + auto editor = new TextPropertyEdit(parent); + editor->lineEdit()->setPlaceholderText(placeholderText()); + + auto syncEditor = [=] { + const QSignalBlocker blocker(editor); + editor->setText(value()); + }; + syncEditor(); + + connect(this, &StringProperty::valueChanged, editor, syncEditor); + connect(this, &StringProperty::placeholderTextChanged, + editor->lineEdit(), &QLineEdit::setPlaceholderText); + connect(editor, &TextPropertyEdit::textChanged, this, &StringProperty::setValue); + + return editor; +} + +QWidget *UrlProperty::createEditor(QWidget *parent) +{ + auto editor = new FileEdit(parent); + editor->setFilter(m_filter); + + auto syncEditor = [=] { + editor->setFileUrl(value()); + }; + syncEditor(); + + connect(this, &Property::valueChanged, editor, syncEditor); + connect(editor, &FileEdit::fileUrlChanged, this, &UrlProperty::setValue); + + return editor; +} + +QWidget *IntProperty::createEditor(QWidget *parent) +{ + auto widget = new QWidget(parent); + auto layout = new QHBoxLayout(widget); + layout->setContentsMargins(QMargins()); + + if (m_sliderEnabled) { + QSlider *slider = new QSlider(Qt::Horizontal, widget); + slider->setRange(m_minimum, m_maximum); + slider->setSingleStep(m_singleStep); + slider->setValue(value()); + + layout->addWidget(slider); + + connect(this, &Property::valueChanged, slider, [this, slider] { + const QSignalBlocker blocker(slider); + slider->setValue(value()); + }); + connect(slider, &QSlider::valueChanged, this, &IntProperty::setValue); + } + + auto spinBox = new SpinBox(parent); + spinBox->setRange(m_minimum, m_maximum); + spinBox->setSingleStep(m_singleStep); + spinBox->setSuffix(m_suffix); + spinBox->setValue(value()); + + layout->addWidget(spinBox); + + connect(this, &Property::valueChanged, spinBox, [this, spinBox] { + const QSignalBlocker blocker(spinBox); + spinBox->setValue(value()); + }); + connect(spinBox, qOverload(&SpinBox::valueChanged), + this, &IntProperty::setValue); + + return widget; +} + +QWidget *FloatProperty::createEditor(QWidget *parent) +{ + auto editor = new DoubleSpinBox(parent); + editor->setRange(m_minimum, m_maximum); + editor->setSingleStep(m_singleStep); + editor->setSuffix(m_suffix); + + auto syncEditor = [=] { + const QSignalBlocker blocker(editor); + editor->setValue(value()); + }; + syncEditor(); + + connect(this, &Property::valueChanged, editor, syncEditor); + connect(editor, qOverload(&DoubleSpinBox::valueChanged), + this, &FloatProperty::setValue); + + return editor; +} + +QWidget *BoolProperty::createEditor(QWidget *parent) +{ + auto editor = new QCheckBox(name(), parent); + auto syncEditor = [=] { + const QSignalBlocker blocker(editor); + bool checked = value(); + editor->setChecked(checked); + + // Reflect modified state on the checkbox, since we're not showing the + // property label. + auto font = editor->font(); + font.setBold(isModified()); + editor->setFont(font); + }; + syncEditor(); + + connect(this, &Property::valueChanged, editor, syncEditor); + connect(this, &Property::modifiedChanged, editor, syncEditor); + connect(editor, &QCheckBox::toggled, this, &BoolProperty::setValue); + + return editor; +} + +QWidget *PointProperty::createEditor(QWidget *parent) +{ + auto editor = new PointEdit(parent); + editor->setSuffix(m_suffix); + + auto syncEditor = [this, editor] { + const QSignalBlocker blocker(editor); + editor->setValue(value()); + }; + syncEditor(); + + connect(this, &Property::valueChanged, editor, syncEditor); + connect(editor, &PointEdit::valueChanged, this, [this, editor] { + setValue(editor->value()); + }); + + return editor; +} + +QWidget *PointFProperty::createEditor(QWidget *parent) +{ + auto editor = new PointFEdit(parent); + editor->setSingleStep(m_singleStep); + + auto syncEditor = [this, editor] { + const QSignalBlocker blocker(editor); + editor->setValue(value()); + }; + syncEditor(); + + connect(this, &Property::valueChanged, editor, syncEditor); + connect(editor, &PointFEdit::valueChanged, this, [this, editor] { + this->setValue(editor->value()); + }); + + return editor; +} + +QWidget *SizeProperty::createEditor(QWidget *parent) +{ + auto editor = new SizeEdit(parent); + editor->setMinimum(m_minimum); + editor->setSuffix(m_suffix); + + auto syncEditor = [this, editor] { + const QSignalBlocker blocker(editor); + editor->setValue(value()); + }; + syncEditor(); + + connect(this, &Property::valueChanged, editor, syncEditor); + connect(editor, &SizeEdit::valueChanged, this, [this, editor] { + setValue(editor->value()); + }); + + return editor; +} + +QWidget *SizeFProperty::createEditor(QWidget *parent) +{ + auto editor = new SizeFEdit(parent); + auto syncEditor = [this, editor] { + const QSignalBlocker blocker(editor); + editor->setValue(value()); + }; + syncEditor(); + + connect(this, &Property::valueChanged, editor, syncEditor); + connect(editor, &SizeFEdit::valueChanged, this, [this, editor] { + setValue(editor->value()); + }); + + return editor; +} + +QWidget *RectProperty::createEditor(QWidget *parent) +{ + auto editor = new RectEdit(parent); + editor->setConstraint(m_constraint); + + auto syncEditor = [this, editor] { + const QSignalBlocker blocker(editor); + editor->setValue(value()); + }; + syncEditor(); + + connect(this, &Property::valueChanged, editor, syncEditor); + connect(editor, &RectEdit::valueChanged, this, [this, editor] { + setValue(editor->value()); + }); + connect(this, &RectProperty::constraintChanged, + editor, &RectEdit::setConstraint); + + return editor; +} + +void RectProperty::setConstraint(const QRect &constraint) +{ + if (m_constraint != constraint) { + m_constraint = constraint; + emit constraintChanged(m_constraint); + } +} + +QWidget *RectFProperty::createEditor(QWidget *parent) +{ + auto editor = new RectFEdit(parent); + auto syncEditor = [this, editor] { + const QSignalBlocker blocker(editor); + editor->setValue(value()); + }; + syncEditor(); + + connect(this, &Property::valueChanged, editor, syncEditor); + connect(editor, &RectFEdit::valueChanged, this, [this, editor] { + setValue(editor->value()); + }); + + return editor; +} + +// todo: needs to handle invalid color (unset value) +QWidget *ColorProperty::createEditor(QWidget *parent) +{ + auto editor = new ColorButton(parent); + auto syncEditor = [=] { + const QSignalBlocker blocker(editor); + editor->setColor(value()); + }; + syncEditor(); + + connect(this, &Property::valueChanged, editor, syncEditor); + connect(editor, &ColorButton::colorChanged, this, [this, editor] { + setValue(editor->color()); + }); + + return editor; +} + +QWidget *FontProperty::createEditor(QWidget *parent) +{ + auto editor = new QWidget(parent); + auto fontComboBox = new QFontComboBox(editor); + + auto sizeSpinBox = new QSpinBox(editor); + sizeSpinBox->setRange(1, 999); + sizeSpinBox->setSuffix(tr(" px")); + sizeSpinBox->setKeyboardTracking(false); + + auto bold = new QToolButton(editor); + bold->setIcon(QIcon(QStringLiteral("://images/scalable/text-bold-symbolic.svg"))); + bold->setToolTip(tr("Bold")); + bold->setCheckable(true); + + auto italic = new QToolButton(editor); + italic->setIcon(QIcon(QStringLiteral("://images/scalable/text-italic-symbolic.svg"))); + italic->setToolTip(tr("Italic")); + italic->setCheckable(true); + + auto underline = new QToolButton(editor); + underline->setIcon(QIcon(QStringLiteral("://images/scalable/text-underline-symbolic.svg"))); + underline->setToolTip(tr("Underline")); + underline->setCheckable(true); + + auto strikeout = new QToolButton(editor); + strikeout->setIcon(QIcon(QStringLiteral("://images/scalable/text-strikethrough-symbolic.svg"))); + strikeout->setToolTip(tr("Strikethrough")); + strikeout->setCheckable(true); + + auto kerning = new QCheckBox(tr("Kerning"), editor); + + auto layout = new QVBoxLayout(editor); + layout->setContentsMargins(QMargins()); + layout->setSpacing(Utils::dpiScaled(4)); + layout->addWidget(fontComboBox); + layout->addWidget(sizeSpinBox); + + auto buttonsLayout = new QHBoxLayout; + buttonsLayout->setContentsMargins(QMargins()); + buttonsLayout->addWidget(bold); + buttonsLayout->addWidget(italic); + buttonsLayout->addWidget(underline); + buttonsLayout->addWidget(strikeout); + buttonsLayout->addStretch(); + + layout->addLayout(buttonsLayout); + layout->addWidget(kerning); + + auto syncEditor = [=] { + const auto font = value(); + const QSignalBlocker fontBlocker(fontComboBox); + const QSignalBlocker sizeBlocker(sizeSpinBox); + const QSignalBlocker boldBlocker(bold); + const QSignalBlocker italicBlocker(italic); + const QSignalBlocker underlineBlocker(underline); + const QSignalBlocker strikeoutBlocker(strikeout); + const QSignalBlocker kerningBlocker(kerning); + fontComboBox->setCurrentFont(font); + sizeSpinBox->setValue(font.pixelSize()); + bold->setChecked(font.bold()); + italic->setChecked(font.italic()); + underline->setChecked(font.underline()); + strikeout->setChecked(font.strikeOut()); + kerning->setChecked(font.kerning()); + }; + + auto syncProperty = [=] { + auto font = fontComboBox->currentFont(); + font.setPixelSize(sizeSpinBox->value()); + font.setBold(bold->isChecked()); + font.setItalic(italic->isChecked()); + font.setUnderline(underline->isChecked()); + font.setStrikeOut(strikeout->isChecked()); + font.setKerning(kerning->isChecked()); + setValue(font); + }; + + syncEditor(); + + connect(this, &Property::valueChanged, fontComboBox, syncEditor); + connect(fontComboBox, &QFontComboBox::currentFontChanged, this, syncProperty); + connect(sizeSpinBox, qOverload(&QSpinBox::valueChanged), this, syncProperty); + connect(bold, &QAbstractButton::toggled, this, syncProperty); + connect(italic, &QAbstractButton::toggled, this, syncProperty); + connect(underline, &QAbstractButton::toggled, this, syncProperty); + connect(strikeout, &QAbstractButton::toggled, this, syncProperty); + connect(kerning, &QAbstractButton::toggled, this, syncProperty); + + return editor; +} + +QWidget *QtAlignmentProperty::createEditor(QWidget *parent) +{ + auto editor = new QWidget(parent); + auto layout = new QGridLayout(editor); + layout->setContentsMargins(QMargins()); + layout->setSpacing(Utils::dpiScaled(4)); + + auto horizontalLabel = new ElidingLabel(tr("Horizontal"), editor); + layout->addWidget(horizontalLabel, 0, 0); + + auto verticalLabel = new ElidingLabel(tr("Vertical"), editor); + layout->addWidget(verticalLabel, 1, 0); + + auto horizontalComboBox = new QComboBox(editor); + horizontalComboBox->addItem(tr("Left"), Qt::AlignLeft); + horizontalComboBox->addItem(tr("Center"), Qt::AlignHCenter); + horizontalComboBox->addItem(tr("Right"), Qt::AlignRight); + horizontalComboBox->addItem(tr("Justify"), Qt::AlignJustify); + layout->addWidget(horizontalComboBox, 0, 1); + + auto verticalComboBox = new QComboBox(editor); + verticalComboBox->addItem(tr("Top"), Qt::AlignTop); + verticalComboBox->addItem(tr("Center"), Qt::AlignVCenter); + verticalComboBox->addItem(tr("Bottom"), Qt::AlignBottom); + layout->addWidget(verticalComboBox, 1, 1); + + layout->setColumnStretch(1, 1); + + auto syncEditor = [=] { + const QSignalBlocker horizontalBlocker(horizontalComboBox); + const QSignalBlocker verticalBlocker(verticalComboBox); + const auto alignment = value(); + horizontalComboBox->setCurrentIndex(horizontalComboBox->findData(static_cast(alignment & Qt::AlignHorizontal_Mask))); + verticalComboBox->setCurrentIndex(verticalComboBox->findData(static_cast(alignment & Qt::AlignVertical_Mask))); + }; + + auto syncProperty = [=] { + setValue(Qt::Alignment(horizontalComboBox->currentData().toInt() | + verticalComboBox->currentData().toInt())); + }; + + syncEditor(); + + connect(this, &Property::valueChanged, editor, syncEditor); + connect(horizontalComboBox, qOverload(&QComboBox::currentIndexChanged), this, syncProperty); + connect(verticalComboBox, qOverload(&QComboBox::currentIndexChanged), this, syncProperty); + + return editor; +} + + +VariantEditor::VariantEditor(QWidget *parent) + : QWidget(parent) +{ + m_layout = new QVBoxLayout(this); + + m_layout->setContentsMargins(QMargins()); + m_layout->setSpacing(0); +} + +/** + * Removes all properties from the editor. The properties are not deleted. + */ +void VariantEditor::clear() +{ + QHashIterator it(m_propertyWidgets); + while (it.hasNext()) { + it.next(); + auto &widgets = it.value(); + Utils::deleteAllFromLayout(widgets.layout); + delete widgets.layout; + delete widgets.children; + + it.key()->disconnect(this); + } + m_propertyWidgets.clear(); +} + +/** + * Adds the given property to the editor. Does not take ownership of the + * property. + */ +void VariantEditor::addProperty(Property *property) +{ + auto &widgets = m_propertyWidgets[property]; + const auto displayMode = property->displayMode(); + + connect(property, &Property::destroyed, this, [this](QObject *object) { + removeProperty(static_cast(object)); + }); + + const auto halfSpacing = Utils::dpiScaled(2); + + widgets.layout = new QHBoxLayout; + widgets.layout->setSpacing(halfSpacing * 2); + + if (displayMode == Property::DisplayMode::Separator) { + auto separator = new QFrame(this); + widgets.layout->setContentsMargins(0, halfSpacing, 0, halfSpacing); + separator->setFrameShape(QFrame::HLine); + separator->setFrameShadow(QFrame::Plain); + separator->setForegroundRole(QPalette::Mid); + widgets.layout->addWidget(separator); + m_layout->addLayout(widgets.layout); + return; + } + + auto label = new PropertyLabel(m_level, this); + + if (displayMode != Property::DisplayMode::NoLabel) { + label->setText(property->name()); + label->setToolTip(property->toolTip()); + label->setEnabled(property->isEnabled()); + label->setModified(property->isModified()); + connect(property, &Property::toolTipChanged, label, &QWidget::setToolTip); + connect(property, &Property::enabledChanged, label, &QWidget::setEnabled); + connect(property, &Property::modifiedChanged, label, &PropertyLabel::setModified); + } + + if (displayMode == Property::DisplayMode::Header) + label->setHeader(true); + else + widgets.layout->setContentsMargins(0, halfSpacing, halfSpacing * 2, halfSpacing); + + widgets.layout->addWidget(label, LabelStretch, Qt::AlignTop); + + QHBoxLayout *editorLayout = widgets.layout; + const auto editor = property->createEditor(this); + + if (editor && property->actions()) { + editorLayout = new QHBoxLayout; + widgets.layout->addLayout(editorLayout, WidgetStretch); + } + + if (editor) { + editor->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Fixed); + editor->setToolTip(property->toolTip()); + editor->setEnabled(property->isEnabled()); + connect(property, &Property::toolTipChanged, editor, &QWidget::setToolTip); + connect(property, &Property::enabledChanged, editor, &QWidget::setEnabled); + + editorLayout->addWidget(editor, WidgetStretch, Qt::AlignTop); + } + + if (property->actions()) { + if (property->actions() & Property::Reset) { + auto resetButton = new QToolButton; + resetButton->setToolTip(tr("Reset")); + resetButton->setAutoRaise(true); + resetButton->setEnabled(property->isModified()); + Utils::setThemeIcon(resetButton, "edit-clear"); + editorLayout->addWidget(resetButton, 0, Qt::AlignTop); + connect(resetButton, &QAbstractButton::clicked, property, &Property::resetRequested); + connect(property, &Property::modifiedChanged, resetButton, &QWidget::setEnabled); + } + + if (property->actions() & Property::Remove) { + auto removeButton = new QToolButton; + removeButton->setToolTip(tr("Remove")); + removeButton->setAutoRaise(true); + Utils::setThemeIcon(removeButton, "remove"); + editorLayout->addWidget(removeButton, 0, Qt::AlignTop); + connect(removeButton, &QAbstractButton::clicked, property, &Property::removeRequested); + } + } + + m_layout->addLayout(widgets.layout); + + if (auto groupProperty = dynamic_cast(property)) { + label->setExpandable(true); + label->setExpanded(label->isHeader()); + + auto children = new VariantEditor(this); + if (label->isHeader()) + children->setContentsMargins(0, halfSpacing, 0, halfSpacing); + children->setLevel(m_level + 1); + children->setVisible(label->isExpanded()); + for (auto property : groupProperty->subProperties()) + children->addProperty(property); + + connect(groupProperty, &GroupProperty::propertyAdded, + children, &VariantEditor::addProperty); + + connect(label, &PropertyLabel::toggled, children, [=](bool expanded) { + children->setVisible(expanded); + + // needed to avoid flickering when hiding the editor + QWidget *widget = this; + while (widget && widget->layout()) { + widget->layout()->activate(); + widget = widget->parentWidget(); + } + }); + + widgets.children = children; + m_layout->addWidget(widgets.children); + } +} + +/** + * Removes the given property from the editor. The property is not deleted. + */ +void VariantEditor::removeProperty(Property *property) +{ + auto it = m_propertyWidgets.constFind(property); + Q_ASSERT(it != m_propertyWidgets.constEnd()); + + if (it != m_propertyWidgets.end()) { + auto &widgets = it.value(); + Utils::deleteAllFromLayout(widgets.layout); + delete widgets.layout; + delete widgets.children; + + m_propertyWidgets.erase(it); + } + + property->disconnect(this); +} + +void VariantEditor::setLevel(int level) +{ + m_level = level; + + setBackgroundRole((m_level % 2) ? QPalette::AlternateBase + : QPalette::Base); + setAutoFillBackground(m_level > 1); +} + + +QWidget *BaseEnumProperty::createEnumEditor(QWidget *parent) +{ + auto editor = new QComboBox(parent); + // This allows the combo box to shrink horizontally. + editor->setSizeAdjustPolicy(QComboBox::AdjustToMinimumContentsLengthWithIcon); + + for (qsizetype i = 0; i < m_enumData.names.size(); ++i) { + auto value = m_enumData.values.value(i, i); + editor->addItem(m_enumData.icons[value], + m_enumData.names[i], + value); + } + + auto syncEditor = [this, editor] { + const QSignalBlocker blocker(editor); + editor->setCurrentIndex(editor->findData(value())); + }; + syncEditor(); + + QObject::connect(this, &Property::valueChanged, editor, syncEditor); + QObject::connect(editor, qOverload(&QComboBox::currentIndexChanged), this, + [editor, this] { + setValue(editor->currentData().toInt()); + }); + + return editor; +} + +QWidget *BaseEnumProperty::createFlagsEditor(QWidget *parent) +{ + auto editor = new QWidget(parent); + auto layout = new QVBoxLayout(editor); + layout->setContentsMargins(0, 0, 0, 0); + layout->setSpacing(0); + + for (qsizetype i = 0; i < m_enumData.names.size(); ++i) { + auto checkBox = new QCheckBox(m_enumData.names[i], editor); + layout->addWidget(checkBox); + + QObject::connect(checkBox, &QCheckBox::toggled, this, [=](bool checked) { + const auto enumItemValue = m_enumData.values.value(i, 1 << i); + int flags = value(); + if (checked) + flags |= enumItemValue; + else + flags &= ~enumItemValue; + setValue(flags); + }); + } + + auto syncEditor = [=] { + for (int i = 0; i < layout->count(); ++i) { + auto checkBox = qobject_cast(layout->itemAt(i)->widget()); + if (checkBox) { + const auto enumItemValue = m_enumData.values.value(i, 1 << i); + + QSignalBlocker blocker(checkBox); + checkBox->setChecked((value() & enumItemValue) == enumItemValue); + } + } + }; + + syncEditor(); + + QObject::connect(this, &Property::valueChanged, editor, syncEditor); + + return editor; +} + +/** + * Creates a property that wraps a QObject property. + */ +Property *createQObjectProperty(QObject *qObject, + const char *propertyName, + const QString &displayName) +{ + auto metaObject = qObject->metaObject(); + auto propertyIndex = metaObject->indexOfProperty(propertyName); + if (propertyIndex < 0) + return nullptr; + + auto metaProperty = metaObject->property(propertyIndex); + auto property = createVariantProperty( + displayName.isEmpty() ? QString::fromUtf8(propertyName) + : displayName, + [=] { + return metaProperty.read(qObject); + }, + [=] (const QVariant &value) { + metaProperty.write(qObject, value); + }); + + // If the property has a notify signal, forward it to valueChanged + auto notify = metaProperty.notifySignal(); + if (notify.isValid()) { + auto propertyMetaObject = property->metaObject(); + auto valuePropertyIndex = propertyMetaObject->indexOfProperty("value"); + auto valueProperty = propertyMetaObject->property(valuePropertyIndex); + auto valueChanged = valueProperty.notifySignal(); + + QObject::connect(qObject, notify, property, valueChanged); + } + + property->setEnabled(metaProperty.isWritable()); + + return property; +} + +template +Property *createTypedProperty(const QString &name, + std::function get, + std::function set) +{ + return new PropertyClass(name, + [get = std::move(get)] { return get().value(); }, + [set = std::move(set)] (const typename PropertyClass::ValueType &v) { set(QVariant::fromValue(v)); }); +} + +/** + * Creates a property with the given name and get/set functions. The + * value type determines the kind of property that will be created. + */ +Property *createVariantProperty(const QString &name, + std::function get, + std::function set) +{ + const auto type = get().userType(); + switch (type) { + case QMetaType::QString: + return createTypedProperty(name, get, set); + case QMetaType::QUrl: + return createTypedProperty(name, get, set); + case QMetaType::Int: + return createTypedProperty(name, get, set); + case QMetaType::Double: + return createTypedProperty(name, get, set); + case QMetaType::Bool: + return createTypedProperty(name, get, set); + case QMetaType::QColor: + return createTypedProperty(name, get, set); + case QMetaType::QFont: + return createTypedProperty(name, get, set); + case QMetaType::QPoint: + return createTypedProperty(name, get, set); + case QMetaType::QPointF: + return createTypedProperty(name, get, set); + case QMetaType::QRect: + return createTypedProperty(name, get, set); + case QMetaType::QRectF: + return createTypedProperty(name, get, set); + case QMetaType::QSize: + return createTypedProperty(name, get, set); + case QMetaType::QSizeF: + return createTypedProperty(name, get, set); + default: + if (type == qMetaTypeId()) + return createTypedProperty(name, get, set); + } + + return nullptr; +} + +} // namespace Tiled + +#include "moc_varianteditor.cpp" diff --git a/src/tiled/varianteditor.h b/src/tiled/varianteditor.h new file mode 100644 index 0000000000..c4dc66a928 --- /dev/null +++ b/src/tiled/varianteditor.h @@ -0,0 +1,479 @@ +/* + * varianteditor.h + * Copyright 2024, Thorbjørn Lindeijer + * + * This file is part of Tiled. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +class QHBoxLayout; +class QVBoxLayout; + +namespace Tiled { + +class PropertyLabel; + +/** + * A property represents a named value that can create its own edit widget. + */ +class Property : public QObject +{ + Q_OBJECT + Q_PROPERTY(QString name READ name CONSTANT) + Q_PROPERTY(QString toolTip READ toolTip WRITE setToolTip NOTIFY toolTipChanged) + Q_PROPERTY(bool enabled READ isEnabled WRITE setEnabled NOTIFY enabledChanged) + Q_PROPERTY(bool modified READ isModified WRITE setModified NOTIFY modifiedChanged) + +public: + enum class DisplayMode { + Default, + NoLabel, + Header, + Separator + }; + + enum Action { + Reset = 0x01, + Remove = 0x02, + }; + Q_DECLARE_FLAGS(Actions, Action) + + Property(const QString &name, QObject *parent = nullptr) + : QObject(parent) + , m_name(name) + {} + + const QString &name() const { return m_name; } + + const QString &toolTip() const { return m_toolTip; } + void setToolTip(const QString &toolTip); + + bool isEnabled() const { return m_enabled; } + void setEnabled(bool enabled); + + bool isModified() const { return m_modified; } + void setModified(bool modified); + + Actions actions() const { return m_actions; } + void setActions(Actions actions) { m_actions = actions; } + + virtual DisplayMode displayMode() const { return DisplayMode::Default; } + + virtual QWidget *createEditor(QWidget *parent) = 0; + +signals: + void toolTipChanged(const QString &toolTip); + void valueChanged(); + void enabledChanged(bool enabled); + void modifiedChanged(bool modified); + + void resetRequested(); + void removeRequested(); + +private: + friend class GroupProperty; + + QString m_name; + QString m_toolTip; + bool m_enabled = true; + bool m_modified = false; + Actions m_actions; +}; + +class Separator final : public Property +{ + Q_OBJECT + +public: + Separator(QObject *parent = nullptr) + : Property(QString(), parent) + {} + + DisplayMode displayMode() const override { return DisplayMode::Separator; } + QWidget *createEditor(QWidget */*parent*/) override { return nullptr; } +}; + +/** + * A property that can have sub-properties. The GroupProperty owns the sub- + * properties and will delete them when it is deleted. + */ +class GroupProperty : public Property +{ + Q_OBJECT + +public: + GroupProperty(const QString &name, QObject *parent = nullptr) + : Property(name, parent) + {} + + ~GroupProperty() override { clear(); } + + DisplayMode displayMode() const override + { return m_header ? DisplayMode::Header : DisplayMode::Default; } + + QWidget *createEditor(QWidget */* parent */) override { return nullptr; } + + void setHeader(bool header) { m_header = header; } + + void clear() + { + qDeleteAll(m_subProperties); + m_subProperties.clear(); + } + + void addProperty(Property *property) + { + m_subProperties.append(property); + emit propertyAdded(property); + } + + void deleteProperty(Property *property) + { + m_subProperties.removeOne(property); + delete property; + } + + void addSeparator() { addProperty(new Separator(this)); } + + const QList &subProperties() const { return m_subProperties; } + +signals: + void propertyAdded(Property *property); + +private: + bool m_header = true; + QList m_subProperties; +}; + +/** + * A helper class for creating a property that wraps a value of a given type. + */ +template +class PropertyTemplate : public Property +{ +public: + using ValueType = Type; + + PropertyTemplate(const QString &name, + std::function get, + std::function set = {}, + QObject *parent = nullptr) + : Property(name, parent) + , m_get(std::move(get)) + , m_set(std::move(set)) + {} + + Type value() const { return m_get(); } + void setValue(const Type &value) { if (m_set) m_set(value); } + +private: + std::function m_get; + std::function m_set; +}; + +struct StringProperty : PropertyTemplate +{ + Q_OBJECT + +public: + using PropertyTemplate::PropertyTemplate; + QWidget *createEditor(QWidget *parent) override; + + void setPlaceholderText(const QString &placeholderText); + const QString &placeholderText() const { return m_placeholderText; } + +signals: + void placeholderTextChanged(const QString &placeholderText); + +private: + QString m_placeholderText; +}; + +struct MultilineStringProperty : StringProperty +{ + using StringProperty::StringProperty; + QWidget *createEditor(QWidget *parent) override; +}; + +struct UrlProperty : PropertyTemplate +{ + using PropertyTemplate::PropertyTemplate; + QWidget *createEditor(QWidget *parent) override; + void setFilter(const QString &filter) { m_filter = filter; } +private: + QString m_filter; +}; + +struct IntProperty : PropertyTemplate +{ + using PropertyTemplate::PropertyTemplate; + QWidget *createEditor(QWidget *parent) override; + + void setMinimum(int minimum) { m_minimum = minimum; } + void setMaximum(int maximum) { m_maximum = maximum; } + void setSingleStep(int singleStep) { m_singleStep = singleStep; } + void setSuffix(const QString &suffix) { m_suffix = suffix; } + void setRange(int minimum, int maximum) + { + setMinimum(minimum); + setMaximum(maximum); + } + void setSliderEnabled(bool enabled) { m_sliderEnabled = enabled; } + +protected: + int m_minimum = std::numeric_limits::min(); + int m_maximum = std::numeric_limits::max(); + int m_singleStep = 1; + QString m_suffix; + bool m_sliderEnabled = false; +}; + +struct FloatProperty : PropertyTemplate +{ + using PropertyTemplate::PropertyTemplate; + QWidget *createEditor(QWidget *parent) override; + + void setMinimum(double minimum) { m_minimum = minimum; } + void setMaximum(double maximum) { m_maximum = maximum; } + void setSingleStep(double singleStep) { m_singleStep = singleStep; } + void setSuffix(const QString &suffix) { m_suffix = suffix; } + void setRange(double minimum, double maximum) + { + setMinimum(minimum); + setMaximum(maximum); + } + +private: + double m_minimum = -std::numeric_limits::max(); + double m_maximum = std::numeric_limits::max(); + double m_singleStep = 1.0; + QString m_suffix; +}; + +struct BoolProperty : PropertyTemplate +{ + using PropertyTemplate::PropertyTemplate; + DisplayMode displayMode() const override { return DisplayMode::NoLabel; } + QWidget *createEditor(QWidget *parent) override; +}; + +struct PointProperty : PropertyTemplate +{ + using PropertyTemplate::PropertyTemplate; + QWidget *createEditor(QWidget *parent) override; + + void setSuffix(const QString &suffix) { m_suffix = suffix; } + +private: + QString m_suffix; +}; + +struct PointFProperty : PropertyTemplate +{ + using PropertyTemplate::PropertyTemplate; + QWidget *createEditor(QWidget *parent) override; + + void setSingleStep(double singleStep) { m_singleStep = singleStep; } + +private: + double m_singleStep = 1.0; +}; + +struct SizeProperty : PropertyTemplate +{ + using PropertyTemplate::PropertyTemplate; + QWidget *createEditor(QWidget *parent) override; + + void setMinimum(int minimum) { m_minimum = minimum; } + void setSuffix(const QString &suffix) { m_suffix = suffix; } + +private: + int m_minimum; + QString m_suffix; +}; + +struct SizeFProperty : PropertyTemplate +{ + using PropertyTemplate::PropertyTemplate; + QWidget *createEditor(QWidget *parent) override; +}; + +struct RectProperty : PropertyTemplate +{ + Q_OBJECT + +public: + using PropertyTemplate::PropertyTemplate; + QWidget *createEditor(QWidget *parent) override; + + void setConstraint(const QRect &constraint); + +signals: + void constraintChanged(const QRect &constraint); + +private: + QRect m_constraint; +}; + +struct RectFProperty : PropertyTemplate +{ + using PropertyTemplate::PropertyTemplate; + QWidget *createEditor(QWidget *parent) override; +}; + +// todo: needs to handle invalid color (unset value) +struct ColorProperty : PropertyTemplate +{ + using PropertyTemplate::PropertyTemplate; + QWidget *createEditor(QWidget *parent) override; +}; + +struct FontProperty : PropertyTemplate +{ + using PropertyTemplate::PropertyTemplate; + QWidget *createEditor(QWidget *parent) override; +}; + +struct QtAlignmentProperty : PropertyTemplate +{ + using PropertyTemplate::PropertyTemplate; + QWidget *createEditor(QWidget *parent) override; +}; + + +Property *createQObjectProperty(QObject *qObject, + const char *propertyName, + const QString &displayName = {}); + +Property *createVariantProperty(const QString &name, + std::function get, + std::function set); + +struct EnumData +{ + EnumData(const QStringList &names = {}, + const QList &values = {}, + const QMap &icons = {}) + : names(names) + , values(values) + , icons(icons) + {} + + QStringList names; + QList values; // optional + QMap icons; // optional +}; + +template +EnumData enumData() +{ + return {{}}; +} + +/** + * A property that wraps an integer value and creates either a combo box or a + * list of checkboxes based on the given EnumData. + */ +class BaseEnumProperty : public IntProperty +{ + Q_OBJECT + +public: + using IntProperty::IntProperty; + + void setEnumData(const EnumData &enumData) { m_enumData = enumData; } + void setFlags(bool flags) { m_flags = flags; } + + QWidget *createEditor(QWidget *parent) override + { + return m_flags ? createFlagsEditor(parent) + : createEnumEditor(parent); + } + +protected: + QWidget *createFlagsEditor(QWidget *parent); + QWidget *createEnumEditor(QWidget *parent); + + EnumData m_enumData; + bool m_flags = false; +}; + +/** + * A property that wraps an enum value and automatically sets the EnumData + * based on the given type. + */ +template +class EnumProperty : public BaseEnumProperty +{ +public: + EnumProperty(const QString &name, + std::function get, + std::function set, + QObject *parent = nullptr) + : BaseEnumProperty(name, + [get] { + return static_cast(get()); + }, + set ? [set](const int &value){ set(static_cast(value)); } + : std::function(), + parent) + { + setEnumData(enumData()); + } +}; + + +class VariantEditor : public QWidget +{ + Q_OBJECT + +public: + VariantEditor(QWidget *parent = nullptr); + + void clear(); + void addProperty(Property *property); + void removeProperty(Property *property); + + void setLevel(int level); + +private: + static constexpr int LabelStretch = 4; + static constexpr int WidgetStretch = 6; + + struct PropertyWidgets + { + QHBoxLayout *layout = nullptr; + QWidget *children = nullptr; + }; + + QVBoxLayout *m_layout; + QHash m_propertyWidgets; + int m_level = 0; +}; + +} // namespace Tiled + +Q_DECLARE_OPERATORS_FOR_FLAGS(Tiled::Property::Actions) diff --git a/src/tiled/wangcolormodel.cpp b/src/tiled/wangcolormodel.cpp index 28c177df27..65665cc088 100644 --- a/src/tiled/wangcolormodel.cpp +++ b/src/tiled/wangcolormodel.cpp @@ -20,7 +20,9 @@ #include "wangcolormodel.h" +#include "changeevents.h" #include "changewangcolordata.h" +#include "mapdocument.h" #include "tileset.h" #include "tilesetdocument.h" #include "wangset.h" @@ -130,24 +132,28 @@ void WangColorModel::setName(WangColor *wangColor, const QString &name) { wangColor->setName(name); emitDataChanged(wangColor); + emitToTilesetAndMaps(WangColorChangeEvent(wangColor, WangColorChangeEvent::NameProperty)); } void WangColorModel::setImage(WangColor *wangColor, int imageId) { wangColor->setImageId(imageId); emitDataChanged(wangColor); + emitToTilesetAndMaps(WangColorChangeEvent(wangColor, WangColorChangeEvent::ImageProperty)); } void WangColorModel::setColor(WangColor *wangColor, const QColor &color) { wangColor->setColor(color); emitDataChanged(wangColor); + emitToTilesetAndMaps(WangColorChangeEvent(wangColor, WangColorChangeEvent::ColorProperty)); } void WangColorModel::setProbability(WangColor *wangColor, qreal probability) { wangColor->setProbability(probability); // no data changed signal because probability not exposed by model + emitToTilesetAndMaps(WangColorChangeEvent(wangColor, WangColorChangeEvent::ProbabilityProperty)); } void WangColorModel::emitDataChanged(WangColor *wangColor) @@ -156,4 +162,15 @@ void WangColorModel::emitDataChanged(WangColor *wangColor) emit dataChanged(i, i); } +void WangColorModel::emitToTilesetAndMaps(const ChangeEvent &event) +{ + emit mTilesetDocument->changed(event); + + // todo: this doesn't work reliably because it only reaches maps that use + // the tileset, whereas the Properties view can be showing stuff from any + // tileset. + for (MapDocument *mapDocument : mTilesetDocument->mapDocuments()) + emit mapDocument->changed(event); +} + #include "moc_wangcolormodel.cpp" diff --git a/src/tiled/wangcolormodel.h b/src/tiled/wangcolormodel.h index f07de6a1bb..050f3c5fed 100644 --- a/src/tiled/wangcolormodel.h +++ b/src/tiled/wangcolormodel.h @@ -28,6 +28,7 @@ namespace Tiled { class Tileset; +class ChangeEvent; class TilesetDocument; class WangColorModel : public QAbstractListModel @@ -71,6 +72,7 @@ class WangColorModel : public QAbstractListModel private: void emitDataChanged(WangColor *wangColor); + void emitToTilesetAndMaps(const ChangeEvent &event); TilesetDocument *mTilesetDocument; WangSet *mWangSet; diff --git a/src/tiled/wangdock.cpp b/src/tiled/wangdock.cpp index 5ab91b6533..25db17baf5 100644 --- a/src/tiled/wangdock.cpp +++ b/src/tiled/wangdock.cpp @@ -484,7 +484,7 @@ void WangDock::documentChanged(const ChangeEvent &change) } break; case ChangeEvent::WangSetChanged: - if (static_cast(change).properties & WangSetChangeEvent::TypeProperty) + if (static_cast(change).property == WangSetChangeEvent::TypeProperty) mWangTemplateModel->wangSetChanged(); break; default: