From 85ef006ff3de79ff6b5a7a301025f35538a1f77e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thorbj=C3=B8rn=20Lindeijer?= Date: Wed, 13 Nov 2024 17:30:33 +0100 Subject: [PATCH] Made Add Property action use inline widgets Use a special property that creates a name edit as label and a type combo box as editor, instead of a modal Add Property dialog. VariantEditorView::focusProperty now makes sure that the focused property widget is visible. The "add value" property is automatically removed when it loses focus. To achieve this, each property now gets its own parent widget rather than just having its widgets added to a nested horizontal layout. This way, we can check whether the focus remains within the same parent widget. The Add button has Qt::StrongFocus policy now, otherwise the focus would be lost to the properties view when clicking it. --- src/tiled/addpropertydialog.cpp | 124 -------------- src/tiled/addpropertydialog.h | 54 ------ src/tiled/addpropertydialog.ui | 103 ------------ src/tiled/libtilededitor.qbs | 3 - src/tiled/propertieswidget.cpp | 26 ++- src/tiled/propertieswidget.h | 5 +- src/tiled/propertyeditorwidgets.cpp | 7 +- src/tiled/propertyeditorwidgets.h | 2 +- src/tiled/propertytypeseditor.cpp | 21 ++- src/tiled/propertytypeseditor.h | 3 + src/tiled/varianteditor.cpp | 249 +++++++++++++++++----------- src/tiled/varianteditor.h | 31 ++-- src/tiled/variantmapproperty.cpp | 163 +++++++++++++++++- src/tiled/variantmapproperty.h | 49 ++++++ 14 files changed, 430 insertions(+), 410 deletions(-) delete mode 100644 src/tiled/addpropertydialog.cpp delete mode 100644 src/tiled/addpropertydialog.h delete mode 100644 src/tiled/addpropertydialog.ui diff --git a/src/tiled/addpropertydialog.cpp b/src/tiled/addpropertydialog.cpp deleted file mode 100644 index 230ba545ad..0000000000 --- a/src/tiled/addpropertydialog.cpp +++ /dev/null @@ -1,124 +0,0 @@ -/* - * addpropertydialog.cpp - * Copyright 2015, CaptainFrog - * Copyright 2016, 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 "addpropertydialog.h" -#include "ui_addpropertydialog.h" - -#include "documentmanager.h" -#include "object.h" -#include "preferences.h" -#include "properties.h" -#include "propertytypesmodel.h" -#include "session.h" -#include "utils.h" - -#include - -using namespace Tiled; - -namespace session { -static SessionOption propertyType { "property.type", QStringLiteral("string") }; -} // namespace session - -AddPropertyDialog::AddPropertyDialog(QWidget *parent) - : QDialog(parent) - , mUi(new Ui::AddPropertyDialog) -{ - initialize(nullptr); -} - -AddPropertyDialog::AddPropertyDialog(const ClassPropertyType *parentClassType, QWidget *parent) - : QDialog(parent) - , mUi(new Ui::AddPropertyDialog) -{ - initialize(parentClassType); -} - -void AddPropertyDialog::initialize(const Tiled::ClassPropertyType *parentClassType) -{ - mUi->setupUi(this); - resize(Utils::dpiScaled(size())); - - const QIcon plain(QStringLiteral("://images/scalable/property-type-plain.svg")); - - // Add possible types from QVariant - mUi->typeBox->addItem(plain, typeToName(QMetaType::Bool), false); - mUi->typeBox->addItem(plain, typeToName(QMetaType::QColor), QColor()); - mUi->typeBox->addItem(plain, typeToName(QMetaType::Double), 0.0); - mUi->typeBox->addItem(plain, typeToName(filePathTypeId()), QVariant::fromValue(FilePath())); - mUi->typeBox->addItem(plain, typeToName(QMetaType::Int), 0); - mUi->typeBox->addItem(plain, typeToName(objectRefTypeId()), QVariant::fromValue(ObjectRef())); - mUi->typeBox->addItem(plain, typeToName(QMetaType::QString), QString()); - - for (const auto propertyType : Object::propertyTypes()) { - // Avoid suggesting the creation of circular dependencies between types - if (parentClassType && !parentClassType->canAddMemberOfType(propertyType)) - continue; - - // Avoid suggesting classes not meant to be used as property value - if (propertyType->isClass()) - if (!static_cast(propertyType)->isPropertyValueType()) - continue; - - const QVariant var = propertyType->wrap(propertyType->defaultValue()); - const QIcon icon = PropertyTypesModel::iconForPropertyType(propertyType->type); - mUi->typeBox->addItem(icon, propertyType->name, var); - } - - mUi->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); - - // Restore previously used type - mUi->typeBox->setCurrentText(session::propertyType); - - connect(mUi->name, &QLineEdit::textChanged, - this, &AddPropertyDialog::nameChanged); - connect(mUi->typeBox, &QComboBox::currentTextChanged, - this, &AddPropertyDialog::typeChanged); - - mUi->name->setFocus(); -} - -AddPropertyDialog::~AddPropertyDialog() -{ - delete mUi; -} - -QString AddPropertyDialog::propertyName() const -{ - return mUi->name->text(); -} - -QVariant AddPropertyDialog::propertyValue() const -{ - return mUi->typeBox->currentData(); -} - -void AddPropertyDialog::nameChanged(const QString &text) -{ - mUi->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(!text.isEmpty()); -} - -void AddPropertyDialog::typeChanged(const QString &text) -{ - session::propertyType = text; -} - -#include "moc_addpropertydialog.cpp" diff --git a/src/tiled/addpropertydialog.h b/src/tiled/addpropertydialog.h deleted file mode 100644 index ba36977958..0000000000 --- a/src/tiled/addpropertydialog.h +++ /dev/null @@ -1,54 +0,0 @@ -/* - * addpropertydialog.h - * Copyright 2015, CaptainFrog - * Copyright 2016, 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 - -namespace Tiled { -class ClassPropertyType; -} - -namespace Ui { -class AddPropertyDialog; -} - -class AddPropertyDialog : public QDialog -{ - Q_OBJECT - -public: - explicit AddPropertyDialog(QWidget *parent = nullptr); - AddPropertyDialog(const Tiled::ClassPropertyType *parentClassType, QWidget *parent = nullptr); - ~AddPropertyDialog() override; - - QString propertyName() const; - QVariant propertyValue() const; - -private: - void initialize(const Tiled::ClassPropertyType *parentClassType); - - void nameChanged(const QString &text); - void typeChanged(const QString &text); - - Ui::AddPropertyDialog *mUi; -}; diff --git a/src/tiled/addpropertydialog.ui b/src/tiled/addpropertydialog.ui deleted file mode 100644 index 1b927c33b5..0000000000 --- a/src/tiled/addpropertydialog.ui +++ /dev/null @@ -1,103 +0,0 @@ - - - AddPropertyDialog - - - - 0 - 0 - 320 - 134 - - - - - 0 - 0 - - - - Add Property - - - - QLayout::SetMinAndMaxSize - - - - - - - - Qt::Horizontal - - - - 214 - 18 - - - - - - - - Property name - - - - - - - - - - Qt::Horizontal - - - QDialogButtonBox::Cancel|QDialogButtonBox::Ok - - - - - - - typeBox - name - - - - - buttonBox - accepted() - AddPropertyDialog - accept() - - - 302 - 96 - - - 157 - 105 - - - - - buttonBox - rejected() - AddPropertyDialog - reject() - - - 338 - 96 - - - 286 - 105 - - - - - diff --git a/src/tiled/libtilededitor.qbs b/src/tiled/libtilededitor.qbs index 328af5f4d7..6d73e57a8f 100644 --- a/src/tiled/libtilededitor.qbs +++ b/src/tiled/libtilededitor.qbs @@ -94,9 +94,6 @@ DynamicLibrary { "actionmanager.h", "actionsearch.cpp", "actionsearch.h", - "addpropertydialog.cpp", - "addpropertydialog.h", - "addpropertydialog.ui", "addremovelayer.cpp", "addremovelayer.h", "addremovemapobject.cpp", diff --git a/src/tiled/propertieswidget.cpp b/src/tiled/propertieswidget.cpp index 86559141ac..0cd8f74c09 100644 --- a/src/tiled/propertieswidget.cpp +++ b/src/tiled/propertieswidget.cpp @@ -21,7 +21,6 @@ #include "propertieswidget.h" #include "actionmanager.h" -#include "addpropertydialog.h" #include "changeimagelayerproperty.h" #include "changelayer.h" #include "changemapobject.h" @@ -2166,18 +2165,20 @@ PropertiesWidget::PropertiesWidget(QWidget *parent) mActionAddProperty->setEnabled(false); mActionAddProperty->setIcon(QIcon(QLatin1String(":/images/16/add.png"))); connect(mActionAddProperty, &QAction::triggered, - this, &PropertiesWidget::openAddPropertyDialog); + this, &PropertiesWidget::showAddValueProperty); mActionRemoveProperty = new QAction(this); mActionRemoveProperty->setEnabled(false); mActionRemoveProperty->setIcon(QIcon(QLatin1String(":/images/16/remove.png"))); mActionRemoveProperty->setShortcuts(QKeySequence::Delete); + mActionRemoveProperty->setPriority(QAction::LowPriority); connect(mActionRemoveProperty, &QAction::triggered, this, &PropertiesWidget::removeProperties); mActionRenameProperty = new QAction(this); mActionRenameProperty->setEnabled(false); mActionRenameProperty->setIcon(QIcon(QLatin1String(":/images/16/rename.png"))); + mActionRenameProperty->setPriority(QAction::LowPriority); // connect(mActionRenameProperty, &QAction::triggered, // this, &PropertiesWidget::renameProperty); @@ -2188,6 +2189,7 @@ PropertiesWidget::PropertiesWidget(QWidget *parent) QToolBar *toolBar = new QToolBar; toolBar->setFloatable(false); toolBar->setMovable(false); + toolBar->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); toolBar->setIconSize(Utils::smallIconSize()); toolBar->addAction(mActionAddProperty); toolBar->addAction(mActionRemoveProperty); @@ -2516,11 +2518,23 @@ void PropertiesWidget::pasteProperties() } } -void PropertiesWidget::openAddPropertyDialog() +void PropertiesWidget::showAddValueProperty() { - AddPropertyDialog dialog(mPropertyBrowser); - if (dialog.exec() == AddPropertyDialog::Accepted) - addProperty(dialog.propertyName(), dialog.propertyValue()); + if (!mAddValueProperty) { + mAddValueProperty = new AddValueProperty(mCustomProperties); + + connect(mAddValueProperty, &Property::addRequested, this, [this] { + addProperty(mAddValueProperty->name(), mAddValueProperty->value()); + mCustomProperties->deleteProperty(mAddValueProperty); + }); + connect(mAddValueProperty, &Property::removeRequested, this, [this] { + mCustomProperties->deleteProperty(mAddValueProperty); + }); + + mCustomProperties->addProperty(mAddValueProperty); + } + + mPropertyBrowser->focusProperty(mAddValueProperty, VariantEditor::FocusLabel); } void PropertiesWidget::addProperty(const QString &name, const QVariant &value) diff --git a/src/tiled/propertieswidget.h b/src/tiled/propertieswidget.h index 2c9a5f0432..050fa8cf95 100644 --- a/src/tiled/propertieswidget.h +++ b/src/tiled/propertieswidget.h @@ -21,6 +21,7 @@ #pragma once #include +#include #include class QScrollArea; @@ -29,6 +30,7 @@ namespace Tiled { class Object; +class AddValueProperty; class CustomProperties; class Document; class GroupProperty; @@ -73,7 +75,7 @@ public slots: void cutProperties(); bool copyProperties(); void pasteProperties(); - void openAddPropertyDialog(); + void showAddValueProperty(); void addProperty(const QString &name, const QVariant &value); void removeProperties(); void renameProperty(const QString &name); @@ -84,6 +86,7 @@ public slots: Document *mDocument = nullptr; ObjectProperties *mPropertiesObject = nullptr; CustomProperties *mCustomProperties = nullptr; + QPointer mAddValueProperty; QMap mExpandedStates; VariantEditorView *mPropertyBrowser; QAction *mActionAddProperty; diff --git a/src/tiled/propertyeditorwidgets.cpp b/src/tiled/propertyeditorwidgets.cpp index 3bf358f9fb..ff95d55757 100644 --- a/src/tiled/propertyeditorwidgets.cpp +++ b/src/tiled/propertyeditorwidgets.cpp @@ -692,15 +692,18 @@ void ElidingLabel::paintEvent(QPaintEvent *) } -PropertyLabel::PropertyLabel(int level, QWidget *parent) +PropertyLabel::PropertyLabel(QWidget *parent) : ElidingLabel(parent) { setMinimumWidth(Utils::dpiScaled(50)); - setLevel(level); + updateContentMargins(); } void PropertyLabel::setLevel(int level) { + if (m_level == level) + return; + m_level = level; updateContentMargins(); } diff --git a/src/tiled/propertyeditorwidgets.h b/src/tiled/propertyeditorwidgets.h index 3727d3bfa2..63721a98b6 100644 --- a/src/tiled/propertyeditorwidgets.h +++ b/src/tiled/propertyeditorwidgets.h @@ -309,7 +309,7 @@ class PropertyLabel : public ElidingLabel Q_OBJECT public: - PropertyLabel(int level, QWidget *parent = nullptr); + PropertyLabel(QWidget *parent = nullptr); void setLevel(int level); diff --git a/src/tiled/propertytypeseditor.cpp b/src/tiled/propertytypeseditor.cpp index 9a1906015f..53fe0bde95 100644 --- a/src/tiled/propertytypeseditor.cpp +++ b/src/tiled/propertytypeseditor.cpp @@ -21,7 +21,6 @@ #include "propertytypeseditor.h" #include "ui_propertytypeseditor.h" -#include "addpropertydialog.h" #include "colorbutton.h" #include "objecttypes.h" #include "preferences.h" @@ -567,13 +566,23 @@ void PropertyTypesEditor::openAddMemberDialog() if (!propertyType || !propertyType->isClass()) return; - AddPropertyDialog dialog(static_cast(propertyType), this); - dialog.setWindowTitle(tr("Add Member")); + if (!mAddValueProperty) { + mAddValueProperty = new AddValueProperty(mMembersProperty); + mAddValueProperty->setPlaceholderText(tr("Member name")); + mAddValueProperty->setParentClassType(static_cast(propertyType)); - if (dialog.exec() == AddPropertyDialog::Accepted) - addMember(dialog.propertyName(), QVariant(dialog.propertyValue())); + connect(mAddValueProperty, &Property::addRequested, this, [this] { + addMember(mAddValueProperty->name(), mAddValueProperty->value()); + mMembersProperty->deleteProperty(mAddValueProperty); + }); + connect(mAddValueProperty, &Property::removeRequested, this, [this] { + mMembersProperty->deleteProperty(mAddValueProperty); + }); + + mMembersProperty->addProperty(mAddValueProperty); + } - activateWindow(); + mMembersEditor->focusProperty(mAddValueProperty, VariantEditor::FocusLabel); } void PropertyTypesEditor::addMember(const QString &name, const QVariant &value) diff --git a/src/tiled/propertytypeseditor.h b/src/tiled/propertytypeseditor.h index d3eb4aeefa..afd4041eda 100644 --- a/src/tiled/propertytypeseditor.h +++ b/src/tiled/propertytypeseditor.h @@ -23,6 +23,7 @@ #include "propertytype.h" #include +#include class QCheckBox; class QComboBox; @@ -40,6 +41,7 @@ class PropertyTypesEditor; namespace Tiled { +class AddValueProperty; class ColorButton; class PropertyTypesModel; class VariantEditorView; @@ -145,6 +147,7 @@ class PropertyTypesEditor : public QDialog QPushButton *mClassOfButton = nullptr; VariantEditorView *mMembersEditor = nullptr; VariantMapProperty *mMembersProperty = nullptr; + QPointer mAddValueProperty; bool mSettingPrefPropertyTypes = false; bool mSettingName = false; diff --git a/src/tiled/varianteditor.cpp b/src/tiled/varianteditor.cpp index 5f74ebda1c..962c155ba9 100644 --- a/src/tiled/varianteditor.cpp +++ b/src/tiled/varianteditor.cpp @@ -32,7 +32,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -80,6 +82,29 @@ void Property::setActions(Actions actions) } } +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()); + connect(this, &Property::nameChanged, label, &PropertyLabel::setText); + connect(this, &Property::toolTipChanged, label, &PropertyLabel::setToolTip); + connect(this, &Property::modifiedChanged, label, &PropertyLabel::setModified); + } + + if (displayMode() == Property::DisplayMode::Header) + label->setHeader(true); + + return label; +} + Property::DisplayMode GroupProperty::displayMode() const { if (name().isEmpty()) @@ -91,6 +116,18 @@ Property::DisplayMode GroupProperty::displayMode() const return DisplayMode::Default; } +QWidget *GroupProperty::createLabel(int level, QWidget *parent) +{ + auto label = static_cast(Property::createLabel(level, parent)); + label->setExpandable(true); + label->setExpanded(isExpanded()); + + connect(this, &GroupProperty::expandedChanged, label, &PropertyLabel::setExpanded); + connect(label, &PropertyLabel::toggled, this, &GroupProperty::setExpanded); + + return label; +} + void GroupProperty::setExpanded(bool expanded) { if (m_expanded != expanded) { @@ -580,9 +617,7 @@ void VariantEditor::clear() QHashIterator it(m_propertyWidgets); while (it.hasNext()) { it.next(); - auto &widgets = it.value(); - Utils::deleteAllFromLayout(widgets.layout); - delete widgets.layout; + delete it.value().rowWidget; it.key()->disconnect(this); } @@ -595,7 +630,7 @@ void VariantEditor::clear() */ void VariantEditor::addProperty(Property *property) { - m_layout->addLayout(createPropertyLayout(property)); + m_layout->addWidget(createPropertyWidget(property)); } /** @@ -604,7 +639,7 @@ void VariantEditor::addProperty(Property *property) */ void VariantEditor::insertProperty(int index, Property *property) { - m_layout->insertLayout(index, createPropertyLayout(property)); + m_layout->insertWidget(index, createPropertyWidget(property)); } /** @@ -615,47 +650,69 @@ void VariantEditor::removeProperty(Property *property) auto it = m_propertyWidgets.constFind(property); Q_ASSERT(it != m_propertyWidgets.constEnd()); - if (it != m_propertyWidgets.constEnd()) { - auto &widgets = it.value(); - Utils::deleteAllFromLayout(widgets.layout); - delete widgets.layout; + if (it == m_propertyWidgets.constEnd()) + return; + + // Immediately remove from layout, but delete later to avoid deleting + // widgets while they are still handling events. + auto rowWidget = it.value().rowWidget; + m_layout->removeWidget(rowWidget); + rowWidget->deleteLater(); - m_propertyWidgets.erase(it); + m_propertyWidgets.erase(it); + + // This appears to be necessary to avoid flickering due to relayouting + // not being done before the next paint. + QWidget *widget = this; + while (widget && widget->layout()) { + widget->layout()->activate(); + widget = widget->parentWidget(); } property->disconnect(this); } /** - * Focuses the editor for the given property. Makes sure any parent group - * properties are expanded. + * Focuses the editor or label for the given property. Makes sure any parent + * group properties are expanded. * * When the given property is a group property, the group property is expanded * and the first child property is focused. + * + * Returns the focused widget or nullptr if the property was not found. */ -bool VariantEditor::focusProperty(Property *property) +QWidget *VariantEditor::focusProperty(Property *property, FocusTarget target) { for (auto it = m_propertyWidgets.constBegin(); it != m_propertyWidgets.constEnd(); ++it) { auto &widgets = it.value(); if (it.key() == property) { - if (widgets.editor) + if (target == FocusEditor && widgets.editor) { widgets.editor->setFocus(); - else if (auto groupProperty = qobject_cast(it.key())) { + return widgets.editor; + } + if (target == FocusLabel && widgets.label) { + widgets.label->setFocus(); + return widgets.label; + } + if (auto groupProperty = qobject_cast(it.key())) { groupProperty->setExpanded(true); if (widgets.children && !groupProperty->subProperties().isEmpty()) - widgets.children->focusProperty(groupProperty->subProperties().first()); + widgets.children->focusProperty(groupProperty->subProperties().first(), target); + return widgets.children; } - return true; + return nullptr; } else if (auto groupProperty = qobject_cast(it.key())) { - if (widgets.children && widgets.children->focusProperty(property)) { - groupProperty->setExpanded(true); - return true; + if (widgets.children) { + if (auto w = widgets.children->focusProperty(property, target)) { + groupProperty->setExpanded(true); + return w; + } } } } - return false; + return nullptr; } void VariantEditor::setLevel(int level) @@ -667,8 +724,10 @@ void VariantEditor::setLevel(int level) setAutoFillBackground(m_level > 0); } -QLayout *VariantEditor::createPropertyLayout(Property *property) +QWidget *VariantEditor::createPropertyWidget(Property *property) { + Q_ASSERT(!m_propertyWidgets.contains(property)); + auto &widgets = m_propertyWidgets[property]; const auto displayMode = property->displayMode(); @@ -680,118 +739,109 @@ QLayout *VariantEditor::createPropertyLayout(Property *property) if (displayMode == Property::DisplayMode::ChildrenOnly) { if (auto groupProperty = qobject_cast(property)) { - widgets.childrenLayout = new QVBoxLayout; - widgets.layout = widgets.childrenLayout; + auto containerWidget = new QWidget(this); + widgets.childrenLayout = new QVBoxLayout(containerWidget); + widgets.childrenLayout->setContentsMargins(0, 0, 0, 0); setPropertyChildrenExpanded(groupProperty, true); - return widgets.layout; + widgets.rowWidget = containerWidget; + return widgets.rowWidget; } } - auto rowLayout = new QHBoxLayout; + auto rowWidget = new QWidget(this); + auto rowLayout = new QHBoxLayout(rowWidget); rowLayout->setSpacing(halfSpacing * 2); + rowLayout->setContentsMargins(0, 0, 0, 0); - widgets.layout = rowLayout; + widgets.rowWidget = rowWidget; if (displayMode == Property::DisplayMode::Separator) { - auto separator = new QFrame(this); + auto separator = new QFrame(rowWidget); rowLayout->setContentsMargins(0, halfSpacing, 0, halfSpacing); separator->setFrameShape(QFrame::HLine); separator->setFrameShadow(QFrame::Plain); separator->setForegroundRole(QPalette::Mid); rowLayout->addWidget(separator); - return widgets.layout; + return widgets.rowWidget; } - widgets.label = new PropertyLabel(m_level, this); + widgets.label = property->createLabel(m_level, rowWidget); - connect(widgets.label, &PropertyLabel::contextMenuRequested, - property, &Property::contextMenuRequested); - - if (displayMode != Property::DisplayMode::NoLabel) { - widgets.label->setText(property->name()); - widgets.label->setModified(property->isModified()); - connect(property, &Property::modifiedChanged, widgets.label, &PropertyLabel::setModified); + if (displayMode != Property::DisplayMode::Header) { + if (isLeftToRight()) + rowLayout->setContentsMargins(0, halfSpacing, halfSpacing * 2, halfSpacing); + else + rowLayout->setContentsMargins(halfSpacing * 2, halfSpacing, 0, halfSpacing); } - if (displayMode == Property::DisplayMode::Header) - widgets.label->setHeader(true); - else if (isLeftToRight()) - rowLayout->setContentsMargins(0, halfSpacing, halfSpacing * 2, halfSpacing); - else - rowLayout->setContentsMargins(halfSpacing * 2, halfSpacing, 0, halfSpacing); - - rowLayout->addWidget(widgets.label, LabelStretch, Qt::AlignTop); + if (widgets.label) + rowLayout->addWidget(widgets.label, LabelStretch, Qt::AlignTop); - widgets.editorLayout = new QHBoxLayout; - widgets.editor = property->createEditor(this); + auto editorLayout = new QHBoxLayout; + widgets.editor = property->createEditor(rowWidget); if (widgets.editor) { widgets.editor->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Fixed); - widgets.editorLayout->addWidget(widgets.editor, EditorStretch, Qt::AlignTop); - rowLayout->addLayout(widgets.editorLayout, EditorStretch); + editorLayout->addWidget(widgets.editor, EditorStretch, Qt::AlignTop); + rowLayout->addLayout(editorLayout, EditorStretch); } else { - rowLayout->addLayout(widgets.editorLayout, 0); + rowLayout->addLayout(editorLayout, 0); } - widgets.resetButton = new QToolButton(this); + widgets.resetButton = new QToolButton(rowWidget); widgets.resetButton->setToolTip(tr("Reset")); widgets.resetButton->setIcon(m_resetIcon); widgets.resetButton->setAutoRaise(true); widgets.resetButton->setEnabled(property->isModified()); Utils::setThemeIcon(widgets.resetButton, "edit-clear"); - widgets.editorLayout->addWidget(widgets.resetButton, 0, Qt::AlignTop); + editorLayout->addWidget(widgets.resetButton, 0, Qt::AlignTop); connect(widgets.resetButton, &QAbstractButton::clicked, property, &Property::resetRequested); connect(property, &Property::modifiedChanged, widgets.resetButton, &QWidget::setEnabled); - widgets.removeButton = new QToolButton(this); + widgets.removeButton = new QToolButton(rowWidget); widgets.removeButton->setToolTip(tr("Remove")); widgets.removeButton->setIcon(m_removeIcon); widgets.removeButton->setAutoRaise(true); Utils::setThemeIcon(widgets.removeButton, "remove"); - widgets.editorLayout->addWidget(widgets.removeButton, 0, Qt::AlignTop); + editorLayout->addWidget(widgets.removeButton, 0, Qt::AlignTop); connect(widgets.removeButton, &QAbstractButton::clicked, property, &Property::removeRequested); - widgets.addButton = new QToolButton(this); + widgets.addButton = new QToolButton(rowWidget); widgets.addButton->setToolTip(tr("Add")); widgets.addButton->setIcon(m_addIcon); widgets.addButton->setAutoRaise(true); + widgets.addButton->setFocusPolicy(Qt::StrongFocus); // needed for AddValueProperty Utils::setThemeIcon(widgets.addButton, "add"); - widgets.editorLayout->addWidget(widgets.addButton, 0, Qt::AlignTop); + editorLayout->addWidget(widgets.addButton, 0, Qt::AlignTop); connect(widgets.addButton, &QAbstractButton::clicked, property, &Property::addRequested); if (auto groupProperty = qobject_cast(property)) { - widgets.childrenLayout = new QVBoxLayout; - widgets.childrenLayout->addLayout(rowLayout); - widgets.layout = widgets.childrenLayout; + auto containerWidget = new QWidget(this); + widgets.childrenLayout = new QVBoxLayout(containerWidget); + widgets.childrenLayout->setContentsMargins(0, 0, 0, 0); + widgets.childrenLayout->setSpacing(0); + widgets.childrenLayout->addWidget(rowWidget); - connect(groupProperty, &GroupProperty::expandedChanged, widgets.label, &PropertyLabel::setExpanded); - connect(widgets.label, &PropertyLabel::toggled, this, [=](bool expanded) { + connect(groupProperty, &GroupProperty::expandedChanged, this, [=](bool expanded) { setPropertyChildrenExpanded(groupProperty, expanded); - groupProperty->setExpanded(expanded); }); - widgets.label->setExpandable(true); - widgets.label->setExpanded(groupProperty->isExpanded()); + setPropertyChildrenExpanded(groupProperty, groupProperty->isExpanded()); + + widgets.rowWidget = containerWidget; } updatePropertyEnabled(widgets, property->isEnabled()); - updatePropertyToolTip(widgets, property->toolTip()); updatePropertyActions(widgets, property->actions()); - connect(property, &Property::nameChanged, this, [=] (const QString &name) { - updatePropertyName(m_propertyWidgets[property], name); - }); connect(property, &Property::enabledChanged, this, [=] (bool enabled) { updatePropertyEnabled(m_propertyWidgets[property], enabled); }); - connect(property, &Property::toolTipChanged, this, [=] (const QString &toolTip) { - updatePropertyToolTip(m_propertyWidgets[property], toolTip); - }); connect(property, &Property::actionsChanged, this, [=] (Property::Actions actions) { updatePropertyActions(m_propertyWidgets[property], actions); }); - return widgets.layout; + return widgets.rowWidget; } void VariantEditor::setPropertyChildrenExpanded(GroupProperty *groupProperty, bool expanded) @@ -801,11 +851,12 @@ void VariantEditor::setPropertyChildrenExpanded(GroupProperty *groupProperty, bo // Create the children editor on-demand if (expanded && !widgets.children) { const auto halfSpacing = Utils::dpiScaled(2); + const auto displayMode = groupProperty->displayMode(); widgets.children = new VariantEditor(this); - if (widgets.label && widgets.label->isHeader()) + if (widgets.label && displayMode == Property::DisplayMode::Header) widgets.children->setContentsMargins(0, halfSpacing, 0, halfSpacing); - if (groupProperty->displayMode() == Property::DisplayMode::Default) + if (displayMode == Property::DisplayMode::Default) widgets.children->setLevel(m_level + 1); widgets.children->setEnabled(groupProperty->isEnabled()); for (auto property : groupProperty->subProperties()) @@ -831,12 +882,6 @@ void VariantEditor::setPropertyChildrenExpanded(GroupProperty *groupProperty, bo } } -void VariantEditor::updatePropertyName(const PropertyWidgets &widgets, const QString &name) -{ - if (widgets.label) - widgets.label->setText(name); -} - void VariantEditor::updatePropertyEnabled(const PropertyWidgets &widgets, bool enabled) { if (widgets.label) @@ -847,19 +892,14 @@ void VariantEditor::updatePropertyEnabled(const PropertyWidgets &widgets, bool e widgets.children->setEnabled(enabled); } -void VariantEditor::updatePropertyToolTip(const PropertyWidgets &widgets, const QString &toolTip) +void VariantEditor::updatePropertyActions(const PropertyWidgets &widgets, + Property::Actions actions) { - if (widgets.label) - widgets.label->setToolTip(toolTip); - if (widgets.editor) - widgets.editor->setToolTip(toolTip); -} + widgets.resetButton->setVisible(actions.testFlag(Property::Action::Reset)); + widgets.removeButton->setVisible(actions.testFlag(Property::Action::Remove)); + widgets.addButton->setVisible(actions.testFlag(Property::Action::Add)); -void VariantEditor::updatePropertyActions(const PropertyWidgets &widgets, Property::Actions actions) -{ - widgets.resetButton->setVisible(actions & Property::Action::Reset); - widgets.removeButton->setVisible(actions & Property::Action::Remove); - widgets.addButton->setVisible(actions & Property::Action::Add); + widgets.addButton->setEnabled(!actions.testFlag(Property::Action::AddDisabled)); } @@ -1040,9 +1080,32 @@ VariantEditorView::VariantEditorView(QWidget *parent) setWidget(scrollWidget); } -void VariantEditorView::focusProperty(Property *property) +void VariantEditorView::focusProperty(Property *property, + VariantEditor::FocusTarget target) +{ + if (auto widget = m_editor->focusProperty(property, target)) { + if (widget->isVisible()) { + ensureWidgetVisible(widget); + } else { + // Install event filter to detect when widget becomes visible + widget->installEventFilter(this); + } + } +} + +bool VariantEditorView::eventFilter(QObject *watched, QEvent *event) { - m_editor->focusProperty(property); + if (event->type() == QEvent::Show) { + if (QPointer widget = qobject_cast(watched)) { + // Schedule after all pending events including layout + QMetaObject::invokeMethod(this, [=] { + if (widget) + ensureWidgetVisible(widget); + }, Qt::QueuedConnection); + widget->removeEventFilter(this); + } + } + return QScrollArea::eventFilter(watched, event); } } // namespace Tiled diff --git a/src/tiled/varianteditor.h b/src/tiled/varianteditor.h index 5fb0329b70..0fe33c35e9 100644 --- a/src/tiled/varianteditor.h +++ b/src/tiled/varianteditor.h @@ -20,13 +20,10 @@ #pragma once -#include #include #include -#include #include #include -#include #include #include @@ -63,6 +60,7 @@ class Property : public QObject Reset = 0x01, Remove = 0x02, Add = 0x04, + AddDisabled = Add | 0x08, }; Q_DECLARE_FLAGS(Actions, Action) @@ -88,6 +86,7 @@ class Property : public QObject virtual DisplayMode displayMode() const { return DisplayMode::Default; } + virtual QWidget *createLabel(int level, QWidget *parent); virtual QWidget *createEditor(QWidget *parent) = 0; signals: @@ -152,6 +151,7 @@ class GroupProperty : public Property DisplayMode displayMode() const override; + QWidget *createLabel(int level, QWidget *parent) override; QWidget *createEditor(QWidget */* parent */) override { return nullptr; } void setHeader(bool header) { m_header = header; } @@ -504,7 +504,12 @@ class VariantEditor : public QWidget void insertProperty(int index, Property *property); void removeProperty(Property *property); - bool focusProperty(Property *property); + enum FocusTarget { + FocusLabel, + FocusEditor, + }; + + QWidget *focusProperty(Property *property, FocusTarget target); void setLevel(int level); @@ -514,10 +519,9 @@ class VariantEditor : public QWidget struct PropertyWidgets { - QLayout *layout = nullptr; - QHBoxLayout *editorLayout = nullptr; + QWidget *rowWidget = nullptr; QVBoxLayout *childrenLayout = nullptr; - PropertyLabel *label = nullptr; + QWidget *label = nullptr; QWidget *editor = nullptr; QToolButton *resetButton = nullptr; QToolButton *removeButton = nullptr; @@ -525,14 +529,13 @@ class VariantEditor : public QWidget VariantEditor *children = nullptr; }; - QLayout *createPropertyLayout(Property *property); + QWidget *createPropertyWidget(Property *property); void setPropertyChildrenExpanded(GroupProperty *groupProperty, bool expanded); - void updatePropertyName(const PropertyWidgets &widgets, const QString &name); void updatePropertyEnabled(const PropertyWidgets &widgets, bool enabled); - void updatePropertyToolTip(const PropertyWidgets &widgets, const QString &toolTip); - void updatePropertyActions(const PropertyWidgets &widgets, Property::Actions actions); + void updatePropertyActions(const PropertyWidgets &widgets, + Property::Actions actions); QIcon m_resetIcon; QIcon m_removeIcon; @@ -555,7 +558,11 @@ class VariantEditorView : public QScrollArea void addProperty(Property *property) { m_editor->addProperty(property); } - void focusProperty(Property *property); + void focusProperty(Property *property, + VariantEditor::FocusTarget target = VariantEditor::FocusEditor); + +protected: + bool eventFilter(QObject *watched, QEvent *event) override; private: VariantEditor *m_editor; diff --git a/src/tiled/variantmapproperty.cpp b/src/tiled/variantmapproperty.cpp index ab42d22446..41a41aa39b 100644 --- a/src/tiled/variantmapproperty.cpp +++ b/src/tiled/variantmapproperty.cpp @@ -23,13 +23,24 @@ #include "mapdocument.h" #include "objectrefedit.h" #include "preferences.h" +#include "propertyeditorwidgets.h" +#include "propertytypesmodel.h" +#include "session.h" #include "utils.h" +#include +#include +#include #include #include namespace Tiled { +namespace session { +static SessionOption propertyType { "property.type", QStringLiteral("string") }; +} // namespace session + + class ObjectRefProperty : public PropertyTemplate { Q_OBJECT @@ -340,15 +351,21 @@ void VariantMapProperty::removeMember(const QString &name) void VariantMapProperty::addMember(const QString &name, const QVariant &value) { - const auto oldValue = mValue.value(name, mSuggestions.value(name)); - - mValue.insert(name, value); + int index = 0; if (auto property = mPropertyMap.value(name)) { - const int index = indexOfProperty(property); - createOrUpdateProperty(index, name, oldValue, value); + index = indexOfProperty(property); + } else for (auto it = mValue.keyBegin(); it != mValue.keyEnd(); ++it) { + if (*it < name) + ++index; + else + break; } + const auto oldValue = mValue.value(name, mSuggestions.value(name)); + mValue.insert(name, value); + createOrUpdateProperty(index, name, oldValue, value); + emitMemberValueChanged({ name }, value); } @@ -460,6 +477,142 @@ void VariantMapProperty::memberContextMenuRequested(Property *property, const QS menu.exec(globalPos); } + +AddValueProperty::AddValueProperty(QObject *parent) + : Property(QString(), parent) + , m_plainTypeIcon(QStringLiteral("://images/scalable/property-type-plain.svg")) + , m_placeholderText(tr("Property name")) +{ + setActions(Action::AddDisabled); +} + +void AddValueProperty::setPlaceholderText(const QString &text) +{ + if (m_placeholderText == text) + return; + + m_placeholderText = text; + emit placeholderTextChanged(text); +} + +QWidget *AddValueProperty::createLabel(int level, QWidget *parent) +{ + constexpr int QLineEditPrivate_horizontalMargin = 2; + const int spacing = Utils::dpiScaled(3); + const int branchIndicatorWidth = Utils::dpiScaled(14); + const int indent = branchIndicatorWidth * (level + 1); + + auto nameEdit = new LineEdit(parent); + nameEdit->setText(name()); + nameEdit->setPlaceholderText(m_placeholderText); + + QStyleOptionFrame option; + option.initFrom(nameEdit); + const int frameWidth = nameEdit->style()->pixelMetric(QStyle::PM_DefaultFrameWidth, &option, nameEdit); + const int nativeMargin = QLineEditPrivate_horizontalMargin + frameWidth; + + QMargins margins; + + if (parent->isLeftToRight()) + margins = QMargins(spacing + indent - nativeMargin, 0, spacing - nativeMargin, 0); + else + margins = QMargins(spacing - nativeMargin, 0, spacing + indent - nativeMargin, 0); + + nameEdit->setContentsMargins(margins); + + connect(nameEdit, &QLineEdit::textChanged, this, &Property::setName); + connect(nameEdit, &QLineEdit::returnPressed, this, [this] { + if (!name().isEmpty()) + emit addRequested(); + }); + connect(this, &Property::nameChanged, this, [=](const QString &name) { + setActions(name.isEmpty() ? Action::AddDisabled : Action::Add); + }); + connect(this, &AddValueProperty::placeholderTextChanged, + nameEdit, &QLineEdit::setPlaceholderText); + + nameEdit->installEventFilter(this); + + connect(qApp, &QApplication::focusChanged, nameEdit, [=] (QWidget *, QWidget *focusWidget) { + // Ignore focus in different windows (popups, dialogs, etc.) + if (!focusWidget || focusWidget->window() != parent->window()) + return; + + // Request removal if focus moved elsewhere + if (!parent->isAncestorOf(focusWidget)) + emit removeRequested(); + }); + + return nameEdit; +} + +QWidget *AddValueProperty::createEditor(QWidget *parent) +{ + // Create combo box with property types + auto typeBox = new ComboBox(parent); + + // Add possible types from QVariant + typeBox->addItem(m_plainTypeIcon, typeToName(QMetaType::Bool), false); + typeBox->addItem(m_plainTypeIcon, typeToName(QMetaType::QColor), QColor()); + typeBox->addItem(m_plainTypeIcon, typeToName(QMetaType::Double), 0.0); + typeBox->addItem(m_plainTypeIcon, typeToName(filePathTypeId()), QVariant::fromValue(FilePath())); + typeBox->addItem(m_plainTypeIcon, typeToName(QMetaType::Int), 0); + typeBox->addItem(m_plainTypeIcon, typeToName(objectRefTypeId()), QVariant::fromValue(ObjectRef())); + typeBox->addItem(m_plainTypeIcon, typeToName(QMetaType::QString), QString()); + + for (const auto propertyType : Object::propertyTypes()) { + // Avoid suggesting the creation of circular dependencies between types + if (m_parentClassType && !m_parentClassType->canAddMemberOfType(propertyType)) + continue; + + // Avoid suggesting classes not meant to be used as property value + if (propertyType->isClass()) + if (!static_cast(propertyType)->isPropertyValueType()) + continue; + + const QVariant var = propertyType->wrap(propertyType->defaultValue()); + const QIcon icon = PropertyTypesModel::iconForPropertyType(propertyType->type); + typeBox->addItem(icon, propertyType->name, var); + } + + // Restore previously used type + typeBox->setCurrentText(session::propertyType); + if (typeBox->currentIndex() == -1) + typeBox->setCurrentIndex(typeBox->findData(QString())); + + m_value = typeBox->currentData(); + + connect(typeBox, qOverload(&QComboBox::currentIndexChanged), this, [=](int index) { + m_value = typeBox->itemData(index); + session::propertyType = typeBox->currentText(); + }); + + typeBox->installEventFilter(this); + + return typeBox; +} + +bool AddValueProperty::eventFilter(QObject *watched, QEvent *event) +{ + switch (event->type()) { + case QEvent::KeyPress: { + // When Escape is pressed while the name edit or the type combo has + // focus, request the removal of this property. + auto keyEvent = static_cast(event); + bool isNameEdit = qobject_cast(watched); + bool isTypeCombo = qobject_cast(watched); + if ((isNameEdit || isTypeCombo) && keyEvent->key() == Qt::Key_Escape) { + emit removeRequested(); + return true; + } + break; + } + default: + break; + } + return false; +} + } // namespace Tiled #include "moc_variantmapproperty.cpp" diff --git a/src/tiled/variantmapproperty.h b/src/tiled/variantmapproperty.h index df0e144072..f0d8d40f23 100644 --- a/src/tiled/variantmapproperty.h +++ b/src/tiled/variantmapproperty.h @@ -23,10 +23,15 @@ #include "propertytype.h" #include "varianteditor.h" +#include + namespace Tiled { class Document; +/** + * A property that creates child properties based on a QVariantMap value. + */ class VariantMapProperty : public GroupProperty { Q_OBJECT @@ -92,4 +97,48 @@ inline Property *VariantMapProperty::property(const QString &name) const return mPropertyMap.value(name); } + +/** + * A property that creates widgets for adding a value with a certain name and + * type. + */ +class AddValueProperty : public Property +{ + Q_OBJECT + +public: + AddValueProperty(QObject *parent = nullptr); + + void setPlaceholderText(const QString &text); + void setParentClassType(const ClassPropertyType *parentClassType); + + QVariant value() const; + + QWidget *createLabel(int level, QWidget *parent) override; + QWidget *createEditor(QWidget *parent) override; + +signals: + void placeholderTextChanged(const QString &text); + +protected: + bool eventFilter(QObject *watched, QEvent *event) override; + +private: + QIcon m_plainTypeIcon; + QString m_placeholderText; + QVariant m_value; + bool m_hasFocus = false; + const ClassPropertyType *m_parentClassType = nullptr; +}; + +inline void AddValueProperty::setParentClassType(const ClassPropertyType *parentClassType) +{ + m_parentClassType = parentClassType; +} + +inline QVariant AddValueProperty::value() const +{ + return m_value; +} + } // namespace Tiled