From 58db1d02b1b8ed9919ce17c8732562b366f3d297 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thorbj=C3=B8rn=20Lindeijer?= Date: Thu, 21 Nov 2024 17:27:28 +0100 Subject: [PATCH] WIP: Made custom properties selectable Implemented: - Top-level properties can be selected by clicking. - Holding Ctrl allows selecting multiple. - Remove and Rename actions on tool bar update enabled state (most of the time) and are implemented. - Copy/paste shortcuts work. ToDo: - Select range of properties when holding Shift. - Allow changing the selected property with Up/Down keys, adding to selection when holding Shift. - Resolve interference between selecting and expanding properties. - Resolve issue with selectedPropertiesChanged now getting emitted when a selected property is removed. - Add Copy/Paste actions to context menu. --- src/tiled/propertieswidget.cpp | 56 ++++++++------- src/tiled/propertieswidget.h | 1 + src/tiled/propertyeditorwidgets.cpp | 101 ++++++++++++++++++++++++---- src/tiled/propertyeditorwidgets.h | 33 ++++++++- src/tiled/varianteditor.cpp | 55 +++++++++++++-- src/tiled/varianteditor.h | 11 ++- src/tiled/variantmapproperty.cpp | 30 ++++++++- src/tiled/variantmapproperty.h | 5 ++ 8 files changed, 241 insertions(+), 51 deletions(-) diff --git a/src/tiled/propertieswidget.cpp b/src/tiled/propertieswidget.cpp index 8ea9597a15..48164ffc50 100644 --- a/src/tiled/propertieswidget.cpp +++ b/src/tiled/propertieswidget.cpp @@ -2181,8 +2181,8 @@ PropertiesWidget::PropertiesWidget(QWidget *parent) mActionRenameProperty->setEnabled(false); mActionRenameProperty->setIcon(QIcon(QLatin1String(":/images/16/rename.png"))); mActionRenameProperty->setPriority(QAction::LowPriority); - // connect(mActionRenameProperty, &QAction::triggered, - // this, &PropertiesWidget::renameProperty); + connect(mActionRenameProperty, &QAction::triggered, + this, &PropertiesWidget::renameSelectedProperty); Utils::setThemeIcon(mActionAddProperty, "add"); Utils::setThemeIcon(mActionRemoveProperty, "remove"); @@ -2207,8 +2207,8 @@ PropertiesWidget::PropertiesWidget(QWidget *parent) mPropertyBrowser->setContextMenuPolicy(Qt::CustomContextMenu); connect(mPropertyBrowser, &QWidget::customContextMenuRequested, this, &PropertiesWidget::showContextMenu); - // connect(mPropertyBrowser, &PropertyBrowser::selectedItemsChanged, - // this, &PropertiesWidget::updateActions); + connect(mCustomProperties, &VariantMapProperty::selectedPropertiesChanged, + this, &PropertiesWidget::updateActions); connect(mCustomProperties, &VariantMapProperty::renameRequested, this, &PropertiesWidget::renameProperty); @@ -2430,18 +2430,16 @@ void CustomProperties::setPropertyValue(const QStringList &path, const QVariant void PropertiesWidget::updateActions() { -#if 0 - const QList items = mPropertyBrowser->selectedItems(); - bool allCustomProperties = !items.isEmpty() && mPropertyBrowser->allCustomPropertyItems(items); + const auto properties = mCustomProperties->selectedProperties(); bool editingTileset = mDocument && mDocument->type() == Document::TilesetDocumentType; - bool isTileset = mPropertyBrowser->object() && mPropertyBrowser->object()->isPartOfTileset(); - bool canModify = allCustomProperties && (!isTileset || editingTileset); + bool isTileset = mDocument && mDocument->currentObject() && mDocument->currentObject()->isPartOfTileset(); + bool canModify = (!isTileset || editingTileset); // Disable remove and rename actions when none of the selected objects // actually have the selected property (it may be inherited). if (canModify) { - for (QtBrowserItem *item : items) { - if (!anyObjectHasProperty(mDocument->currentObjects(), item->property()->propertyName())) { + for (auto property : properties) { + if (!anyObjectHasProperty(mDocument->currentObjects(), property->name())) { canModify = false; break; } @@ -2449,8 +2447,7 @@ void PropertiesWidget::updateActions() } mActionRemoveProperty->setEnabled(canModify); - mActionRenameProperty->setEnabled(canModify && items.size() == 1); -#endif + mActionRenameProperty->setEnabled(canModify && properties.size() == 1); } void PropertiesWidget::cutProperties() @@ -2461,19 +2458,15 @@ void PropertiesWidget::cutProperties() bool PropertiesWidget::copyProperties() { -#if 0 - Object *object = mPropertyBrowser->object(); + Object *object = mDocument ? mDocument->currentObject() : nullptr; if (!object) return false; Properties properties; - const QList items = mPropertyBrowser->selectedItems(); - for (QtBrowserItem *item : items) { - if (!mPropertyBrowser->isCustomPropertyItem(item)) - return false; - - const QString name = item->property()->propertyName(); + const auto selectedProperties = mCustomProperties->selectedProperties(); + for (auto property : selectedProperties) { + const QString name = property->name(); const QVariant value = object->property(name); if (!value.isValid()) return false; @@ -2482,7 +2475,7 @@ bool PropertiesWidget::copyProperties() } ClipboardManager::instance()->setProperties(properties); -#endif + return true; } @@ -2564,18 +2557,15 @@ void PropertiesWidget::addProperty(const QString &name, const QVariant &value) void PropertiesWidget::removeProperties() { -#if 0 Object *object = mDocument->currentObject(); if (!object) return; - const QList items = mPropertyBrowser->selectedItems(); - if (items.isEmpty() || !mPropertyBrowser->allCustomPropertyItems(items)) - return; + const auto properties = mCustomProperties->selectedProperties(); QStringList propertyNames; - for (QtBrowserItem *item : items) - propertyNames.append(item->property()->propertyName()); + for (auto property : properties) + propertyNames.append(property->name()); QUndoStack *undoStack = mDocument->undoStack(); undoStack->beginMacro(QCoreApplication::translate("Tiled::PropertiesDock", @@ -2590,7 +2580,15 @@ void PropertiesWidget::removeProperties() } undoStack->endMacro(); -#endif +} + +void PropertiesWidget::renameSelectedProperty() +{ + const auto properties = mCustomProperties->selectedProperties(); + if (properties.size() != 1) + return; + + renameProperty(properties.first()->name()); } void PropertiesWidget::renameProperty(const QString &name) diff --git a/src/tiled/propertieswidget.h b/src/tiled/propertieswidget.h index 050fa8cf95..8a752a1871 100644 --- a/src/tiled/propertieswidget.h +++ b/src/tiled/propertieswidget.h @@ -78,6 +78,7 @@ public slots: void showAddValueProperty(); void addProperty(const QString &name, const QVariant &value); void removeProperties(); + void renameSelectedProperty(); void renameProperty(const QString &name); void showContextMenu(const QPoint &pos); diff --git a/src/tiled/propertyeditorwidgets.cpp b/src/tiled/propertyeditorwidgets.cpp index ff95d55757..06e6b07af3 100644 --- a/src/tiled/propertyeditorwidgets.cpp +++ b/src/tiled/propertyeditorwidgets.cpp @@ -659,6 +659,15 @@ void ElidingLabel::setToolTip(const QString &toolTip) QLabel::setToolTip(m_toolTip); } +void ElidingLabel::setSelected(bool selected) +{ + if (m_selected == selected) + return; + + m_selected = selected; + update(); +} + QSize ElidingLabel::minimumSizeHint() const { auto hint = QLabel::minimumSizeHint(); @@ -688,7 +697,8 @@ void ElidingLabel::paintEvent(QPaintEvent *) } QStylePainter p(this); - p.drawItemText(cr, flags, opt.palette, isEnabled(), elidedText, foregroundRole()); + QPalette::ColorRole role = m_selected ? QPalette::HighlightedText : foregroundRole(); + p.drawItemText(cr, flags, opt.palette, isEnabled(), elidedText, role); } @@ -748,23 +758,27 @@ void PropertyLabel::setModified(bool modified) bool PropertyLabel::event(QEvent *event) { + switch (event->type()) { // Handled here instead of in mousePressEvent because we want it to be // expandable also when the label is disabled. - if (event->type() == QEvent::MouseButtonPress && m_expandable) { - auto mouseEvent = static_cast(event); - if (mouseEvent->button() == Qt::LeftButton) { - setExpanded(!m_expanded); - return true; + case QEvent::MouseButtonPress: + case QEvent::MouseButtonDblClick: + if (m_expandable) { + auto mouseEvent = static_cast(event); + if (mouseEvent->button() == Qt::LeftButton) { + setExpanded(!m_expanded); + return true; + } } - } - - if (event->type() == QEvent::ContextMenu) { - emit contextMenuRequested(static_cast(event)->globalPos()); - return true; - } + break; - if (event->type() == QEvent::LayoutDirectionChange) + case QEvent::LayoutDirectionChange: updateContentMargins(); + break; + + default: + break; + } return ElidingLabel::event(event); } @@ -785,6 +799,8 @@ void PropertyLabel::paintEvent(QPaintEvent *event) else branchOption.rect = QRect(width() - indent - branchIndicatorWidth - spacing, 0, branchIndicatorWidth + spacing, height()); + if (isSelected()) + branchOption.state |= QStyle::State_Selected; if (m_expandable) branchOption.state |= QStyle::State_Children; if (m_expanded) @@ -836,6 +852,65 @@ QSize PropertyLabel::sizeHint() const return style()->sizeFromContents(QStyle::CT_LineEdit, &opt, QSize(w, h), this); } + +PropertyWidget::PropertyWidget(QWidget *parent) + : QWidget(parent) +{ + setContextMenuPolicy(Qt::CustomContextMenu); +} + +void PropertyWidget::setSelectable(bool selectable) +{ + if (m_selectable == selectable) + return; + + m_selectable = selectable; + + if (!selectable) + setSelected(false); +} + +void PropertyWidget::setSelected(bool selected) +{ + if (m_selected == selected) + return; + + m_selected = selected; + update(); +} + +void PropertyWidget::paintEvent(QPaintEvent *event) +{ + QWidget::paintEvent(event); + + const auto halfSpacing = Utils::dpiScaled(2); + const QRect r = rect().adjusted(halfSpacing, 0, -halfSpacing, 0); + QStylePainter painter(this); + + if (isSelected()) + painter.fillRect(r, palette().highlight()); + + if (hasFocus()) { + QStyleOptionFocusRect option; + option.initFrom(this); + option.rect = r; + option.backgroundColor = palette().color(backgroundRole()); + option.state |= QStyle::State_KeyboardFocusChange; + + painter.drawPrimitive(QStyle::PE_FrameFocusRect, option); + } +} + +void PropertyWidget::mousePressEvent(QMouseEvent *event) +{ + if (event->button() == Qt::LeftButton && m_selectable) + emit clicked(event->modifiers()); + + setFocus(Qt::MouseFocusReason); + + QWidget::mousePressEvent(event); +} + } // namespace Tiled #include "moc_propertyeditorwidgets.cpp" diff --git a/src/tiled/propertyeditorwidgets.h b/src/tiled/propertyeditorwidgets.h index 63721a98b6..93b6559161 100644 --- a/src/tiled/propertyeditorwidgets.h +++ b/src/tiled/propertyeditorwidgets.h @@ -291,6 +291,9 @@ class ElidingLabel : public QLabel void setToolTip(const QString &toolTip); + void setSelected(bool selected); + bool isSelected() const { return m_selected; } + QSize minimumSizeHint() const override; protected: @@ -299,6 +302,7 @@ class ElidingLabel : public QLabel private: QString m_toolTip; bool m_isElided = false; + bool m_selected = false; }; /** @@ -328,7 +332,6 @@ class PropertyLabel : public ElidingLabel signals: void toggled(bool expanded); - void contextMenuRequested(const QPoint &globalPos); protected: bool event(QEvent *event) override; @@ -343,4 +346,32 @@ class PropertyLabel : public ElidingLabel bool m_expanded = false; }; +/** + * A widget that represents a single property. + */ +class PropertyWidget : public QWidget +{ + Q_OBJECT + +public: + PropertyWidget(QWidget *parent = nullptr); + + void setSelectable(bool selectable); + bool isSelectable() const { return m_selectable; } + + void setSelected(bool selected); + bool isSelected() const { return m_selected; } + +signals: + void clicked(Qt::KeyboardModifiers modifiers); + +protected: + void paintEvent(QPaintEvent *event) override; + void mousePressEvent(QMouseEvent *event) override; + +private: + bool m_selectable = false; + bool m_selected = false; +}; + } // namespace Tiled diff --git a/src/tiled/varianteditor.cpp b/src/tiled/varianteditor.cpp index 50c957eb25..07ec187c98 100644 --- a/src/tiled/varianteditor.cpp +++ b/src/tiled/varianteditor.cpp @@ -74,6 +74,14 @@ void Property::setModified(bool modified) } } +void Property::setSelected(bool selected) +{ + if (m_selected != selected) { + m_selected = selected; + emit selectedChanged(selected); + } +} + void Property::setActions(Actions actions) { if (m_actions != actions) { @@ -87,16 +95,15 @@ QWidget *Property::createLabel(int level, QWidget *parent) auto label = new PropertyLabel(parent); label->setLevel(level); - connect(label, &PropertyLabel::contextMenuRequested, - this, &Property::contextMenuRequested); - if (displayMode() != Property::DisplayMode::NoLabel) { label->setText(name()); - label->setModified(isModified()); label->setToolTip(toolTip()); + label->setModified(isModified()); + label->setSelected(isSelected()); connect(this, &Property::nameChanged, label, &PropertyLabel::setText); connect(this, &Property::toolTipChanged, label, &PropertyLabel::setToolTip); connect(this, &Property::modifiedChanged, label, &PropertyLabel::setModified); + connect(this, &Property::selectedChanged, label, &PropertyLabel::setSelected); } if (displayMode() == Property::DisplayMode::Header) @@ -780,11 +787,34 @@ QWidget *VariantEditor::createPropertyWidget(Property *property) const auto halfSpacing = Utils::dpiScaled(2); - auto rowWidget = new QWidget(this); + auto rowWidget = new PropertyWidget(this); auto rowLayout = new QHBoxLayout(rowWidget); rowLayout->setSpacing(halfSpacing * 2); rowLayout->setContentsMargins(0, 0, 0, 0); + rowWidget->setSelectable(property->actions().testFlag(Property::Action::Select)); + rowWidget->setSelected(property->isSelected()); + + connect(property, &Property::selectedChanged, rowWidget, &PropertyWidget::setSelected); + + connect(rowWidget, &QWidget::customContextMenuRequested, + property, [=] (const QPoint &pos) { + emit property->contextMenuRequested(rowWidget->mapToGlobal(pos)); + }); + + connect(rowWidget, &PropertyWidget::clicked, this, [=](Qt::KeyboardModifiers modifiers) { + if (modifiers & Qt::ShiftModifier) { + // TODO: Select range + } else if (modifiers & Qt::ControlModifier) { + // Toggle selection + property->setSelected(!property->isSelected()); + } else { + // Select only the clicked property + for (auto it = m_propertyWidgets.begin(); it != m_propertyWidgets.end(); ++it) + it.key()->setSelected(it.key() == property); + } + }); + widgets.rowWidget = rowWidget; if (displayMode == Property::DisplayMode::Separator) { @@ -927,6 +957,9 @@ void VariantEditor::updatePropertyEnabled(const PropertyWidgets &widgets, bool e void VariantEditor::updatePropertyActions(const PropertyWidgets &widgets, Property::Actions actions) { + if (auto rowWidget = qobject_cast(widgets.rowWidget)) + rowWidget->setSelectable(actions.testFlag(Property::Action::Select)); + widgets.resetButton->setVisible(actions.testFlag(Property::Action::Reset)); widgets.removeButton->setVisible(actions.testFlag(Property::Action::Remove)); widgets.addButton->setVisible(actions.testFlag(Property::Action::Add)); @@ -1140,6 +1173,18 @@ bool VariantEditorView::eventFilter(QObject *watched, QEvent *event) return QScrollArea::eventFilter(watched, event); } +void VariantEditorView::keyPressEvent(QKeyEvent *event) +{ + switch (event->key()) { + case Qt::Key_Down: + case Qt::Key_Up: + // TODO: Support changing selected property by keyboard + break; + } + + QScrollArea::keyPressEvent(event); +} + } // namespace Tiled #include "moc_varianteditor.cpp" diff --git a/src/tiled/varianteditor.h b/src/tiled/varianteditor.h index 0fe33c35e9..434ea273ac 100644 --- a/src/tiled/varianteditor.h +++ b/src/tiled/varianteditor.h @@ -45,6 +45,7 @@ class Property : public QObject 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) + Q_PROPERTY(bool selected READ isSelected WRITE setSelected NOTIFY selectedChanged) Q_PROPERTY(Actions actions READ actions WRITE setActions NOTIFY actionsChanged) public: @@ -61,6 +62,7 @@ class Property : public QObject Remove = 0x02, Add = 0x04, AddDisabled = Add | 0x08, + Select = 0x10, }; Q_DECLARE_FLAGS(Actions, Action) @@ -81,6 +83,9 @@ class Property : public QObject bool isModified() const { return m_modified; } void setModified(bool modified); + bool isSelected() const { return m_selected; } + void setSelected(bool selected); + Actions actions() const { return m_actions; } void setActions(Actions actions); @@ -95,6 +100,7 @@ class Property : public QObject void valueChanged(); void enabledChanged(bool enabled); void modifiedChanged(bool modified); + void selectedChanged(bool selected); void actionsChanged(Actions actions); void resetRequested(); @@ -110,6 +116,7 @@ class Property : public QObject QString m_toolTip; bool m_enabled = true; bool m_modified = false; + bool m_selected = false; Actions m_actions; }; @@ -436,7 +443,7 @@ struct EnumData template EnumData enumData() { - return {{}}; + return {}; } /** @@ -564,6 +571,8 @@ class VariantEditorView : public QScrollArea protected: bool eventFilter(QObject *watched, QEvent *event) override; + void keyPressEvent(QKeyEvent *event) override; + private: VariantEditor *m_editor; }; diff --git a/src/tiled/variantmapproperty.cpp b/src/tiled/variantmapproperty.cpp index 076b85d4a3..0e973f9349 100644 --- a/src/tiled/variantmapproperty.cpp +++ b/src/tiled/variantmapproperty.cpp @@ -180,6 +180,9 @@ bool VariantMapProperty::createOrUpdateProperty(int index, addMember(name, mSuggestions.value(name)); }); + connect(property, &Property::selectedChanged, + this, &VariantMapProperty::selectedPropertiesChanged); + insertProperty(index, property); mPropertyMap.insert(name, property); } else { @@ -191,10 +194,10 @@ bool VariantMapProperty::createOrUpdateProperty(int index, if (property) { if (mValue.contains(name)) { property->setEnabled(true); - property->setActions(Property::Action::Remove); + property->setActions(Property::Action::Select | Property::Action::Remove); } else { property->setEnabled(false); - property->setActions(Property::Action::Add); + property->setActions(Property::Action::Select | Property::Action::Add); } updateModifiedRecursively(property, newValue); @@ -439,6 +442,14 @@ void VariantMapProperty::memberContextMenuRequested(Property *property, const QS menu.addAction(tr("Collapse All"), groupProperty, &GroupProperty::collapseAll); } + if (path.size() == 1) { + auto selected = selectedProperties(); + if (!selected.contains(property)) { + selected = { property }; + setSelectedProperties(selected); + } + } + // Provide the Add, Remove and Reset actions also here if (isEnabled() && property->actions()) { menu.addSeparator(); @@ -477,6 +488,21 @@ void VariantMapProperty::memberContextMenuRequested(Property *property, const QS menu.exec(globalPos); } +QList VariantMapProperty::selectedProperties() const +{ + QList selected; + for (auto property : subProperties()) + if (property->isSelected()) + selected.append(property); + return selected; +} + +void VariantMapProperty::setSelectedProperties(const QList &properties) +{ + for (auto property : subProperties()) + property->setSelected(properties.contains(property)); +} + AddValueProperty::AddValueProperty(QObject *parent) : Property(QString(), parent) diff --git a/src/tiled/variantmapproperty.h b/src/tiled/variantmapproperty.h index f0d8d40f23..e49b1237b9 100644 --- a/src/tiled/variantmapproperty.h +++ b/src/tiled/variantmapproperty.h @@ -46,10 +46,15 @@ class VariantMapProperty : public GroupProperty Property *property(const QString &name) const; + QList selectedProperties() const; + void setSelectedProperties(const QList &properties); + signals: void memberValueChanged(const QStringList &path, const QVariant &value); void renameRequested(const QString &name); + void selectedPropertiesChanged(); + protected: virtual void propertyTypesChanged();