From 2e93156093834c995370e2c18ae847b0f301e68c Mon Sep 17 00:00:00 2001 From: jessequ Date: Mon, 27 Jul 2026 21:54:57 +0800 Subject: [PATCH] feat: add QFont type editor with FontBox and fix property name top-alignment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add FontBox QML editor for QFont properties — collapsed display shows "family, size pt", expanded inline dropdown provides family ComboBox (populated from QFontDatabase), compact +/- size buttons, and Bold/Italic/Underline toggles. QFont is a Q_GADGET type classified as Object by parserType(), so it requires both a PropertyTypeCustomization_Font (overrides customizeHeaderRow to create the value editor, customizeChildren as no-op) and a mMetaTypeCustomizationMap entry — the Object branch of getCustomPropertyType() is extended to check this map first. Also fix property name column to always top-align (anchors.top + implicitHeight) so it stays fixed when inline editors like FontBox expand vertically, rather than drifting to row center. Files (8 changed): - resources/Qml/ValueEditor/FontBox.qml (new) - source/src/Customization/PropertyTypeCustomization_Font.h (new) - source/src/Customization/PropertyTypeCustomization_Font.cpp (new) - resources.qrc - source/src/QQuickDetailsViewBasicTypeEditor.cpp - source/src/QQuickDetailsViewMananger.cpp - source/src/QQuickDetailsViewLayoutBuilder.cpp - source/src/PropertyHandleImpl/IPropertyHandleImpl.cpp --- PropertyEditor/resources.qrc | 1 + .../resources/Qml/ValueEditor/FontBox.qml | 355 ++++++++++++++++++ .../PropertyTypeCustomization_Font.cpp | 23 ++ .../PropertyTypeCustomization_Font.h | 12 + .../IPropertyHandleImpl.cpp | 2 +- .../src/QQuickDetailsViewBasicTypeEditor.cpp | 31 ++ .../src/QQuickDetailsViewLayoutBuilder.cpp | 6 +- .../source/src/QQuickDetailsViewMananger.cpp | 10 + 8 files changed, 436 insertions(+), 4 deletions(-) create mode 100644 PropertyEditor/resources/Qml/ValueEditor/FontBox.qml create mode 100644 PropertyEditor/source/src/Customization/PropertyTypeCustomization_Font.cpp create mode 100644 PropertyEditor/source/src/Customization/PropertyTypeCustomization_Font.h diff --git a/PropertyEditor/resources.qrc b/PropertyEditor/resources.qrc index 55b790d..5f86164 100644 --- a/PropertyEditor/resources.qrc +++ b/PropertyEditor/resources.qrc @@ -23,5 +23,6 @@ resources/Qml/ValueEditor/FileSelector.qml resources/Qml/ValueEditor/LineTextBox.qml resources/Qml/ValueEditor/MultiLineTextBox.qml + resources/Qml/ValueEditor/FontBox.qml diff --git a/PropertyEditor/resources/Qml/ValueEditor/FontBox.qml b/PropertyEditor/resources/Qml/ValueEditor/FontBox.qml new file mode 100644 index 0000000..2218474 --- /dev/null +++ b/PropertyEditor/resources/Qml/ValueEditor/FontBox.qml @@ -0,0 +1,355 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import ColorPalette + +Item { + id: control + property font value + property var fontFamilies: [] + property bool expanded: false + property bool internalUpdate: false + property int sizeValue: control.value.pointSize + + readonly property int kRowHeight: 25 + readonly property int kEditRowCount: 5 + readonly property int kExpandedHeight: kRowHeight + kEditRowCount * kRowHeight + (kEditRowCount - 1) * 2 + + implicitHeight: height + implicitWidth: 100 + height: expanded ? kExpandedHeight : kRowHeight + + signal asValueChanged(value: var) + + function setValue(newValue: var) { + if (internalUpdate) return + if (newValue !== value) { + internalUpdate = true + value = newValue + sizeValue = value.pointSize + internalUpdate = false + asValueChanged(value) + } + } + + function applySizeEdit() { + if (control.internalUpdate) return + control.internalUpdate = true + control.value.pointSize = sizeValue + control.internalUpdate = false + control.asValueChanged(control.value) + } + + // Header row + Rectangle { + id: headerRow + height: kRowHeight + width: parent.width + color: ColorPalette.theme.textBoxBackground + border.color: expanded ? ColorPalette.theme.comboBoxItemBackgroundHover : ColorPalette.theme.textBoxBackground + border.width: 1 + + Text { + anchors.fill: parent + anchors.leftMargin: 5 + anchors.rightMargin: 20 + verticalAlignment: Text.AlignVCenter + text: value.family + ", " + value.pointSize + "pt" + color: ColorPalette.theme.textPrimary + elide: Text.ElideRight + } + + Text { + anchors.right: parent.right + anchors.rightMargin: 5 + anchors.verticalCenter: parent.verticalCenter + text: expanded ? "▲" : "▼" + color: ColorPalette.theme.labelPrimary + font.pixelSize: 10 + } + + MouseArea { + anchors.fill: parent + onClicked: control.expanded = !control.expanded + } + } + + // Edit pane — 5 rows + Column { + id: editPane + visible: control.expanded + anchors.top: headerRow.bottom + anchors.left: parent.left + anchors.right: parent.right + spacing: 2 + + // Row 1: Font Family + ComboBox { + id: familyCombo + width: parent.width + height: kRowHeight + model: control.fontFamilies.length > 0 ? control.fontFamilies : [] + currentIndex: -1 + clip: true + + background: Rectangle { + color: ColorPalette.theme.comboBoxBackground + border.color: familyCombo.activeFocus ? ColorPalette.theme.comboBoxItemBackgroundHover : ColorPalette.theme.rowBorder + border.width: 1 + } + + contentItem: Text { + text: familyCombo.currentIndex >= 0 ? familyCombo.currentText : value.family + color: ColorPalette.theme.textPrimary + verticalAlignment: Text.AlignVCenter + leftPadding: 6 + elide: Text.ElideRight + } + + popup: Popup { + y: familyCombo.height + width: familyCombo.width + implicitHeight: Math.min(contentItem.contentHeight, 200) + padding: 1 + + contentItem: ListView { + clip: true + implicitHeight: Math.min(contentHeight, 200) + model: familyCombo.delegateModel + currentIndex: familyCombo.highlightedIndex + ScrollIndicator.vertical: ScrollIndicator {} + } + + background: Rectangle { + color: ColorPalette.theme.comboBoxItemBackground + border.color: ColorPalette.theme.rowBorder + } + } + + // Sync ComboBox selection when fontFamilies arrives (C++ sets it after value) + function syncComboIndex() { + if (control.fontFamilies.length > 0 && value.family) { + var idx = find(value.family) + if (idx >= 0) currentIndex = idx + } + } + + Component.onCompleted: syncComboIndex() + onModelChanged: syncComboIndex() + + onActivated: control.applyEdit() + } + + // Row 2: Font Size — left spacer aligns "Size" with checkbox text below + RowLayout { + width: parent.width + height: kRowHeight + spacing: 0 + + // Spacer to align "Size" with checkbox text (indicator 14 + spacing 6 = 20) + Item { Layout.preferredWidth: 20 } + + Text { + text: "Size" + color: ColorPalette.theme.labelPrimary + verticalAlignment: Text.AlignVCenter + Layout.preferredWidth: 28 + Layout.alignment: Qt.AlignVCenter + } + + // Decrease button + Rectangle { + implicitWidth: 22; implicitHeight: kRowHeight + Layout.alignment: Qt.AlignVCenter + color: sizeDownMa.pressed ? ColorPalette.theme.comboBoxItemBackgroundHover : "transparent" + border.color: ColorPalette.theme.rowBorder + border.width: 0.5 + Text { + anchors.centerIn: parent + text: "−" + font.pixelSize: 12 + color: ColorPalette.theme.labelPrimary + } + MouseArea { + id: sizeDownMa + anchors.fill: parent + onClicked: { if (sizeValue > 1) sizeValue--; applySizeEdit() } + } + } + + // Number input + Rectangle { + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + implicitHeight: kRowHeight + color: ColorPalette.theme.comboBoxBackground + border.color: sizeInput.activeFocus ? ColorPalette.theme.comboBoxItemBackgroundHover : ColorPalette.theme.rowBorder + border.width: 1 + TextInput { + id: sizeInput + anchors.fill: parent + anchors.leftMargin: 4 + anchors.rightMargin: 4 + color: ColorPalette.theme.textPrimary + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + validator: IntValidator { bottom: 1; top: 512 } + inputMethodHints: Qt.ImhDigitsOnly + text: sizeValue + onEditingFinished: { sizeValue = parseInt(text) || 1; applySizeEdit() } + } + } + + // Increase button + Rectangle { + implicitWidth: 22; implicitHeight: kRowHeight + Layout.alignment: Qt.AlignVCenter + color: sizeUpMa.pressed ? ColorPalette.theme.comboBoxItemBackgroundHover : "transparent" + border.color: ColorPalette.theme.rowBorder + border.width: 0.5 + Text { + anchors.centerIn: parent + text: "+" + font.pixelSize: 12 + color: ColorPalette.theme.labelPrimary + } + MouseArea { + id: sizeUpMa + anchors.fill: parent + onClicked: { if (sizeValue < 512) sizeValue++; applySizeEdit() } + } + } + + Text { + text: "pt" + color: ColorPalette.theme.labelPrimary + verticalAlignment: Text.AlignVCenter + Layout.preferredWidth: 20 + Layout.alignment: Qt.AlignVCenter + Layout.leftMargin: 2 + } + } + + // Row 3: Bold + CheckBox { + id: boldCheck + text: "Bold" + height: kRowHeight + checked: control.value.bold + leftPadding: 0 + spacing: 6 + + indicator: Rectangle { + implicitWidth: 14; implicitHeight: 14 + x: boldCheck.leftPadding + y: (boldCheck.height - height) / 2 + color: boldCheck.checked ? ColorPalette.theme.comboBoxItemBackgroundHover : "transparent" + border.color: boldCheck.checked ? ColorPalette.theme.comboBoxItemBackgroundHover : ColorPalette.theme.rowBorder + border.width: 1.5 + radius: 2 + Rectangle { + visible: boldCheck.checked + anchors.centerIn: parent + width: 8; height: 8 + color: ColorPalette.theme.labelPrimary + radius: 1 + } + } + + contentItem: Text { + text: boldCheck.text + color: ColorPalette.theme.textPrimary + verticalAlignment: Text.AlignVCenter + leftPadding: boldCheck.indicator.width + boldCheck.spacing + } + + onToggled: control.applyEdit() + } + + // Row 4: Italic + CheckBox { + id: italicCheck + text: "Italic" + height: kRowHeight + checked: control.value.italic + leftPadding: 0 + spacing: 6 + + indicator: Rectangle { + implicitWidth: 14; implicitHeight: 14 + x: italicCheck.leftPadding + y: (italicCheck.height - height) / 2 + color: italicCheck.checked ? ColorPalette.theme.comboBoxItemBackgroundHover : "transparent" + border.color: italicCheck.checked ? ColorPalette.theme.comboBoxItemBackgroundHover : ColorPalette.theme.rowBorder + border.width: 1.5 + radius: 2 + Rectangle { + visible: italicCheck.checked + anchors.centerIn: parent + width: 8; height: 8 + color: ColorPalette.theme.labelPrimary + radius: 1 + } + } + + contentItem: Text { + text: italicCheck.text + color: ColorPalette.theme.textPrimary + verticalAlignment: Text.AlignVCenter + leftPadding: italicCheck.indicator.width + italicCheck.spacing + } + + onToggled: control.applyEdit() + } + + // Row 5: Underline + CheckBox { + id: underlineCheck + text: "Underline" + height: kRowHeight + checked: control.value.underline + leftPadding: 0 + spacing: 6 + + indicator: Rectangle { + implicitWidth: 14; implicitHeight: 14 + x: underlineCheck.leftPadding + y: (underlineCheck.height - height) / 2 + color: underlineCheck.checked ? ColorPalette.theme.comboBoxItemBackgroundHover : "transparent" + border.color: underlineCheck.checked ? ColorPalette.theme.comboBoxItemBackgroundHover : ColorPalette.theme.rowBorder + border.width: 1.5 + radius: 2 + Rectangle { + visible: underlineCheck.checked + anchors.centerIn: parent + width: 8; height: 8 + color: ColorPalette.theme.labelPrimary + radius: 1 + } + } + + contentItem: Text { + text: underlineCheck.text + color: ColorPalette.theme.textPrimary + verticalAlignment: Text.AlignVCenter + leftPadding: underlineCheck.indicator.width + underlineCheck.spacing + } + + onToggled: control.applyEdit() + } + } + + function applyEdit() { + if (control.internalUpdate) return + control.internalUpdate = true + control.value = Qt.font({ + family: familyCombo.currentText || value.family, + pointSize: sizeValue, + bold: boldCheck.checked, + italic: italicCheck.checked, + underline: underlineCheck.checked + }) + control.internalUpdate = false + control.asValueChanged(control.value) + } +} diff --git a/PropertyEditor/source/src/Customization/PropertyTypeCustomization_Font.cpp b/PropertyEditor/source/src/Customization/PropertyTypeCustomization_Font.cpp new file mode 100644 index 0000000..02cb1db --- /dev/null +++ b/PropertyEditor/source/src/Customization/PropertyTypeCustomization_Font.cpp @@ -0,0 +1,23 @@ +#include "PropertyTypeCustomization_Font.h" +#include "QQuickDetailsViewLayoutBuilder.h" +#include "QQuickDetailsViewMananger.h" +#include "QPropertyHandle.h" + +void PropertyTypeCustomization_Font::customizeHeaderRow(QPropertyHandle* inPropertyHandle, QQuickDetailsViewRowBuilder* inBuilder) +{ + // Same as makePropertyRow but uses the manager's registered type editor + // instead of the Object impl's createValueEditor (which returns nullptr). + auto slotPair = inBuilder->makeNameValueSlot(); + + QQuickItem* nameEditor = inPropertyHandle->setupNameEditor(slotPair.first); + QQuickItem* valueEditor = QQuickDetailsViewManager::Get()->createValueEditor(inPropertyHandle, slotPair.second); + + QQmlEngine* engine = qmlEngine(inBuilder->rootItem()); + QQmlContext* context = qmlContext(inBuilder->rootItem()); + context->parentContext()->setContextProperty("heightProxy", valueEditor ? valueEditor : nameEditor); +} + +void PropertyTypeCustomization_Font::customizeChildren(QPropertyHandle* inPropertyHandle, QQuickDetailsViewLayoutBuilder* inBuilder) +{ + // QFont has no children — don't expand as tree +} diff --git a/PropertyEditor/source/src/Customization/PropertyTypeCustomization_Font.h b/PropertyEditor/source/src/Customization/PropertyTypeCustomization_Font.h new file mode 100644 index 0000000..bbe86db --- /dev/null +++ b/PropertyEditor/source/src/Customization/PropertyTypeCustomization_Font.h @@ -0,0 +1,12 @@ +#ifndef PropertyTypeCustomization_Font_h__ +#define PropertyTypeCustomization_Font_h__ + +#include "IPropertyTypeCustomization.h" + +class PropertyTypeCustomization_Font : public IPropertyTypeCustomization { +protected: + virtual void customizeHeaderRow(QPropertyHandle* inPropertyHandle, QQuickDetailsViewRowBuilder* inBuilder) override; + virtual void customizeChildren(QPropertyHandle* inPropertyHandle, QQuickDetailsViewLayoutBuilder* inBuilder) override; +}; + +#endif // PropertyTypeCustomization_Font_h__ diff --git a/PropertyEditor/source/src/PropertyHandleImpl/IPropertyHandleImpl.cpp b/PropertyEditor/source/src/PropertyHandleImpl/IPropertyHandleImpl.cpp index c95c6ff..0d4acec 100644 --- a/PropertyEditor/source/src/PropertyHandleImpl/IPropertyHandleImpl.cpp +++ b/PropertyEditor/source/src/PropertyHandleImpl/IPropertyHandleImpl.cpp @@ -18,7 +18,7 @@ QQuickItem* IPropertyHandleImpl::createNameEditor(QQuickItem* inParent) Item{ implicitHeight: 25 width: parent.width - anchors.verticalCenter: parent.verticalCenter + anchors.top: parent.top Text { anchors.fill: parent verticalAlignment: Text.AlignVCenter diff --git a/PropertyEditor/source/src/QQuickDetailsViewBasicTypeEditor.cpp b/PropertyEditor/source/src/QQuickDetailsViewBasicTypeEditor.cpp index 4933e8a..ef36aaf 100644 --- a/PropertyEditor/source/src/QQuickDetailsViewBasicTypeEditor.cpp +++ b/PropertyEditor/source/src/QQuickDetailsViewBasicTypeEditor.cpp @@ -3,6 +3,8 @@ #include "QQuickFunctionLibrary.h" #include #include +#include +#include #include #define REGINTER_NUMBER_EDITOR_CREATOR(TypeName, DefaultPrecision) \ @@ -235,4 +237,33 @@ void QQuickDetailsViewManager::RegisterBasicTypeEditor() { connect(handle, SIGNAL(asRequestRollback(QVariant)), valueEditor, SLOT(setValue(QVariant))); return valueEditor; }); + + registerTypeEditor(QMetaType::fromType(), [](QPropertyHandle* handle, QQuickItem* parent)->QQuickItem* { + QQmlEngine* engine = qmlEngine(parent); + QQmlContext* context = qmlContext(parent); + QQmlComponent comp(engine); + comp.setData(R"( + import QtQuick; + import QtQuick.Controls; + import "qrc:/resources/Qml/ValueEditor" + FontBox{ + anchors.verticalCenter: parent.verticalCenter + width: parent.width + } + )", QUrl()); + QVariantMap initialProperties; + initialProperties["parent"] = QVariant::fromValue(parent); + auto valueEditor = qobject_cast(comp.createWithInitialProperties(initialProperties, context)); + if (!comp.errors().isEmpty()) { + qDebug() << comp.errorString(); + } + valueEditor->setParentItem(parent); + valueEditor->setProperty("value", handle->getVar()); + + valueEditor->setProperty("fontFamilies", QFontDatabase::families()); + + connect(valueEditor, SIGNAL(asValueChanged(QVariant)), handle, SLOT(setVar(QVariant))); + connect(handle, SIGNAL(asRequestRollback(QVariant)), valueEditor, SLOT(setValue(QVariant))); + return valueEditor; + }); } diff --git a/PropertyEditor/source/src/QQuickDetailsViewLayoutBuilder.cpp b/PropertyEditor/source/src/QQuickDetailsViewLayoutBuilder.cpp index 2bf2561..59ac8e6 100644 --- a/PropertyEditor/source/src/QQuickDetailsViewLayoutBuilder.cpp +++ b/PropertyEditor/source/src/QQuickDetailsViewLayoutBuilder.cpp @@ -50,16 +50,16 @@ QPair QQuickDetailsViewRowBuilder::makeNameValueSlot() } Item{ id: nameEditorContent - height: parent.height + implicitHeight: 25 + anchors.top: parent.top anchors.left: parent.left anchors.leftMargin: padding + (detailsDelegate.isTreeNode ? (detailsDelegate.depth + 1) * detailsDelegate.indent : 0) anchors.right: splitter.left - anchors.verticalCenter: parent.verticalCenter } Item{ id: valueEditorContent implicitHeight: 25 - anchors.left: splitter.left + anchors.left: splitter.left anchors.leftMargin: 10 anchors.rightMargin: 10 anchors.right: parent.right diff --git a/PropertyEditor/source/src/QQuickDetailsViewMananger.cpp b/PropertyEditor/source/src/QQuickDetailsViewMananger.cpp index 91e7779..91cb3c7 100644 --- a/PropertyEditor/source/src/QQuickDetailsViewMananger.cpp +++ b/PropertyEditor/source/src/QQuickDetailsViewMananger.cpp @@ -7,8 +7,10 @@ #include "Customization/PropertyTypeCustomization_Associative.h" #include "Customization/PropertyTypeCustomization_ObjectDefault.h" #include "Customization/PropertyTypeCustomization_Matrix4x4.h" +#include "Customization/PropertyTypeCustomization_Font.h" #include #include +#include QQuickDetailsViewManager* QQuickDetailsViewManager::Get() { @@ -87,6 +89,10 @@ QSharedPointer QQuickDetailsViewManager::getCustomPr return QSharedPointer::create(); } else if (inHandle->getPropertyType() == QPropertyHandle::Object) { + // Check metaType-level customizations first (for Q_GADGET types like QFont) + if (auto it = mMetaTypeCustomizationMap.find(inHandle->getType()); it != mMetaTypeCustomizationMap.end()) { + return it.value()(); + } const QMetaObject* metaObject = inHandle->metaObject(); for (const auto& It : mClassCustomizationMap.asKeyValueRange()) { if (It.first == metaObject) { @@ -134,5 +140,9 @@ QQuickDetailsViewManager::QQuickDetailsViewManager() RegisterBasicTypeEditor(); registerPropertyTypeCustomization(); + + mMetaTypeCustomizationMap.insert(QMetaType::fromType(), []() { + return QSharedPointer::create(); + }); }