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