feat: add QFont type editor with FontBox and fix property name top-alignment

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
This commit is contained in:
jessequ 2026-07-27 21:54:57 +08:00
parent 8ea5033f0d
commit 2e93156093
8 changed files with 436 additions and 4 deletions

View File

@ -23,5 +23,6 @@
<file>resources/Qml/ValueEditor/FileSelector.qml</file>
<file>resources/Qml/ValueEditor/LineTextBox.qml</file>
<file>resources/Qml/ValueEditor/MultiLineTextBox.qml</file>
<file>resources/Qml/ValueEditor/FontBox.qml</file>
</qresource>
</RCC>

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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__

View File

@ -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

View File

@ -3,6 +3,8 @@
#include "QQuickFunctionLibrary.h"
#include <QRegularExpression>
#include <QDir>
#include <QFont>
#include <QFontDatabase>
#include <QMetaType>
#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<QFont>(), [](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<QQuickItem*>(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;
});
}

View File

@ -50,11 +50,11 @@ QPair<QQuickItem*, QQuickItem*> 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

View File

@ -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 <QtQuickControls2/QQuickStyle>
#include <QMatrix4x4>
#include <QFont>
QQuickDetailsViewManager* QQuickDetailsViewManager::Get()
{
@ -87,6 +89,10 @@ QSharedPointer<IPropertyTypeCustomization> QQuickDetailsViewManager::getCustomPr
return QSharedPointer<PropertyTypeCustomization_Associative>::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<QMatrix4x4, PropertyTypeCustomization_Matrix4x4>();
mMetaTypeCustomizationMap.insert(QMetaType::fromType<QFont>(), []() {
return QSharedPointer<PropertyTypeCustomization_Font>::create();
});
}