diff options
| author | Richard Moe Gustavsen <richard.gustavsen@qt.io> | 2025-11-19 15:49:47 +0100 |
|---|---|---|
| committer | Richard Moe Gustavsen <richard.gustavsen@qt.io> | 2025-11-27 12:22:49 +0100 |
| commit | c056aaa055187af7290d363d5ef02a4948cd8813 (patch) | |
| tree | d5df750ffefe1d9dbb366177587a9b151d113c78 /src | |
| parent | 8890b4a2d56ebdd51bf550d307f48d60ccf9a632 (diff) | |
StyleKit: add StyleKit to labs
This patch introduces a new styling API for Controls called
StyleKit. StyleKit provides a higher-level, key–value–based
approach to styling applications, serving as an alternative
to working directly with the lower-level Templates API.
The primary goal of StyleKit is to offer a unified API for
styling both Controls and Widgets. The current Templates-based
approach relies heavily on JavaScript, which makes it unsuitable
for use with Widgets. This initial version supports Controls
only; support for Widgets will be added in a future update.
StyleKit is designed to make it easier for designers and UI
developers to:
- Focus on visual styling rather than Template logic (such as
geometry, delegate positioning, and handle placement).
- Allow style properties to propagate, enabling you to factor
out common styling into shared control types and override only
what differs in the more specific control types.
- Style controls independently in each of their states, without
needing nested ternary expressions to check state.
- Define and apply multiple themes with minimal effort.
- Provide different style variations depending on context.
For example, styling a Switch differently when it appears
inside a ToolBar.
[ChangeLog][Qt labs] Introduced new QML module 'StyleKit'.
StyleKit provides a flexible styling framework for Qt Quick
Controls, enabling developers to define reusable styles and
themes using a simple key-value property format.
Change-Id: Iae25324486aea7a7b9b2ce52135327ec7e9b6f59
Reviewed-by: Mitch Curtis <mitch.curtis@qt.io>
Diffstat (limited to 'src')
67 files changed, 8810 insertions, 0 deletions
diff --git a/src/labs/CMakeLists.txt b/src/labs/CMakeLists.txt index ae964701cf..4658c883f0 100644 --- a/src/labs/CMakeLists.txt +++ b/src/labs/CMakeLists.txt @@ -25,6 +25,10 @@ if(QT_FEATURE_quick_shadereffect AND TARGET Qt::Quick) add_subdirectory(wavefrontmesh) endif() +if(QT_FEATURE_quickcontrols2_stylekit) + add_subdirectory(stylekit) +endif() + if(QT_FEATURE_systemsemaphore AND TARGET Qt::Quick) add_subdirectory(sharedimage) endif() diff --git a/src/labs/stylekit/Button.qml b/src/labs/stylekit/Button.qml new file mode 100644 index 0000000000..ba36633142 --- /dev/null +++ b/src/labs/stylekit/Button.qml @@ -0,0 +1,56 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +// Qt-Security score:significant reason:default + +import QtQuick +import QtQuick.Controls.impl +import QtQuick.Templates as T +import Qt.labs.StyleKit +import Qt.labs.StyleKit.impl + +T.Button { + id: control + + implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, + implicitContentWidth + leftPadding + rightPadding) + implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, + implicitContentHeight + topPadding + bottomPadding) + + leftPadding: styleReader.leftPadding + topPadding: styleReader.topPadding + rightPadding: styleReader.rightPadding + bottomPadding: styleReader.bottomPadding + spacing: styleReader.spacing + + icon.width: 24 + icon.height: 24 + icon.color: styleReader.text.color + + StyleKitReader { + id: styleReader + type: control.flat ? StyleKitReader.FlatButton : StyleKitReader.Button + enabled: control.enabled + focused: control.activeFocus + checked: control.checked + hovered: control.hovered || control.pressed + pressed: control.pressed + palette: control.palette + } + + contentItem: IconLabel { + spacing: control.spacing + mirrored: control.mirrored + display: control.display + + icon: control.icon + text: control.text + font: control.font + color: styleReader.text.color + alignment: styleReader.text.alignment + } + + background: BackgroundDelegate { + parentControl: control + backgroundProperties: styleReader.background + } +} diff --git a/src/labs/stylekit/CMakeLists.txt b/src/labs/stylekit/CMakeLists.txt new file mode 100644 index 0000000000..b981bcac62 --- /dev/null +++ b/src/labs/stylekit/CMakeLists.txt @@ -0,0 +1,59 @@ +# Copyright (C) 2025 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +##################################################################### +## StyleKit Module: +##################################################################### + +add_subdirectory(impl) + +qt_internal_add_qml_module(QtQuickStyleKit + URI Qt.labs.StyleKit + IMPORTS QtQuick.Controls.Basic + SOURCES + qqstylekit_p.h qqstylekit.cpp + qqstylekitcontrol_p.h qqstylekitcontrol.cpp + qqstylekitcontrols_p.h qqstylekitcontrols.cpp + qqstylekitcontrolstate_p.h qqstylekitcontrolstate.cpp + qqstylekitcontrolproperties_p.h qqstylekitcontrolproperties.cpp + qqstylekitcustomcontrol_p.h qqstylekitcustomcontrol.cpp + qqstylekitdebug_p.h qqstylekitdebug.cpp + qqstylekitglobal_p.h qqstylekitglobal.cpp + qqstylekitpalette_p.h qqstylekitpalette.cpp + qqstylekitfont_p.h qqstylekitfont.cpp + qqstylekitpropertyresolver_p.h qqstylekitpropertyresolver.cpp + qqstylekitreader_p.h qqstylekitreader.cpp + qqstylekitstorage_p.h qqstylekitstorage.cpp + qqstylekitstyle_p.h qqstylekitstyle.cpp + qqstylekittheme_p.h qqstylekittheme.cpp + qqstylekitthemeproperties_p.h qqstylekitthemeproperties.cpp + qqstylekitcustomtheme_p.h qqstylekitcustomtheme.cpp + qqstylekitvariation_p.h qqstylekitvariation.cpp + DEFINES + QT_NO_CAST_FROM_ASCII + QT_NO_CAST_TO_ASCII + QML_FILES + Button.qml + CheckBox.qml + ComboBox.qml + Frame.qml + ItemDelegate.qml + RadioButton.qml + RangeSlider.qml + Slider.qml + Style.qml + StyleKitAnimation.qml + StyleKitDelegate.qml + SpinBox.qml + Switch.qml + TextField.qml + Page.qml + Pane.qml + Popup.qml + LIBRARIES + Qt6::GuiPrivate + Qt6::Quick + Qt6::QuickPrivate + Qt6::QuickTemplates2 + Qt6::QuickTemplates2Private +) diff --git a/src/labs/stylekit/CheckBox.qml b/src/labs/stylekit/CheckBox.qml new file mode 100644 index 0000000000..b8750a40f7 --- /dev/null +++ b/src/labs/stylekit/CheckBox.qml @@ -0,0 +1,87 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +// Qt-Security score:significant reason:default + +import QtQuick +import QtQuick.Templates as T +import QtQuick.Controls.impl +import Qt.labs.StyleKit +import Qt.labs.StyleKit.impl + +T.CheckBox { + id: control + + implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, + checkBoxLayout.implicitWidth + implicitContentWidth) + implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, + implicitContentHeight + styleReader.topPadding + styleReader.bottomPadding, + checkBoxLayout.implicitHeight) + + leftPadding: checkBoxLayout.padding.left + topPadding: checkBoxLayout.padding.top + rightPadding: checkBoxLayout.padding.right + bottomPadding: checkBoxLayout.padding.bottom + + spacing: styleReader.spacing + + StyleKitReader { + id: styleReader + type: StyleKitReader.CheckBox + enabled: control.enabled + focused: control.activeFocus + checked: control.checked + hovered: control.hovered + pressed: control.pressed + palette: control.palette + } + + StyleKitLayout { + id: checkBoxLayout + container: control + contentMargins { + left: styleReader.leftPadding + right: styleReader.rightPadding + top: styleReader.topPadding + bottom: styleReader.bottomPadding + } + layoutItems: [ + // We don't lay out the contentItem here because it occupies the remaining space + // as calculated by control internal logic. + StyleKitLayoutItem { + id: indicatorItem + item: control.indicator + alignment: styleReader.indicator.alignment + margins.left: styleReader.indicator.leftMargin + margins.right: styleReader.indicator.rightMargin + margins.top: styleReader.indicator.topMargin + margins.bottom: styleReader.indicator.bottomMargin + fillWidth: styleReader.indicator.implicitWidth === Style.Stretch + fillHeight: styleReader.indicator.implicitHeight === Style.Stretch + } + ] + spacing: styleReader.spacing + mirrored: control.mirrored + } + + indicator: IndicatorDelegate { + parentControl: control + indicatorProperties: styleReader.indicator + x: indicatorItem.x + y: indicatorItem.y + width: indicatorItem.width + height: indicatorItem.height + } + + contentItem: CheckLabel { + text: control.text + font: control.font + color: styleReader.text.color + horizontalAlignment: styleReader.text.alignment & Qt.AlignHorizontal_Mask + verticalAlignment: styleReader.text.alignment & Qt.AlignVertical_Mask + } + + background: BackgroundDelegate { + parentControl: control + backgroundProperties: styleReader.background + } +} diff --git a/src/labs/stylekit/ComboBox.qml b/src/labs/stylekit/ComboBox.qml new file mode 100644 index 0000000000..d51c8906ef --- /dev/null +++ b/src/labs/stylekit/ComboBox.qml @@ -0,0 +1,135 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +// Qt-Security score:significant reason:default + +import QtQuick +import QtQuick.Controls.impl +import QtQuick.Templates as T +import Qt.labs.StyleKit +import Qt.labs.StyleKit.impl + +T.ComboBox { + id: control + + implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, + comboLayout.implicitWidth + implicitContentWidth) + implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, + implicitContentHeight + styleReader.topPadding + styleReader.bottomPadding, + comboLayout.implicitHeight) + + leftPadding: comboLayout.padding.left + rightPadding: comboLayout.padding.right + topPadding: comboLayout.padding.top + bottomPadding: comboLayout.padding.bottom + + spacing: styleReader.spacing + + StyleKitReader { + id: styleReader + type: StyleKitReader.ComboBox + enabled: control.enabled + focused: control.activeFocus + hovered: control.hovered || control.pressed + pressed: control.down + highlighted: control.highlighted + // variations: editable, flat + palette: control.palette + } + + StyleKitLayout { + id: comboLayout + container: control + contentMargins { + left: styleReader.leftPadding + right: styleReader.rightPadding + top: styleReader.topPadding + bottom: styleReader.bottomPadding + } + layoutItems: [ + // We don't lay out the contentItem here because it occupies the remaining space + // as calculated by control internal logic. + StyleKitLayoutItem { + id: indicatorItem + item: control.indicator + alignment: Qt.AlignRight | Qt.AlignVCenter + margins.left: styleReader.indicator.leftMargin + margins.right: styleReader.indicator.rightMargin + margins.top: styleReader.indicator.topMargin + margins.bottom: styleReader.indicator.bottomMargin + fillWidth: styleReader.indicator.implicitWidth === Style.Stretch + fillHeight: styleReader.indicator.implicitHeight === Style.Stretch + } + ] + spacing: styleReader.spacing + mirrored: control.mirrored + } + + // TODO: Use the ItemDelegate control as is for now, + // later we might want to customize it seperately for combobox using control "variations" + delegate: ItemDelegate { + required property var model + required property int index + + width: ListView.view.width + text: model[control.textRole] + palette.text: control.palette.text + palette.highlightedText: control.palette.highlightedText + highlighted: control.highlightedIndex === index + hoverEnabled: control.hoverEnabled + } + + indicator: IndicatorDelegate { + parentControl: control + indicatorProperties: styleReader.indicator + x: indicatorItem.x + y: indicatorItem.y + width: indicatorItem.width + height: indicatorItem.height + } + + contentItem: T.TextField { + text: control.editable ? control.editText : control.displayText + enabled: control.editable + autoScroll: control.editable + readOnly: control.down + inputMethodHints: control.inputMethodHints + validator: control.validator + selectByMouse: control.selectTextByMouse + + color: styleReader.text.color + verticalAlignment: styleReader.text.alignment & Qt.AlignVertical_Mask + horizontalAlignment: styleReader.text.alignment & Qt.AlignHorizontal_Mask + + selectionColor: control.palette.highlight + selectedTextColor: control.palette.highlightedText + } + + background: BackgroundDelegate { + parentControl: control + backgroundProperties: styleReader.background + } + + // TODO: Use the Popup control as is for now, + // later we might want to customize it seperately for combobox using control "variations" + popup: Popup { + y: control.height + width: control.width + height: Math.min(contentItem.implicitHeight, control.Window.height - topMargin - bottomMargin) + + palette.text: control.palette.text + palette.highlight: control.palette.highlight + palette.highlightedText: control.palette.highlightedText + palette.windowText: control.palette.windowText + palette.buttonText: control.palette.buttonText + + contentItem: ListView { + clip: true + implicitHeight: contentHeight + model: control.delegateModel + currentIndex: control.highlightedIndex + highlightMoveDuration: 0 + + T.ScrollIndicator.vertical: ScrollIndicator { } + } + } +} diff --git a/src/labs/stylekit/Frame.qml b/src/labs/stylekit/Frame.qml new file mode 100644 index 0000000000..9d09b258f9 --- /dev/null +++ b/src/labs/stylekit/Frame.qml @@ -0,0 +1,35 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +// Qt-Security score:significant reason:default + +import QtQuick +import QtQuick.Controls.impl +import QtQuick.Templates as T +import Qt.labs.StyleKit +import Qt.labs.StyleKit.impl + +T.Frame { + id: control + implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, + implicitContentWidth + leftPadding + rightPadding) + implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, + implicitContentHeight + topPadding + bottomPadding) + + leftPadding: styleReader.leftPadding + topPadding: styleReader.topPadding + rightPadding: styleReader.rightPadding + bottomPadding: styleReader.bottomPadding + + StyleKitReader { + id: styleReader + type: StyleKitReader.Frame + enabled: control.enabled + focused: control.activeFocus + palette: control.palette + } + + background: BackgroundDelegate { + parentControl: control + backgroundProperties: styleReader.background + } +} diff --git a/src/labs/stylekit/ItemDelegate.qml b/src/labs/stylekit/ItemDelegate.qml new file mode 100644 index 0000000000..6542eb239b --- /dev/null +++ b/src/labs/stylekit/ItemDelegate.qml @@ -0,0 +1,57 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +// Qt-Security score:significant reason:default + +import QtQuick +import QtQuick.Controls.impl +import QtQuick.Templates as T +import Qt.labs.StyleKit +import Qt.labs.StyleKit.impl + +T.ItemDelegate { + id: control + + implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, + implicitContentWidth + leftPadding + rightPadding) + implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, + implicitContentHeight + topPadding + bottomPadding, + implicitIndicatorHeight + topPadding + bottomPadding) + + leftPadding: styleReader.leftPadding + topPadding: styleReader.topPadding + rightPadding: styleReader.rightPadding + bottomPadding: styleReader.bottomPadding + spacing: styleReader.spacing + + icon.width: 16 + icon.height: 16 + icon.color: styleReader.text.color + + StyleKitReader { + id: styleReader + type: StyleKitReader.ItemDelegate + enabled: control.enabled + focused: control.activeFocus + checked: control.checked + hovered: control.hovered + pressed: control.pressed + highlighted: control.highlighted + palette: control.palette + } + + contentItem: IconLabel { + spacing: control.spacing + mirrored: control.mirrored + display: control.display + icon: control.icon + text: control.text + font: control.font + color: styleReader.text.color + alignment: styleReader.text.alignment + } + + background: BackgroundDelegate { + parentControl: control + backgroundProperties: styleReader.background + } +} diff --git a/src/labs/stylekit/Page.qml b/src/labs/stylekit/Page.qml new file mode 100644 index 0000000000..cf2d58cc80 --- /dev/null +++ b/src/labs/stylekit/Page.qml @@ -0,0 +1,39 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +// Qt-Security score:significant reason:default + +import QtQuick +import QtQuick.Controls.impl +import QtQuick.Templates as T +import Qt.labs.StyleKit +import Qt.labs.StyleKit.impl + +T.Page { + id: control + + implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, + implicitContentWidth + leftPadding + rightPadding, + implicitHeaderWidth, implicitFooterWidth) + implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, + implicitContentHeight + topPadding + bottomPadding + + (implicitHeaderHeight > 0 ? implicitHeaderHeight + spacing : 0) + + (implicitFooterHeight > 0 ? implicitFooterHeight + spacing : 0)) + + leftPadding: styleReader.leftPadding + topPadding: styleReader.topPadding + rightPadding: styleReader.rightPadding + bottomPadding: styleReader.bottomPadding + + StyleKitReader { + id: styleReader + type: StyleKitReader.Page + enabled: control.enabled + focused: control.activeFocus + palette: control.palette + } + + background: BackgroundDelegate { + parentControl: control + backgroundProperties: styleReader.background + } +} diff --git a/src/labs/stylekit/Pane.qml b/src/labs/stylekit/Pane.qml new file mode 100644 index 0000000000..615e6db7f2 --- /dev/null +++ b/src/labs/stylekit/Pane.qml @@ -0,0 +1,36 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +// Qt-Security score:significant reason:default + +import QtQuick +import QtQuick.Controls.impl +import QtQuick.Templates as T +import Qt.labs.StyleKit +import Qt.labs.StyleKit.impl + +T.Pane { + id: control + + implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, + implicitContentWidth + leftPadding + rightPadding) + implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, + implicitContentHeight + topPadding + bottomPadding) + + leftPadding: styleReader.leftPadding + topPadding: styleReader.topPadding + rightPadding: styleReader.rightPadding + bottomPadding: styleReader.bottomPadding + + StyleKitReader { + id: styleReader + type: StyleKitReader.Pane + enabled: control.enabled + focused: control.activeFocus + palette: control.palette + } + + background: BackgroundDelegate { + parentControl: control + backgroundProperties: styleReader.background + } +} diff --git a/src/labs/stylekit/Popup.qml b/src/labs/stylekit/Popup.qml new file mode 100644 index 0000000000..adfcc74f08 --- /dev/null +++ b/src/labs/stylekit/Popup.qml @@ -0,0 +1,38 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +// Qt-Security score:significant reason:default + +import QtQuick +import QtQuick.Controls.impl +import QtQuick.Templates as T +import Qt.labs.StyleKit +import Qt.labs.StyleKit.impl + +T.Popup { + id: control + + readonly property StyleKitReader __styleReader: StyleKitReader { + // TODO: making StyleKitReader a child object of T.Popup makes the + // popup not open on press. So use a __styleReader property for now + // until we know the reason why. + type: StyleKitReader.Popup + enabled: control.enabled + focused: control.activeFocus + palette: control.palette + } + + implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, + implicitContentWidth + leftPadding + rightPadding) + implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, + implicitContentHeight + topPadding + bottomPadding) + + leftPadding: __styleReader.leftPadding + topPadding: __styleReader.topPadding + rightPadding: __styleReader.rightPadding + bottomPadding: __styleReader.bottomPadding + + background: BackgroundDelegate { + parentControl: control + backgroundProperties: control.__styleReader.background + } +} diff --git a/src/labs/stylekit/RadioButton.qml b/src/labs/stylekit/RadioButton.qml new file mode 100644 index 0000000000..77c03ccd56 --- /dev/null +++ b/src/labs/stylekit/RadioButton.qml @@ -0,0 +1,87 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +// Qt-Security score:significant reason:default + +import QtQuick +import QtQuick.Controls.impl +import QtQuick.Templates as T +import Qt.labs.StyleKit +import Qt.labs.StyleKit.impl + +T.RadioButton { + id: control + + implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, + radioButtonLayout.implicitWidth + implicitContentWidth) + implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, + implicitContentHeight + styleReader.topPadding + styleReader.bottomPadding, + radioButtonLayout.implicitHeight) + + leftPadding: radioButtonLayout.padding.left + topPadding: radioButtonLayout.padding.top + rightPadding: radioButtonLayout.padding.right + bottomPadding: radioButtonLayout.padding.bottom + + spacing: styleReader.spacing + + StyleKitReader { + id: styleReader + type: StyleKitReader.RadioButton + enabled: control.enabled + focused: control.activeFocus + checked: control.checked + hovered: control.hovered + pressed: control.pressed + palette: control.palette + } + + StyleKitLayout { + id: radioButtonLayout + container: control + contentMargins { + left: styleReader.leftPadding + right: styleReader.rightPadding + top: styleReader.topPadding + bottom: styleReader.bottomPadding + } + layoutItems: [ + // We don't lay out the contentItem here because it occupies the remaining space + // as calculated by control internal logic. + StyleKitLayoutItem { + id: indicatorItem + item: control.indicator + alignment: styleReader.indicator.alignment + margins.left: styleReader.indicator.leftMargin + margins.right: styleReader.indicator.rightMargin + margins.top: styleReader.indicator.topMargin + margins.bottom: styleReader.indicator.bottomMargin + fillWidth: styleReader.indicator.implicitWidth === Style.Stretch + fillHeight: styleReader.indicator.implicitHeight === Style.Stretch + } + ] + spacing: styleReader.spacing + mirrored: control.mirrored + } + + indicator: IndicatorDelegate { + parentControl: control + indicatorProperties: styleReader.indicator + x: indicatorItem.x + y: indicatorItem.y + width: indicatorItem.width + height: indicatorItem.height + } + + contentItem: CheckLabel { + text: control.text + font: control.font + color: styleReader.text.color + horizontalAlignment: styleReader.text.alignment & Qt.AlignHorizontal_Mask + verticalAlignment: styleReader.text.alignment & Qt.AlignVertical_Mask + } + + background: BackgroundDelegate { + parentControl: control + backgroundProperties: styleReader.background + } +} diff --git a/src/labs/stylekit/RangeSlider.qml b/src/labs/stylekit/RangeSlider.qml new file mode 100644 index 0000000000..2467f23450 --- /dev/null +++ b/src/labs/stylekit/RangeSlider.qml @@ -0,0 +1,135 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +// Qt-Security score:significant reason:default + +import QtQuick +import QtQuick.Templates as T +import Qt.labs.StyleKit +import Qt.labs.StyleKit.impl + +T.RangeSlider { + id: control + + implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, + first.implicitHandleWidth + leftPadding + rightPadding, + second.implicitHandleWidth + leftPadding + rightPadding, + implicitContentWidth + leftPadding + rightPadding) + implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, + first.implicitHandleHeight + topPadding + bottomPadding, + second.implicitHandleHeight + topPadding + bottomPadding, + implicitContentHeight + topPadding + bottomPadding) + + leftPadding: styleReaderFirst.leftPadding + topPadding: styleReaderFirst.topPadding + rightPadding: styleReaderFirst.rightPadding + bottomPadding: styleReaderFirst.bottomPadding + spacing: styleReaderFirst.spacing + + states: [ + /* The delegate logic is moved out of the delegate, to relieve the style + * developer from having to re-invent it if he changes the delegate. To + * disable it, 'states' can be set to an empty array from the outside. */ + State { + when: horizontal + PropertyChanges { + // The width of the handle is fixed. But its margins are used + // to describe the area within the content area that the handle + // is allowed to move. + control.first.handle.x: leftPadding + styleReaderFirst.handle.leftMargin + + (first.visualPosition * (availableWidth - styleReaderFirst.handle.leftMargin + - styleReaderFirst.handle.rightMargin - first.handle.width)) + // The height of the handle is fixed. But the margins are used to + // shift its position up or down from the center of the content area. + control.first.handle.y: topPadding + + styleReaderFirst.handle.topMargin - styleReaderFirst.handle.bottomMargin + + (availableHeight - first.handle.height) / 2 + + control.second.handle.x: leftPadding + styleReaderSecond.handle.leftMargin + + (second.visualPosition * (availableWidth - styleReaderSecond.handle.leftMargin + - styleReaderSecond.handle.rightMargin - second.handle.width)) + + control.second.handle.y: topPadding + + styleReaderSecond.handle.topMargin - styleReaderSecond.handle.bottomMargin + + (availableHeight - second.handle.height) / 2 + } + }, + State { + // Note: we deliberatly flip margins (but not padding) in vertical + // mode since a vertical slider is logically a flipped horizontal slider. + when: control.vertical + PropertyChanges { + control.first.handle.x: leftPadding + + styleReaderFirst.handle.topMargin - styleReaderFirst.handle.bottomMargin + + (availableWidth + first.handle.height) / 2 + control.first.handle.y: topPadding + styleReaderFirst.handle.leftMargin + + (first.visualPosition * (availableHeight - styleReaderFirst.handle.leftMargin + - styleReaderFirst.handle.rightMargin - first.handle.width)) + control.first.handle.rotation: 90 + control.first.handle.transformOrigin: Item.TopLeft + + control.second.handle.x: leftPadding + + styleReaderSecond.handle.topMargin - styleReaderSecond.handle.bottomMargin + + (availableWidth + second.handle.height) / 2 + control.second.handle.y: topPadding + styleReaderSecond.handle.leftMargin + + (second.visualPosition * (availableHeight - styleReaderSecond.handle.leftMargin + - styleReaderSecond.handle.rightMargin - second.handle.width)) + control.second.handle.rotation: 90 + control.second.handle.transformOrigin: Item.TopLeft + } + } + ] + + readonly property StyleKitReader styleReader: styleReaderFirst + + StyleKitReader { + id: styleReaderFirst + type: StyleKitReader.Slider + enabled: control.enabled + focused: control.activeFocus + hovered: control.first.hovered + pressed: control.first.pressed + palette: control.palette + vertical: !control.horizontal + } + + StyleKitReader { + id: styleReaderSecond + type: StyleKitReader.Slider + enabled: control.enabled + focused: control.activeFocus + hovered: control.second.hovered + pressed: control.second.pressed + palette: control.palette + vertical: !control.horizontal + } + + StyleKitReader { + id: styleReaderIndicator + type: StyleKitReader.Slider + enabled: control.enabled + focused: control.activeFocus + hovered: control.hovered || control.first.hovered || control.second.hovered + pressed: control.first.pressed || control.second.pressed + palette: control.palette + vertical: !control.horizontal + } + + first.handle: HandleDelegate { + parentControl: control + handleProperties: styleReaderFirst.handle.first + } + + second.handle: HandleDelegate { + parentControl: control + handleProperties: styleReaderSecond.handle.second + } + + background: BackgroundAndIndicatorDelegate { + parentControl: control + indicatorProperties: styleReaderIndicator.indicator + backgroundProperties: styleReaderIndicator.background + indicator.firstProgress: control.first.position + indicator.secondProgress: control.second.position + vertical: control.vertical + } +} diff --git a/src/labs/stylekit/Slider.qml b/src/labs/stylekit/Slider.qml new file mode 100644 index 0000000000..2b4ca71c96 --- /dev/null +++ b/src/labs/stylekit/Slider.qml @@ -0,0 +1,87 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +// Qt-Security score:significant reason:default + +import QtQuick +import QtQuick.Templates as T +import Qt.labs.StyleKit +import Qt.labs.StyleKit.impl + +T.Slider { + id: control + + implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, + implicitHandleWidth + leftPadding + rightPadding, + implicitContentWidth + leftPadding + rightPadding) + + implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, + implicitHandleHeight + topPadding + bottomPadding, + implicitContentHeight + topPadding + bottomPadding) + + leftPadding: styleReader.leftPadding + topPadding: styleReader.topPadding + rightPadding: styleReader.rightPadding + bottomPadding: styleReader.bottomPadding + spacing: styleReader.spacing + + states: [ + /* The delegate logic is moved out of the delegate, to relieve the style + * developer from having to re-invent it if he changes the delegate. To + * disable it, 'states' can be set to an empty array from the outside. */ + State { + when: horizontal + PropertyChanges { + // The width of the handle is fixed. But its margins are is used + // to describe the area within the content area that the handle + // is allowed to move. + control.handle.x: leftPadding + styleReader.handle.leftMargin + + (visualPosition * (availableWidth - styleReader.handle.leftMargin + - styleReader.handle.rightMargin - handle.width)) + // The height of the handle is fixed. But the margins are used to + // shift its position up or down from the center of the content area. + control.handle.y: topPadding + + styleReader.handle.topMargin - styleReader.handle.bottomMargin + + (availableHeight - control.handle.height) / 2 + } + }, + State { + // Note: we deliberatly flip margins (but not padding) in vertical + // mode since a vertical slider is logically a flipped horizontal slider. + when: control.vertical + PropertyChanges { + control.handle.x: leftPadding + + styleReader.handle.topMargin - styleReader.handle.bottomMargin + + (availableWidth + handle.height) / 2 + control.handle.y: topPadding + styleReader.handle.leftMargin + + (visualPosition * (availableHeight - styleReader.handle.leftMargin + - styleReader.handle.rightMargin - handle.width)) + control.handle.rotation: 90 + control.handle.transformOrigin: Item.TopLeft + } + } + ] + + StyleKitReader { + id: styleReader + type: StyleKitReader.Slider + enabled: control.enabled + focused: control.activeFocus + hovered: control.hovered + pressed: control.pressed + palette: control.palette + vertical: !control.horizontal + } + + handle: HandleDelegate { + parentControl: control + handleProperties: styleReader.handle + } + + background: BackgroundAndIndicatorDelegate { + parentControl: control + indicatorProperties: styleReader.indicator + backgroundProperties: styleReader.background + indicator.secondProgress: control.position + vertical: control.vertical + } +} diff --git a/src/labs/stylekit/SpinBox.qml b/src/labs/stylekit/SpinBox.qml new file mode 100644 index 0000000000..ac2ea61e2c --- /dev/null +++ b/src/labs/stylekit/SpinBox.qml @@ -0,0 +1,140 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +// Qt-Security score:significant reason:default + +import QtQuick +import QtQuick.Controls.impl +import QtQuick.Templates as T +import Qt.labs.StyleKit +import Qt.labs.StyleKit.impl + +T.SpinBox { + id: control + + implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, + implicitContentWidth + leftPadding + rightPadding) + implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, + implicitContentHeight + topPadding + bottomPadding) + + leftPadding: spinBoxLayout.padding.left + topPadding: spinBoxLayout.padding.top + rightPadding: spinBoxLayout.padding.right + bottomPadding: spinBoxLayout.padding.bottom + spacing: styleReader.spacing + + StyleKitReader { + id: styleReader + type: StyleKitReader.SpinBox + enabled: control.enabled + focused: control.activeFocus + hovered: control.hovered || control.down.hovered || control.up.hovered + pressed: control.down.pressed || control.up.pressed + palette: control.palette + } + + StyleKitReader { + id: upProperties + type: StyleKitReader.SpinBox + enabled: control.enabled + focused: control.activeFocus + hovered: control.up.hovered + pressed: control.up.pressed + palette: control.palette + } + + StyleKitReader { + id: downProperties + type: StyleKitReader.SpinBox + enabled: control.enabled + focused: control.activeFocus + hovered: control.down.hovered + pressed: control.down.pressed + palette: control.palette + } + + StyleKitLayout { + id: spinBoxLayout + container: control + contentMargins { + // Copy the other styles, and add indicator width to padding + left: styleReader.leftPadding + right: styleReader.rightPadding + top: styleReader.topPadding + bottom: styleReader.bottomPadding + } + layoutItems: [ + // We don't lay out the contentItem here because it occupies the remaining space + // as calculated by control internal logic. + StyleKitLayoutItem { + id: upIndicatorItem + item: control.up.indicator + alignment: styleReader.indicator.up.alignment + margins.left: styleReader.indicator.up.leftMargin + margins.right: styleReader.indicator.up.rightMargin + margins.top: styleReader.indicator.up.topMargin + margins.bottom: styleReader.indicator.up.bottomMargin + fillWidth: styleReader.indicator.up.implicitWidth === Style.Stretch + fillHeight: styleReader.indicator.up.implicitHeight === Style.Stretch + }, + StyleKitLayoutItem { + id: downIndicatorItem + item: control.down.indicator + alignment: styleReader.indicator.down.alignment + margins.left: styleReader.indicator.down.leftMargin + margins.right: styleReader.indicator.down.rightMargin + margins.top: styleReader.indicator.down.topMargin + margins.bottom: styleReader.indicator.down.bottomMargin + fillWidth: styleReader.indicator.down.implicitWidth === Style.Stretch + fillHeight: styleReader.indicator.down.implicitHeight === Style.Stretch + } + ] + spacing: styleReader.spacing + mirrored: control.mirrored + } + + validator: IntValidator { + locale: control.locale.name + bottom: Math.min(control.from, control.to) + top: Math.max(control.from, control.to) + } + + contentItem: TextInput { + z: 2 + text: control.displayText + + font: control.font + selectionColor: control.palette.highlight + selectedTextColor: control.palette.highlightedText + color: styleReader.text.color + horizontalAlignment: styleReader.text.alignment & Qt.AlignHorizontal_Mask + verticalAlignment: styleReader.text.alignment & Qt.AlignVertical_Mask + + readOnly: !control.editable + validator: control.validator + inputMethodHints: control.inputMethodHints + clip: width < implicitWidth + } + + up.indicator: IndicatorDelegate { + parentControl: control + indicatorProperties: upProperties.indicator.up + x: upIndicatorItem.x + y: upIndicatorItem.y + width: upIndicatorItem.width + height: upIndicatorItem.height + } + + down.indicator: IndicatorDelegate { + parentControl: control + indicatorProperties: downProperties.indicator.down + x: downIndicatorItem.x + y: downIndicatorItem.y + width: downIndicatorItem.width + height: downIndicatorItem.height + } + + background: BackgroundDelegate { + parentControl: control + backgroundProperties: styleReader.background + } +} diff --git a/src/labs/stylekit/Style.qml b/src/labs/stylekit/Style.qml new file mode 100644 index 0000000000..20163c250b --- /dev/null +++ b/src/labs/stylekit/Style.qml @@ -0,0 +1,23 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +// Qt-Security score:significant reason:default + +import QtQuick +import Qt.labs.StyleKit +import Qt.labs.StyleKit.impl + +BaseStyle { + id: style + + fallbackStyle: FallbackStyle {} + + /* This function is a work-around (called from c++) since we haven't found a way to + * instantiate a Theme inside the context of a Style from c++ otherwise. Doing so is + * needed in order to allow custom style properties to be added as children of a + * Style, and at the same time, be able to access them from within a Theme instance. + * For this to work, a style with custom properties also needs to set + * 'pragma ComponentBehavior: Bound'. */ + function createThemeInsideStyle(themeComponent) { + return themeComponent.createObject(style) + } +} diff --git a/src/labs/stylekit/StyleKitAnimation.qml b/src/labs/stylekit/StyleKitAnimation.qml new file mode 100644 index 0000000000..e7947eb48a --- /dev/null +++ b/src/labs/stylekit/StyleKitAnimation.qml @@ -0,0 +1,122 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +// Qt-Security score:significant reason:default + +import QtQuick + +ParallelAnimation { + id: root + property alias easing: colorAnimation.easing + property alias duration: colorAnimation.duration + + property bool animateColors: false + property bool animateControlGeometry: false + + property bool animateBackgroundColors: false + property bool animateBackgroundGeometry: false + property bool animateBackgroundRadii: false + property bool animateBackgroundBorder: false + property bool animateBackgroundShadow: false + property bool animateBackgroundScaleAndRotation: false + + property bool animateHandleColors: false + property bool animateHandleGeometry: false + property bool animateHandleRadii: false + property bool animateHandleBorder: false + property bool animateHandleShadow: false + property bool animateHandleScaleAndRotation: false + + property bool animateIndicatorColors: false + property bool animateIndicatorGeometry: false + property bool animateIndicatorBorder: false + property bool animateIndicatorRadii: false + property bool animateIndicatorShadow: false + property bool animateIndicatorScaleAndRotation: false + + readonly property string __geometryProps: + "$.implicitWidth, $.implicitHeight, " + + "$.margins, $.leftMargin, $.rightMargin, $.topMargin, $.bottomMargin, " + + "$.leftPadding, $.rightPadding, $.topPadding, $.bottomPadding, " + + "$.minimumWidth, " + readonly property string __colorProps: + "$.color, $.border.color, $.image.color, $.shadow.color, " + readonly property string __radiiProps: + "$.radius, $.topLeftRadius, $.topRightRadius, $.bottomLeftRadius, $.bottomRightRadius, " + readonly property string __scaleAndRotationProps: "$.scale, $.rotation, " + readonly property string __borderProps: "$.border.width, " + readonly property string __shadowProps: + "$.shadow.verticalOffset, $.shadow.horizontalOffset, " + + "$.shadow.scale, $.shadow.blur, " + + + function __animateIndicators(doAnimate, anim, props) { + if (!doAnimate) + return + anim.properties += props.replace(/\$/g, "indicator") + anim.properties += props.replace(/\$/g, "indicator.foreground") + anim.properties += props.replace(/\$/g, "indicator.up") + anim.properties += props.replace(/\$/g, "indicator.up.foreground") + anim.properties += props.replace(/\$/g, "indicator.down") + anim.properties += props.replace(/\$/g, "indicator.down.foreground") + } + + function __animateHandles(doAnimate, anim, props) { + if (!doAnimate) + return + anim.properties += props.replace(/\$/g, "handle") + anim.properties += props.replace(/\$/g, "handle.first") + anim.properties += props.replace(/\$/g, "handle.second") + } + + Component.onCompleted: { + if (animateControlGeometry) + numberAnimation.properties += "spacing, padding, leftPadding, rightPadding, topPadding, bottomPadding" + + if (animateBackgroundGeometry) + numberAnimation.properties += __geometryProps.replace(/\$/g, "background") + if (animateBackgroundRadii) + numberAnimation.properties += __radiiProps.replace(/\$/g, "background") + if (animateBackgroundBorder) + numberAnimation.properties += __borderProps.replace(/\$/g, "background") + if (animateBackgroundShadow) + numberAnimation.properties += __shadowProps.replace(/\$/g, "background") + if (animateBackgroundScaleAndRotation) + numberAnimation.properties += __scaleAndRotationProps.replace(/\$/g, "background") + + __animateIndicators(animateIndicatorGeometry, numberAnimation, __geometryProps) + __animateIndicators(animateIndicatorRadii, numberAnimation, __radiiProps) + __animateIndicators(animateIndicatorBorder, numberAnimation, __borderProps) + __animateIndicators(animateIndicatorShadow, numberAnimation, __shadowProps) + __animateIndicators(animateIndicatorScaleAndRotation, numberAnimation, __scaleAndRotationProps) + + __animateHandles(animateHandleGeometry, numberAnimation, __geometryProps) + __animateHandles(animateHandleRadii, numberAnimation, __radiiProps) + __animateHandles(animateHandleBorder, numberAnimation, __borderProps) + __animateHandles(animateHandleShadow, numberAnimation, __shadowProps) + __animateHandles(animateHandleScaleAndRotation, numberAnimation, __scaleAndRotationProps) + + if (!animateColors) { + /* Note: a ColorAnimation will animate all colors by default if 'properties' + * is empty. So we take advantage of that, and only add the per-delegate properties + * when not all colors should animate. For the same reason, we need to set properties + * to something else than "" if no colors should animate. */ + if (animateBackgroundColors) + colorAnimation.properties += __colorProps.replace(/\$/g, "background") + __animateIndicators(animateIndicatorColors, colorAnimation, __colorProps) + __animateHandles(animateHandleColors, colorAnimation, __colorProps) + + if (colorAnimation.properties === "") + colorAnimation.properties = "_none_" + } + } + + ColorAnimation { + id: colorAnimation + } + + NumberAnimation { + id: numberAnimation + easing: root.easing + duration: root.duration + } +} diff --git a/src/labs/stylekit/StyleKitDelegate.qml b/src/labs/stylekit/StyleKitDelegate.qml new file mode 100644 index 0000000000..d1f741e674 --- /dev/null +++ b/src/labs/stylekit/StyleKitDelegate.qml @@ -0,0 +1,151 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +// Qt-Security score:significant reason:default + +import QtQuick +import QtQuick.Effects +import QtQuick.Controls.impl +import Qt.labs.StyleKit +import Qt.labs.StyleKit.impl + +/* This delegate takes care of loading the individual elements that are needed + in order to draw it according to its configuration in the style. + Since some delegates have a gradient, others an image, and some perhaps + both, we use dynamic loading to reduce memory usage. We could use Loaders + for this, but from testing, using JS appears visually more performant. + TODO: move most of this code to c++? Collapse all elements into a single + scene-graph node? Redraw everything from a single update, rather than using + property bindings? +*/ +Item { + id: root + + // If an implicit size has been set on the delegate in the style, it + // wins. Otherwise we use the implicit size of the image + implicitWidth: __imageInstance ? __imageInstance.implicitWidth : 0 + implicitHeight: __imageInstance ? __imageInstance.implicitHeight : 0 + width: parent.width + height: parent.height + opacity: delegateProperties.opacity + + required property QtObject control + required property StyleKitDelegateProperties delegateProperties + + property Item __colorInstance: null + property Item __gradientInstance: null + property Item __imageInstance: null + + onDelegatePropertiesChanged: { + instantiateColorDelegate() + instantiateGradientDelegate() + instantiateImageDelegate() + } + + Connections { + target: delegateProperties + function onColorChanged() { instantiateColorDelegate() } + function onGradientChanged() { instantiateGradientDelegate() } + } + + Connections { + target: delegateProperties.image + function onSourceChanged() { instantiateImageDelegate() } + function onColorChanged() { instantiateImageDelegate() } + } + + Component { + id: colorDelegate + Rectangle { + required property StyleKitDelegateProperties delegateProperties + width: parent.width + height: parent.height + topLeftRadius: delegateProperties.topLeftRadius + topRightRadius: delegateProperties.topRightRadius + bottomLeftRadius: delegateProperties.bottomLeftRadius + bottomRightRadius: delegateProperties.bottomRightRadius + color: delegateProperties.color + border.width: delegateProperties.border.width + border.color: delegateProperties.border.color + } + } + + Component { + id: gradientDelegate + Rectangle { + required property StyleKitDelegateProperties delegateProperties + width: parent.width + height: parent.height + topLeftRadius: delegateProperties.topLeftRadius + topRightRadius: delegateProperties.topRightRadius + bottomLeftRadius: delegateProperties.bottomLeftRadius + bottomRightRadius: delegateProperties.bottomRightRadius + border.width: delegateProperties.border.width + border.color: delegateProperties.border.color + gradient: delegateProperties.gradient + visible: delegateProperties.gradient !== null + } + } + + Component { + id: imageDelegate + ColorImage { + required property StyleKitDelegateProperties delegateProperties + width: parent.width + height: parent.height + color: delegateProperties.image.color + source: delegateProperties.image.source + fillMode: delegateProperties.image.fillMode + visible: status === Image.Ready + } + } + + function instantiateColorDelegate() { + if (__colorInstance) + return + + // Delay instantiating element until needed + if (delegateProperties.color.a === 0 && + (delegateProperties.border.color.a === 0 + || delegateProperties.border.width === 0)) + return + + /* Note: we adjust z to ensure the elements are stacked correct, no matter the + * order in which they are instantiated. And we use negative z values to ensure + * that any children (with a default z === 0) of a "subclass" of the StyleKitDelegate + * ends up on top of this again. */ + let prevInstance = __colorInstance + __colorInstance = colorDelegate.createObject(root, { z: -3, delegateProperties: root.delegateProperties }) + if (prevInstance) + prevInstance.destroy() + } + + + function instantiateGradientDelegate() { + if (__gradientInstance) + return + + // Delay instantiating element until needed + if (delegateProperties.gradient === null) + return + + let prevInstance = __gradientInstance + __gradientInstance = gradientDelegate.createObject(root, { z: -2, delegateProperties: root.delegateProperties }) + if (prevInstance) + prevInstance.destroy() + } + + function instantiateImageDelegate() { + if (__imageInstance) + return + + // Delay instantiating element until needed + if (delegateProperties.image.source === "" || delegateProperties.image.color.a === 0) + return + + let prevInstance = __imageInstance + __imageInstance = imageDelegate.createObject(root, { z: -1, delegateProperties: root.delegateProperties }) + if (prevInstance) + prevInstance.destroy() + } + +} diff --git a/src/labs/stylekit/Switch.qml b/src/labs/stylekit/Switch.qml new file mode 100644 index 0000000000..c8e1a6237d --- /dev/null +++ b/src/labs/stylekit/Switch.qml @@ -0,0 +1,97 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +// Qt-Security score:significant reason:default + +import QtQuick +import QtQuick.Templates as T +import QtQuick.Controls.impl +import Qt.labs.StyleKit +import Qt.labs.StyleKit.impl + +T.Switch { + id: control + implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, + implicitContentWidth + leftPadding + rightPadding) + implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, + implicitContentHeight + topPadding + bottomPadding, + implicitIndicatorHeight + topPadding + bottomPadding) + + leftPadding: switchLayout.padding.left + topPadding: switchLayout.padding.top + rightPadding: switchLayout.padding.right + bottomPadding: switchLayout.padding.bottom + spacing: styleReader.spacing + + StyleKitReader { + id: styleReader + type: StyleKitReader.SwitchControl + enabled: control.enabled + focused: control.activeFocus + hovered: control.hovered + pressed: control.pressed + checked: control.checked + palette: control.palette + } + + StyleKitLayout { + id: switchLayout + container: control + contentMargins { + left: styleReader.leftPadding + right: styleReader.rightPadding + top: styleReader.topPadding + bottom: styleReader.bottomPadding + } + layoutItems: [ + // We don't lay out the contentItem here because it occupies the remaining space + // as calculated by control internal logic. + StyleKitLayoutItem { + id: indicatorItem + item: control.indicator + alignment: styleReader.indicator.alignment + margins.left: styleReader.indicator.leftMargin + margins.right: styleReader.indicator.rightMargin + margins.top: styleReader.indicator.topMargin + margins.bottom: styleReader.indicator.bottomMargin + fillWidth: styleReader.indicator.implicitWidth === Style.Stretch + fillHeight: styleReader.indicator.implicitHeight === Style.Stretch + } + ] + spacing: styleReader.spacing + mirrored: control.mirrored + } + + indicator: IndicatorDelegate { + parentControl: control + indicatorProperties: styleReader.indicator + x: indicatorItem.x + y: indicatorItem.y + width: indicatorItem.width + height: indicatorItem.height + + HandleDelegate { + parentControl: control + handleProperties: styleReader.handle + x: control.checked + ? indicator.width - width - styleReader.handle.rightMargin + : styleReader.handle.leftMargin + y: styleReader.handle.topMargin - styleReader.handle.bottomMargin + + (indicator.height - height) / 2 + z: 1 + Behavior on x { NumberAnimation { duration: 50 } } // factor animation out to a style property! + } + } + + contentItem: CheckLabel { + text: control.text + font: control.font + color: styleReader.text.color + horizontalAlignment: styleReader.text.alignment & Qt.AlignHorizontal_Mask + verticalAlignment: styleReader.text.alignment & Qt.AlignVertical_Mask + } + + background: BackgroundDelegate { + parentControl: control + backgroundProperties: styleReader.background + } +} diff --git a/src/labs/stylekit/TextField.qml b/src/labs/stylekit/TextField.qml new file mode 100644 index 0000000000..55d75498be --- /dev/null +++ b/src/labs/stylekit/TextField.qml @@ -0,0 +1,62 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +// Qt-Security score:significant reason:default + +import QtQuick +import QtQuick.Controls.impl +import QtQuick.Templates as T +import Qt.labs.StyleKit +import Qt.labs.StyleKit.impl + +T.TextField { + id: control + + implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, + Math.max(contentWidth, placeholder.implicitWidth) + leftPadding + rightPadding) + implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, + contentHeight + topPadding + bottomPadding, + placeholder.implicitHeight + topPadding + bottomPadding) + + leftPadding: styleReader.leftPadding + topPadding: styleReader.topPadding + rightPadding: styleReader.rightPadding + bottomPadding: styleReader.bottomPadding + + color: styleReader.text.color + verticalAlignment: styleReader.text.alignment & Qt.AlignVertical_Mask + horizontalAlignment: styleReader.text.alignment & Qt.AlignHorizontal_Mask + + selectionColor: control.palette.highlight + selectedTextColor: control.palette.highlightedText + placeholderTextColor: control.palette.placeholderText + + StyleKitReader { + id: styleReader + type: StyleKitReader.TextField + enabled: control.enabled + focused: control.activeFocus + hovered: control.hovered + palette: control.palette + } + + PlaceholderText { + id: placeholder + x: control.leftPadding + y: control.topPadding + width: control.width - (control.leftPadding + control.rightPadding) + height: control.height - (control.topPadding + control.bottomPadding) + + text: control.placeholderText + font: control.font + color: control.placeholderTextColor + verticalAlignment: control.verticalAlignment + visible: !control.length && !control.preeditText && (!control.activeFocus || control.horizontalAlignment !== Qt.AlignHCenter) + elide: Text.ElideRight + renderType: control.renderType + } + + background: BackgroundDelegate { + parentControl: control + backgroundProperties: styleReader.background + } +} diff --git a/src/labs/stylekit/impl/BackgroundAndIndicatorDelegate.qml b/src/labs/stylekit/impl/BackgroundAndIndicatorDelegate.qml new file mode 100644 index 0000000000..4e9f5d81e3 --- /dev/null +++ b/src/labs/stylekit/impl/BackgroundAndIndicatorDelegate.qml @@ -0,0 +1,94 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +// Qt-Security score:significant reason:default + +import QtQuick +import QtQuick.Templates as T +import Qt.labs.StyleKit +import Qt.labs.StyleKit.impl + +/* + This delegate is a composition of the background and the indicator into + a single delegate. This is needed by some controls, since they only have + a background delegate, which is responsible for also drawing the "indicator". + An example is a Slider, which draws both the background, groove and track in + the background delegate. +*/ +Item { + id: root + implicitWidth: !vertical + ? Math.max(background.implicitWidth, + indicatorLayout.implicitWidth) + : Math.max(background.implicitHeight, + indicatorLayout.implicitHeight) + implicitHeight: !vertical + ? Math.max(background.implicitHeight, + indicatorLayout.implicitHeight) + : Math.max(background.implicitWidth, + indicatorLayout.implicitWidth) + + required property StyleKitDelegateProperties indicatorProperties + required property StyleKitDelegateProperties backgroundProperties + required property T.Control parentControl + property alias indicator: indicator + property bool vertical: false + + StyleKitLayout { + id: indicatorLayout + container: root + contentMargins { + left: parentControl.leftPadding + right: parentControl.rightPadding + top: parentControl.topPadding + bottom: parentControl.bottomPadding + } + layoutItems: [ + StyleKitLayoutItem { + id: indicatorItem + item: root.indicator + alignment: indicatorProperties.alignment + margins.left: indicatorProperties.leftMargin + margins.right: indicatorProperties.rightMargin + margins.top: indicatorProperties.topMargin + margins.bottom: indicatorProperties.bottomMargin + fillWidth: indicatorProperties.implicitWidth === Style.Stretch + fillHeight: indicatorProperties.implicitHeight === Style.Stretch + } + ] + mirrored: parentControl.mirrored + } + + states: State { + /* The delegate logic is moved out of the delegate, to relieve the style + * developer from having to re-invent it if he changes the delegate. To + * disable it, 'states' can be set to an empty array from the outside. */ + when: true + PropertyChanges { + background.parent: root + background.x: parentControl.leftInset + background.y: parentControl.topInset + background.width: root.width - parentControl.leftInset - parentControl.rightInset + background.height: root.height - parentControl.topInset - parentControl.bottomInset + } + } + + BackgroundDelegate { + id: background + parentControl: root.parentControl + delegateProperties: root.backgroundProperties + } + + IndicatorDelegate { + id: indicator + parentControl: root.parentControl + indicatorProperties: root.indicatorProperties + vertical: parentControl.vertical + z: 1 + x: !vertical ? indicatorItem.x : parentControl.leftPadding + (parentControl.availableWidth - height) / 2 + y: !vertical ? indicatorItem.y : parentControl.topPadding + width + width: !vertical ? indicatorItem.width : __stretchBgWidth ? parentControl.availableHeight : implicitWidth//indicatorItem.height + height: !vertical ? indicatorItem.height : __stretchBgHeight ? parentControl.availableWidth : implicitHeight//indicatorItem.width + readonly property bool __stretchBgWidth: root.indicatorProperties.implicitWidth === Style.Stretch + readonly property bool __stretchBgHeight: root.indicatorProperties.implicitHeight === Style.Stretch + } +} diff --git a/src/labs/stylekit/impl/BackgroundDelegate.qml b/src/labs/stylekit/impl/BackgroundDelegate.qml new file mode 100644 index 0000000000..3704f3fdac --- /dev/null +++ b/src/labs/stylekit/impl/BackgroundDelegate.qml @@ -0,0 +1,12 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +// Qt-Security score:significant reason:default + +import QtQuick +import Qt.labs.StyleKit +import Qt.labs.StyleKit.impl + +DelegateContainer { + required property StyleKitDelegateProperties backgroundProperties + delegateProperties: backgroundProperties +} diff --git a/src/labs/stylekit/impl/CMakeLists.txt b/src/labs/stylekit/impl/CMakeLists.txt new file mode 100644 index 0000000000..14f2849e9e --- /dev/null +++ b/src/labs/stylekit/impl/CMakeLists.txt @@ -0,0 +1,32 @@ +# Copyright (C) 2025 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +##################################################################### +## StyleKitImpl Module: +##################################################################### + +set_source_files_properties(DelegateSingleton.qml + PROPERTIES QT_QML_SINGLETON_TYPE TRUE) + +qt_internal_add_qml_module(QtQuickStyleKitImpl + URI Qt.labs.StyleKit.impl + DEPENDENCIES + QtQuick/auto + SOURCES + qqstylekitlayout_p.h qqstylekitlayout.cpp + QML_FILES + BackgroundAndIndicatorDelegate.qml + BackgroundDelegate.qml + DelegateContainer.qml + DelegateSingleton.qml + FallbackStyle.qml + HandleDelegate.qml + IndicatorDelegate.qml + Shadow.qml + LIBRARIES + Qt6::GuiPrivate + Qt6::Quick + Qt6::QuickPrivate + Qt6::QuickTemplates2 + Qt6::QuickTemplates2Private +) diff --git a/src/labs/stylekit/impl/DelegateContainer.qml b/src/labs/stylekit/impl/DelegateContainer.qml new file mode 100644 index 0000000000..496b4f9f5e --- /dev/null +++ b/src/labs/stylekit/impl/DelegateContainer.qml @@ -0,0 +1,171 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +// Qt-Security score:significant reason:default + +import QtQuick +import QtQuick.Effects +import QtQuick.Controls.impl +import QtQuick.Templates as T +import Qt.labs.StyleKit +import Qt.labs.StyleKit.impl + +// This container takes care of loading, positioning, and resizing the delegate. +Item { + id: root + + /* If an implicit size has been set the style, it wins. + * Otherwise we use the implicit size of the instantiated delegate. */ + implicitWidth: delegateProperties.implicitWidth > 0 + ? delegateProperties.implicitWidth + : delegateInstance + ? delegateInstance.implicitWidth + : 0 + + implicitHeight: delegateProperties.implicitHeight > 0 + ? delegateProperties.implicitHeight + : delegateInstance + ? delegateInstance.implicitHeight + : 0 + + scale: delegateProperties.scale + rotation: delegateProperties.rotation + visible: delegateProperties.visible + + required property QtObject parentControl + required property StyleKitDelegateProperties delegateProperties + + property real leftMargin: delegateProperties.leftMargin + property real rightMargin: delegateProperties.rightMargin + property real topMargin: delegateProperties.topMargin + property real bottomMargin: delegateProperties.bottomMargin + + readonly property real marginWidth: leftMargin + rightMargin + readonly property real marginHeight: topMargin + bottomMargin + readonly property real subDelegateWidth: width + readonly property real subDelegateHeight: height + + onDelegatePropertiesChanged: { + instantiateDelegate() + instantiateShadowDelegate(); + } + + onVisibleChanged: { + instantiateDelegate() + instantiateShadowDelegate(); + } + + Connections { + target: delegateProperties + function onDelegateChanged() { instantiateDelegate() } + } + + Connections { + target: delegateProperties.shadow + function onDelegateChanged() { instantiateShadowDelegate() } + function onColorChanged() { instantiateShadowDelegate() } + function onOpacityChanged() { instantiateShadowDelegate() } + function onVisibleChanged() { instantiateShadowDelegate() } + } + + Item { + id: centerContainer + width: root.subDelegateWidth + height: root.subDelegateHeight + } + + // Uncomment to draw overlay: + // Rectangle { + // id: debugRectForMargins + // x: centerContainer.x + // y: centerContainer.y + // z: 5 + // width: root.subDelegateWidth + // height: root.subDelegateHeight + // color: "transparent" + // border.color: "limegreen" + // border.width: 1 + // } + + // Rectangle { + // id: debugRectForDelegate + // z: 5 + // width: root.width + // height: root.height + // color: "transparent" + // border.color: "magenta" + // border.width: 1 + // } + + // ------------------------------------------ + + property Item delegateInstance: null + property Component effectiveDelegate: null + + // onImplicitWidthChanged: { + // if (parentControl && parentControl instanceof T.CheckBox) { + // print("DelegateContainer delegateInstance changed to " + delegateInstance) + // print("delegate instance implicitWidth: " + (delegateInstance ? delegateInstance.implicitWidth : "N/A")) + // } + // } + // onWidthChanged: { + // if (parentControl && parentControl instanceof T.CheckBox) { + // print("DelegateContainer width changed to " + width) + // print("delegate instance width: " + (delegateInstance ? delegateInstance.width : "N/A")) + // } + // } + + function instantiateDelegate() { + if (!visible || !StyleKit.styleLoaded()) + return + + let delegate = root.delegateProperties.delegate + if (!delegate) + delegate = DelegateSingleton.defaultDelegate + if (delegate === effectiveDelegate) + return + + effectiveDelegate = delegate + let prevInstance = delegateInstance + if (delegate) + delegateInstance = delegate + ? delegate.createObject( + centerContainer, + { delegateProperties: root.delegateProperties, control: root.parentControl }) + : null + + if (prevInstance) + prevInstance.destroy() + } + + // ------------------------------------------ + + property Item shadowInstance: null + property Component effectiveShadowDelegate: null + + function instantiateShadowDelegate() { + if (!visible || !StyleKit.styleLoaded()) + return + + const shadowProps = root.delegateProperties.shadow + const delegate = shadowProps.delegate + ? shadowProps.delegate : DelegateSingleton.defaultShadowDelegate + if (delegate === effectiveShadowDelegate) + return + + // Delay instantiating delegate until needed + if (!shadowProps.visible || shadowProps.color.a === 0 || shadowProps.opacity === 0) + return + + let prevDelegateInstance = shadowInstance + effectiveShadowDelegate = delegate + shadowInstance = delegate + ? delegate.createObject( + root, + { z: -1, delegateProperties: root.delegateProperties, control: root.parentControl }) + : null + + if (prevDelegateInstance) + prevDelegateInstance.destroy() + } + +} diff --git a/src/labs/stylekit/impl/DelegateSingleton.qml b/src/labs/stylekit/impl/DelegateSingleton.qml new file mode 100644 index 0000000000..c119a5b9b6 --- /dev/null +++ b/src/labs/stylekit/impl/DelegateSingleton.qml @@ -0,0 +1,14 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +// Qt-Security score:significant reason:default + +pragma Singleton + +import QtQuick +import Qt.labs.StyleKit +import Qt.labs.StyleKit.impl + +QtObject { + readonly property Component defaultDelegate: StyleKitDelegate {} + readonly property Component defaultShadowDelegate: Shadow {} +} diff --git a/src/labs/stylekit/impl/FallbackStyle.qml b/src/labs/stylekit/impl/FallbackStyle.qml new file mode 100644 index 0000000000..d989fcc63a --- /dev/null +++ b/src/labs/stylekit/impl/FallbackStyle.qml @@ -0,0 +1,226 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +// Qt-Security score:significant reason:default + +import QtQuick +import Qt.labs.StyleKit + +BaseStyle { + id: style + + /* Properties and Controls left unspecified in a Style will be read from Style.fallbackStyle + * instead (that is, this file, unless the fallback style is changed). Here we can give the + * properties some sensible defaults. */ + + readonly property real indicatorSize: 24 + + control { + spacing: 5 + padding: 5 + + background { + implicitWidth: 100 + implicitHeight: 40 + border.width: 1 + border.color: "black" + color: style.palette.base + } + + indicator { + implicitWidth: style.indicatorSize + implicitHeight: style.indicatorSize + color: style.palette.base + border.color: "black" + border.width: 1 + foreground { + implicitWidth: Style.Stretch + implicitHeight: Style.Stretch + margins: 1 + color: style.palette.accent + image.color: style.palette.accent + /* Note: don't set implicit size here, since the DelegateContainer will (and should) + * fall back to use the size of the image if not set. So if we hard-code a size here, + * it cannot be unset to be the size of the image (if any) again from the Style. */ + } + } + + handle { + implicitWidth: style.indicatorSize + implicitHeight: style.indicatorSize + radius: style.indicatorSize / 2 + border.width: 1 + border.color: "black" + color: style.palette.window + } + } + + textInput { + padding: 5 + background { + implicitWidth: 150 + implicitHeight: 40 + border.width: 1 + border.color: "black" + color: style.palette.base + } + } + + popup { + background { + implicitWidth: 200 + implicitHeight: 200 + border.width: 1 + border.color: "black" + color: style.palette.base + } + } + + pane { + background { + implicitWidth: 200 + implicitHeight: 200 + color: style.palette.window + } + } + + page { + background.border.width: 0 + } + + frame { + background.color: style.palette.base + } + + flatButton { + background.visible: false + hovered.background.visible: true + } + + checkBox { + background.visible: false + indicator { + foreground { + visible: false + color: "transparent" + image.source: "qrc:/qt-project.org/imports/QtQuick/Controls/Basic/images/check.png" + } + } + text.alignment: Qt.AlignVCenter | Qt.AlignLeft + checked.indicator.foreground.visible: true + } + + comboBox { + text.alignment: Qt.AlignVCenter + background.implicitWidth: 150 + indicator { + color: style.palette.base + border.width: 0 + foreground { + margins: 4 + color: "transparent" + image.source: "qrc:/qt-project.org/imports/QtQuick/Controls/Basic/images/drop-indicator.png" + image.color: style.palette.buttonText + } + } + } + + spinBox { + text.alignment: Qt.AlignHCenter | Qt.AlignVCenter + indicator { + implicitHeight: Style.Stretch + color: style.palette.base + border.width: 0 + margins: 0 + + foreground { + color: "transparent" + image.fillMode: Image.PreserveAspectFit + image.source: "qrc:/qt-project.org/imports/QtQuick/Controls/Basic/images/arrow-indicator.png" + image.color: "black" + + // Note: by setting a rotation, we cannot at the same time set the implicit size to + // Stretch, since Stretch will be based on the unrotated geometry of the indicator. + // So if the height of the indicator is different from the width, things will look wrong. + implicitWidth: 14 + implicitHeight: 14 + alignment: Qt.AlignCenter + } + + down { + alignment: Qt.AlignLeft + foreground.rotation: 90 + } + + up { + alignment: Qt.AlignRight + foreground.rotation: -90 + } + } + } + + radioButton { + background.visible: false + indicator { + radius: 255 + foreground { + margins: 4 + visible: false + radius: 255 + } + } + text.alignment: Qt.AlignVCenter | Qt.AlignLeft + checked.indicator.foreground.visible: true + } + + itemDelegate { + text.alignment: Qt.AlignVCenter | Qt.AlignLeft + background { + // Make it flat + color: palette.base + border.width: 0 + shadow.visible: false + } + hovered.background.color: palette.highlight + } + + textField { + text.alignment: Qt.AlignVCenter + background { + implicitWidth: 150 + color: style.palette.base + gradient: null + } + } + + slider { + background { + visible: false + implicitWidth: 150 + } + indicator { + implicitWidth: Style.Stretch + implicitHeight: 8 + radius: 8 + foreground { + radius: 7 + } + } + } + + switchControl { + background.visible: false + text.alignment: Qt.AlignVCenter + indicator { + implicitWidth: style.indicatorSize * 2 + implicitHeight: style.indicatorSize + alignment: Qt.AlignLeft | Qt.AlignVCenter + radius: style.indicatorSize / 2 + foreground { + color: style.palette.base + radius: style.indicatorSize / 2 + } + } + checked.indicator.foreground.color: style.palette.accent + } + +} diff --git a/src/labs/stylekit/impl/HandleDelegate.qml b/src/labs/stylekit/impl/HandleDelegate.qml new file mode 100644 index 0000000000..73ab71c332 --- /dev/null +++ b/src/labs/stylekit/impl/HandleDelegate.qml @@ -0,0 +1,22 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +// Qt-Security score:significant reason:default + +import QtQuick +import QtQuick.Templates as T +import Qt.labs.StyleKit +import Qt.labs.StyleKit.impl + +DelegateContainer { + /* The margins are not added to the handle itself, but to + * the area (groove) where the handle moves. This is taken + * care of by the control that uses the handle */ + leftMargin: 0 + rightMargin: 0 + topMargin: 0 + bottomMargin: 0 + + delegateProperties: handleProperties + + required property StyleKitDelegateProperties handleProperties +} diff --git a/src/labs/stylekit/impl/IndicatorDelegate.qml b/src/labs/stylekit/impl/IndicatorDelegate.qml new file mode 100644 index 0000000000..634d6bdedf --- /dev/null +++ b/src/labs/stylekit/impl/IndicatorDelegate.qml @@ -0,0 +1,76 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +// Qt-Security score:significant reason:default + +import QtQuick +import QtQuick.Templates as T +import Qt.labs.StyleKit +import Qt.labs.StyleKit.impl + +DelegateContainer { + id: root + + implicitWidth: Math.max(_delegateImplicitWidth, indicatorLayout.implicitWidth) + implicitHeight: Math.max(_delegateImplicitHeight, indicatorLayout.implicitHeight) + + transformOrigin: Item.TopLeft + rotation: vertical ? 90 : 0 + scale: vertical ? -1 : 1 + delegateProperties: root.indicatorProperties + + required property QtObject parentControl + required property StyleKitDelegateProperties indicatorProperties + property bool vertical: false + /* Some indicators (Slider, RangeSlider) should let the foreground delegate + * only fill up a certain amount of the available foreground space (that is, the + * track / progress). This amount be controlled with firstProgress and secondProgress. */ + property real firstProgress: 0.0 + property real secondProgress: 1.0 + + readonly property real _delegateImplicitWidth: root.indicatorProperties.implicitWidth > 0 + ? root.indicatorProperties.implicitWidth + : delegateInstance + ? delegateInstance.implicitWidth + : 0 + readonly property real _delegateImplicitHeight: root.indicatorProperties.implicitHeight > 0 + ? root.indicatorProperties.implicitHeight + : delegateInstance + ? delegateInstance.implicitHeight + : 0 + + StyleKitLayout { + id: indicatorLayout + container: root + enabled: true + layoutItems: [ + StyleKitLayoutItem { + id: fgItem + item: foreground + alignment: indicatorProperties.foreground.alignment + margins.left: indicatorProperties.foreground.leftMargin + margins.right: indicatorProperties.foreground.rightMargin + margins.top: indicatorProperties.foreground.topMargin + margins.bottom: indicatorProperties.foreground.bottomMargin + fillWidth: indicatorProperties.foreground.implicitWidth === Style.Stretch + fillHeight: indicatorProperties.foreground.implicitHeight === Style.Stretch + } + ] + mirrored: parentControl.mirrored + } + + DelegateContainer { + id: foreground + parent: root + parentControl: root.parentControl + delegateProperties: root.indicatorProperties.foreground + x: fgItem.x + firstProgress * (fgItem.fillWidth + ? fgItem.width - indicatorProperties.foreground.minimumWidth + : fgItem.width) + y: fgItem.y + width: fgItem.fillWidth ? (indicatorProperties.foreground.minimumWidth + + ((secondProgress - firstProgress) * (fgItem.width + - indicatorProperties.foreground.minimumWidth))) + : (secondProgress - firstProgress) * fgItem.width + height: fgItem.height + } +} diff --git a/src/labs/stylekit/impl/Shadow.qml b/src/labs/stylekit/impl/Shadow.qml new file mode 100644 index 0000000000..6014ea7224 --- /dev/null +++ b/src/labs/stylekit/impl/Shadow.qml @@ -0,0 +1,28 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +// Qt-Security score:significant reason:default + +import QtQuick +import QtQuick.Effects +import Qt.labs.StyleKit +import Qt.labs.StyleKit.impl + +RectangularShadow { + required property QtObject control + required property StyleKitDelegateProperties delegateProperties + + width: parent.width + height: parent.height + color: delegateProperties.shadow.color + opacity: delegateProperties.shadow.opacity + blur: delegateProperties.shadow.blur + scale: delegateProperties.shadow.scale + radius: delegateProperties.radius + topLeftRadius: delegateProperties.topLeftRadius + topRightRadius: delegateProperties.topRightRadius + bottomLeftRadius: delegateProperties.bottomLeftRadius + bottomRightRadius: delegateProperties.bottomRightRadius + offset: Qt.vector2d( + delegateProperties.shadow.horizontalOffset, + delegateProperties.shadow.verticalOffset) +} diff --git a/src/labs/stylekit/impl/qqstylekitlayout.cpp b/src/labs/stylekit/impl/qqstylekitlayout.cpp new file mode 100644 index 0000000000..f29bcc4c57 --- /dev/null +++ b/src/labs/stylekit/impl/qqstylekitlayout.cpp @@ -0,0 +1,538 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qqstylekitlayout_p.h" + +QT_BEGIN_NAMESPACE + +static qreal width(QQStyleKitLayoutItem *li, qreal availableWidth = .0) +{ + Q_ASSERT(li); + Q_ASSERT(li->item()); + QQuickItem *item = li->item(); + qreal w = item->implicitWidth(); + if (li->fillWidth()) + w = qMax(.0, availableWidth - li->margins().left() - li->margins().right()); + return qMax(.0, w); +} + +static qreal height(QQStyleKitLayoutItem *li, qreal availableHeight = .0) +{ + Q_ASSERT(li); + Q_ASSERT(li->item()); + QQuickItem *item = li->item(); + qreal h = item->implicitHeight(); + if (li->fillHeight()) + h = qMax(.0, availableHeight - li->margins().top() - li->margins().bottom()); + return qMax(.0, h); +} + +static qreal totalWidth(const QVector<QQStyleKitLayoutItem *> &items, qreal spacing) +{ + qreal total = .0; + for (QQStyleKitLayoutItem *li : items) { + if (li->item() && li->item()->isVisible()) + total += width(li) + li->margins().left() + li->margins().right() + spacing; + } + return total; +} + +static qreal totalHeight(const QVector<QQStyleKitLayoutItem *> &items) +{ + qreal maxHeight = .0; + for (QQStyleKitLayoutItem *li : items) { + if (li->item() && li->item()->isVisible()) { + const qreal h = height(li) + li->margins().top() + li->margins().bottom(); + if (h > maxHeight) + maxHeight = h; + } + } + return maxHeight; +} + +static qreal vAlignY(QQStyleKitLayoutItem *li, qreal containerY, qreal containerHeight) +{ + Q_ASSERT(li); + Q_ASSERT(li->item()); + + const auto itemHeight = height(li, containerHeight); + const auto vAlign = li->alignment() & Qt::AlignVertical_Mask; + const auto margins = li->margins(); + switch (vAlign) { + case Qt::AlignTop: + return containerY + margins.top(); + case Qt::AlignBottom: + return containerY + containerHeight - itemHeight - margins.bottom(); + default: // AlignVCenter + return containerY + margins.top() + + (containerHeight - margins.top() - margins.bottom() - itemHeight) / 2.0; + } +} + +QQStyleKitLayoutItem::QQStyleKitLayoutItem(QObject *parent) + : QObject(parent) +{ +} + +QQuickItem *QQStyleKitLayoutItem::item() const +{ + return m_item; +} + +void QQStyleKitLayoutItem::setItem(QQuickItem *item) +{ + if (m_item == item) + return; + + if (m_item) + disconnect(m_item, nullptr, this, nullptr); + + m_item = item; + if (m_item) { + connect(m_item, &QQuickItem::implicitWidthChanged, this, [this]() { emit itemChanged(); }); + connect(m_item, &QQuickItem::implicitHeightChanged, this, [this]() { emit itemChanged(); }); + connect(m_item, &QQuickItem::visibleChanged, this, [this]() { emit itemChanged(); }); + } + // TODO: parentchanged + emit itemChanged(); +} + +qreal QQStyleKitLayoutItem::x() const +{ + return m_x; +} + +void QQStyleKitLayoutItem::setX(qreal x) +{ + if (qFuzzyCompare(m_x, x)) + return; + + m_x = x; + emit xChanged(); +} + +qreal QQStyleKitLayoutItem::y() const +{ + return m_y; +} + +void QQStyleKitLayoutItem::setY(qreal y) +{ + if (qFuzzyCompare(m_y, y)) + return; + + m_y = y; + emit yChanged(); +} + +qreal QQStyleKitLayoutItem::width() const +{ + return m_width; +} + +void QQStyleKitLayoutItem::setWidth(qreal width) +{ + if (qFuzzyCompare(m_width, width)) + return; + + m_width = width; + emit widthChanged(); +} + +qreal QQStyleKitLayoutItem::height() const +{ + return m_height; +} + +void QQStyleKitLayoutItem::setHeight(qreal height) +{ + if (qFuzzyCompare(m_height, height)) + return; + + m_height = height; + emit heightChanged(); +} + +Qt::Alignment QQStyleKitLayoutItem::alignment() const +{ + return m_alignment; +} + +void QQStyleKitLayoutItem::setAlignment(Qt::Alignment alignment) +{ + if (m_alignment == alignment) + return; + + m_alignment = alignment; + emit alignmentChanged(); +} + +QMarginsF QQStyleKitLayoutItem::margins() const +{ + return m_margins; +} + +void QQStyleKitLayoutItem::setMargins(const QMarginsF &margins) +{ + if (m_margins == margins) + return; + + m_margins = margins; + emit marginsChanged(); +} + +bool QQStyleKitLayoutItem::fillWidth() const +{ + return m_fillWidth; +} + +void QQStyleKitLayoutItem::setFillWidth(bool fill) +{ + if (m_fillWidth == fill) + return; + + m_fillWidth = fill; + emit fillWidthChanged(); +} + +bool QQStyleKitLayoutItem::fillHeight() const +{ + return m_fillHeight; +} + +void QQStyleKitLayoutItem::setFillHeight(bool fill) +{ + if (m_fillHeight == fill) + return; + + m_fillHeight = fill; + emit fillHeightChanged(); +} + +QQStyleKitLayout::QQStyleKitLayout(QObject *parent) + : QObject(parent) + , m_mirrored(false) + , m_enabled(true) + , m_updatingLayout(false) +{ + m_updateTimer.setSingleShot(true); + connect(&m_updateTimer, &QTimer::timeout, this, &QQStyleKitLayout::updateLayout); +} + +QQuickItem *QQStyleKitLayout::container() const +{ + return m_container; +} + +void QQStyleKitLayout::setContainer(QQuickItem *container) +{ + if (m_container == container) + return; + + m_container = container; + emit containerChanged(); + connect(m_container, &QQuickItem::widthChanged, this, &QQStyleKitLayout::scheduleUpdate); + connect(m_container, &QQuickItem::heightChanged, this, &QQStyleKitLayout::scheduleUpdate); + + scheduleUpdate(); +} + +QQmlListProperty<QQStyleKitLayoutItem> QQStyleKitLayout::layoutItems() +{ + return QQmlListProperty<QQStyleKitLayoutItem>(const_cast<QQStyleKitLayout *>(this), + nullptr, + &QQStyleKitLayout::layoutItem_append, + &QQStyleKitLayout::layoutItem_count, + &QQStyleKitLayout::layoutItem_at, + &QQStyleKitLayout::layoutItem_clear); +} + +void QQStyleKitLayout::layoutItem_append(QQmlListProperty<QQStyleKitLayoutItem> *list, QQStyleKitLayoutItem *item) +{ + QQStyleKitLayout *layout = qobject_cast<QQStyleKitLayout *>(list->object); + if (layout && item) { + layout->m_layoutItems.append(item); + connect(item, &QQStyleKitLayoutItem::itemChanged, layout, &QQStyleKitLayout::scheduleUpdate); + connect(item, &QQStyleKitLayoutItem::alignmentChanged, layout, &QQStyleKitLayout::scheduleUpdate); + connect(item, &QQStyleKitLayoutItem::marginsChanged, layout, &QQStyleKitLayout::scheduleUpdate); + connect(item, &QQStyleKitLayoutItem::fillWidthChanged, layout, &QQStyleKitLayout::scheduleUpdate); + connect(item, &QQStyleKitLayoutItem::fillHeightChanged, layout, &QQStyleKitLayout::scheduleUpdate); + emit layout->layoutItemsChanged(); + layout->scheduleUpdate(); + } +} + +qsizetype QQStyleKitLayout::layoutItem_count(QQmlListProperty<QQStyleKitLayoutItem> *list) +{ + QQStyleKitLayout *layout = qobject_cast<QQStyleKitLayout *>(list->object); + if (layout) + return layout->m_layoutItems.size(); + return 0; +} + +QQStyleKitLayoutItem *QQStyleKitLayout::layoutItem_at(QQmlListProperty<QQStyleKitLayoutItem> *list, qsizetype index) +{ + QQStyleKitLayout *layout = qobject_cast<QQStyleKitLayout *>(list->object); + if (layout) + return layout->m_layoutItems.value(index); + return nullptr; +} + +void QQStyleKitLayout::layoutItem_clear(QQmlListProperty<QQStyleKitLayoutItem> *list) +{ + QQStyleKitLayout *layout = qobject_cast<QQStyleKitLayout *>(list->object); + if (layout) { + layout->m_layoutItems.clear(); + emit layout->layoutItemsChanged(); + layout->scheduleUpdate(); + } +} + +QMarginsF QQStyleKitLayout::padding() const +{ + return m_padding; +} + +QMarginsF QQStyleKitLayout::contentMargins() const +{ + return m_contentMargins; +} + +void QQStyleKitLayout::setContentMargins(const QMarginsF &margins) +{ + if (m_contentMargins == margins) + return; + + m_contentMargins = margins; + emit contentMarginsChanged(); + scheduleUpdate(); +} + +qreal QQStyleKitLayout::spacing() const +{ + return m_spacing; +} + +void QQStyleKitLayout::setSpacing(qreal spacing) +{ + if (qFuzzyCompare(m_spacing, spacing)) + return; + + m_spacing = spacing; + emit spacingChanged(); + scheduleUpdate(); +} + +bool QQStyleKitLayout::isMirrored() const +{ + return m_mirrored; +} + +void QQStyleKitLayout::setMirrored(bool mirrored) +{ + if (m_mirrored == mirrored) + return; + + m_mirrored = mirrored; + emit mirroredChanged(); + scheduleUpdate(); +} + +qreal QQStyleKitLayout::implicitWidth() const +{ + return m_implicitWidth; +} + +qreal QQStyleKitLayout::implicitHeight() const +{ + return m_implicitHeight; +} + +void QQStyleKitLayout::setImplicitWidth(qreal width) +{ + if (qFuzzyCompare(m_implicitWidth, width)) + return; + + m_implicitWidth = width; + emit implicitWidthChanged(); + scheduleUpdate(); +} + +void QQStyleKitLayout::setImplicitHeight(qreal height) +{ + if (qFuzzyCompare(m_implicitHeight, height)) + return; + + m_implicitHeight = height; + emit implicitHeightChanged(); + scheduleUpdate(); +} + +bool QQStyleKitLayout::isEnabled() const +{ + return m_enabled; +} + +void QQStyleKitLayout::setEnabled(bool enabled) +{ + if (m_enabled == enabled) + return; + + m_enabled = enabled; + emit enabledChanged(); + + if (m_enabled) + scheduleUpdate(); +} + +void QQStyleKitLayout::updateLayout() +{ + if (!m_enabled) + return; + + if (!m_container || m_container->width() <= 0 || m_container->height() <= 0) + return; + + if (m_updatingLayout) + return; + m_updatingLayout = true; + + QVector<QQStyleKitLayoutItem *> left; + QVector<QQStyleKitLayoutItem *> right; + QVector<QQStyleKitLayoutItem *> center; + + for (QQStyleKitLayoutItem *li : m_layoutItems) { + if (!li->item() || !li->item()->isVisible()) + continue; + const auto hAlign = li->alignment() & Qt::AlignHorizontal_Mask; + const bool isMirrored = m_mirrored && !(hAlign & Qt::AlignAbsolute); + switch (hAlign) { + case Qt::AlignLeft: + if (isMirrored) + right.append(li); + else + left.append(li); + break; + case Qt::AlignRight: + if (isMirrored) + left.append(li); + else + right.append(li); + break; + default: + center.append(li); + break; + } + } + + const qreal containerWidth = m_container->width() ? m_container->width() : m_container->implicitWidth(); + const qreal containerHeight = m_container->height() ? m_container->height() : m_container->implicitHeight(); + const qreal paddedX = m_contentMargins.left(); + const qreal paddedY = m_contentMargins.top(); + const qreal paddedWidth = qMax(containerWidth - m_contentMargins.left() - m_contentMargins.right(), .0); + const qreal paddedHeight = qMax(containerHeight - m_contentMargins.top() - m_contentMargins.bottom(), .0); + + // Position left-aligned items + { + qreal x = paddedX; + for (QQStyleKitLayoutItem *li : left) { + QQuickItem *item = li->item(); + if (!item || !item->isVisible()) + continue; + + const QMarginsF margins = li->margins(); + const qreal itemWidth = width(li, paddedWidth); + const qreal itemHeight = height(li, paddedHeight); + auto y = vAlignY(li, paddedY, paddedHeight); + li->setX(x + margins.left()); + li->setY(y); + li->setWidth(itemWidth); + li->setHeight(itemHeight); + x += itemWidth + margins.left() + margins.right() + m_spacing; + } + } + + // Position right-aligned items + { + qreal x = paddedX + paddedWidth; + for (QQStyleKitLayoutItem *li : right) { + QQuickItem *item = li->item(); + if (!item || !item->isVisible()) + continue; + + const QMarginsF margins = li->margins(); + const qreal itemWidth = width(li, paddedWidth); + const qreal itemHeight = height(li, paddedHeight); + x -= itemWidth + margins.right() + margins.left(); + auto y = vAlignY(li, paddedY, paddedHeight); + li->setX(x + margins.left()); + li->setY(y); + li->setWidth(itemWidth); + li->setHeight(itemHeight); + x -= m_spacing; + } + } + + // Position center-aligned items + { + qreal x = paddedX + (paddedWidth - totalWidth(center, m_spacing)) / 2; + for (QQStyleKitLayoutItem *li : center) { + QQuickItem *item = li->item(); + if (!item || !item->isVisible()) + continue; + + const QMarginsF margins = li->margins(); + const qreal itemWidth = width(li, paddedWidth); + const qreal itemHeight = height(li, paddedHeight); + auto y = vAlignY(li, paddedY, paddedHeight); + li->setX(x + margins.left()); + li->setY(y); + li->setWidth(itemWidth); + li->setHeight(itemHeight); + x += itemWidth + margins.left() + margins.right() + m_spacing; + } + } + + const auto leftWidth = totalWidth(left, m_spacing); + const auto leftHeight = totalHeight(left); + const auto rightWidth = totalWidth(right, m_spacing); + const auto rightHeight = totalHeight(right); + const auto centerWidth = totalWidth(center, m_spacing); + const auto centerHeight = totalHeight(center); + + const auto implicitWidth = leftWidth + rightWidth + centerWidth + - m_spacing * (left.isEmpty() ? 0 : 1) + - m_spacing * (right.isEmpty() ? 0 : 1) + - m_spacing * (center.isEmpty() ? 0 : 1) + + m_contentMargins.left() + m_contentMargins.right(); + setImplicitWidth(implicitWidth); + const auto implicitHeight = qMax(qMax(leftHeight, rightHeight), centerHeight) + + m_contentMargins.top() + m_contentMargins.bottom(); + setImplicitHeight(implicitHeight); + + // HACK for control's contentItem + // QQuickControl determines the contentItem geometry based on the control size and padding + // So we include the layout's left/right widths into the padding calculation + auto leftPadding = m_contentMargins.left() + leftWidth; + auto topPadding = m_contentMargins.top(); // TODO: support vertical layout items + auto rightPadding = m_contentMargins.right() + rightWidth; + auto bottomPadding = m_contentMargins.bottom(); // TODO: support vertical layout items + if (isMirrored()) + std::swap(leftPadding, rightPadding); + QMarginsF newPadding = QMarginsF(leftPadding, topPadding, rightPadding, bottomPadding); + if (m_padding != newPadding) { + m_padding = newPadding; + emit paddingChanged(); + } + m_updatingLayout = false; +} + +void QQStyleKitLayout::scheduleUpdate() +{ + if (!m_updateTimer.isActive()) + m_updateTimer.start(0); +} + +QT_END_NAMESPACE + +#include "moc_qqstylekitlayout_p.cpp" diff --git a/src/labs/stylekit/impl/qqstylekitlayout_p.h b/src/labs/stylekit/impl/qqstylekitlayout_p.h new file mode 100644 index 0000000000..d6a0e74cd1 --- /dev/null +++ b/src/labs/stylekit/impl/qqstylekitlayout_p.h @@ -0,0 +1,168 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QQUICKUNIFIEDLAYOUT_H +#define QQUICKUNIFIEDLAYOUT_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + + +#include <QtQml/QtQml> +#include <QtCore/qtimer.h> +#include <QtQuickTemplates2/private/qquickcontrol_p.h> + +QT_BEGIN_NAMESPACE + +class QQStyleKitLayoutItem : public QObject +{ + Q_OBJECT + Q_PROPERTY(QQuickItem *item READ item WRITE setItem NOTIFY itemChanged FINAL) + Q_PROPERTY(qreal x READ x NOTIFY xChanged FINAL) + Q_PROPERTY(qreal y READ y NOTIFY yChanged FINAL) + Q_PROPERTY(qreal width READ width NOTIFY widthChanged FINAL) + Q_PROPERTY(qreal height READ height NOTIFY heightChanged FINAL) + Q_PROPERTY(Qt::Alignment alignment READ alignment WRITE setAlignment NOTIFY alignmentChanged FINAL) + Q_PROPERTY(QMarginsF margins READ margins WRITE setMargins NOTIFY marginsChanged FINAL) + Q_PROPERTY(bool fillWidth READ fillWidth WRITE setFillWidth NOTIFY fillWidthChanged FINAL) + Q_PROPERTY(bool fillHeight READ fillHeight WRITE setFillHeight NOTIFY fillHeightChanged FINAL) + QML_NAMED_ELEMENT(StyleKitLayoutItem) + +public: + QQStyleKitLayoutItem(QObject *parent = nullptr); + QQuickItem *item() const; + void setItem(QQuickItem *item); + + qreal x() const; + void setX(qreal x); + qreal y() const; + void setY(qreal y); + qreal width() const; + void setWidth(qreal width); + qreal height() const; + void setHeight(qreal height); + + Qt::Alignment alignment() const; + void setAlignment(Qt::Alignment alignment); + + QMarginsF margins() const; + void setMargins(const QMarginsF &margins); + + bool fillWidth() const; + void setFillWidth(bool fill); + + bool fillHeight() const; + void setFillHeight(bool fill); + +signals: + void itemChanged(); + void xChanged(); + void yChanged(); + void widthChanged(); + void heightChanged(); + void alignmentChanged(); + void marginsChanged(); + void fillWidthChanged(); + void fillHeightChanged(); + +private: + QPointer<QQuickItem> m_item; + Qt::Alignment m_alignment = Qt::AlignLeft | Qt::AlignVCenter; + QMarginsF m_margins; + bool m_fillWidth = false; + bool m_fillHeight = false; + qreal m_x = 0; + qreal m_y = 0; + qreal m_width = 0; + qreal m_height = 0; +}; + +class QQStyleKitLayout : public QObject +{ + Q_OBJECT + Q_PROPERTY(QQuickItem *container READ container WRITE setContainer NOTIFY containerChanged FINAL) + Q_PROPERTY(QQmlListProperty<QQStyleKitLayoutItem> layoutItems READ layoutItems NOTIFY layoutItemsChanged FINAL) + Q_PROPERTY(QMarginsF padding READ padding NOTIFY paddingChanged FINAL) + Q_PROPERTY(QMarginsF contentMargins READ contentMargins WRITE setContentMargins NOTIFY contentMarginsChanged FINAL) + Q_PROPERTY(qreal spacing READ spacing WRITE setSpacing NOTIFY spacingChanged FINAL) + Q_PROPERTY(bool mirrored READ isMirrored WRITE setMirrored NOTIFY mirroredChanged FINAL) + Q_PROPERTY(qreal implicitWidth READ implicitWidth NOTIFY implicitWidthChanged FINAL) + Q_PROPERTY(qreal implicitHeight READ implicitHeight NOTIFY implicitHeightChanged FINAL) + Q_PROPERTY(bool enabled READ isEnabled WRITE setEnabled NOTIFY enabledChanged FINAL) + Q_CLASSINFO("DefaultProperty", "layoutItems") + QML_NAMED_ELEMENT(StyleKitLayout) + +public: + QQStyleKitLayout(QObject *parent = nullptr); + + QQuickItem *container() const; + void setContainer(QQuickItem *item); + + QQmlListProperty<QQStyleKitLayoutItem> layoutItems(); + + QMarginsF padding() const; + + QMarginsF contentMargins() const; + void setContentMargins(const QMarginsF &margins); + + qreal spacing() const; + void setSpacing(qreal spacing); + + bool isMirrored() const; + void setMirrored(bool mirrored); + + qreal implicitWidth() const; + qreal implicitHeight() const; + + void setImplicitWidth(qreal width); + void setImplicitHeight(qreal height); + + bool isEnabled() const; + void setEnabled(bool enabled); + +signals: + void containerChanged(); + void layoutItemsChanged(); + void layoutChanged(); + void paddingChanged(); + void contentMarginsChanged(); + void spacingChanged(); + void mirroredChanged(); + void implicitWidthChanged(); + void implicitHeightChanged(); + void enabledChanged(); + +private: + void updateLayout(); + void scheduleUpdate(); + + static void layoutItem_append(QQmlListProperty<QQStyleKitLayoutItem> *list, QQStyleKitLayoutItem *item); + static qsizetype layoutItem_count(QQmlListProperty<QQStyleKitLayoutItem> *list); + static QQStyleKitLayoutItem *layoutItem_at(QQmlListProperty<QQStyleKitLayoutItem> *list, qsizetype index); + static void layoutItem_clear(QQmlListProperty<QQStyleKitLayoutItem> *list); + + QPointer<QQuickItem> m_container; + QList<QQStyleKitLayoutItem *> m_layoutItems; + QMarginsF m_contentMargins; + QMarginsF m_padding; + qreal m_spacing = 0; + qreal m_implicitWidth = 0;; + qreal m_implicitHeight = 0; + + bool m_mirrored: 1; + bool m_enabled: 1; + bool m_updatingLayout: 1; + + QTimer m_updateTimer; +}; + +QT_END_NAMESPACE +#endif // QQUICKUNIFIEDLAYOUT_H diff --git a/src/labs/stylekit/qqstylekit.cpp b/src/labs/stylekit/qqstylekit.cpp new file mode 100644 index 0000000000..6f7ad2f2f3 --- /dev/null +++ b/src/labs/stylekit/qqstylekit.cpp @@ -0,0 +1,131 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qqstylekit_p.h" +#include "qqstylekitstyle_p.h" +#include "qqstylekittheme_p.h" +#include "qqstylekitreader_p.h" + +#include <QtGui/QStyleHints> + +QT_BEGIN_NAMESPACE + +QPointer<QQStyleKitAttached> QQStyleKitAttached::s_instance; +bool QQStyleKitAttached::s_transitionsEnabled = true; + +QQStyleKit::QQStyleKit(QObject *parent) + : QObject(parent) +{ +} + +QQStyleKitAttached *QQStyleKit::qmlAttachedProperties(QObject *obj) +{ + /* QQStyleKitAttached is a singleton. It doesn't matter where it's + * used from in the application, it will always represent the same + * application global style and theme. */ + if (!QQStyleKitAttached::s_instance) + QQStyleKitAttached::s_instance = new QQStyleKitAttached(QGuiApplication::instance()); + if (!QQStyleKitAttached::s_instance->m_engine && obj) + QQStyleKitAttached::s_instance->m_engine = qmlEngine(obj); + return QQStyleKitAttached::s_instance.get(); +} + +QQStyleKitAttached::QQStyleKitAttached(QObject *parent) + : QObject(parent) +{ + connect(QGuiApplication::styleHints(), &QStyleHints::colorSchemeChanged, this, [this](){ + if (m_style) + m_style->recreateTheme(); + }); +} + +QQStyleKitAttached::~QQStyleKitAttached() +{ + QQStyleKitAttached::s_instance = nullptr; + if (m_ownsStyle) { + delete m_style; + m_style = nullptr; + } +} + +QQStyleKitStyle *QQStyleKitAttached::style() const +{ + return m_style; +} + +void QQStyleKitAttached::setStyle(QQStyleKitStyle *style) +{ + if (m_style == style) + return; + + if (m_ownsStyle) + m_style->deleteLater(); + + m_style = style; + + if (m_style && m_style->m_theme) + m_style->m_theme->updateQuickTheme(); + if (m_style->loaded()) + QQStyleKitReader::resetAll(); + + emit styleChanged(); +} + +QString QQStyleKitAttached::styleUrl() const +{ + return m_styleUrl; +} + +void QQStyleKitAttached::setStyleUrl(const QString &styleUrl) +{ + if (m_styleUrl == styleUrl) + return; + + m_styleUrl = styleUrl; + + Q_ASSERT(m_engine); + QQmlComponent comp(m_engine, QUrl(styleUrl), this); + if (!comp.errors().isEmpty()) { + qmlWarning(this) << "Could not create a StyleKit style: " << comp.errorString(); + return; + } + auto *style = qobject_cast<QQStyleKitStyle *>(comp.create()); + if (!style) { + qmlWarning(this) << "Could not create a StyleKit style from url: " << styleUrl; + return; + } + + style->componentComplete(); + setStyle(style); + m_ownsStyle = true; + emit styleUrlChanged(); +} + +bool QQStyleKitAttached::transitionsEnabled() const +{ + return s_transitionsEnabled; +} + +void QQStyleKitAttached::setTransitionsEnabled(bool enabled) +{ + if (enabled == s_transitionsEnabled) + return; + + s_transitionsEnabled = enabled; + emit transitionsEnabledChanged(); +} + +QQStyleKitDebug *QQStyleKitAttached::debug() const +{ + return &const_cast<QQStyleKitAttached *>(this)->m_debug; +} + +bool QQStyleKitAttached::styleLoaded() const +{ + return m_style && m_style->loaded(); +} + +QT_END_NAMESPACE + +#include "moc_qqstylekit_p.cpp" + diff --git a/src/labs/stylekit/qqstylekit_p.h b/src/labs/stylekit/qqstylekit_p.h new file mode 100644 index 0000000000..bd8263f767 --- /dev/null +++ b/src/labs/stylekit/qqstylekit_p.h @@ -0,0 +1,86 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +// Qt-Security score:significant reason:default + +#ifndef QQSTYLEKIT_P_H +#define QQSTYLEKIT_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qqstylekitdebug_p.h" + +#include <QtQml/QtQml> + +QT_BEGIN_NAMESPACE + +class QQStyleKitAttached; + +class QQStyleKit : public QObject +{ + Q_OBJECT + QML_NAMED_ELEMENT(StyleKit) + QML_ATTACHED(QQStyleKitAttached) + +public: + QQStyleKit(QObject *parent = nullptr); + static QQStyleKitAttached *qmlAttachedProperties(QObject *obj = nullptr); + +private: + Q_DISABLE_COPY(QQStyleKit) +}; + +class QQStyleKitAttached : public QObject +{ + Q_OBJECT + Q_PROPERTY(QQStyleKitStyle *style READ style WRITE setStyle NOTIFY styleChanged FINAL) + Q_PROPERTY(QString styleUrl READ styleUrl WRITE setStyleUrl NOTIFY styleUrlChanged FINAL) + Q_PROPERTY(bool transitionsEnabled READ transitionsEnabled WRITE setTransitionsEnabled NOTIFY transitionsEnabledChanged FINAL) + Q_PROPERTY(QQStyleKitDebug *debug READ debug FINAL) + +public: + QQStyleKitAttached(QObject *parent); + ~QQStyleKitAttached(); + + QQStyleKitStyle *style() const; + void setStyle(QQStyleKitStyle *style); + + QString styleUrl() const; + void setStyleUrl(const QString &styleUrl); + + bool transitionsEnabled() const; + void setTransitionsEnabled(bool enabled); + + QQStyleKitDebug *debug() const; + + Q_INVOKABLE bool styleLoaded() const; + +signals: + void styleChanged(); + void styleUrlChanged(); + void transitionsEnabledChanged(); + +private: + bool m_ownsStyle = false; + QString m_styleUrl; + QPointer<QQmlEngine> m_engine; + QPointer<QQStyleKitStyle> m_style; + QQStyleKitDebug m_debug; + + static QPointer<QQStyleKitAttached> s_instance; + static bool s_transitionsEnabled; + + friend class QQStyleKit; +}; + +QT_END_NAMESPACE + +#endif // QQSTYLEKIT_P_H diff --git a/src/labs/stylekit/qqstylekitcontrol.cpp b/src/labs/stylekit/qqstylekitcontrol.cpp new file mode 100644 index 0000000000..41f4f33028 --- /dev/null +++ b/src/labs/stylekit/qqstylekitcontrol.cpp @@ -0,0 +1,59 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qqstylekitcontrol_p.h" +#include "qqstylekitpropertyresolver_p.h" + +QT_BEGIN_NAMESPACE + +QQStyleKitControl::QQStyleKitControl(QObject *parent) + : QQStyleKitControlState(parent) +{ +} + +QQmlListProperty<QQStyleKitVariation> QQStyleKitControl::variations() +{ + const bool isWrite = subclass() == QQSK::Subclass::QQStyleKitState; + if (isWrite) { + if (!QQStyleKitPropertyResolver::hasLocalStyleProperty(this, QQSK::Property::Variations)) { + /* We need to handle both the setter and the getter in the getter, since QML doesn't + * use a separate setter for QQmlListProperty. This means that we need to initialize + * the storage with an empty QList, since we need to be prepared for the QML engine + * inserting elements into it behind our back. A negative side-effect of this logic + * is that a simple read of the property for a QQStyleKitState will also accidentally + * populate the local storage of that QQStylKitState, and thereby affect propagation. + * Note: since Variations is not a part of QQStyleKitProperties, it's not possible, + * and there is also no point, in emitting a changed signal globally, since a + * QQuickStyleKitReader inherits QQStyleKitProperties and not QQStyleKitControl, + * and as such, doesn't have a 'variations' property. */ + auto *newList = new QList<QQStyleKitVariation *>(); + setStyleProperty(QQSK::Property::Variations, newList); + } + } + + /* Read the property, taking propagation into account. Note that since QQmlListProperty takes + * a pointer to a QList as argument, we need to store the list as a pointer as well */ + const QVariant variant = QQStyleKitPropertyResolver::readStyleProperty(this, QQSK::Property::Variations); + if (!variant.isValid()) { + // Return a read-only list. Trying to change this list from the app has no effect. + static auto *emptyList = new QList<QQStyleKitVariation *>(); + return QQmlListProperty<QQStyleKitVariation>(this, emptyList); + } + + const auto value = qvariant_cast<QList<QQStyleKitVariation *> *>(variant); + return QQmlListProperty<QQStyleKitVariation>(this, value); +} + +QVariant QQStyleKitControl::readStyleProperty(PropertyStorageId key) const +{ + return m_storage.value(key); +} + +void QQStyleKitControl::writeStyleProperty(PropertyStorageId key, const QVariant &value) +{ + m_storage.insert(key, value); +} + +QT_END_NAMESPACE + +#include "moc_qqstylekitcontrol_p.cpp" diff --git a/src/labs/stylekit/qqstylekitcontrol_p.h b/src/labs/stylekit/qqstylekitcontrol_p.h new file mode 100644 index 0000000000..307046a198 --- /dev/null +++ b/src/labs/stylekit/qqstylekitcontrol_p.h @@ -0,0 +1,55 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +// Qt-Security score:significant reason:default + +#ifndef QQSTYLEKITCONTROL_P_H +#define QQSTYLEKITCONTROL_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtQml/QtQml> +#include "qqstylekitcontrolstate_p.h" +#include "qqstylekitstorage_p.h" + +QT_BEGIN_NAMESPACE + +class QQStyleKitVariation; + +class QQStyleKitControl : public QQStyleKitControlState +{ + Q_OBJECT + Q_PROPERTY(QQmlListProperty<QQStyleKitVariation> variations READ variations FINAL) + QML_NAMED_ELEMENT(StyleKitControl) + +public: + QQStyleKitControl(QObject *parent = nullptr); + + QQmlListProperty<QQStyleKitVariation> variations(); + +private: + QVariant readStyleProperty(PropertyStorageId key) const; + void writeStyleProperty(PropertyStorageId key, const QVariant &value); + +private: + Q_DISABLE_COPY(QQStyleKitControl) + + QList<QQStyleKitVariation *> m_variations; + mutable QQStyleKitPropertyStorage m_storage; + QQSK::State m_writtenStates = QQSK::StateFlag::NoState; + + friend class QQStyleKitPropertyResolver; + friend class QQStyleKitControls; +}; + +QT_END_NAMESPACE + +#endif // QQSTYLEKITCONTROL_P_H diff --git a/src/labs/stylekit/qqstylekitcontrolproperties.cpp b/src/labs/stylekit/qqstylekitcontrolproperties.cpp new file mode 100644 index 0000000000..9b11b560e5 --- /dev/null +++ b/src/labs/stylekit/qqstylekitcontrolproperties.cpp @@ -0,0 +1,1014 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qqstylekitcontrol_p.h" +#include "qqstylekitstyle_p.h" +#include "qqstylekittheme_p.h" +#include "qqstylekitcontrolproperties_p.h" +#include "qqstylekitpropertyresolver_p.h" + +QT_BEGIN_NAMESPACE + +// ************* QQStyleKitPropertyGroup **************** + +QQStyleKitPropertyGroup::QQStyleKitPropertyGroup(QQSK::PropertyGroup group, QObject *parent) + : QObject(parent) + , m_group(group) +{ +} + +/* This macro will check if the caller has the same group path as \a GROUP_PATH. + * This is needed since a QQSK::Property (e.g Color) can sometimes be a + * property in several different subclasses of QQStyleKitPropertyGroup. + * For example, both control.background.color and control.indicator.color has a + * color property. But the group path differs, so they are in reality two completely + * different properties. And in that case, when the former changes value, we want to + * emit changes globally only to that property, and not the latter. + * The caller of this macro will therefore need to go through all the usages of its + * subclass in the API, to figure out which group itself is an instance of. For the + * one that is a match, the macro will go through all readers and emit the same + * signal for them. */ +#define CONDITIONALLY_EMIT_SIGNALS_GLOBALLY_FOR(CONTROL_PROPERTIES, GROUP_PATH) \ +if (this == CONTROL_PROPERTIES -> GROUP_PATH ) { \ + for (QQStyleKitReader *reader : QQStyleKitReader::s_allReaders) { \ + reader->clearLocalStorage(); \ + ((reader-> GROUP_PATH ->*changedSignals)(), ...); \ + } \ + return; \ +} + +template<typename SUBCLASS> +void QQStyleKitPropertyGroup::handleStylePropertyChanged(void (SUBCLASS::*changedSignal)()) { + handleStylePropertiesChanged<SUBCLASS>(changedSignal); +} + +template <typename SUBCLASS, typename... CHANGED_SIGNALS> +void QQStyleKitPropertyGroup::handleStylePropertiesChanged(CHANGED_SIGNALS... changedSignals) +{ + /* This function will check which subclass of QQStyleKitProperties this + * group is (nested) inside. Based on that, it will decide if the signals + * should be emitted locally or not, and if the changed properties affects + * all existing QQStyleKitReaders, and therefore will need to be + * emitted 'globally'. Note that it only makes sense to call this function + * for changed properties that are available from a QQStyleKitReader. + * Properities only available from e.g QQStyleKitControl (such as + * variations), are anyway not readable from a QQStyleKitReader. */ + static_assert(std::is_base_of<QQStyleKitPropertyGroup, SUBCLASS>::value, + "SUBCLASS must inherit QQStyleKitPropertyGroup"); + + auto *group = static_cast<SUBCLASS *>(this); + const QQStyleKitControlProperties *root = controlProperties(); + const QQSK::Subclass objectWrittenTo = root->subclass(); + + if (objectWrittenTo == QQSK::Subclass::QQStyleKitState) { + ((group->*changedSignals)(), ...); + if (shouldEmitGlobally()) + group->emitGlobally(changedSignals...); + return; + } + + if (objectWrittenTo == QQSK::Subclass::QQStyleKitReader) { + /* Unless the StyleReader has told us not to emit any signals (because it's only + * syncing it's own local storage with old values before starting a transition), we + * emit the signal like normal. This will cause the control to repaint (perhaps + * using a transition). */ + if (shouldEmitLocally()) + ((group->*changedSignals)(), ...); + return; + } + + Q_UNREACHABLE(); +} + +const QQStyleKitControlProperties *QQStyleKitPropertyGroup::controlProperties() const +{ + const QQStyleKitPropertyGroup *group = this; + while (!group->isControlProperties()) { + group = static_cast<QQStyleKitPropertyGroup *>(group->parent()); + Q_ASSERT(group); + } + return group->asControlProperties(); +} + +std::tuple< + const QQStyleKitControlProperties *, + const QQSK::PropertyGroup, + const QQSK::PathFlags> +QQStyleKitPropertyGroup::inspectGroupPath() const +{ + /* The path of a property can sometimes contain groups that are hints to the property + * resolver. E.g in the path 'QQStyleKitReader.global.handle.first.padding', 'global' + * hints that the property should be read directly from the style, circumventing the local + * cache that stores interpolated transition values. 'first' hints that the group is inside + * a sub type, which will affect how the style reader handles propagation. */ + QQSK::PathFlags pathFlags = QQSK::PathFlag::NoFlag; + QQSK::PropertyGroup subType = QQSK::PropertyGroup::NoGroup; + + const QQStyleKitPropertyGroup *group = this; + while (!group->isControlProperties()) { + if (group->m_group == QQSK::PropertyGroup::DelegateSubType1) + subType = QQSK::PropertyGroup::DelegateSubType1; + else if (group->m_group == QQSK::PropertyGroup::DelegateSubType2) + subType = QQSK::PropertyGroup::DelegateSubType2; + else if (group->m_group == QQSK::PropertyGroup::globalFlag) + pathFlags |= QQSK::PathFlag::StyleDirect; + + group = group->parentGroup(); + Q_ASSERT(group); + } + + const auto *controlProperties = group->asControlProperties(); + return std::make_tuple(controlProperties, subType, pathFlags); +} + +const QQStyleKitControlProperties *QQStyleKitPropertyGroup::asControlProperties() const +{ + Q_ASSERT(isControlProperties()); + return static_cast<const QQStyleKitControlProperties *>(this); +} + +void QQStyleKitPropertyGroup::emitChangedForAllStylePropertiesRecursive() +{ + /* This function will emit changed signals for all style properties in the + * StyleKit API (for a single QQStyleKitReader), which is needed after + * doing a style-, or theme change. */ + const int startIndex = QQStyleKitPropertyGroup::staticMetaObject.propertyOffset(); + const QMetaObject* meta = metaObject(); + for (int i = startIndex; i < meta->propertyCount(); ++i) { + const QMetaProperty prop = meta->property(i); + const QMetaObject* metaObject = QMetaType::fromName(prop.typeName()).metaObject(); + if (metaObject) { + if (metaObject->inherits(&QQStyleKitDelegateProperties::staticMetaObject)) { + /* Skip recursing into QQStyleKitDelegateProperties, because those are lazy + * created when read, and reading them from here would accidentally + * create them. */ + continue; + } + if (metaObject->inherits(&QQStyleKitPropertyGroup::staticMetaObject)) { + // The property is of type QQStyleKitPropertyGroup, so recurse into it + QObject *childObj = qvariant_cast<QObject *>(property(prop.name())); + if (auto *child = qobject_cast<QQStyleKitPropertyGroup *>(childObj)) + child->emitChangedForAllStylePropertiesRecursive(); + continue; + } + } + + // Emit the changed signal for the property + Q_ASSERT(prop.hasNotifySignal()); + QMetaMethod notify = prop.notifySignal(); + notify.invoke(this, Qt::DirectConnection); + } +} + +bool QQStyleKitPropertyGroup::shouldEmitLocally() +{ + const QQStyleKitControlProperties *root = controlProperties(); + return !root->asQQStyleKitReader()->dontEmitChangedSignals(); +} + +bool QQStyleKitPropertyGroup::shouldEmitGlobally() +{ + const QQStyleKitControlProperties *root = controlProperties(); + QQStyleKitStyle *parentStyle = root->style(); + if (!parentStyle) + return false; + + if (parentStyle->loaded() && !parentStyle->m_isUpdatingPalette) { + /* When a property has changed in the 'global' QQStyleKitStyle itself, it can + * potentially affect all control instances. We therefore need to go through all + * QQStyleKitReaders and inform that their own local property that matches the + * 'global' property needs to be re-read. We emit the signals directly, omitting any + * applied transitions in the QQStyleKitReaders, to optimize for speed. The exception + * is if we're just updating the palette in the Style to match the palette in the current + * control / QQStyleKitReader. Such a change will only affect a single control. */ + return parentStyle == QQStyleKitStyle::current(); + } + return false; +} + +// ************* QQStyleKitImageProperties **************** + +QQStyleKitImageProperties::QQStyleKitImageProperties(QQSK::PropertyGroup group, QQStyleKitPropertyGroup *parent) + : QQStyleKitPropertyGroup(group, parent) +{ +} + +template <typename... CHANGED_SIGNALS> +void QQStyleKitImageProperties::emitGlobally(CHANGED_SIGNALS... changedSignals) const +{ + // Go through all instances of QQStyleKitImageProperties + const QQStyleKitControlProperties *cp = controlProperties(); + CONDITIONALLY_EMIT_SIGNALS_GLOBALLY_FOR(cp, background()->image()); + CONDITIONALLY_EMIT_SIGNALS_GLOBALLY_FOR(cp, handle()->image()); + CONDITIONALLY_EMIT_SIGNALS_GLOBALLY_FOR(cp, indicator()->image()); + CONDITIONALLY_EMIT_SIGNALS_GLOBALLY_FOR(cp, indicator()->foreground()->image()); +} + +QUrl QQStyleKitImageProperties::source() const +{ + return styleProperty<QUrl>(QQSK::Property::Source); +} + +void QQStyleKitImageProperties::setSource(const QUrl &source) +{ + if (setStyleProperty(QQSK::Property::Source, source)) + handleStylePropertyChanged(&QQStyleKitImageProperties::sourceChanged); +} + +QColor QQStyleKitImageProperties::color() const +{ + return styleProperty<QColor>(QQSK::Property::Color); +} + +void QQStyleKitImageProperties::setColor(const QColor &color) +{ + if (setStyleProperty(QQSK::Property::Color, color)) + handleStylePropertyChanged(&QQStyleKitImageProperties::colorChanged); +} + +QQuickImage::FillMode QQStyleKitImageProperties::fillMode() const +{ + return styleProperty<QQuickImage::FillMode>(QQSK::Property::FillMode); +} + +void QQStyleKitImageProperties::setFillMode(QQuickImage::FillMode fillMode) +{ + if (setStyleProperty(QQSK::Property::FillMode, fillMode)) + handleStylePropertyChanged(&QQStyleKitImageProperties::fillModeChanged); +} + +// ************* QQStyleKitBorderProperties **************** + +QQStyleKitBorderProperties::QQStyleKitBorderProperties(QQSK::PropertyGroup group, QQStyleKitPropertyGroup *parent) + : QQStyleKitPropertyGroup(group, parent) +{ +} + +template <typename... CHANGED_SIGNALS> +void QQStyleKitBorderProperties::emitGlobally(CHANGED_SIGNALS... changedSignals) const +{ + // Go through all instances of QQStyleKitBorderProperties + const QQStyleKitControlProperties *cp = controlProperties(); + CONDITIONALLY_EMIT_SIGNALS_GLOBALLY_FOR(cp, background()->border()); + CONDITIONALLY_EMIT_SIGNALS_GLOBALLY_FOR(cp, handle()->border()); + CONDITIONALLY_EMIT_SIGNALS_GLOBALLY_FOR(cp, indicator()->border()); + CONDITIONALLY_EMIT_SIGNALS_GLOBALLY_FOR(cp, indicator()->foreground()->border()); +} + +qreal QQStyleKitBorderProperties::width() const +{ + return styleProperty<qreal>(QQSK::Property::Width); +} + +void QQStyleKitBorderProperties::setWidth(qreal width) +{ + if (setStyleProperty(QQSK::Property::Width, width)) + handleStylePropertyChanged(&QQStyleKitBorderProperties::widthChanged); +} + +QColor QQStyleKitBorderProperties::color() const +{ + return styleProperty<QColor>(QQSK::Property::Color, Qt::transparent); +} + +void QQStyleKitBorderProperties::setColor(const QColor &color) +{ + if (setStyleProperty(QQSK::Property::Color, color)) + handleStylePropertyChanged(&QQStyleKitBorderProperties::colorChanged); +} + +// ************* QQStyleKitShadowProperties **************** + +QQStyleKitShadowProperties::QQStyleKitShadowProperties(QQSK::PropertyGroup group, QQStyleKitPropertyGroup *parent) + : QQStyleKitPropertyGroup(group, parent) +{ +} + +template <typename... CHANGED_SIGNALS> +void QQStyleKitShadowProperties::emitGlobally(CHANGED_SIGNALS... changedSignals) const +{ + // Go through all instances of QQStyleKitShadowProperties + const QQStyleKitControlProperties *cp = controlProperties(); + CONDITIONALLY_EMIT_SIGNALS_GLOBALLY_FOR(cp, background()->shadow()); + CONDITIONALLY_EMIT_SIGNALS_GLOBALLY_FOR(cp, handle()->shadow()); + CONDITIONALLY_EMIT_SIGNALS_GLOBALLY_FOR(cp, indicator()->shadow()); + CONDITIONALLY_EMIT_SIGNALS_GLOBALLY_FOR(cp, indicator()->foreground()->shadow()); +} + +QColor QQStyleKitShadowProperties::color() const +{ + return styleProperty<QColor>(QQSK::Property::Color, Qt::transparent); +} + +void QQStyleKitShadowProperties::setColor(QColor color) +{ + if (setStyleProperty(QQSK::Property::Color, color)) + handleStylePropertyChanged(&QQStyleKitShadowProperties::colorChanged); +} + +qreal QQStyleKitShadowProperties::opacity() const +{ + return styleProperty<qreal>(QQSK::Property::Opacity, 1.0); +} + +void QQStyleKitShadowProperties::setOpacity(qreal opacity) +{ + if (setStyleProperty(QQSK::Property::Opacity, opacity)) + handleStylePropertyChanged(&QQStyleKitShadowProperties::opacityChanged); +} + +qreal QQStyleKitShadowProperties::scale() const +{ + return styleProperty<qreal>(QQSK::Property::Scale, 1.0); +} + +void QQStyleKitShadowProperties::setScale(qreal scale) +{ + if (setStyleProperty(QQSK::Property::Scale, scale)) + handleStylePropertyChanged(&QQStyleKitShadowProperties::scaleChanged); +} + +qreal QQStyleKitShadowProperties::verticalOffset() const +{ + return styleProperty<qreal>(QQSK::Property::VOffset); +} + +void QQStyleKitShadowProperties::setVerticalOffset(qreal verticalOffset) +{ + if (setStyleProperty(QQSK::Property::VOffset, verticalOffset)) + handleStylePropertyChanged(&QQStyleKitShadowProperties::verticalOffsetChanged); +} + +qreal QQStyleKitShadowProperties::horizontalOffset() const +{ + return styleProperty<qreal>(QQSK::Property::HOffset); +} + +void QQStyleKitShadowProperties::setHorizontalOffset(qreal horizontalOffset) +{ + if (setStyleProperty(QQSK::Property::HOffset, horizontalOffset)) + handleStylePropertyChanged(&QQStyleKitShadowProperties::horizontalOffsetChanged); +} + +qreal QQStyleKitShadowProperties::blur() const +{ + return styleProperty<qreal>(QQSK::Property::Blur, 10.0); +} + +void QQStyleKitShadowProperties::setBlur(qreal blur) +{ + if (setStyleProperty(QQSK::Property::Blur, blur)) + handleStylePropertyChanged(&QQStyleKitShadowProperties::blurChanged); +} + +bool QQStyleKitShadowProperties::visible() const +{ + return styleProperty<bool>(QQSK::Property::Visible, true); +} + +void QQStyleKitShadowProperties::setVisible(bool visible) +{ + if (setStyleProperty(QQSK::Property::Visible, visible)) + handleStylePropertyChanged(&QQStyleKitShadowProperties::visibleChanged); +} + +QQmlComponent *QQStyleKitShadowProperties::delegate() const +{ + return styleProperty<QQmlComponent *>(QQSK::Property::Delegate); +} + +void QQStyleKitShadowProperties::setDelegate(QQmlComponent *delegate) +{ + if (setStyleProperty(QQSK::Property::Delegate, delegate)) + handleStylePropertyChanged(&QQStyleKitShadowProperties::delegateChanged); +} + +// ************* QQStyleKitDelegateProperties **************** + +QQStyleKitDelegateProperties::QQStyleKitDelegateProperties(QQSK::PropertyGroup group, QQStyleKitPropertyGroup *parent) + : QQStyleKitPropertyGroup(group, parent) +{ +} + +template <typename... CHANGED_SIGNALS> +void QQStyleKitDelegateProperties::emitGlobally(CHANGED_SIGNALS... changedSignals) const +{ + // Go through all instances of QQStyleKitDelegateProperties + const QQStyleKitControlProperties *cp = controlProperties(); + CONDITIONALLY_EMIT_SIGNALS_GLOBALLY_FOR(cp, background()); + CONDITIONALLY_EMIT_SIGNALS_GLOBALLY_FOR(cp, handle()); + CONDITIONALLY_EMIT_SIGNALS_GLOBALLY_FOR(cp, indicator()); + CONDITIONALLY_EMIT_SIGNALS_GLOBALLY_FOR(cp, indicator()->foreground()); +} + +qreal QQStyleKitDelegateProperties::radius() const +{ + return styleProperty<qreal>(QQSK::Property::Radius); +} + +void QQStyleKitDelegateProperties::setRadius(qreal radius) +{ + if (setStyleProperty(QQSK::Property::Radius, radius)) + handleStylePropertiesChanged<QQStyleKitDelegateProperties>( + &QQStyleKitDelegateProperties::radiusChanged, + &QQStyleKitDelegateProperties::topLeftRadiusChanged, + &QQStyleKitDelegateProperties::topRightRadiusChanged, + &QQStyleKitDelegateProperties::bottomLeftRadiusChanged, + &QQStyleKitDelegateProperties::bottomRightRadiusChanged); +} + +qreal QQStyleKitDelegateProperties::topLeftRadius() const +{ + return styleProperty<qreal>(QQSK::Property::TopLeftRadius, QQSK::Property::Radius); +} + +void QQStyleKitDelegateProperties::setTopLeftRadius(qreal radius) +{ + if (setStyleProperty(QQSK::Property::TopLeftRadius, radius)) + handleStylePropertyChanged(&QQStyleKitDelegateProperties::topLeftRadiusChanged); +} + +qreal QQStyleKitDelegateProperties::topRightRadius() const +{ + return styleProperty<qreal>(QQSK::Property::TopRightRadius, QQSK::Property::Radius); +} + +void QQStyleKitDelegateProperties::setTopRightRadius(qreal radius) +{ + if (setStyleProperty(QQSK::Property::TopRightRadius, radius)) + handleStylePropertyChanged(&QQStyleKitDelegateProperties::topRightRadiusChanged); +} + +qreal QQStyleKitDelegateProperties::bottomLeftRadius() const +{ + return styleProperty<qreal>(QQSK::Property::BottomLeftRadius, QQSK::Property::Radius); +} + +void QQStyleKitDelegateProperties::setBottomLeftRadius(qreal radius) +{ + if (setStyleProperty(QQSK::Property::BottomLeftRadius, radius)) + handleStylePropertyChanged(&QQStyleKitDelegateProperties::bottomLeftRadiusChanged); +} + +qreal QQStyleKitDelegateProperties::bottomRightRadius() const +{ + return styleProperty<qreal>(QQSK::Property::BottomRightRadius, QQSK::Property::Radius); +} + +void QQStyleKitDelegateProperties::setBottomRightRadius(qreal radius) +{ + if (setStyleProperty(QQSK::Property::BottomRightRadius, radius)) + handleStylePropertyChanged(&QQStyleKitDelegateProperties::bottomRightRadiusChanged); +} + +qreal QQStyleKitDelegateProperties::scale() const +{ + return styleProperty<qreal>(QQSK::Property::Scale, 1.0); +} + +void QQStyleKitDelegateProperties::setScale(qreal scale) +{ + if (setStyleProperty(QQSK::Property::Scale, scale)) + handleStylePropertyChanged(&QQStyleKitDelegateProperties::scaleChanged); +} + +qreal QQStyleKitDelegateProperties::rotation() const +{ + return styleProperty<qreal>(QQSK::Property::Rotation); +} + +void QQStyleKitDelegateProperties::setRotation(qreal rotation) +{ + if (setStyleProperty(QQSK::Property::Rotation, rotation)) + handleStylePropertyChanged(&QQStyleKitDelegateProperties::rotationChanged); +} + +qreal QQStyleKitDelegateProperties::implicitWidth() const +{ + return styleProperty<qreal>(QQSK::Property::ImplicitWidth); +} + +void QQStyleKitDelegateProperties::setImplicitWidth(qreal width) +{ + if (setStyleProperty(QQSK::Property::ImplicitWidth, width)) + handleStylePropertyChanged(&QQStyleKitDelegateProperties::implicitWidthChanged); +} + +qreal QQStyleKitDelegateProperties::implicitHeight() const +{ + return styleProperty<qreal>(QQSK::Property::ImplicitHeight); +} + +void QQStyleKitDelegateProperties::setImplicitHeight(qreal height) +{ + if (setStyleProperty(QQSK::Property::ImplicitHeight, height)) + handleStylePropertyChanged(&QQStyleKitDelegateProperties::implicitHeightChanged); +} + +qreal QQStyleKitDelegateProperties::minimumWidth() const +{ + return styleProperty<qreal>(QQSK::Property::MinimumWidth); +} + +void QQStyleKitDelegateProperties::setMinimumWidth(qreal width) +{ + if (setStyleProperty(QQSK::Property::MinimumWidth, width)) + handleStylePropertyChanged(&QQStyleKitDelegateProperties::minimumWidthChanged); +} + +qreal QQStyleKitDelegateProperties::margins() const +{ + return styleProperty<qreal>(QQSK::Property::Margins); +} + +void QQStyleKitDelegateProperties::setMargins(qreal margins) +{ + if (setStyleProperty(QQSK::Property::Margins, margins)) + handleStylePropertiesChanged<QQStyleKitDelegateProperties>( + &QQStyleKitDelegateProperties::marginsChanged, + &QQStyleKitDelegateProperties::leftMarginChanged, + &QQStyleKitDelegateProperties::rightMarginChanged, + &QQStyleKitDelegateProperties::topMarginChanged, + &QQStyleKitDelegateProperties::bottomMarginChanged); +} + +qreal QQStyleKitDelegateProperties::leftMargin() const +{ + return styleProperty<qreal>(QQSK::Property::LeftMargin, QQSK::Property::Margins); +} + +void QQStyleKitDelegateProperties::setLeftMargin(qreal margin) +{ + if (setStyleProperty(QQSK::Property::LeftMargin, margin)) + handleStylePropertyChanged(&QQStyleKitDelegateProperties::leftMarginChanged); +} + +qreal QQStyleKitDelegateProperties::rightMargin() const +{ + return styleProperty<qreal>(QQSK::Property::RightMargin, QQSK::Property::Margins); +} + +void QQStyleKitDelegateProperties::setRightMargin(qreal margin) +{ + if (setStyleProperty(QQSK::Property::RightMargin, margin)) + handleStylePropertyChanged(&QQStyleKitDelegateProperties::rightMarginChanged); +} + +qreal QQStyleKitDelegateProperties::topMargin() const +{ + return styleProperty<qreal>(QQSK::Property::TopMargin, QQSK::Property::Margins); +} + +void QQStyleKitDelegateProperties::setTopMargin(qreal margin) +{ + if (setStyleProperty(QQSK::Property::TopMargin, margin)) + handleStylePropertyChanged(&QQStyleKitDelegateProperties::topMarginChanged); +} + +qreal QQStyleKitDelegateProperties::bottomMargin() const +{ + return styleProperty<qreal>(QQSK::Property::BottomMargin, QQSK::Property::Margins); +} + +void QQStyleKitDelegateProperties::setBottomMargin(qreal margin) +{ + if (setStyleProperty(QQSK::Property::BottomMargin, margin)) + handleStylePropertyChanged(&QQStyleKitDelegateProperties::bottomMarginChanged); +} + +Qt::Alignment QQStyleKitDelegateProperties::alignment() const +{ + return styleProperty<Qt::Alignment>(QQSK::Property::Alignment, Qt::AlignLeft | Qt::AlignVCenter); +} + +void QQStyleKitDelegateProperties::setAlignment(Qt::Alignment alignment) +{ + if (setStyleProperty(QQSK::Property::Alignment, alignment)) + handleStylePropertyChanged(&QQStyleKitDelegateProperties::alignmentChanged); +} + +qreal QQStyleKitDelegateProperties::opacity() const +{ + return styleProperty<qreal>(QQSK::Property::Opacity, 1.0); +} + +void QQStyleKitDelegateProperties::setOpacity(qreal opacity) +{ + if (setStyleProperty(QQSK::Property::Opacity, opacity)) + handleStylePropertyChanged(&QQStyleKitDelegateProperties::opacityChanged); +} + +QColor QQStyleKitDelegateProperties::color() const +{ + return styleProperty<QColor>(QQSK::Property::Color, Qt::transparent); +} + +void QQStyleKitDelegateProperties::setColor(const QColor &color) +{ + if (setStyleProperty(QQSK::Property::Color, color)) + handleStylePropertyChanged(&QQStyleKitDelegateProperties::colorChanged); +} + +bool QQStyleKitDelegateProperties::visible() const +{ + return styleProperty<bool>(QQSK::Property::Visible, true); +} + +void QQStyleKitDelegateProperties::setVisible(bool visible) +{ + if (setStyleProperty(QQSK::Property::Visible, visible)) + handleStylePropertyChanged(&QQStyleKitDelegateProperties::visibleChanged); +} + +bool QQStyleKitDelegateProperties::clip() const +{ + return styleProperty<bool>(QQSK::Property::Clip, false); +} + +void QQStyleKitDelegateProperties::setClip(bool clip) +{ + if (setStyleProperty(QQSK::Property::Clip, clip)) + handleStylePropertyChanged(&QQStyleKitDelegateProperties::clipChanged); +} + +QQuickGradient *QQStyleKitDelegateProperties::gradient() const +{ + return styleProperty<QQuickGradient *>(QQSK::Property::Gradient); +} + +void QQStyleKitDelegateProperties::setGradient(QQuickGradient *gradient) +{ + if (setStyleProperty(QQSK::Property::Gradient, gradient)) + handleStylePropertyChanged(&QQStyleKitDelegateProperties::gradientChanged); +} + +QObject *QQStyleKitDelegateProperties::data() const +{ + return styleProperty<QObject *>(QQSK::Property::Data); +} + +void QQStyleKitDelegateProperties::setData(QObject *data) +{ + if (setStyleProperty(QQSK::Property::Data, data)) + handleStylePropertyChanged(&QQStyleKitDelegateProperties::dataChanged); +} + +QQmlComponent *QQStyleKitDelegateProperties::delegate() const +{ + return styleProperty<QQmlComponent *>(QQSK::Property::Delegate); +} + +void QQStyleKitDelegateProperties::setDelegate(QQmlComponent *delegate) +{ + if (setStyleProperty(QQSK::Property::Delegate, delegate)) + handleStylePropertyChanged(&QQStyleKitDelegateProperties::delegateChanged); +} + +QQStyleKitBorderProperties *QQStyleKitDelegateProperties::border() const +{ + return lazyCreateGroup(m_border, QQSK::PropertyGroup::Border); +} + +QQStyleKitShadowProperties *QQStyleKitDelegateProperties::shadow() const +{ + return lazyCreateGroup(m_shadow, QQSK::PropertyGroup::Shadow); +} + +QQStyleKitImageProperties *QQStyleKitDelegateProperties::image() const +{ + return lazyCreateGroup(m_image, QQSK::PropertyGroup::Image); +} + +// ************* QQStyleKitHandleProperties **************** + +QQStyleKitHandleProperties::QQStyleKitHandleProperties(QQSK::PropertyGroup group, QQStyleKitPropertyGroup *parent) + : QQStyleKitDelegateProperties(group, parent) +{ +} + +QQStyleKitDelegateProperties *QQStyleKitHandleProperties::first() const +{ + return lazyCreateGroup(m_first, QQSK::PropertyGroup::DelegateSubType1); +} + +QQStyleKitDelegateProperties *QQStyleKitHandleProperties::second() const +{ + return lazyCreateGroup(m_second, QQSK::PropertyGroup::DelegateSubType2); +} + +// ************* QQStyleKitIndicatorProperties **************** + +QQStyleKitIndicatorProperties::QQStyleKitIndicatorProperties( + QQSK::PropertyGroup group, QQStyleKitPropertyGroup *parent) + : QQStyleKitDelegateProperties(group, parent) +{ +} + +template <typename... CHANGED_SIGNALS> +void QQStyleKitIndicatorProperties::emitGlobally(CHANGED_SIGNALS... changedSignals) const +{ + // Go through all instances of QQStyleKitIndicatorProperties + const QQStyleKitControlProperties *cp = controlProperties(); + CONDITIONALLY_EMIT_SIGNALS_GLOBALLY_FOR(cp, indicator()->up()); + CONDITIONALLY_EMIT_SIGNALS_GLOBALLY_FOR(cp, indicator()->down()); +} + +QQStyleKitDelegateProperties *QQStyleKitIndicatorProperties::foreground() const +{ + return lazyCreateGroup(m_foreground, QQSK::PropertyGroup::Foreground); +} + +// ************* QQStyleKitIndicatorWithSubTypes **************** + +QQStyleKitIndicatorWithSubTypes::QQStyleKitIndicatorWithSubTypes( + QQSK::PropertyGroup group, QQStyleKitPropertyGroup *parent) + : QQStyleKitDelegateProperties(group, parent) +{ +} + +template <typename... CHANGED_SIGNALS> +void QQStyleKitIndicatorWithSubTypes::emitGlobally(CHANGED_SIGNALS... changedSignals) const +{ + // Go through all instances of QQStyleKitIndicatorWithSubTypes + const QQStyleKitControlProperties *cp = controlProperties(); + CONDITIONALLY_EMIT_SIGNALS_GLOBALLY_FOR(cp, indicator()); +} + +QQStyleKitDelegateProperties *QQStyleKitIndicatorWithSubTypes::foreground() const +{ + return lazyCreateGroup(m_foreground, QQSK::PropertyGroup::Foreground); +} + +QQStyleKitIndicatorProperties *QQStyleKitIndicatorWithSubTypes::up() const +{ + return lazyCreateGroup(m_up, QQSK::PropertyGroup::DelegateSubType1); +} + +QQStyleKitIndicatorProperties *QQStyleKitIndicatorWithSubTypes::down() const +{ + return lazyCreateGroup(m_down, QQSK::PropertyGroup::DelegateSubType2); +} + +// ************* QQStyleKitTextProperties **************** +QQStyleKitTextProperties::QQStyleKitTextProperties(QQSK::PropertyGroup group, QQStyleKitPropertyGroup *parent) + : QQStyleKitPropertyGroup(group, parent) +{ +} + +template <typename... CHANGED_SIGNALS> +void QQStyleKitTextProperties::emitGlobally(CHANGED_SIGNALS... changedSignals) const +{ + const QQStyleKitControlProperties *cp = controlProperties(); + CONDITIONALLY_EMIT_SIGNALS_GLOBALLY_FOR(cp, text()); +} + +QColor QQStyleKitTextProperties::color() const +{ + return styleProperty<QColor>(QQSK::Property::Color); +} + +void QQStyleKitTextProperties::setColor(const QColor &color) +{ + if (setStyleProperty(QQSK::Property::Color, color)) + handleStylePropertyChanged(&QQStyleKitTextProperties::colorChanged); +} + +Qt::Alignment QQStyleKitTextProperties::alignment() const +{ + return styleProperty<Qt::Alignment>(QQSK::Property::Alignment); +} + +void QQStyleKitTextProperties::setAlignment(Qt::Alignment alignment) +{ + if (setStyleProperty(QQSK::Property::Alignment, alignment)) + handleStylePropertyChanged(&QQStyleKitTextProperties::alignmentChanged); +} + +// ************* QQStyleKitControlProperties **************** + +QQStyleKitControlProperties::QQStyleKitControlProperties(QQSK::PropertyGroup group, QObject *parent) + : QQStyleKitPropertyGroup(group, parent) +{ +} + +QQStyleKitStyle *QQStyleKitControlProperties::style() const +{ + if (subclass() == QQSK::Subclass::QQStyleKitState) { + /* A QQStyleKitControlState (and its subclasses) should always be a (grand)child of a + * QQStyleKitStyle. And it belongs to that style, even it that style is not the + * currently active application style. This is opposed to a QQStyleKitReader, + * that normally belongs / communicates with the currently active style. + * NOTE: a style can also be a fallback style for another style (which can be recursive, + * meaning that a fallback style can also have its own fallback style, and so on). But + * this function will return the nearest style, and not the root style */ + QObject *obj = parent(); + while (obj && !obj->metaObject()->inherits(&QQStyleKitStyle::staticMetaObject)) + obj = obj->parent(); + return obj ? static_cast<QQStyleKitStyle *>(obj) : nullptr; + } + + /* A style reader belongs to the currently active application style. We could in theory + * support being able to point a QQStyleKitReader to any style, which would basically + * mean that you could mix controls from several styles inside the same application. But + * there is currently no API (or use-case?) in Controls that lets you to do that, so its + * disabled for now. */ + return QQStyleKitStyle::current(); +} + +QQSK::Subclass QQStyleKitControlProperties::subclass() const +{ + /* QQStyleKitControlProperties is subclassed by several different classes in this + * framework. As such, it's basically just an interface because it only declares the + * different properties that can be read or written to in a QQStyleKitStyle, such as + * hovered.background.color or pressed.indicator.foreground.color. It says nothing + * about how those properties are stored, instead that is up to each individual + * subclass to decide */ + if (metaObject()->inherits(&QQStyleKitReader::staticMetaObject)) + return QQSK::Subclass::QQStyleKitReader; + if (metaObject()->inherits(&QQStyleKitControlState::staticMetaObject)) + return QQSK::Subclass::QQStyleKitState; + Q_UNREACHABLE(); +} + +QQStyleKitReader *QQStyleKitControlProperties::asQQStyleKitReader() const +{ + Q_ASSERT(subclass() == QQSK::Subclass::QQStyleKitReader); + return static_cast<QQStyleKitReader *>(const_cast<QQStyleKitControlProperties *>(this)); +} + +QQStyleKitControlState *QQStyleKitControlProperties::asQQStyleKitState() const +{ + Q_ASSERT(subclass() == QQSK::Subclass::QQStyleKitState); + Q_ASSERT(metaObject()->inherits(&QQStyleKitControlState::staticMetaObject)); + return static_cast<QQStyleKitControlState *>(const_cast<QQStyleKitControlProperties *>(this)); +} + +QQStyleKitControl *QQStyleKitControlProperties::asQQStyleKitControl() const +{ + Q_ASSERT(subclass() == QQSK::Subclass::QQStyleKitState); + Q_ASSERT(metaObject()->inherits(&QQStyleKitControl::staticMetaObject)); + return static_cast<QQStyleKitControl *>(const_cast<QQStyleKitControlProperties *>(this)); +} + +void QQStyleKitControlProperties::forEachUsedDelegate( + std::function<void (QQStyleKitDelegateProperties *, QQSK::Delegate, const QString &)> f) +{ + // If adding more delegates here, remember to keep StyleKitAnimation.qml in sync + if (m_background) + f(m_background, QQSK::Delegate::Background, "background"_L1); + + if (m_indicator) { + f(m_indicator, QQSK::Delegate::Indicator, "indicator"_L1); + if (m_indicator->m_foreground) + f(m_indicator->m_foreground, QQSK::Delegate::IndicatorForeground, "indicator.foreground"_L1); + if (m_indicator->m_up) { + f(m_indicator->m_up, QQSK::Delegate::IndicatorUp, "indicator.up"_L1); + if (m_indicator->m_up->m_foreground) + f(m_indicator->m_up->m_foreground, QQSK::Delegate::IndicatorUpForeground, "indicator.up.foreground"_L1); + } + if (m_indicator->m_down) { + f(m_indicator->m_down, QQSK::Delegate::IndicatorDown, "indicator.down"_L1); + if (m_indicator->m_down->m_foreground) + f(m_indicator->m_down->m_foreground, QQSK::Delegate::IndicatorDownForeground, "indicator.down.foreground"_L1); + } + } + + if (m_handle) { + f(m_handle, QQSK::Delegate::Handle, "handle"_L1); + if (m_handle->m_first) + f(m_handle->m_first, QQSK::Delegate::HandleFirst, "handle.first"_L1); + if (m_handle->m_second) + f(m_handle->m_second, QQSK::Delegate::HandleSecond, "handle.second"_L1); + } +} + +void QQStyleKitControlProperties::emitChangedForAllStyleProperties() +{ + /* This brute-force function will emit update signals for _all_ style properties + * in the QQStyleKitStyle API. Doing so is typically needed after a style-, or theme + * change, as we don't know which properties are affected by such a big change. */ + emit leftPaddingChanged(); + emit rightPaddingChanged(); + emit topPaddingChanged(); + emit bottomPaddingChanged(); + emit spacingChanged(); + emit transitionChanged(); + emit textChanged(); + + forEachUsedDelegate([](QQStyleKitDelegateProperties *delegate, QQSK::Delegate, const QString &){ + delegate->emitChangedForAllStylePropertiesRecursive(); + }); +} + +template <typename... CHANGED_SIGNALS> +void QQStyleKitControlProperties::emitGlobally(CHANGED_SIGNALS... changedSignals) const +{ + for (QQStyleKitReader *reader : QQStyleKitReader::s_allReaders) + ((reader->*changedSignals)(), ...); +} + +qreal QQStyleKitControlProperties::spacing() const +{ + return styleProperty<qreal>(QQSK::Property::Spacing); +} + +void QQStyleKitControlProperties::setSpacing(qreal spacing) +{ + if (setStyleProperty(QQSK::Property::Spacing, spacing)) + handleStylePropertyChanged(&QQStyleKitControlProperties::spacingChanged); +} + +qreal QQStyleKitControlProperties::padding() const +{ + return styleProperty<qreal>(QQSK::Property::Padding); +} + +void QQStyleKitControlProperties::setPadding(qreal padding) +{ + if (setStyleProperty(QQSK::Property::Padding, padding)) + handleStylePropertiesChanged<QQStyleKitControlProperties>( + &QQStyleKitControlProperties::paddingChanged, + &QQStyleKitControlProperties::leftPaddingChanged, + &QQStyleKitControlProperties::rightPaddingChanged, + &QQStyleKitControlProperties::topPaddingChanged, + &QQStyleKitControlProperties::bottomPaddingChanged); +} + +qreal QQStyleKitControlProperties::leftPadding() const +{ + return styleProperty<qreal>(QQSK::Property::LeftPadding, QQSK::Property::Padding); +} + +void QQStyleKitControlProperties::setLeftPadding(qreal leftPadding) +{ + if (setStyleProperty(QQSK::Property::LeftPadding, leftPadding)) + handleStylePropertyChanged(&QQStyleKitControlProperties::leftPaddingChanged); +} + +qreal QQStyleKitControlProperties::rightPadding() const +{ + return styleProperty<qreal>(QQSK::Property::RightPadding, QQSK::Property::Padding); +} + +void QQStyleKitControlProperties::setRightPadding(qreal rightPadding) +{ + if (setStyleProperty(QQSK::Property::RightPadding, rightPadding)) + handleStylePropertyChanged(&QQStyleKitControlProperties::rightPaddingChanged); +} + +qreal QQStyleKitControlProperties::topPadding() const +{ + return styleProperty<qreal>(QQSK::Property::TopPadding, QQSK::Property::Padding); +} + +void QQStyleKitControlProperties::setTopPadding(qreal topPadding) +{ + if (setStyleProperty(QQSK::Property::TopPadding, topPadding)) + handleStylePropertyChanged(&QQStyleKitControlProperties::topPaddingChanged); +} + +qreal QQStyleKitControlProperties::bottomPadding() const +{ + return styleProperty<qreal>(QQSK::Property::BottomPadding, QQSK::Property::Padding); +} + +void QQStyleKitControlProperties::setBottomPadding(qreal bottomPadding) +{ + if (setStyleProperty(QQSK::Property::BottomPadding, bottomPadding)) + handleStylePropertyChanged(&QQStyleKitControlProperties::bottomPaddingChanged); +} + +QQuickTransition *QQStyleKitControlProperties::transition() const +{ + return styleProperty<QQuickTransition *>(QQSK::Property::Transition); +} + +void QQStyleKitControlProperties::setTransition(QQuickTransition *transition) +{ + if (setStyleProperty(QQSK::Property::Transition, transition)) + handleStylePropertyChanged(&QQStyleKitControlProperties::transitionChanged); +} + +QQStyleKitTextProperties *QQStyleKitControlProperties::text() const +{ + return lazyCreateGroup(m_text, QQSK::PropertyGroup::Text); +} + +QQStyleKitDelegateProperties *QQStyleKitControlProperties::background() const +{ + return lazyCreateGroup(m_background, QQSK::PropertyGroup::Background); +} + +QQStyleKitHandleProperties *QQStyleKitControlProperties::handle() const +{ + return lazyCreateGroup(m_handle, QQSK::PropertyGroup::Handle); +} + +QQStyleKitIndicatorWithSubTypes *QQStyleKitControlProperties::indicator() const +{ + return lazyCreateGroup(m_indicator, QQSK::PropertyGroup::Indicator); +} + +QT_END_NAMESPACE + +#include "moc_qqstylekitcontrolproperties_p.cpp" diff --git a/src/labs/stylekit/qqstylekitcontrolproperties_p.h b/src/labs/stylekit/qqstylekitcontrolproperties_p.h new file mode 100644 index 0000000000..5bd04c3fd9 --- /dev/null +++ b/src/labs/stylekit/qqstylekitcontrolproperties_p.h @@ -0,0 +1,591 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +// Qt-Security score:significant reason:default + +#ifndef QQSTYLEKITCONTROLPROPERTIES_P_H +#define QQSTYLEKITCONTROLPROPERTIES_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtQml/QtQml> +#include <QtGui/QColor> +#include <QtQuick/private/qquickrectangle_p.h> +#include <QtQuick/private/qquickimage_p.h> +#include <QtQuick/private/qquicktransition_p.h> +#include <QtQuick/private/qquicktext_p.h> + +#include "qqstylekitglobal_p.h" +#include "qqstylekitpropertyresolver_p.h" + +QT_BEGIN_NAMESPACE + +using namespace Qt::StringLiterals; + +class QQStyleKitStyle; +class QQStyleKitControl; +class QQStyleKitControlState; +class QQStyleKitReader; +class QQStyleKitControlProperties; +class QQStyleKitDelegateProperties; + +class QQStyleKitPropertyGroup: public QObject +{ + Q_OBJECT + +public: + QQStyleKitPropertyGroup(QQSK::PropertyGroup group, QObject *parent); + + inline bool isControlProperties() const { return m_group == QQSK::PropertyGroup::Control; } + inline bool isPathFlag() const { return m_group == QQSK::PropertyGroup::globalFlag; } + inline bool isDelegateSubType() const { + return m_group == QQSK::PropertyGroup::DelegateSubType1 || + m_group == QQSK::PropertyGroup::DelegateSubType2; + } + + inline QQSK::PropertyGroup group() const { return m_group; } + + template<typename T> + inline T styleProperty( + QQSK::Property property, + QQSK::Property alternative = QQSK::Property::NoProperty) const + { + return qvariant_cast<T>(QQStyleKitPropertyResolver::readStyleProperty(this, property, alternative)); + } + + template<typename T> + inline T styleProperty(QQSK::Property property, const T &defaultValue) const + { + const QVariant value = QQStyleKitPropertyResolver::readStyleProperty(this, property); + return value.isValid() ? qvariant_cast<T>(value) : defaultValue; + } + + template<typename T> + inline bool setStyleProperty(QQSK::Property property, T value) + { + // This function will return true if the new value differes from the old one + return QQStyleKitPropertyResolver::writeStyleProperty(this, property, QVariant::fromValue(value)); + } + + template<typename SUBCLASS> + inline void handleStylePropertyChanged(void (SUBCLASS::*changedSignal)()); + + template <typename SUBCLASS, typename... CHANGED_SIGNALS> + inline void handleStylePropertiesChanged(CHANGED_SIGNALS... changedSignals); + + template <typename T> + inline T *lazyCreateGroup(T *const &ptr, QQSK::PropertyGroup group) const + { + return QQSK::lazyCreate(ptr, this, group); + } + + void emitChangedForAllStylePropertiesRecursive(); + + const QQStyleKitControlProperties *controlProperties() const; + + std::tuple< + const QQStyleKitControlProperties *, + const QQSK::PropertyGroup, + const QQSK::PathFlags> inspectGroupPath() const; + + inline QQStyleKitPropertyGroup *parentGroup() const { + Q_ASSERT(!parent() || qobject_cast<QQStyleKitPropertyGroup *>(parent())); + return static_cast<QQStyleKitPropertyGroup *>(parent()); + } + + const QQStyleKitControlProperties *asControlProperties() const; + +private: + bool shouldEmitLocally(); + bool shouldEmitGlobally(); + +private: + QQSK::PropertyGroup m_group; +}; + +// ************* QQStyleKitImageProperties **************** + +class QQStyleKitImageProperties : public QQStyleKitPropertyGroup +{ + Q_OBJECT + Q_PROPERTY(QUrl source READ source WRITE setSource NOTIFY sourceChanged FINAL) + Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged FINAL) + Q_PROPERTY(QQuickImage::FillMode fillMode READ fillMode WRITE setFillMode NOTIFY fillModeChanged FINAL) + QML_UNCREATABLE("This component can only be instantiated by StyleKit") + QML_NAMED_ELEMENT(StyleKitImageProperties) + +public: + QQStyleKitImageProperties(QQSK::PropertyGroup group, QQStyleKitPropertyGroup *parent = nullptr); + + template <typename... CHANGED_SIGNALS> + void emitGlobally(CHANGED_SIGNALS... changedSignals) const; + + QUrl source() const; + void setSource(const QUrl &source); + + QColor color() const; + void setColor(const QColor &color); + + QQuickImage::FillMode fillMode() const; + void setFillMode(QQuickImage::FillMode fillMode); + +signals: + void sourceChanged(); + void colorChanged(); + void fillModeChanged(); +}; + +// ************* QQStyleKitBorderProperties **************** + +class QQStyleKitBorderProperties : public QQStyleKitPropertyGroup +{ + Q_OBJECT + Q_PROPERTY(qreal width READ width WRITE setWidth NOTIFY widthChanged FINAL) + Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged FINAL) + QML_UNCREATABLE("This component can only be instantiated by StyleKit") + QML_NAMED_ELEMENT(StyleKitBorderProperties) + +public: + QQStyleKitBorderProperties(QQSK::PropertyGroup group, QQStyleKitPropertyGroup *parent = nullptr); + + template <typename... CHANGED_SIGNALS> + void emitGlobally(CHANGED_SIGNALS... changedSignals) const; + + qreal width() const; + void setWidth(qreal width); + + QColor color() const; + void setColor(const QColor &color); + +signals: + void widthChanged(); + void colorChanged(); +}; + +// ************* QQStyleKitShadowProperties **************** + +class QQStyleKitShadowProperties : public QQStyleKitPropertyGroup +{ + Q_OBJECT + Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged FINAL) + Q_PROPERTY(qreal opacity READ opacity WRITE setOpacity NOTIFY opacityChanged FINAL) + Q_PROPERTY(qreal scale READ scale WRITE setScale NOTIFY scaleChanged FINAL) + Q_PROPERTY(qreal verticalOffset READ verticalOffset WRITE setVerticalOffset NOTIFY verticalOffsetChanged FINAL) + Q_PROPERTY(qreal horizontalOffset READ horizontalOffset WRITE setHorizontalOffset NOTIFY horizontalOffsetChanged FINAL) + Q_PROPERTY(qreal blur READ blur WRITE setBlur NOTIFY blurChanged FINAL) + Q_PROPERTY(bool visible READ visible WRITE setVisible NOTIFY visibleChanged FINAL) + Q_PROPERTY(QQmlComponent *delegate READ delegate WRITE setDelegate NOTIFY delegateChanged FINAL) + QML_UNCREATABLE("This component can only be instantiated by StyleKit") + QML_NAMED_ELEMENT(StyleKitShadowProperties) + +public: + QQStyleKitShadowProperties(QQSK::PropertyGroup group, QQStyleKitPropertyGroup *parent = nullptr); + + template <typename... CHANGED_SIGNALS> + void emitGlobally(CHANGED_SIGNALS... changedSignals) const; + + QColor color() const; + void setColor(QColor color); + + qreal opacity() const; + void setOpacity(qreal opacity); + + qreal scale() const; + void setScale(qreal scale); + + qreal verticalOffset() const; + void setVerticalOffset(qreal verticalOffset); + + qreal horizontalOffset() const; + void setHorizontalOffset(qreal horizontalOffset); + + qreal blur() const; + void setBlur(qreal blur); + + bool visible() const; + void setVisible(bool visible); + + QQmlComponent *delegate() const; + void setDelegate(QQmlComponent *delegate); + +signals: + void colorChanged(); + void opacityChanged(); + void scaleChanged(); + void verticalOffsetChanged(); + void horizontalOffsetChanged(); + void blurChanged(); + void visibleChanged(); + void delegateChanged(); +}; + +// ************* QQStyleKitDelegateProperties **************** + +class QQStyleKitDelegateProperties : public QQStyleKitPropertyGroup +{ + Q_OBJECT + Q_PROPERTY(qreal implicitWidth READ implicitWidth WRITE setImplicitWidth NOTIFY implicitWidthChanged FINAL) + Q_PROPERTY(qreal implicitHeight READ implicitHeight WRITE setImplicitHeight NOTIFY implicitHeightChanged FINAL) + Q_PROPERTY(qreal minimumWidth READ minimumWidth WRITE setMinimumWidth NOTIFY minimumWidthChanged FINAL) + Q_PROPERTY(qreal margins READ margins WRITE setMargins NOTIFY marginsChanged FINAL) + Q_PROPERTY(qreal leftMargin READ leftMargin WRITE setLeftMargin NOTIFY leftMarginChanged FINAL) + Q_PROPERTY(qreal rightMargin READ rightMargin WRITE setRightMargin NOTIFY rightMarginChanged FINAL) + Q_PROPERTY(qreal topMargin READ topMargin WRITE setTopMargin NOTIFY topMarginChanged FINAL) + Q_PROPERTY(qreal bottomMargin READ bottomMargin WRITE setBottomMargin NOTIFY bottomMarginChanged FINAL) + Q_PROPERTY(Qt::Alignment alignment READ alignment WRITE setAlignment NOTIFY alignmentChanged FINAL) + Q_PROPERTY(qreal radius READ radius WRITE setRadius NOTIFY radiusChanged FINAL) + Q_PROPERTY(qreal topLeftRadius READ topLeftRadius WRITE setTopLeftRadius NOTIFY topLeftRadiusChanged FINAL) + Q_PROPERTY(qreal topRightRadius READ topRightRadius WRITE setTopRightRadius NOTIFY topRightRadiusChanged FINAL) + Q_PROPERTY(qreal bottomLeftRadius READ bottomLeftRadius WRITE setBottomLeftRadius NOTIFY bottomLeftRadiusChanged FINAL) + Q_PROPERTY(qreal bottomRightRadius READ bottomRightRadius WRITE setBottomRightRadius NOTIFY bottomRightRadiusChanged FINAL) + Q_PROPERTY(qreal scale READ scale WRITE setScale NOTIFY scaleChanged FINAL) + Q_PROPERTY(qreal rotation READ rotation WRITE setRotation NOTIFY rotationChanged FINAL) + Q_PROPERTY(qreal opacity READ opacity WRITE setOpacity NOTIFY opacityChanged FINAL) + Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged FINAL) + Q_PROPERTY(bool visible READ visible WRITE setVisible NOTIFY visibleChanged FINAL) + Q_PROPERTY(bool clip READ clip WRITE setClip NOTIFY clipChanged FINAL) + Q_PROPERTY(QQuickGradient *gradient READ gradient WRITE setGradient NOTIFY gradientChanged FINAL) + Q_PROPERTY(QQStyleKitImageProperties *image READ image NOTIFY imageChanged FINAL) + Q_PROPERTY(QQStyleKitBorderProperties *border READ border NOTIFY borderChanged FINAL) + Q_PROPERTY(QQStyleKitShadowProperties *shadow READ shadow NOTIFY shadowChanged FINAL) + Q_PROPERTY(QObject *data READ data WRITE setData NOTIFY dataChanged FINAL) + Q_PROPERTY(QQmlComponent *delegate READ delegate WRITE setDelegate NOTIFY delegateChanged FINAL) + QML_UNCREATABLE("This component can only be instantiated by StyleKit") + QML_NAMED_ELEMENT(StyleKitDelegateProperties) + +public: + QQStyleKitDelegateProperties(QQSK::PropertyGroup group, QQStyleKitPropertyGroup *parent = nullptr); + + template <typename... CHANGED_SIGNALS> + void emitGlobally(CHANGED_SIGNALS... changedSignals) const; + + qreal radius() const; + void setRadius(qreal radius); + + qreal topLeftRadius() const; + void setTopLeftRadius(qreal radius); + + qreal topRightRadius() const; + void setTopRightRadius(qreal radius); + + qreal bottomLeftRadius() const; + void setBottomLeftRadius(qreal radius); + + qreal bottomRightRadius() const; + void setBottomRightRadius(qreal radius); + + qreal scale() const; + void setScale(qreal scale); + + qreal rotation() const; + void setRotation(qreal rotation); + + qreal implicitWidth() const; + void setImplicitWidth(qreal width); + + qreal implicitHeight() const; + void setImplicitHeight(qreal height); + + qreal minimumWidth() const; + void setMinimumWidth(qreal width); + + qreal margins() const; + void setMargins(qreal margins); + + qreal leftMargin() const; + void setLeftMargin(qreal margin); + + qreal rightMargin() const; + void setRightMargin(qreal margin); + + qreal topMargin() const; + void setTopMargin(qreal margin); + + qreal bottomMargin() const; + void setBottomMargin(qreal margin); + + Qt::Alignment alignment() const; + void setAlignment(Qt::Alignment alignment); + + qreal opacity() const; + void setOpacity(qreal opacity); + + QColor color() const; + void setColor(const QColor &color); + + bool visible() const; + void setVisible(bool visible); + + bool clip() const; + void setClip(bool clip); + + QQuickGradient *gradient() const; + void setGradient(QQuickGradient *gradient); + + QObject *data() const; + void setData(QObject *data); + + QQmlComponent *delegate() const; + void setDelegate(QQmlComponent *delegate); + + QQStyleKitImageProperties *image() const; + QQStyleKitBorderProperties *border() const; + QQStyleKitShadowProperties *shadow() const; + +signals: + void colorChanged(); + void radiusChanged(); + void topLeftRadiusChanged(); + void topRightRadiusChanged(); + void bottomLeftRadiusChanged(); + void bottomRightRadiusChanged(); + void scaleChanged(); + void rotationChanged(); + void visibleChanged(); + void clipChanged(); + void borderChanged(); + void shadowChanged(); + void imageChanged(); + void gradientChanged(); + void colorImageChanged(); + void implicitWidthChanged(); + void implicitHeightChanged(); + void minimumWidthChanged(); + void marginsChanged(); + void leftMarginChanged(); + void rightMarginChanged(); + void topMarginChanged(); + void bottomMarginChanged(); + void alignmentChanged(); + void opacityChanged(); + void dataChanged(); + void delegateChanged(); + +private: + QPointer<QQuickGradient> m_gradient; + QQStyleKitBorderProperties *m_border = nullptr; + QQStyleKitShadowProperties *m_shadow = nullptr; + QQStyleKitImageProperties *m_image = nullptr; +}; + +// ************* QQStyleKitHandleProperties **************** + +class QQStyleKitHandleProperties : public QQStyleKitDelegateProperties +{ + Q_OBJECT + Q_PROPERTY(QQStyleKitDelegateProperties *first READ first NOTIFY firstChanged FINAL) + Q_PROPERTY(QQStyleKitDelegateProperties *second READ second NOTIFY secondChanged FINAL) + QML_UNCREATABLE("This component can only be instantiated by StyleKit") + QML_NAMED_ELEMENT(StyleKitHandleProperties) + +public: + QQStyleKitHandleProperties(QQSK::PropertyGroup group, QQStyleKitPropertyGroup *parent = nullptr); + QQStyleKitDelegateProperties *first() const; + QQStyleKitDelegateProperties *second() const; + +signals: + void firstChanged(); + void secondChanged(); + +private: + QQStyleKitDelegateProperties *m_first = nullptr; + QQStyleKitDelegateProperties *m_second = nullptr; + + friend class QQStyleKitReader; + friend class QQStyleKitControlProperties; +}; + +// ************* QQStyleKitIndicatorProperties **************** + +class QQStyleKitIndicatorProperties : public QQStyleKitDelegateProperties +{ + Q_OBJECT + Q_PROPERTY(QQStyleKitDelegateProperties *foreground READ foreground NOTIFY foregroundChanged FINAL) + QML_UNCREATABLE("This component can only be instantiated by StyleKit") + QML_NAMED_ELEMENT(StyleKitIndicatorProperties) + +public: + QQStyleKitIndicatorProperties(QQSK::PropertyGroup group, QQStyleKitPropertyGroup *parent = nullptr); + + template <typename... CHANGED_SIGNALS> + void emitGlobally(CHANGED_SIGNALS... changedSignals) const; + + QQStyleKitDelegateProperties *foreground() const; + +signals: + void foregroundChanged(); + +private: + QQStyleKitDelegateProperties *m_foreground = nullptr; + + friend class QQStyleKitReader; + friend class QQStyleKitControlProperties; +}; + +// ************* QQStyleKitIndicatorWithSubTypes **************** + +class QQStyleKitIndicatorWithSubTypes : public QQStyleKitDelegateProperties +{ + Q_OBJECT + Q_PROPERTY(QQStyleKitDelegateProperties *foreground READ foreground NOTIFY foregroundChanged FINAL) + Q_PROPERTY(QQStyleKitIndicatorProperties *up READ up NOTIFY upChanged FINAL) + Q_PROPERTY(QQStyleKitIndicatorProperties *down READ down NOTIFY downChanged FINAL) + QML_UNCREATABLE("This component can only be instantiated by StyleKit") + QML_NAMED_ELEMENT(StyleKitIndicatorPropertiesWithSubTypes) + +public: + QQStyleKitIndicatorWithSubTypes(QQSK::PropertyGroup group, QQStyleKitPropertyGroup *parent = nullptr); + + template <typename... CHANGED_SIGNALS> + void emitGlobally(CHANGED_SIGNALS... changedSignals) const; + + QQStyleKitDelegateProperties *foreground() const; + QQStyleKitIndicatorProperties *up() const; + QQStyleKitIndicatorProperties *down() const; + +signals: + void foregroundChanged(); + void upChanged(); + void downChanged(); + +private: + QQStyleKitDelegateProperties *m_foreground = nullptr; + QQStyleKitIndicatorProperties *m_up = nullptr; + QQStyleKitIndicatorProperties *m_down = nullptr; + + friend class QQStyleKitReader; + friend class QQStyleKitControlProperties; +}; + +// ************* QQStyleKitTextProperties **************** + +class QQStyleKitTextProperties : public QQStyleKitPropertyGroup +{ + Q_OBJECT + Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged FINAL) + Q_PROPERTY(Qt::Alignment alignment READ alignment WRITE setAlignment NOTIFY alignmentChanged FINAL) + QML_UNCREATABLE("This component can only be instantiated by StyleKit") + QML_NAMED_ELEMENT(StyleKitTextProperties) + +public: + QQStyleKitTextProperties(QQSK::PropertyGroup group, QQStyleKitPropertyGroup *parent = nullptr); + + template <typename... CHANGED_SIGNALS> + void emitGlobally(CHANGED_SIGNALS... changedSignals) const; + + QColor color() const; + void setColor(const QColor &color); + + Qt::Alignment alignment() const; + void setAlignment(Qt::Alignment alignment); + +signals: + void colorChanged(); + void alignmentChanged(); +}; + +/************* QQStyleKitControlProperties **************** + * QQStyleKitControlProperties (and all other subclasses of QQStyleKitPropertyGroup), + * is just an empty interface declaring the properties available for styling and reading. + * It contains as little data as possible since it will be instantiated a lot. E.g a style + * will instantiate them for every style.button, style.slider, etc defined, the same for a + * theme, and also each instance of a StyleKitReader. Those are all subclasses of this + * class. Each subclass will determine if the properties can be written to (as opposed to + * only be read), and if so, offer a storage for storing those values. That storage is typically + * a map that stores _only_ the properties that are written to, and nothing else. + */ +class QQStyleKitControlProperties : public QQStyleKitPropertyGroup +{ + Q_OBJECT + Q_PROPERTY(qreal spacing READ spacing WRITE setSpacing NOTIFY spacingChanged FINAL) + Q_PROPERTY(qreal padding READ padding WRITE setPadding NOTIFY paddingChanged FINAL) + Q_PROPERTY(qreal leftPadding READ leftPadding WRITE setLeftPadding NOTIFY leftPaddingChanged FINAL) + Q_PROPERTY(qreal rightPadding READ rightPadding WRITE setRightPadding NOTIFY rightPaddingChanged FINAL) + Q_PROPERTY(qreal topPadding READ topPadding WRITE setTopPadding NOTIFY topPaddingChanged FINAL) + Q_PROPERTY(qreal bottomPadding READ bottomPadding WRITE setBottomPadding NOTIFY bottomPaddingChanged FINAL) + Q_PROPERTY(QQStyleKitDelegateProperties *background READ background NOTIFY backgroundChanged FINAL) + Q_PROPERTY(QQStyleKitHandleProperties *handle READ handle NOTIFY handleChanged FINAL) + Q_PROPERTY(QQStyleKitIndicatorWithSubTypes *indicator READ indicator NOTIFY indicatorChanged FINAL) + Q_PROPERTY(QQStyleKitTextProperties *text READ text NOTIFY textChanged FINAL) + Q_PROPERTY(QQuickTransition *transition READ transition WRITE setTransition NOTIFY transitionChanged FINAL) + QML_UNCREATABLE("This component acts as an interface, and cannot be instantiated") + QML_NAMED_ELEMENT(StyleKitControlProperties) + +public: + QQStyleKitControlProperties(QQSK::PropertyGroup group, QObject *parent = nullptr); + + void emitChangedForAllStyleProperties(); + template <typename... CHANGED_SIGNALS> + void emitGlobally(CHANGED_SIGNALS... changedSignals) const; + void forEachUsedDelegate( + std::function<void (QQStyleKitDelegateProperties *, QQSK::Delegate, const QString &)> f); + + QQStyleKitStyle *style() const; + QQSK::Subclass subclass() const; + QQStyleKitReader *asQQStyleKitReader() const; + QQStyleKitControlState *asQQStyleKitState() const; + QQStyleKitControl *asQQStyleKitControl() const; + + qreal spacing() const; + void setSpacing(qreal spacing); + + qreal padding() const; + void setPadding(qreal padding); + + qreal leftPadding() const; + void setLeftPadding(qreal leftPadding); + + qreal rightPadding() const; + void setRightPadding(qreal rightPadding); + + qreal topPadding() const; + void setTopPadding(qreal topPadding); + + qreal bottomPadding() const; + void setBottomPadding(qreal bottomPadding); + + QQuickTransition* transition() const; + void setTransition(QQuickTransition *transition); + + QQStyleKitTextProperties *text() const; + + QQStyleKitDelegateProperties *background() const; + QQStyleKitHandleProperties *handle() const; + QQStyleKitIndicatorWithSubTypes *indicator() const; + +signals: + void backgroundChanged(); + void handleChanged(); + void indicatorChanged(); + void spacingChanged(); + void paddingChanged(); + void leftPaddingChanged(); + void rightPaddingChanged(); + void topPaddingChanged(); + void bottomPaddingChanged(); + void transitionChanged(); + void textChanged(); + +private: + Q_DISABLE_COPY(QQStyleKitControlProperties) + + QQStyleKitDelegateProperties *m_background = nullptr; + QQStyleKitHandleProperties *m_handle = nullptr; + QQStyleKitIndicatorWithSubTypes *m_indicator = nullptr; + QQStyleKitTextProperties *m_text = nullptr; + + friend class QQStyleKitPropertyResolver; + friend class QQStyleKitReader; +}; + +QT_END_NAMESPACE + +#endif // QQSTYLEKITCONTROLPROPERTIES_P_H diff --git a/src/labs/stylekit/qqstylekitcontrols.cpp b/src/labs/stylekit/qqstylekitcontrols.cpp new file mode 100644 index 0000000000..d639d5bfec --- /dev/null +++ b/src/labs/stylekit/qqstylekitcontrols.cpp @@ -0,0 +1,92 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qqstylekitcontrols_p.h" +#include "qqstylekitcontrol_p.h" +#include "qqstylekitcustomcontrol_p.h" + +QT_BEGIN_NAMESPACE + +using namespace Qt::StringLiterals; + +QQStyleKitControls::QQStyleKitControls(QObject *parent) + : QObject(parent) +{ +} + +QQmlListProperty<QObject> QQStyleKitControls::data() +{ + return QQmlListProperty<QObject>(this, &m_data); +} + +const QList<QObject *> QQStyleKitControls::children() const +{ + return m_data; +} + +/* Lazy-create the controls that the style is actually using, when accessed + * them from the style/application (e.g from Style or Theme). We don't lazy + * create any controls while resolving style properties, as undefined controls would + * anyway not contain any property overrides. The properties have setters too, to + * allow the style/application to share custom StyleKitControls the classical + * way, e.g button: StyleKitControl { id: button }. */ +QQStyleKitControl* QQStyleKitControls::getControl(int controlType) const +{ + if (!m_controls.contains(controlType)) + return nullptr; + return m_controls[controlType]; +} + +#define IMPLEMENT_ACCESSORS(NAME, TYPE) \ +QQStyleKitControl *QQStyleKitControls::NAME() const \ +{ \ + if (!m_controls.contains(TYPE)) { \ + auto *self = const_cast<QQStyleKitControls *>(this); \ + auto *control = new QQStyleKitControl(self); \ + self->m_controls.insert(TYPE, control); \ + } \ + return m_controls[TYPE]; \ +} \ +void QQStyleKitControls::set_ ## NAME(QQStyleKitControl *control) \ +{ \ + m_controls.insert(TYPE, control); \ +} + +IMPLEMENT_ACCESSORS(abstractButton, QQStyleKitReader::ControlType::AbstractButton) +IMPLEMENT_ACCESSORS(control, QQStyleKitReader::ControlType::Control) +IMPLEMENT_ACCESSORS(button, QQStyleKitReader::ControlType::Button) +IMPLEMENT_ACCESSORS(flatButton, QQStyleKitReader::ControlType::FlatButton) +IMPLEMENT_ACCESSORS(checkBox, QQStyleKitReader::ControlType::CheckBox) +IMPLEMENT_ACCESSORS(comboBox, QQStyleKitReader::ControlType::ComboBox) +IMPLEMENT_ACCESSORS(slider, QQStyleKitReader::ControlType::Slider) +IMPLEMENT_ACCESSORS(spinBox, QQStyleKitReader::ControlType::SpinBox) +IMPLEMENT_ACCESSORS(switchControl, QQStyleKitReader::ControlType::SwitchControl) +IMPLEMENT_ACCESSORS(textField, QQStyleKitReader::ControlType::TextField) +IMPLEMENT_ACCESSORS(textInput, QQStyleKitReader::ControlType::TextInput) +IMPLEMENT_ACCESSORS(radioButton, QQStyleKitReader::ControlType::RadioButton) +IMPLEMENT_ACCESSORS(itemDelegate, QQStyleKitReader::ControlType::ItemDelegate) +IMPLEMENT_ACCESSORS(popup, QQStyleKitReader::ControlType::Popup) +IMPLEMENT_ACCESSORS(pane, QQStyleKitReader::ControlType::Pane) +IMPLEMENT_ACCESSORS(page, QQStyleKitReader::ControlType::Page) +IMPLEMENT_ACCESSORS(frame, QQStyleKitReader::ControlType::Frame) + +#undef IMPLEMENT_ACCESSORS + +void QQStyleKitControls::componentComplete() +{ + for (auto *obj : children()) { + if (auto *customControl = qobject_cast<QQStyleKitCustomControl *>(obj)) { + const int type = customControl->controlType(); + const int reserved = int(QQStyleKitReader::ControlType::Unknown); + if (type >= reserved) + qmlWarning(this) << "CustomControls must use a controlType less than " << reserved; + if (m_controls.contains(type)) + qmlWarning(this) << "CustomControl registered more than once: " << type; + m_controls.insert(type, customControl); + } + } +} + +QT_END_NAMESPACE + +#include "moc_qqstylekitcontrols_p.cpp" diff --git a/src/labs/stylekit/qqstylekitcontrols_p.h b/src/labs/stylekit/qqstylekitcontrols_p.h new file mode 100644 index 0000000000..673d108328 --- /dev/null +++ b/src/labs/stylekit/qqstylekitcontrols_p.h @@ -0,0 +1,122 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +// Qt-Security score:significant reason:default + +#ifndef QQSTYLEKITCONTROLS_P_H +#define QQSTYLEKITCONTROLS_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtQml/QtQml> + +#include "qqstylekitreader_p.h" + +QT_BEGIN_NAMESPACE + +class QQStyleKitStyle; +class QQStyleKitControl; +class QQStyleKitCustomControl; + +class QQStyleKitControls : public QObject, public QQmlParserStatus +{ + Q_OBJECT + Q_INTERFACES(QQmlParserStatus) + + Q_PROPERTY(QQStyleKitControl *abstractButton READ abstractButton WRITE set_abstractButton NOTIFY abstractButtonChanged FINAL) + Q_PROPERTY(QQStyleKitControl *control READ control WRITE set_control NOTIFY controlChanged FINAL) + Q_PROPERTY(QQStyleKitControl *button READ button WRITE set_button NOTIFY buttonChanged FINAL) + Q_PROPERTY(QQStyleKitControl *checkBox READ checkBox WRITE set_checkBox NOTIFY checkBoxChanged FINAL) + Q_PROPERTY(QQStyleKitControl *comboBox READ comboBox WRITE set_comboBox NOTIFY comboBoxChanged FINAL) + Q_PROPERTY(QQStyleKitControl *flatButton READ flatButton WRITE set_flatButton NOTIFY flatButtonChanged FINAL) + Q_PROPERTY(QQStyleKitControl *slider READ slider WRITE set_slider NOTIFY sliderChanged FINAL) + Q_PROPERTY(QQStyleKitControl *spinBox READ spinBox WRITE set_spinBox NOTIFY spinBoxChanged FINAL) + Q_PROPERTY(QQStyleKitControl *textField READ textField WRITE set_textField NOTIFY textFieldChanged FINAL) + Q_PROPERTY(QQStyleKitControl *textInput READ textInput WRITE set_textInput NOTIFY textInputChanged FINAL) + Q_PROPERTY(QQStyleKitControl *switchControl READ switchControl WRITE set_switchControl NOTIFY switchControlChanged FINAL) + Q_PROPERTY(QQStyleKitControl *radioButton READ radioButton WRITE set_radioButton NOTIFY radioButtonChanged FINAL) + Q_PROPERTY(QQStyleKitControl *itemDelegate READ itemDelegate WRITE set_itemDelegate NOTIFY itemDelegateChanged FINAL) + Q_PROPERTY(QQStyleKitControl *popup READ popup WRITE set_popup NOTIFY popupChanged FINAL) + Q_PROPERTY(QQStyleKitControl *pane READ pane WRITE set_pane NOTIFY paneChanged FINAL) + Q_PROPERTY(QQStyleKitControl *page READ page WRITE set_page NOTIFY pageChanged FINAL) + Q_PROPERTY(QQStyleKitControl *frame READ frame WRITE set_frame NOTIFY frameChanged FINAL) + QML_UNCREATABLE("This component is abstract, and cannot be instantiated") + QML_NAMED_ELEMENT(StyleKitControls) + + Q_PROPERTY(QQmlListProperty<QObject> data READ data NOTIFY dataChanged FINAL) + Q_CLASSINFO("DefaultProperty", "data") + +public: + QQStyleKitControls(QObject *parent = nullptr); + +#define IMPLEMENT_ACCESSORS(NAME) \ + QQStyleKitControl *NAME() const; \ + void set_ ## NAME(QQStyleKitControl *control); + + IMPLEMENT_ACCESSORS(abstractButton) + IMPLEMENT_ACCESSORS(control) + IMPLEMENT_ACCESSORS(button) + IMPLEMENT_ACCESSORS(checkBox) + IMPLEMENT_ACCESSORS(comboBox) + IMPLEMENT_ACCESSORS(flatButton) + IMPLEMENT_ACCESSORS(slider) + IMPLEMENT_ACCESSORS(spinBox) + IMPLEMENT_ACCESSORS(textField) + IMPLEMENT_ACCESSORS(textInput) + IMPLEMENT_ACCESSORS(switchControl) + IMPLEMENT_ACCESSORS(radioButton) + IMPLEMENT_ACCESSORS(itemDelegate) + IMPLEMENT_ACCESSORS(popup) + IMPLEMENT_ACCESSORS(pane) + IMPLEMENT_ACCESSORS(page) + IMPLEMENT_ACCESSORS(frame) + +#undef IMPLEMENT_ACCESSORS + + Q_INVOKABLE QQStyleKitControl *getControl(int controlType) const; + + QQmlListProperty<QObject> data(); + const QList<QObject *> children() const; + +signals: + void dataChanged(); + void abstractButtonChanged(); + void controlChanged(); + void buttonChanged(); + void checkBoxChanged(); + void comboBoxChanged(); + void flatButtonChanged(); + void sliderChanged(); + void spinBoxChanged(); + void textFieldChanged(); + void textInputChanged(); + void switchControlChanged(); + void radioButtonChanged(); + void itemDelegateChanged(); + void popupChanged(); + void paneChanged(); + void pageChanged(); + void frameChanged(); + +protected: + void classBegin() override {} + void componentComplete() override; + +private: + Q_DISABLE_COPY(QQStyleKitControls) + + QList<QObject *> m_data; + QMap<int, QQStyleKitControl *> m_controls; +}; + +QT_END_NAMESPACE + +#endif // QQSTYLEKITCONTROLS_P_H diff --git a/src/labs/stylekit/qqstylekitcontrolstate.cpp b/src/labs/stylekit/qqstylekitcontrolstate.cpp new file mode 100644 index 0000000000..f31ec969b4 --- /dev/null +++ b/src/labs/stylekit/qqstylekitcontrolstate.cpp @@ -0,0 +1,99 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qqstylekitcontrolstate_p.h" +#include "qqstylekitcontrol_p.h" + +QT_BEGIN_NAMESPACE + +QQStyleKitControlState::QQStyleKitControlState(QObject *parent) + : QQStyleKitControlProperties(QQSK::PropertyGroup::Control, parent) +{ +} + +#define IMPLEMENT_ACCESSORS(STATE) \ +QQStyleKitControlState *QQStyleKitControlState::STATE() const \ +{ \ + if (!m_ ## STATE) { \ + auto *self = const_cast<QQStyleKitControlState *>(this); \ + self->m_ ## STATE = new QQStyleKitControlState(self); \ + } \ + return m_ ## STATE; \ +} + +IMPLEMENT_ACCESSORS(pressed); +IMPLEMENT_ACCESSORS(hovered); +IMPLEMENT_ACCESSORS(highlighted); +IMPLEMENT_ACCESSORS(focused); +IMPLEMENT_ACCESSORS(checked); +IMPLEMENT_ACCESSORS(vertical); +IMPLEMENT_ACCESSORS(disabled); + +std::tuple<QQStyleKitControl *, QQSK::State> +QQStyleKitControlState::controlAndState() +{ + /* Follow the parent path back to the QQStyleKitControl, and + * track which nested states we're in along the way. The path of + * states determines the state of the control that the properties + * inside this QQStyleKitControlState should apply for. */ + QQStyleKitControl *control = nullptr; + QQSK::State nestedState = QQSK::StateFlag::NoState; + const QQStyleKitControlState *obj = this; + + if (metaObject()->inherits(&QQStyleKitControl::staticMetaObject)) + control = asQQStyleKitControl(); + + while (true) { + QQStyleKitControlState *parentState = qobject_cast<QQStyleKitControlState *>(obj->parent()); + if (!parentState) + break; + + if (obj == parentState->pressed()) + nestedState.setFlag(QQSK::StateFlag::Pressed); + else if (obj == parentState->hovered()) + nestedState.setFlag(QQSK::StateFlag::Hovered); + else if (obj == parentState->focused()) + nestedState.setFlag(QQSK::StateFlag::Focused); + else if (obj == parentState->highlighted()) + nestedState.setFlag(QQSK::StateFlag::Highlighted); + else if (obj == parentState->checked()) + nestedState.setFlag(QQSK::StateFlag::Checked); + else if (obj == parentState->vertical()) + nestedState.setFlag(QQSK::StateFlag::Vertical); + else if (obj == parentState->disabled()) + nestedState.setFlag(QQSK::StateFlag::Disabled); + else + Q_UNREACHABLE(); + + obj = parentState; + if (obj->metaObject()->inherits(&QQStyleKitControl::staticMetaObject)) + control = obj->asQQStyleKitControl(); + } + + if (nestedState.testFlag(QQSK::StateFlag::Disabled)) { + nestedState.setFlag(QQSK::StateFlag::Pressed, false); + nestedState.setFlag(QQSK::StateFlag::Hovered, false); + nestedState.setFlag(QQSK::StateFlag::Focused, false); + nestedState.setFlag(QQSK::StateFlag::Highlighted, false); + } + + if (nestedState == QQSK::StateFlag::NoState) + nestedState = QQSK::StateFlag::Normal; + + Q_ASSERT(control); + Q_ASSERT(qlonglong(nestedState) <= qlonglong(QQSK::StateFlag::MAX_STATE)); + + return std::make_tuple(control, nestedState); +} + +QQStyleKitControlState *QQStyleKitControlState::parentState() const +{ + Q_ASSERT(subclass() == QQSK::Subclass::QQStyleKitState); + QObject *p = parent(); + Q_ASSERT(p && qobject_cast<QQStyleKitControlState *>(p)); + return static_cast<QQStyleKitControlState *>(p); +} + +QT_END_NAMESPACE + +#include "moc_qqstylekitcontrolstate_p.cpp" diff --git a/src/labs/stylekit/qqstylekitcontrolstate_p.h b/src/labs/stylekit/qqstylekitcontrolstate_p.h new file mode 100644 index 0000000000..e9b7b70e86 --- /dev/null +++ b/src/labs/stylekit/qqstylekitcontrolstate_p.h @@ -0,0 +1,76 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +// Qt-Security score:significant reason:default + +#ifndef QQSTYLEKITCONTROLSTATE_P_H +#define QQSTYLEKITCONTROLSTATE_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtQml/QtQml> + +#include "qqstylekitcontrolproperties_p.h" + +QT_BEGIN_NAMESPACE + +class QQStyleKitControl; + +class QQStyleKitControlState : public QQStyleKitControlProperties +{ + Q_OBJECT + Q_PROPERTY(QQStyleKitControlState *pressed READ pressed NOTIFY pressedChanged FINAL) + Q_PROPERTY(QQStyleKitControlState *hovered READ hovered NOTIFY hoveredChanged FINAL) + Q_PROPERTY(QQStyleKitControlState *focused READ focused NOTIFY focusedChanged FINAL) + Q_PROPERTY(QQStyleKitControlState *checked READ checked NOTIFY checkedChanged FINAL) + Q_PROPERTY(QQStyleKitControlState *disabled READ disabled NOTIFY disabledChanged FINAL) + Q_PROPERTY(QQStyleKitControlState *highlighted READ highlighted NOTIFY highlightedChanged FINAL) + Q_PROPERTY(QQStyleKitControlState *vertical READ vertical NOTIFY verticalChanged FINAL) + QML_NAMED_ELEMENT(StyleKitControlState) + +public: + QQStyleKitControlState(QObject *parent = nullptr); + + QQStyleKitControlState *pressed() const; + QQStyleKitControlState *hovered() const; + QQStyleKitControlState *focused() const; + QQStyleKitControlState *checked() const; + QQStyleKitControlState *disabled() const; + QQStyleKitControlState *highlighted() const; + QQStyleKitControlState *vertical() const; + + QQStyleKitControlState *parentState() const; + std::tuple<QQStyleKitControl *, QQSK::State> controlAndState(); + +signals: + void pressedChanged(); + void hoveredChanged(); + void focusedChanged(); + void checkedChanged(); + void disabledChanged(); + void highlightedChanged(); + void verticalChanged(); + +private: + Q_DISABLE_COPY(QQStyleKitControlState) + + mutable QPointer<QQStyleKitControlState> m_pressed; + mutable QPointer<QQStyleKitControlState> m_hovered; + mutable QPointer<QQStyleKitControlState> m_focused; + mutable QPointer<QQStyleKitControlState> m_checked; + mutable QPointer<QQStyleKitControlState> m_disabled; + mutable QPointer<QQStyleKitControlState> m_highlighted; + mutable QPointer<QQStyleKitControlState> m_vertical; +}; + +QT_END_NAMESPACE + +#endif // QQSTYLEKITCONTROLSTATE_P_H diff --git a/src/labs/stylekit/qqstylekitcustomcontrol.cpp b/src/labs/stylekit/qqstylekitcustomcontrol.cpp new file mode 100644 index 0000000000..316a0f4856 --- /dev/null +++ b/src/labs/stylekit/qqstylekitcustomcontrol.cpp @@ -0,0 +1,32 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qqstylekitcustomcontrol_p.h" + +QT_BEGIN_NAMESPACE + +using namespace Qt::StringLiterals; + +QQStyleKitCustomControl::QQStyleKitCustomControl(QObject *parent) + : QQStyleKitControl(parent) +{ +} + +int QQStyleKitCustomControl::controlType() const +{ + return m_controlType; +} + +void QQStyleKitCustomControl::setControlType(int controlType) +{ + if (m_controlType == controlType) + return; + + m_controlType = controlType; + + emit controlTypeChanged(); +} + +QT_END_NAMESPACE + +#include "moc_qqstylekitcustomcontrol_p.cpp" diff --git a/src/labs/stylekit/qqstylekitcustomcontrol_p.h b/src/labs/stylekit/qqstylekitcustomcontrol_p.h new file mode 100644 index 0000000000..40fc666693 --- /dev/null +++ b/src/labs/stylekit/qqstylekitcustomcontrol_p.h @@ -0,0 +1,47 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +// Qt-Security score:significant reason:default + +#ifndef QQSTYLEKITCUSTOMCONTROL_P_H +#define QQSTYLEKITCUSTOMCONTROL_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtQml/QtQml> +#include "qqstylekitcontrol_p.h" + +QT_BEGIN_NAMESPACE + +class QQStyleKitCustomControl: public QQStyleKitControl +{ + Q_OBJECT + Q_PROPERTY(int controlType READ controlType WRITE setControlType NOTIFY controlTypeChanged FINAL) + QML_NAMED_ELEMENT(CustomControl) + +public: + QQStyleKitCustomControl(QObject *parent = nullptr); + + int controlType() const; + void setControlType(int controlType); + +signals: + void controlTypeChanged(); + +private: + Q_DISABLE_COPY(QQStyleKitCustomControl) + + int m_controlType = 0; +}; + +QT_END_NAMESPACE + +#endif // QQSTYLEKITCUSTOMCONTROL_P_H diff --git a/src/labs/stylekit/qqstylekitcustomtheme.cpp b/src/labs/stylekit/qqstylekitcustomtheme.cpp new file mode 100644 index 0000000000..5944ba550c --- /dev/null +++ b/src/labs/stylekit/qqstylekitcustomtheme.cpp @@ -0,0 +1,42 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qqstylekitcustomtheme_p.h" +#include "qqstylekittheme_p.h" + +QT_BEGIN_NAMESPACE + +QQStyleKitCustomTheme::QQStyleKitCustomTheme(QObject *parent) + : QObject(parent) +{ +} + +QString QQStyleKitCustomTheme::name() const +{ + return m_name; +} + +void QQStyleKitCustomTheme::setName(const QString &newName) +{ + if (m_name == newName) + return; + m_name = newName; + emit nameChanged(); +} + +QQmlComponent *QQStyleKitCustomTheme::theme() const +{ + return m_theme; +} + +void QQStyleKitCustomTheme::setTheme(QQmlComponent *newTheme) +{ + if (m_theme == newTheme) + return; + m_theme = newTheme; + emit themeChanged(); +} + +QT_END_NAMESPACE + +#include "moc_qqstylekitcustomtheme_p.cpp" diff --git a/src/labs/stylekit/qqstylekitcustomtheme_p.h b/src/labs/stylekit/qqstylekitcustomtheme_p.h new file mode 100644 index 0000000000..54b8f24bf4 --- /dev/null +++ b/src/labs/stylekit/qqstylekitcustomtheme_p.h @@ -0,0 +1,54 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +// Qt-Security score:significant reason:default + +#ifndef QQSTYLEKITCUSTOMTHEME_P_H +#define QQSTYLEKITCUSTOMTHEME_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtQml/QtQml> + +QT_BEGIN_NAMESPACE + +class QQStyleKitTheme; + +class QQStyleKitCustomTheme : public QObject +{ + Q_OBJECT + Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged FINAL) + Q_PROPERTY(QQmlComponent *theme READ theme WRITE setTheme NOTIFY themeChanged FINAL) + QML_NAMED_ELEMENT(CustomTheme) + +public: + QQStyleKitCustomTheme(QObject *parent = nullptr); + + QString name() const; + void setName(const QString &newName); + + QQmlComponent *theme() const; + void setTheme(QQmlComponent *newTheme); + +Q_SIGNALS: + void nameChanged(); + void themeChanged(); + +private: + Q_DISABLE_COPY(QQStyleKitCustomTheme) + + QString m_name; + QQmlComponent *m_theme = nullptr; +}; + +QT_END_NAMESPACE + +#endif // QQSTYLEKITCUSTOMTHEME_P_H diff --git a/src/labs/stylekit/qqstylekitdebug.cpp b/src/labs/stylekit/qqstylekitdebug.cpp new file mode 100644 index 0000000000..dcd191959d --- /dev/null +++ b/src/labs/stylekit/qqstylekitdebug.cpp @@ -0,0 +1,361 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qqstylekitdebug_p.h" +#include "qqstylekitcontrol_p.h" +#include "qqstylekitcontrols_p.h" +#include "qqstylekitstyle_p.h" +#include "qqstylekittheme_p.h" + +QT_BEGIN_NAMESPACE + +const QQStyleKitPropertyGroup *QQStyleKitDebug::groupBeingRead = nullptr; +QPointer<QQuickItem> QQStyleKitDebug::m_item; +QString QQStyleKitDebug::m_filter; +int QQStyleKitDebug::m_outputCount = 0; + +static const QChar kDot = '.'_L1; + +template <typename EnumType> +QString QQStyleKitDebug::enumToString(EnumType enumValue) +{ + auto propertyMetaEnum = QMetaEnum::fromType<EnumType>(); + return QString::fromUtf8(propertyMetaEnum.valueToKeys(quint64(enumValue))); +} + +QString QQStyleKitDebug::objectName(const QObject *obj) { + QString str = QString::fromLatin1(obj->metaObject()->className()); + int idx = str.indexOf("_QMLTYPE"_L1); + if (idx != -1) + str = str.left(idx); + else { + const QString prefix("QQStyleKit"_L1); + if (str.startsWith(prefix)) + str = str.mid(prefix.length()); + } + const QString name = obj->objectName(); + if (!name.isEmpty()) + str = str + "("_L1 + name + ")"_L1; + return str; +} + +QString QQStyleKitDebug::stateToString(const QQSK::State state) +{ + const QStringList list = enumToString(state).split('|'_L1); + return "["_L1 + list.join(','_L1) + "]"_L1; +} + +QString QQStyleKitDebug::styleReaderToString(const QQStyleKitReader *reader) +{ + return "StyleKitReader"_L1 + stateToString(reader->controlState()); +} + +QString QQStyleKitDebug::propertyPath(const QQStyleKitPropertyGroup *group, const PropertyPathId property) +{ + QString path = enumToString(property.property()); + path[0] = path[0].toLower(); + const QQStyleKitPropertyGroup *childGroup = group; + const int startIndex = QQStyleKitPropertyGroup::staticMetaObject.propertyOffset(); + + while (childGroup) { + if (childGroup->isControlProperties()) + break; + const QQStyleKitPropertyGroup *parentGroup = + qobject_cast<const QQStyleKitPropertyGroup *>(childGroup->parent()); + if (!parentGroup) + break; + + // Resolve group name by inspecting which property in the parent group it belongs to + const QMetaObject* parentMeta = parentGroup->metaObject(); + for (int i = startIndex; i < parentMeta->propertyCount(); ++i) { + const QMetaProperty prop = parentMeta->property(i); + const QMetaObject* typeMeta = QMetaType::fromName(prop.typeName()).metaObject(); + if (!typeMeta || !typeMeta->inherits(&QQStyleKitPropertyGroup::staticMetaObject)) + continue; + QObject *propGroup = prop.read(parentGroup).value<QQStyleKitPropertyGroup *>(); + if (propGroup == childGroup) { + path.prepend(QString::fromUtf8(prop.name()) + kDot); + break; + } + } + + childGroup = parentGroup; + } + + return path; +} + +QString QQStyleKitDebug::controlToString(const QQStyleKitControlProperties *control) +{ + const QObject *parentObj = control->parent(); + if (!parentObj) + return "<no parent>"_L1; + auto *controls = qobject_cast<const QQStyleKitControls *>(parentObj); + if (!controls) { + return "<"_L1 + QString::fromUtf8(parentObj->metaObject()->className()) + ">"_L1; + } + + const int startIndex = QQStyleKitControlProperties::staticMetaObject.propertyOffset(); + const int endIndex = QQStyleKitControlProperties::staticMetaObject.propertyCount(); + + const QMetaObject* parentMeta = parentObj->metaObject(); + for (int i = startIndex; i < endIndex; ++i) { + const QMetaProperty prop = parentMeta->property(i); + const QMetaObject* typeMeta = QMetaType::fromName(prop.typeName()).metaObject(); + if (!typeMeta || !typeMeta->inherits(&QQStyleKitControl::staticMetaObject)) + continue; + + QObject *propObj = prop.read(parentObj).value<QObject *>(); + if (propObj == control) + return QString::fromUtf8(prop.name()); + } + return "<unknown control: no property found>"_L1; +} + +QString QQStyleKitDebug::objectPath(const QQStyleKitControlProperties *properties, QObject *from) +{ + QString path; + const QObject *obj = properties; + + while (obj) { + if (!path.isEmpty()) + path.prepend(kDot); + + if (auto *theme = qobject_cast<const QQStyleKitCustomTheme *>(obj)) { + path.prepend(theme->name() + kDot); + } else if (auto *theme = qobject_cast<const QQStyleKitTheme *>(obj)) { + // Note: only one theme is instantiated at a time + if (auto style = theme->style()) + path.prepend(style->themeName()); + else + path.prepend(objectName(obj)); + } else if (auto *control = qobject_cast<const QQStyleKitControl *>(obj)) { + path.prepend(controlToString(control)); + } else if (auto *reader = qobject_cast<const QQStyleKitReader *>(obj)) { + path.prepend(styleReaderToString(reader)); + } else { + path.prepend(objectName(obj)); + } + + if (obj == from) + break; + + obj = obj->parent(); + } + + return path; +} + +void QQStyleKitDebug::notifyPropertyRead( + const PropertyPathId property, + const QQStyleKitControlProperties *storage, + const QQSK::State state, + const QVariant &value) +{ + Q_ASSERT(enabled()); + + const QQStyleKitControlProperties *reader = QQStyleKitDebug::groupBeingRead->controlProperties(); + if (reader->subclass() == QQSK::Subclass::QQStyleKitState) { + /* The reader is in the UnfiedStyle, and not in the users application (which can happen + * when e.g resolving local bindings between properties in the style). Those are not + * interesting to print out when inspecting control-to-style mappings. Ignore. */ + return; + } + + if (!insideControl(reader)) { + // We should only debug reads that targets m_item. So return. + return; + } + + const QString _readerPath = objectPath(reader, m_item); + const QString _readPropertyPath = propertyPath(QQStyleKitDebug::groupBeingRead, property); + const QString queriedPath = _readerPath + kDot +_readPropertyPath; + + QString storagePath; + if (storage->subclass() == QQSK::Subclass::QQStyleKitReader) { + /* We read an interpolated value stored directly in the reader itself. While this + * can be interesting to print out whe debugging the styling engine itself, it + * comes across as noise when inspecting control-to-style mappings. Ignore. */ +#if 0 + storagePath = "[local storage] "_L1; +#else + return; +#endif + } else { + const QString _controlPathInStyle = objectPath(storage, storage->style()); + const QString _statePath = stateToString(state); + storagePath = _controlPathInStyle + _statePath; + } + + QString valueString = value.toString(); + if (!value.isValid()) // value was set, but probably to undefined + valueString = "<undefined>"_L1; + else if (valueString.isEmpty()) + valueString = "<object>"_L1; + + const QString output = queriedPath + " -> "_L1 + storagePath + " = "_L1 + valueString; + + if (!QRegularExpression(m_filter).match(output).hasMatch()) + return; + + qDebug().nospace().noquote() << m_outputCount++ << " | [read] "_L1 << output; +} + +void QQStyleKitDebug::notifyPropertyWrite( + const QQStyleKitPropertyGroup *group, + const QQSK::Property property, + const QQStyleKitControlProperties *storage, + const QQSK::State state, + const PropertyStorageId key, + const QVariant &value) +{ +#if 1 + Q_UNUSED(group); + Q_UNUSED(property); + Q_UNUSED(storage); + Q_UNUSED(state); + Q_UNUSED(key); + Q_UNUSED(value); +#else + /* Note: in order to catch _all_ writes, we cannot depend on enabling writes from + * QML using a property, as that would resolve to 'true' too late. */ + QString storagePath; + if (storage->subclass() == QQSK::Subclass::QQStyleKitReader) { + storagePath = "[local storage]"_L1; + } else { + const QString _controlPathInStyle = objectPath(storage, storage->style()); + const QString _statePath = stateToString(state); + storagePath = _controlPathInStyle + _statePath; + } + + QString valueString = value.toString(); + if (!value.isValid()) // value was set, but probably to undefined + valueString = "<undefined>"_L1; + else if (valueString.isEmpty()) + valueString = "<object>"_L1; + + const QString path = propertyPath(group, property); + const QString output = storagePath + kDot + path + " (storage key:"_L1 + QString::number(key) + ") = "_L1 + valueString; + + qDebug().nospace().noquote() << m_outputCount++ << " | [write] "_L1 << output; +#endif +} + +void QQStyleKitDebug::notifyPropertyNotResolved(const PropertyPathId property) +{ + const QQStyleKitControlProperties *reader = QQStyleKitDebug::groupBeingRead->controlProperties(); + if (!insideControl(reader)) { + // We should only debug reads that targets m_item. So return. + return; + } + + const QString _readerPath = objectPath(reader, m_item); + const QString _propertyPath = propertyPath(QQStyleKitDebug::groupBeingRead, property); + const QString queriedPath = _readerPath + kDot +_propertyPath; + const QString output = queriedPath + " -> <property not set>"_L1; + + if (!QRegularExpression(m_filter).match(output).hasMatch()) + return; + + qDebug().nospace().noquote() << m_outputCount++ << " | [read] "_L1 << output; +} + +void QQStyleKitDebug::trace( + const PropertyPathId property, + const QQStyleKitControlProperties *storage, + const QQSK::State state, + const PropertyStorageId key) +{ +#if 1 + Q_UNUSED(property); + Q_UNUSED(storage); + Q_UNUSED(state); + Q_UNUSED(key); +#else + const QQStyleKitControlProperties *reader = QQStyleKitDebug::groupBeingRead->controlProperties(); + if (reader->subclass() == QQSK::Subclass::QQStyleKitState) { + /* The reader is in the UnfiedStyle, and not in the users application (which can happen + * when e.g resolving local bindings between properties in the style). Those are not + * interesting to print out when inspecting control-to-style mappings. Ignore. */ + return; + } + + if (!insideControl(reader)) { + // We should only debug reads that targets m_item. So return. + return; + } + + const QString _readerPath = objectPath(reader, m_item); + const QString _readPropertyPath = propertyPath(QQStyleKitDebug::groupBeingRead, property); + const QString queriedPath = _readerPath + kDot +_readPropertyPath; + + QString storagePath; + if (storage->subclass() == QQSK::Subclass::QQStyleKitReader) { + /* We read an interpolated value stored directly in the reader itself. While this + * can be interesting to print out whe debugging the styling engine itself, it + * comes across as noise when inspecting control-to-style mappings. Ignore. */ +#if 0 + storagePath = "[local storage]"_L1; +#else + return; +#endif + } else { + const QString _controlPathInStyle = objectPath(storage, storage->style()); + const QString _statePath = stateToString(state); + storagePath = _controlPathInStyle + _statePath; + } + + const QString output = queriedPath + ", checking "_L1 + storagePath + " (storage key:"_L1 + QString::number(key)+ ")"_L1; + + if (!QRegularExpression(m_filter).match(output).hasMatch()) + return; + + qDebug().nospace().noquote() << m_outputCount++ << " | [trace] "_L1 << output; +#endif +} + +QQuickItem *QQStyleKitDebug::control() const +{ + return m_item; +} + +void QQStyleKitDebug::setControl(QQuickItem *item) +{ + if (m_item == item) + return; + + m_item = item; + emit controlChanged(); +} + +QString QQStyleKitDebug::filter() const +{ + return m_filter; +} + +void QQStyleKitDebug::setFilter(const QString &filter) +{ + if (m_filter == filter) + return; + + m_filter = filter; + emit filterChanged(); +} + +bool QQStyleKitDebug::insideControl(const QObject *child) +{ + if (!m_item) + return false; + const QObject *obj = child; + do { + if (obj == m_item) + return true; + obj = obj->parent(); + } while (obj); + return false; +} + +QT_END_NAMESPACE + +#include "moc_qqstylekitdebug_p.cpp" + diff --git a/src/labs/stylekit/qqstylekitdebug_p.h b/src/labs/stylekit/qqstylekitdebug_p.h new file mode 100644 index 0000000000..4d30da4251 --- /dev/null +++ b/src/labs/stylekit/qqstylekitdebug_p.h @@ -0,0 +1,87 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +// Qt-Security score:significant reason:default + +#ifndef QQSTYLEKITDEBUG_P_H +#define QQSTYLEKITDEBUG_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtQuick/qquickitem.h> + +#include "qqstylekitcontrolproperties_p.h" +#include "qqstylekitstorage_p.h" + +QT_BEGIN_NAMESPACE + +class QQStyleKitDebug: public QObject +{ + Q_OBJECT + Q_PROPERTY(QQuickItem *control READ control WRITE setControl NOTIFY controlChanged FINAL) + Q_PROPERTY(QString filter READ filter WRITE setFilter NOTIFY filterChanged FINAL) + QML_NAMED_ELEMENT(StyleKitDebug) + +public: + QQuickItem *control() const; + void setControl(QQuickItem *item); + + QString filter() const; + void setFilter(const QString &filter); + + Q_INVOKABLE static bool insideControl(const QObject *child); + +signals: + void controlChanged(); + void filterChanged(); + +private: + static const QQStyleKitPropertyGroup *groupBeingRead; + static QPointer<QQuickItem> m_item; + static QString m_filter; + static int m_outputCount; + +private: + static inline bool enabled() { return m_item != nullptr; } + static void notifyPropertyNotResolved(const PropertyPathId property); + static void notifyPropertyRead( + const PropertyPathId property, + const QQStyleKitControlProperties *resolvedControl, + const QQSK::State state, + const QVariant &value); + static void notifyPropertyWrite( + const QQStyleKitPropertyGroup *group, + const QQSK::Property property, + const QQStyleKitControlProperties *storage, + const QQSK::State state, + const PropertyStorageId key, + const QVariant &value); + static void trace( + const PropertyPathId property, + const QQStyleKitControlProperties *resolvedControl, + const QQSK::State state, + const PropertyStorageId key); + + template <typename EnumType> + static QString enumToString(EnumType enumValue); + static QString objectName(const QObject *obj); + static QString stateToString(const QQSK::State state); + static QString styleReaderToString(const QQStyleKitReader *reader); + static QString controlToString(const QQStyleKitControlProperties *control); + static QString objectPath(const QQStyleKitControlProperties *properties, QObject *from); + static QString propertyPath(const QQStyleKitPropertyGroup *group, const PropertyPathId property); + + friend class QQStyleKitPropertyResolver; +}; + +QT_END_NAMESPACE + +#endif // QQSTYLEKITDEBUG_P_H diff --git a/src/labs/stylekit/qqstylekitfont.cpp b/src/labs/stylekit/qqstylekitfont.cpp new file mode 100644 index 0000000000..2db4eae309 --- /dev/null +++ b/src/labs/stylekit/qqstylekitfont.cpp @@ -0,0 +1,232 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qqstylekitfont_p.h" + +QT_BEGIN_NAMESPACE + +QQStyleKitFont::QQStyleKitFont(QObject *parent) + : QObject(parent) +{ +} + +#define STYLEKIT_FONT_GETTER(propertyName) \ + QFont QQStyleKitFont::propertyName() const \ + { \ + if (!m_##propertyName) { \ + m_##propertyName.reset(new QFont()); \ + } \ + return *m_##propertyName; \ + } + +STYLEKIT_FONT_GETTER(systemFont) +STYLEKIT_FONT_GETTER(buttonFont) +STYLEKIT_FONT_GETTER(checkboxFont) +STYLEKIT_FONT_GETTER(comboBoxFont) +STYLEKIT_FONT_GETTER(groupBoxFont) +STYLEKIT_FONT_GETTER(itemViewFont) +STYLEKIT_FONT_GETTER(labelFont) +STYLEKIT_FONT_GETTER(listViewFont) +STYLEKIT_FONT_GETTER(menuFont) +STYLEKIT_FONT_GETTER(menuBarFont) +STYLEKIT_FONT_GETTER(radioButtonFont) +STYLEKIT_FONT_GETTER(spinBoxFont) +STYLEKIT_FONT_GETTER(switchControlFont) +STYLEKIT_FONT_GETTER(tabBarFont) +STYLEKIT_FONT_GETTER(textAreaFont) +STYLEKIT_FONT_GETTER(textFieldFont) +STYLEKIT_FONT_GETTER(toolBarFont) +STYLEKIT_FONT_GETTER(toolTipFont) +STYLEKIT_FONT_GETTER(tumblerFont) + +void QQStyleKitFont::setSystemFont(const QFont &font) +{ + if (!m_systemFont) + m_systemFont.reset(new QFont(font)); + else + *m_systemFont = font; + emit systemFontChanged(); +} + +void QQStyleKitFont::setButtonFont(const QFont &font) +{ + if (!m_buttonFont) { + m_buttonFont.reset(new QFont(font)); + } else { + *m_buttonFont = font; + } + emit buttonFontChanged(); +} + +void QQStyleKitFont::setCheckboxFont(const QFont &font) +{ + if (!m_checkboxFont) { + m_checkboxFont.reset(new QFont(font)); + } else { + *m_checkboxFont = font; + } + emit checkboxFontChanged(); +} + +void QQStyleKitFont::setComboBoxFont(const QFont &font) +{ + if (!m_comboBoxFont) { + m_comboBoxFont.reset(new QFont(font)); + } else { + *m_comboBoxFont = font; + } + emit comboBoxFontChanged(); +} + +void QQStyleKitFont::setGroupBoxFont(const QFont &font) +{ + if (!m_groupBoxFont) { + m_groupBoxFont.reset(new QFont(font)); + } else { + *m_groupBoxFont = font; + } + emit groupBoxFontChanged(); +} + +void QQStyleKitFont::setItemViewFont(const QFont &font) +{ + if (!m_itemViewFont) { + m_itemViewFont.reset(new QFont(font)); + } else { + *m_itemViewFont = font; + } + emit itemViewFontChanged(); +} + +void QQStyleKitFont::setLabelFont(const QFont &font) +{ + if (!m_labelFont) { + m_labelFont.reset(new QFont(font)); + } else { + *m_labelFont = font; + } + emit labelFontChanged(); +} + +void QQStyleKitFont::setListViewFont(const QFont &font) +{ + if (!m_listViewFont) { + m_listViewFont.reset(new QFont(font)); + } else { + *m_listViewFont = font; + } + emit listViewFontChanged(); +} + +void QQStyleKitFont::setMenuFont(const QFont &font) +{ + if (!m_menuFont) { + m_menuFont.reset(new QFont(font)); + } else { + *m_menuFont = font; + } + emit menuFontChanged(); +} + +void QQStyleKitFont::setMenuBarFont(const QFont &font) +{ + if (!m_menuBarFont) { + m_menuBarFont.reset(new QFont(font)); + } else { + *m_menuBarFont = font; + } + emit menuBarFontChanged(); +} + +void QQStyleKitFont::setRadioButtonFont(const QFont &font) +{ + if (!m_radioButtonFont) { + m_radioButtonFont.reset(new QFont(font)); + } else { + *m_radioButtonFont = font; + } + emit radioButtonFontChanged(); +} + +void QQStyleKitFont::setSpinBoxFont(const QFont &font) +{ + if (!m_spinBoxFont) { + m_spinBoxFont.reset(new QFont(font)); + } else { + *m_spinBoxFont = font; + } + emit spinBoxFontChanged(); +} + +void QQStyleKitFont::setSwitchControlFont(const QFont &font) +{ + if (!m_switchControlFont) { + m_switchControlFont.reset(new QFont(font)); + } else { + *m_switchControlFont = font; + } + emit switchControlFontChanged(); +} + +void QQStyleKitFont::setTabBarFont(const QFont &font) +{ + if (!m_tabBarFont) { + m_tabBarFont.reset(new QFont(font)); + } else { + *m_tabBarFont = font; + } + emit tabBarFontChanged(); +} + +void QQStyleKitFont::setTextAreaFont(const QFont &font) +{ + if (!m_textAreaFont) { + m_textAreaFont.reset(new QFont(font)); + } else { + *m_textAreaFont = font; + } + emit textAreaFontChanged(); +} + +void QQStyleKitFont::setTextFieldFont(const QFont &font) +{ + if (!m_textFieldFont) { + m_textFieldFont.reset(new QFont(font)); + } else { + *m_textFieldFont = font; + } + emit textFieldFontChanged(); +} + +void QQStyleKitFont::setToolBarFont(const QFont &font) +{ + if (!m_toolBarFont) { + m_toolBarFont.reset(new QFont(font)); + } else { + *m_toolBarFont = font; + } + emit toolBarFontChanged(); +} + +void QQStyleKitFont::setToolTipFont(const QFont &font) +{ + if (!m_toolTipFont) { + m_toolTipFont.reset(new QFont(font)); + } else { + *m_toolTipFont = font; + } + emit toolTipFontChanged(); +} + +void QQStyleKitFont::setTumblerFont(const QFont &font) +{ + if (!m_tumblerFont) { + m_tumblerFont.reset(new QFont(font)); + } else { + *m_tumblerFont = font; + } + emit tumblerFontChanged(); +} + +QT_END_NAMESPACE +#include "moc_qqstylekitfont_p.cpp" diff --git a/src/labs/stylekit/qqstylekitfont_p.h b/src/labs/stylekit/qqstylekitfont_p.h new file mode 100644 index 0000000000..eeadcdc0f4 --- /dev/null +++ b/src/labs/stylekit/qqstylekitfont_p.h @@ -0,0 +1,155 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +// Qt-Security score:significant reason:default + +#ifndef QQSTYLEKITFONT_H +#define QQSTYLEKITFONT_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtQml/QtQml> +#include <QtGui/qfont.h> + +QT_BEGIN_NAMESPACE + +class QQStyleKitFont : public QObject +{ + Q_OBJECT + Q_PROPERTY(QFont systemFont READ systemFont WRITE setSystemFont NOTIFY systemFontChanged FINAL) + Q_PROPERTY(QFont buttonFont READ buttonFont WRITE setButtonFont NOTIFY buttonFontChanged FINAL) + Q_PROPERTY(QFont checkboxFont READ checkboxFont WRITE setCheckboxFont NOTIFY checkboxFontChanged FINAL) + Q_PROPERTY(QFont comboBoxFont READ comboBoxFont WRITE setComboBoxFont NOTIFY comboBoxFontChanged FINAL) + Q_PROPERTY(QFont groupBoxFont READ groupBoxFont WRITE setGroupBoxFont NOTIFY groupBoxFontChanged FINAL) + Q_PROPERTY(QFont itemViewFont READ itemViewFont WRITE setItemViewFont NOTIFY itemViewFontChanged FINAL) + Q_PROPERTY(QFont labelFont READ labelFont WRITE setLabelFont NOTIFY labelFontChanged FINAL) + Q_PROPERTY(QFont listViewFont READ listViewFont WRITE setListViewFont NOTIFY listViewFontChanged FINAL) + Q_PROPERTY(QFont menuFont READ menuFont WRITE setMenuFont NOTIFY menuFontChanged FINAL) + Q_PROPERTY(QFont menuBarFont READ menuBarFont WRITE setMenuBarFont NOTIFY menuBarFontChanged FINAL) + Q_PROPERTY(QFont radioButtonFont READ radioButtonFont WRITE setRadioButtonFont NOTIFY radioButtonFontChanged FINAL) + Q_PROPERTY(QFont spinBoxFont READ spinBoxFont WRITE setSpinBoxFont NOTIFY spinBoxFontChanged FINAL) + Q_PROPERTY(QFont switchControlFont READ switchControlFont WRITE setSwitchControlFont NOTIFY switchControlFontChanged FINAL) + Q_PROPERTY(QFont tabBarFont READ tabBarFont WRITE setTabBarFont NOTIFY tabBarFontChanged FINAL) + Q_PROPERTY(QFont textAreaFont READ textAreaFont WRITE setTextAreaFont NOTIFY textAreaFontChanged FINAL) + Q_PROPERTY(QFont textFieldFont READ textFieldFont WRITE setTextFieldFont NOTIFY textFieldFontChanged FINAL) + Q_PROPERTY(QFont toolBarFont READ toolBarFont WRITE setToolBarFont NOTIFY toolBarFontChanged FINAL) + Q_PROPERTY(QFont toolTipFont READ toolTipFont WRITE setToolTipFont NOTIFY toolTipFontChanged FINAL) + Q_PROPERTY(QFont tumblerFont READ tumblerFont WRITE setTumblerFont NOTIFY tumblerFontChanged FINAL) + + QML_NAMED_ELEMENT(StyleKitFont) + +public: + QQStyleKitFont(QObject *parent = nullptr); + QFont systemFont() const; + void setSystemFont(const QFont &font); + + QFont buttonFont() const; + void setButtonFont(const QFont &font); + + QFont checkboxFont() const; + void setCheckboxFont(const QFont &font); + + QFont comboBoxFont() const; + void setComboBoxFont(const QFont &font); + + QFont groupBoxFont() const; + void setGroupBoxFont(const QFont &font); + + QFont itemViewFont() const; + void setItemViewFont(const QFont &font); + + QFont labelFont() const; + void setLabelFont(const QFont &font); + + QFont listViewFont() const; + void setListViewFont(const QFont &font); + + QFont menuFont() const; + void setMenuFont(const QFont &font); + + QFont menuBarFont() const; + void setMenuBarFont(const QFont &font); + + QFont radioButtonFont() const; + void setRadioButtonFont(const QFont &font); + + QFont spinBoxFont() const; + void setSpinBoxFont(const QFont &font); + + QFont switchControlFont() const; + void setSwitchControlFont(const QFont &font); + + QFont tabBarFont() const; + void setTabBarFont(const QFont &font); + + QFont textAreaFont() const; + void setTextAreaFont(const QFont &font); + + QFont textFieldFont() const; + void setTextFieldFont(const QFont &font); + + QFont toolBarFont() const; + void setToolBarFont(const QFont &font); + + QFont toolTipFont() const; + void setToolTipFont(const QFont &font); + + QFont tumblerFont() const; + void setTumblerFont(const QFont &font); + +signals: + void systemFontChanged(); + void buttonFontChanged(); + void checkboxFontChanged(); + void comboBoxFontChanged(); + void groupBoxFontChanged(); + void itemViewFontChanged(); + void labelFontChanged(); + void listViewFontChanged(); + void menuFontChanged(); + void menuBarFontChanged(); + void radioButtonFontChanged(); + void spinBoxFontChanged(); + void switchControlFontChanged(); + void tabBarFontChanged(); + void textAreaFontChanged(); + void textFieldFontChanged(); + void toolBarFontChanged(); + void toolTipFontChanged(); + void tumblerFontChanged(); + +private: + Q_DISABLE_COPY(QQStyleKitFont) + + mutable std::unique_ptr<QFont> m_systemFont; + mutable std::unique_ptr<QFont> m_buttonFont; + mutable std::unique_ptr<QFont> m_checkboxFont; + mutable std::unique_ptr<QFont> m_comboBoxFont; + mutable std::unique_ptr<QFont> m_groupBoxFont; + mutable std::unique_ptr<QFont> m_itemViewFont; + mutable std::unique_ptr<QFont> m_labelFont; + mutable std::unique_ptr<QFont> m_listViewFont; + mutable std::unique_ptr<QFont> m_menuFont; + mutable std::unique_ptr<QFont> m_menuBarFont; + mutable std::unique_ptr<QFont> m_radioButtonFont; + mutable std::unique_ptr<QFont> m_spinBoxFont; + mutable std::unique_ptr<QFont> m_switchControlFont; + mutable std::unique_ptr<QFont> m_tabBarFont; + mutable std::unique_ptr<QFont> m_textAreaFont; + mutable std::unique_ptr<QFont> m_textFieldFont; + mutable std::unique_ptr<QFont> m_toolBarFont; + mutable std::unique_ptr<QFont> m_toolTipFont; + mutable std::unique_ptr<QFont> m_tumblerFont; +}; + +QT_END_NAMESPACE + +#endif // QQSTYLEKITFONT_H diff --git a/src/labs/stylekit/qqstylekitglobal.cpp b/src/labs/stylekit/qqstylekitglobal.cpp new file mode 100644 index 0000000000..481afa3cc6 --- /dev/null +++ b/src/labs/stylekit/qqstylekitglobal.cpp @@ -0,0 +1,10 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qqstylekitglobal_p.h" + +QT_BEGIN_NAMESPACE + +QT_END_NAMESPACE + +#include "moc_qqstylekitglobal_p.cpp" diff --git a/src/labs/stylekit/qqstylekitglobal_p.h b/src/labs/stylekit/qqstylekitglobal_p.h new file mode 100644 index 0000000000..7ff2bf42ce --- /dev/null +++ b/src/labs/stylekit/qqstylekitglobal_p.h @@ -0,0 +1,154 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +// Qt-Security score:significant reason:default + +#ifndef QQSTYLEKITGLOBAL_P_H +#define QQSTYLEKITGLOBAL_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtQml/QtQml> + +QT_BEGIN_NAMESPACE + +class QQSK: public QObject +{ + Q_OBJECT + +public: + enum class Delegate { + NoDelegate = 0x0000, + Control = 0x0001, + Background = 0x0002, + Handle = 0x0004, + HandleFirst = 0x0008, + HandleSecond = 0x0010, + Indicator = 0x0020, + IndicatorForeground = 0x0040, + IndicatorUp = 0x0080, + IndicatorUpForeground = 0x0100, + IndicatorDown = 0x0200, + IndicatorDownForeground = 0x0400, + }; + Q_DECLARE_FLAGS(Delegates, Delegate) + Q_FLAG(Delegate) + + enum class PropertyGroup { + NoGroup, + Control, + Background, + Border, + DelegateSubType1, + DelegateSubType2, + Handle, + Foreground, + Image, + Indicator, + Shadow, + globalFlag, + Text, + COUNT + }; + Q_ENUM(PropertyGroup) + + enum class Property { + NoProperty, + BottomLeftRadius, + BottomMargin, + BottomPadding, + BottomRightRadius, + Clip, + Color, + Data, + Delegate, + FillMode, + Gradient, + HOffset, + Image, + ImplicitWidth, + ImplicitHeight, + LeftMargin, + LeftPadding, + Margins, + MinimumWidth, + Opacity, + Padding, + Radius, + RightMargin, + RightPadding, + Rotation, + Scale, + Source, + Spacing, + TopLeftRadius, + TopMargin, + TopPadding, + TopRightRadius, + Transition, + Variations, + Visible, + VOffset, + Width, + Blur, + Alignment, + COUNT + }; + Q_ENUM(Property) + + enum class StateFlag { + NoState = 0x000, + Normal = 0x001, + Pressed = 0x002, + Hovered = 0x004, + Highlighted = 0x008, + Focused = 0x010, + Checked = 0x020, + Vertical = 0x040, + Disabled = 0x080, + MAX_STATE = 0x100, + }; + Q_DECLARE_FLAGS(State, StateFlag) + Q_FLAG(State) + + enum class Subclass { + QQStyleKitState, + QQStyleKitReader, + }; + Q_ENUM(Subclass) + + enum class PathFlag { + NoFlag = 0x00, + StyleDirect = 0x01 + }; + Q_DECLARE_FLAGS(PathFlags, PathFlag) + Q_FLAG(PathFlag) + +public: + template <typename T, typename Owner, typename... Args> + static inline T *lazyCreate(T *const &ptr, const Owner *self, Args&&... args) + { + if (!ptr) { + auto *mutableSelf = const_cast<Owner *>(self); + auto *&mutablePtr = const_cast<T *&>(ptr); + mutablePtr = new T(std::forward<Args>(args)..., mutableSelf); + } + return ptr; + } +}; + +Q_DECLARE_OPERATORS_FOR_FLAGS(QQSK::State) +Q_DECLARE_OPERATORS_FOR_FLAGS(QQSK::Delegates) +Q_DECLARE_OPERATORS_FOR_FLAGS(QQSK::PathFlags) + +QT_END_NAMESPACE + +#endif // QQSTYLEKITGLOBAL_P_H diff --git a/src/labs/stylekit/qqstylekitpalette.cpp b/src/labs/stylekit/qqstylekitpalette.cpp new file mode 100644 index 0000000000..ec9e3b3f07 --- /dev/null +++ b/src/labs/stylekit/qqstylekitpalette.cpp @@ -0,0 +1,42 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qqstylekitpalette_p.h" + +QT_BEGIN_NAMESPACE + +QQStyleKitPalette::QQStyleKitPalette(QObject *parent) + : QObject(parent) +{ +} + +#define UNIFIED_PALETTE_GETTER(PROP) \ +QQuickPalette *QQStyleKitPalette::PROP() const { \ + if (!m_ ## PROP) \ + m_ ## PROP.reset(new QQuickPalette()); \ + return m_ ## PROP.get(); \ +} + +UNIFIED_PALETTE_GETTER(system) +UNIFIED_PALETTE_GETTER(checkBox) +UNIFIED_PALETTE_GETTER(button) +UNIFIED_PALETTE_GETTER(comboBox) +UNIFIED_PALETTE_GETTER(groupBox) +UNIFIED_PALETTE_GETTER(itemView) +UNIFIED_PALETTE_GETTER(label) +UNIFIED_PALETTE_GETTER(listView) +UNIFIED_PALETTE_GETTER(menu) +UNIFIED_PALETTE_GETTER(menuBar) +UNIFIED_PALETTE_GETTER(radioButton) +UNIFIED_PALETTE_GETTER(spinBox) +UNIFIED_PALETTE_GETTER(switchControl) +UNIFIED_PALETTE_GETTER(tabBar) +UNIFIED_PALETTE_GETTER(textArea) +UNIFIED_PALETTE_GETTER(textField) +UNIFIED_PALETTE_GETTER(toolBar) +UNIFIED_PALETTE_GETTER(toolTip) +UNIFIED_PALETTE_GETTER(tumbler) + +QT_END_NAMESPACE + +#include "moc_qqstylekitpalette_p.cpp" diff --git a/src/labs/stylekit/qqstylekitpalette_p.h b/src/labs/stylekit/qqstylekitpalette_p.h new file mode 100644 index 0000000000..3f93ba982b --- /dev/null +++ b/src/labs/stylekit/qqstylekitpalette_p.h @@ -0,0 +1,124 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +// Qt-Security score:significant reason:default + +#ifndef QQSTYLEKITPALETTE_P_H +#define QQSTYLEKITPALETTE_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtQml/QtQml> +#include <QtQuick/private/qquickpalette_p.h> + +QT_BEGIN_NAMESPACE + +class QQStyleKitPalette : public QObject +{ + Q_OBJECT + Q_PROPERTY(QQuickPalette *system READ system NOTIFY systemChanged FINAL) + Q_PROPERTY(QQuickPalette *checkBox READ checkBox NOTIFY checkBoxChanged FINAL) + Q_PROPERTY(QQuickPalette *button READ button NOTIFY buttonChanged FINAL) + Q_PROPERTY(QQuickPalette *comboBox READ comboBox NOTIFY comboBoxChanged FINAL) + Q_PROPERTY(QQuickPalette *groupBox READ groupBox NOTIFY groupBoxChanged FINAL) + Q_PROPERTY(QQuickPalette *itemView READ itemView NOTIFY itemViewChanged FINAL) + Q_PROPERTY(QQuickPalette *label READ label NOTIFY labelChanged FINAL) + Q_PROPERTY(QQuickPalette *listView READ listView NOTIFY listViewChanged FINAL) + Q_PROPERTY(QQuickPalette *menu READ menu NOTIFY menuChanged FINAL) + Q_PROPERTY(QQuickPalette *menuBar READ menuBar NOTIFY menuBarChanged FINAL) + Q_PROPERTY(QQuickPalette *radioButton READ radioButton NOTIFY radioButtonChanged FINAL) + Q_PROPERTY(QQuickPalette *spinBox READ spinBox NOTIFY spinBoxChanged FINAL) + Q_PROPERTY(QQuickPalette *switchControl READ switchControl NOTIFY switchControlChanged FINAL) + Q_PROPERTY(QQuickPalette *tabBar READ tabBar NOTIFY tabBarChanged FINAL) + Q_PROPERTY(QQuickPalette *textArea READ textArea NOTIFY textAreaChanged FINAL) + Q_PROPERTY(QQuickPalette *textField READ textField NOTIFY textFieldChanged FINAL) + Q_PROPERTY(QQuickPalette *toolBar READ toolBar NOTIFY toolBarChanged FINAL) + Q_PROPERTY(QQuickPalette *toolTip READ toolTip NOTIFY toolTipChanged FINAL) + Q_PROPERTY(QQuickPalette *tumbler READ tumbler NOTIFY tumblerChanged FINAL) + + QML_NAMED_ELEMENT(StyleKitPalette) + +public: + QQStyleKitPalette(QObject *parent = nullptr); + + QQuickPalette *system() const; + QQuickPalette *checkBox() const; + QQuickPalette *button() const; + QQuickPalette *comboBox() const; + QQuickPalette *groupBox() const; + QQuickPalette *itemView() const; + QQuickPalette *label() const; + QQuickPalette *listView() const; + QQuickPalette *menu() const; + QQuickPalette *menuBar() const; + QQuickPalette *radioButton() const; + QQuickPalette *spinBox() const; + QQuickPalette *switchControl() const; + QQuickPalette *tabBar() const; + QQuickPalette *textArea() const; + QQuickPalette *textField() const; + QQuickPalette *toolBar() const; + QQuickPalette *toolTip() const; + QQuickPalette *tumbler() const; + +signals: + void systemChanged(); + void checkBoxChanged(); + void buttonChanged(); + void comboBoxChanged(); + void groupBoxChanged(); + void itemViewChanged(); + void labelChanged(); + void listViewChanged(); + void menuChanged(); + void menuBarChanged(); + void radioButtonChanged(); + void spinBoxChanged(); + void switchControlChanged(); + void tabBarChanged(); + void textAreaChanged(); + void textFieldChanged(); + void toolBarChanged(); + void toolTipChanged(); + void tumblerChanged(); + +private: + Q_DISABLE_COPY(QQStyleKitPalette) + /* + The following properties are lazy-created, since it's unlikely that a style + sets them all. And since a QQuickPalette is not a QObject, we use std::unique_ptr + for memory management. Since each palette is logically independent of this class, + we make them mutable so that the getter functions can be const. + */ + mutable std::unique_ptr<QQuickPalette> m_system; + mutable std::unique_ptr<QQuickPalette> m_checkBox; + mutable std::unique_ptr<QQuickPalette> m_button; + mutable std::unique_ptr<QQuickPalette> m_comboBox; + mutable std::unique_ptr<QQuickPalette> m_groupBox; + mutable std::unique_ptr<QQuickPalette> m_itemView; + mutable std::unique_ptr<QQuickPalette> m_label; + mutable std::unique_ptr<QQuickPalette> m_listView; + mutable std::unique_ptr<QQuickPalette> m_menu; + mutable std::unique_ptr<QQuickPalette> m_menuBar; + mutable std::unique_ptr<QQuickPalette> m_radioButton; + mutable std::unique_ptr<QQuickPalette> m_spinBox; + mutable std::unique_ptr<QQuickPalette> m_switchControl; + mutable std::unique_ptr<QQuickPalette> m_tabBar; + mutable std::unique_ptr<QQuickPalette> m_textArea; + mutable std::unique_ptr<QQuickPalette> m_textField; + mutable std::unique_ptr<QQuickPalette> m_toolBar; + mutable std::unique_ptr<QQuickPalette> m_toolTip; + mutable std::unique_ptr<QQuickPalette> m_tumbler; +}; + +QT_END_NAMESPACE + +#endif // QQSTYLEKITPALETTE_P_H diff --git a/src/labs/stylekit/qqstylekitpropertyresolver.cpp b/src/labs/stylekit/qqstylekitpropertyresolver.cpp new file mode 100644 index 0000000000..211cdaba94 --- /dev/null +++ b/src/labs/stylekit/qqstylekitpropertyresolver.cpp @@ -0,0 +1,674 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qqstylekitpropertyresolver_p.h" +#include "qqstylekitcontrol_p.h" +#include "qqstylekitcontrols_p.h" +#include "qqstylekitvariation_p.h" +#include "qqstylekitstyle_p.h" +#include "qqstylekittheme_p.h" +#include "qqstylekitdebug_p.h" + +#include <QtQuickTemplates2/private/qquickcontrol_p.h> +#include <QtCore/QScopedValueRollback> + +QT_BEGIN_NAMESPACE + +bool QQStyleKitPropertyResolver::s_styleWarningsIssued = false; +bool QQStyleKitPropertyResolver::s_isReadingProperty = false; +QQSK::State QQStyleKitPropertyResolver::s_cachedState = QQSK::StateFlag::NoState; +QVarLengthArray<QQSK::StateFlag, 10> QQStyleKitPropertyResolver::s_cachedStateList; + +PropertyPathId QQStyleKitPropertyResolver::pathId( + const QQStyleKitPropertyGroup *group, const QQSK::Property property, PathId flag) +{ + /* Follow the parent chain of the property up to the QQStyleKitControlProperties + * group it's inside. This group path, together with the enum value of the property, + * will form its unique PropertyPathId. + * E.g the property 'color' will get a different path ID when it's a part of + * 'background.color' compared to 'background.border.color'. + * This path ID will later be used together with different state combinations to + * form different PropertyStorageId's. A storage ID is used as the key into QMaps + * that stores property values for each state in each QQStyleKitControl. */ + if (property == QQSK::Property::NoProperty) + return PropertyPathId(); + + const int propertyCount = int(QQSK::Property::COUNT); + const int groupCount = int(QQSK::PropertyGroup::COUNT); + + /* Deliberatly use extra wide types for calculations, in order + * to do rough overflow checks in the end. */ + quint64 id = qint64(property); + quint64 idSpaceForPreviousLevel = propertyCount; + + const QQStyleKitPropertyGroup *groupParent = group; + + if (flag == PathId::ExcludeSubType && groupParent->isDelegateSubType()) + groupParent = static_cast<QQStyleKitPropertyGroup *>(groupParent->parent()); + + if (groupParent->isPathFlag()) { + /* property 'global' is not a real group, it's just a hint to the property + * resolver that it should read style properties directly from the style, igoring + * any ongoing transition inside the reader. So just skip it. */ + groupParent = static_cast<QQStyleKitPropertyGroup *>(groupParent->parent()); + } + + while (!groupParent->isControlProperties()) { + // Add 1 to the group number since group 0 (with start ID == 0) is + // reserved for properties that are not nested in a group (e.g control.implicitWidth). + const int groupNumber = int(groupParent->group()) + 1; + const quint64 idSpaceForCurrentGroup = groupNumber * idSpaceForPreviousLevel; + + id += idSpaceForCurrentGroup; + + /* Every time we move up one group level, all the possible property paths + * in the previous level can theoretically occur inside each group on this + * level. So we need to multiply this space with group count. */ + idSpaceForPreviousLevel *= groupCount; + + groupParent = static_cast<QQStyleKitPropertyGroup *>(groupParent->parent()); + if (flag == PathId::ExcludeSubType && groupParent->isDelegateSubType()) { + groupParent = static_cast<QQStyleKitPropertyGroup *>(groupParent->parent()); + Q_ASSERT(groupParent); + } + Q_ASSERT(groupParent); + if (groupParent->isPathFlag()) { + groupParent = static_cast<QQStyleKitPropertyGroup *>(groupParent->parent()); + Q_ASSERT(groupParent); + } + } + + const PropertyPathId pathId(property, id, idSpaceForPreviousLevel); + +#ifdef QT_DEBUG + // Check that the id calculation didn't overflow + Q_ASSERT_X(pathId.pathId() == id, + __FUNCTION__, QQStyleKitDebug::propertyPath(group, property).toUtf8().constData()); + + /* Also check in advance that the path ID can be used in combination with + * any possible state combination later on to form a storage ID. */ + const QQSK::StateFlag maxNestedState = QQSK::StateFlag::MAX_STATE; + Q_ASSERT_X(id < pathId.storageId(maxNestedState), + __FUNCTION__, QQStyleKitDebug::propertyPath(group, property).toUtf8().constData()); +#endif + + return pathId; +} + +const QList<int> QQStyleKitPropertyResolver::baseTypesForType(int exactType) +{ + /* The base types should, by default, mirror the class hierarchy in Qt Quick Controls. + * Exceptions: + * ItemDelegate - is normally used as an menu item in a combo, or item in a ListView. It + * is in any case styled quite differently than a button. */ + switch (exactType) { + case QQStyleKitReader::Button: + case QQStyleKitReader::RadioButton: + case QQStyleKitReader::CheckBox: + case QQStyleKitReader::SwitchControl: { + static QList<int> t = { QQStyleKitReader::AbstractButton, QQStyleKitReader::Control }; + return t; } + case QQStyleKitReader::Menu: + case QQStyleKitReader::Dialog: { + static QList<int> t = { QQStyleKitReader::Popup, QQStyleKitReader::Control }; + return t; } + case QQStyleKitReader::Page: + case QQStyleKitReader::Frame: { + static QList<int> t = { QQStyleKitReader::Pane, QQStyleKitReader::Control }; + return t; } + case QQStyleKitReader::TextField: + case QQStyleKitReader::TextArea: { + static QList<int> t = { QQStyleKitReader::TextInput, QQStyleKitReader::Control }; + return t; } + default: { + static QList<int> t = { QQStyleKitReader::Control }; + return t; } + } + + Q_UNREACHABLE(); + return {}; +} + +void QQStyleKitPropertyResolver::cacheReaderState(QQSK::State state) +{ + if (state == s_cachedState) + return; + + s_cachedState = state; + + /* Note: The order in which we add the states below matters. + * The reason is that the s_cachedStateList that we build is used by QQStyleKitPropertyResolver + * later to generate all the different state combinations that should be tested when + * searching for a property. And the states added first to the list will "win" if the + * same property is set in several of the states. */ + s_cachedStateList.clear(); + if (state.testFlag(QQSK::StateFlag::Pressed)) + s_cachedStateList.append(QQSK::StateFlag::Pressed); + if (state.testFlag(QQSK::StateFlag::Hovered)) + s_cachedStateList.append(QQSK::StateFlag::Hovered); + if (state.testFlag(QQSK::StateFlag::Highlighted)) + s_cachedStateList.append(QQSK::StateFlag::Highlighted); + if (state.testFlag(QQSK::StateFlag::Focused)) + s_cachedStateList.append(QQSK::StateFlag::Focused); + if (state.testFlag(QQSK::StateFlag::Checked)) + s_cachedStateList.append(QQSK::StateFlag::Checked); + if (state.testFlag(QQSK::StateFlag::Vertical)) + s_cachedStateList.append(QQSK::StateFlag::Vertical); + if (state.testFlag(QQSK::StateFlag::Disabled)) + s_cachedStateList.append(QQSK::StateFlag::Disabled); +} + +void QQStyleKitPropertyResolver::rebuildParentChainForReader(QQStyleKitReader *styleReader) +{ + /* Build up the parent chain of readers from \a styleReader. If we encounter a + * reader that already has m_parentChainDirty set to false, we can return early. This + * means that the execution of this function will go faster and faster for each call. */ + styleReader->m_parentChainDirty = false; + QQStyleKitReader *childReader = styleReader; + QObject *parentObj = styleReader->parent(); + QQuickItem *parentItem = nullptr; + + while (parentObj) { + // TODO: change code to look for attached prop instead of styleReader prop? + parentItem = qobject_cast<QQuickItem *>(parentObj); + if (parentItem) { + const QVariant readerAsVariant = parentItem->property("styleReader"); + if (readerAsVariant.isValid()) { + if (auto *reader = qvariant_cast<QQStyleKitReader *>(readerAsVariant)) { + /* Whether the reader has a parent reader or not (which we find out in + * the next iteration), we now anyway mark it as resolved. */ + reader->m_parentChainDirty = false; + /* Some controls use several style readers (e.g RangeSlider), which are + * typically siblings of each other. So ensure we don't make a sibling + * reader the parent of another sibling reader. */ + if (reader->parent() != childReader->parent()) + childReader->m_parentReader = reader; + if (reader->m_parentReader) + break; + + childReader = reader; + } + } + parentItem = parentItem->parentItem(); + } + parentObj = parentItem ? parentItem : parentObj->parent(); + } +} + +void QQStyleKitPropertyResolver::rebuildVariationsForReader( + QQStyleKitReader *styleReader, const QQStyleKitStyle *style) +{ + Q_ASSERT(styleReader->m_effectiveVariationsDirty); + styleReader->m_effectiveVariationsDirty = false; + + if (styleReader->m_parentChainDirty) + rebuildParentChainForReader(styleReader); + + // We make the id's static, since they shouldn't change depending on the styleReader + static PropertyPathIds ids; + if (ids.property.property() == QQSK::Property::NoProperty) { + ids.property = pathId(styleReader, QQSK::Property::Variations, PathId::IncludeSubType); + ids.alternative = pathId(styleReader, QQSK::Property::NoProperty, PathId::IncludeSubType); + ids.subTypeProperty = PropertyPathId(); + ids.subTypeAlternative = PropertyPathId(); + } + + /* Go through all the parent readers of \a styleReader (if any), and for each one, + * add the variations attached to them that has the potential to affect the + * type/control represented by styleReader to m_effectiveVariations. The variations + * that are closest to styleReader in the hierarchy will be added first and take + * precendence over the ones added last. */ + styleReader->m_effectiveVariations.clear(); + QList<int> affectedTypes = baseTypesForType(styleReader->type()); + affectedTypes.prepend(styleReader->type()); + QQStyleKitReader *parentReader = styleReader->m_parentReader; + QQStyleKitReader *lastParentWithVariations = nullptr; + + while (parentReader) { + if (!parentReader->m_hierarchyHasVariations) + break; + + /* Note: when we read the variations property from the style, it returns the varations + * set in the most specific storage/state/control (because of propagation). And those + * are the only ones that will take effect. This means that even if there are variations + * in the fallback style for the requested type (control) that overrides some properties, + * and the style/theme has variations that overrides something else, the variations in + * the fallback style will, in that case, be ignored for that type. The properties _not + * set_ in the effective variations will instead propagate back to be read from the + * type in the theme or style, like all other properties. */ + const int parentType = parentReader->type(); + const QList<int> baseTypes = baseTypesForType(parentType); + const QVariant variationVariant = readPropertyInStyle( + ids, parentType, baseTypes, parentReader, style); + + if (variationVariant.isValid()) { + auto variationsInStyle = *qvariant_cast<QList<QQStyleKitVariation *> *>(variationVariant); + for (auto *variation : std::as_const(variationsInStyle)) { + if (!variation) { + // Ignore unsupported elements (text, numbers, etc), added to the array from QML + continue; + } + + lastParentWithVariations = parentReader; + + for (int type : affectedTypes) { + if (variation->getControl(type)) { + styleReader->m_effectiveVariations.append(variation); + // qDebug() << "Found variation" << variation + // << "for" << styleReader->typeAsControlType() + // << "in" << parentReader->typeAsControlType(); + break; + } + } + } + } + + parentReader = parentReader->m_parentReader; + } + + if (lastParentWithVariations) { + /* This function will be called for all style readers / controls after a style or theme + * change. So as an optimization, we do an extra pass in the end where we mark all the + * readers we found above from the parent of lastParentWithVariations and up to the root + * as not having variations. This allows us to return early for all the remaining style + * readers that shares the same, or parts of the same, parent chain. */ + QQStyleKitReader *parentReader = lastParentWithVariations->m_parentReader; + while (parentReader) { + if (!parentReader->m_hierarchyHasVariations) + break; + parentReader->m_hierarchyHasVariations = false; + parentReader = parentReader->m_parentReader; + } + } +} + +template <class T> +QVariant QQStyleKitPropertyResolver::readPropertyInStorageForState( + const PropertyPathId main, const PropertyPathId alternative, + const T *storageProvider, QQSK::State state) +{ + /* If the propertyId or altPropertyId is set in the control storage, we've found the best + * match, and can return the value back to the application. The reason we need an altPropertyId, + * is because some properties can be set more that one way. For example 'topLeftRadius' can be + * set using either 'topLeftRadius' (main) or 'radius' (alternative). The first found in the + * propagation chain wins. This means that when resolving 'topLeftRadius' for a button, if + * 'radius' is set in 'button', and 'topLeftRadius' is set in abstractButton, 'radius' will + * override 'topLeftRadius', and win. */ + Q_ASSERT(qlonglong(state) <= qlonglong(QQSK::StateFlag::MAX_STATE)); + + const PropertyStorageId propertyKey = main.storageId(state); + + if (Q_UNLIKELY(QQStyleKitDebug::enabled())) + QQStyleKitDebug::trace(main, storageProvider, state, propertyKey); + + const QVariant propertyValue = storageProvider->readStyleProperty(propertyKey); + if (propertyValue.isValid()) { + if (Q_UNLIKELY(QQStyleKitDebug::enabled())) + QQStyleKitDebug::notifyPropertyRead(main, storageProvider, state, propertyValue); + return propertyValue; + } + + const PropertyStorageId altPropertyKey = alternative.storageId(state); + + if (Q_UNLIKELY(QQStyleKitDebug::enabled())) + QQStyleKitDebug::trace(alternative, storageProvider, state, altPropertyKey); + + const QVariant altValue = storageProvider->readStyleProperty(altPropertyKey); + if (altValue.isValid()) { + if (Q_UNLIKELY(QQStyleKitDebug::enabled())) + QQStyleKitDebug::notifyPropertyRead(main, storageProvider, state, altValue); + return altValue; + } + + return {}; +} + +template <class INDICES_CONTAINER> +QVariant QQStyleKitPropertyResolver::readPropertyInControlForStates( + const PropertyPathId main, const PropertyPathId alternative, + const QQStyleKitControl *control, INDICES_CONTAINER &stateListIndices, + int startIndex, int recursionLevel) +{ + for (int i = startIndex; i < s_cachedStateList.length(); ++i) { + /* stateListIndices is a helper list to track which index in the state list + * each recursion level is currently processing. The first recursion level + * will iterate through all of the states. The second recursion level will + * only the iterate through the states that comes after the state at the + * previous recursion. And so on recursively. The end result will be a list of + * state combinations (depth first) where no state is repeated more than + * once. And for each combination, we check if the storage has a value assigned + * for the given property for the given state combination. */ + stateListIndices[recursionLevel] = i; + const QQSK::StateFlag stateFlag = s_cachedStateList[i]; + + if (!control->m_writtenStates.testFlag(stateFlag)) { + /* optimization: the control doesn't have any properties for the state + * we're processing. So continue to the next state in the list. */ + continue; + } + + if (recursionLevel < s_cachedStateList.length() - 1) { + // Continue the recursion towards the longest possible nested state + const QVariant value = readPropertyInControlForStates( + main, alternative, control, stateListIndices, i + 1, recursionLevel + 1); + if (value.isValid()) + return value; + } + + // Check the current combination + QQSK::State storageState = QQSK::StateFlag::NoState; + for (int j = 0; j <= recursionLevel; ++j) + storageState.setFlag(s_cachedStateList[stateListIndices[j]]); + const QVariant value = readPropertyInStorageForState(main, alternative, control, storageState); + if (value.isValid()) + return value; + } + + return {}; +} + +QVariant QQStyleKitPropertyResolver::readPropertyInControl( + const PropertyPathIds &ids, const QQStyleKitControl *control) +{ + /* Find the most specific state combination (based on the state of the reader) that + * has a value set for the property in the contol. In case several state combinations + * could be found, the order of the states in the stateList decides the priority. + * If we're reading a property in a sub type, try all state combinations in the sub + * type first, before trying all the state combinations in the super type. */ + QVarLengthArray<int, 10> stateListIndices(s_cachedStateList.length()); + + if (ids.subTypeProperty.property() != QQSK::Property::NoProperty) { + if (s_cachedState != QQSK::StateFlag::NoState) { + QVariant value = readPropertyInControlForStates( + ids.subTypeProperty, ids.subTypeAlternative, control, stateListIndices, 0, 0); + if (value.isValid()) + return value; + } + + if (control->m_writtenStates.testFlag(QQSK::StateFlag::Normal)) { + const QVariant value = readPropertyInStorageForState( + ids.subTypeProperty, ids.subTypeAlternative, control, QQSK::StateFlag::Normal); + if (value.isValid()) + return value; + } + } + + if (s_cachedState != QQSK::StateFlag::NoState) { + const QVariant value = readPropertyInControlForStates( + ids.property, ids.alternative, control, stateListIndices, 0, 0); + if (value.isValid()) + return value; + } + + /* The normal state is the propagation fall back for all state combinations. + * If the normal state has the property set, it'll return a valid QVariant, + * which will cause the propagation to stop. Otherwise we'll return an invalid + * variant which will cause the search to continue. */ + if (control->m_writtenStates.testFlag(QQSK::StateFlag::Normal)) + return readPropertyInStorageForState(ids.property, ids.alternative, control, QQSK::StateFlag::Normal); + + return {}; +} + +QVariant QQStyleKitPropertyResolver::readPropertyInRelevantControls( + const QQStyleKitControls *controls, const PropertyPathIds &ids, + const int exactType, const QList<int> baseTypes) +{ + if (!controls) + return {}; + + if (const QQStyleKitControl *control = controls->getControl(exactType)) { + const QVariant value = readPropertyInControl(ids, control); + if (value.isValid()) + return value; + } + + for (const int type : baseTypes) { + if (const QQStyleKitControl *control = controls->getControl(type)) { + const QVariant value = readPropertyInControl(ids, control); + if (value.isValid()) + return value; + } + } + + return {}; +} + +QVariant QQStyleKitPropertyResolver::readPropertyInStyle( + const PropertyPathIds &ids, const int exactType, const QList<int> baseTypes, + const QQStyleKitReader *styleReader, const QQStyleKitStyle *style) +{ + QVariant value; + + while (true) { + value = readPropertyInRelevantControls(style->theme(), ids, exactType, baseTypes); + if (value.isValid()) + break; + value = readPropertyInRelevantControls(style, ids, exactType, baseTypes); + if (value.isValid()) + break; + + if (auto *fallbackStyle = style->fallbackStyle()) { + /* Recurse into the fallback style, and search for the property there. If not + * found, and the fallback style has a fallback style, the recursion continues. */ + fallbackStyle->setPalette(styleReader->palette()); + value = readPropertyInStyle(ids, exactType, baseTypes, styleReader, fallbackStyle); + if (value.isValid()) + break; + } + + break; + } + + if (Q_UNLIKELY(QQStyleKitDebug::enabled())) { + if (!value.isValid()) + QQStyleKitDebug::notifyPropertyNotResolved(ids.property); + } + + return value; +} + +QVariant QQStyleKitPropertyResolver::readProperty( + const PropertyPathIds &ids, QQStyleKitReader *styleReader, const QQStyleKitStyle *style) +{ + if (styleReader->m_effectiveVariationsDirty) + rebuildVariationsForReader(styleReader, style); + + const int exactType = styleReader->type(); + const QList<int> baseTypes = baseTypesForType(exactType); + + QVariant value; + + while (true) { + for (const auto *variation : styleReader->m_effectiveVariations) { + value = readPropertyInRelevantControls(variation, ids, exactType, baseTypes); + if (value.isValid()) + break; + } + if (value.isValid()) + break; + + value = readPropertyInStyle(ids, exactType, baseTypes, styleReader, style); + break; + } + + if (Q_UNLIKELY(QQStyleKitDebug::enabled())) { + if (!value.isValid()) + QQStyleKitDebug::notifyPropertyNotResolved(ids.property); + } + + return value; +} + +QVariant QQStyleKitPropertyResolver::readStyleProperty( + const QQStyleKitPropertyGroup *group, + const QQSK::Property property, + const QQSK::Property alternative) +{ + auto [controlProperties, subType, pathFlags] = group->inspectGroupPath(); + const QQSK::Subclass subclass = controlProperties->subclass(); + + if (subclass == QQSK::Subclass::QQStyleKitState) { + /* Because of propagation, we need to know the state of the reader in order to + * resolve the value for the given property. E.g the color of a background delegate + * will take a difference search path, and end up with a different value, depending + * of if the reader is a Slider or a Button, and if it's hovered or pressed etc. + * Therefore, when a property is instead accessed from a control (or a state in a + * control) in the UnifedStyle, we cannot support propagation. But to still allow + * static (as in non-propagating) bindings between the properties in a Style, + * we fall back to simply return the value specified in the accessed control. */ + const auto [control, nestedState] = controlProperties->asQQStyleKitState()->controlAndState(); + const PropertyPathId propertyPathId = pathId(group, property, PathId::IncludeSubType); + const PropertyStorageId key = propertyPathId.storageId(nestedState); + return control->readStyleProperty(key); + } + + QQStyleKitStyle *style = controlProperties->style(); + if (!style) { + if (!s_styleWarningsIssued) { + s_styleWarningsIssued = true; + qmlWarning(group) << "style properties cannot be read: No StyleKit style has been set!"; + } + return {}; + } + + if (!style->loaded()) { + // Optimization: Skip reads until both the style and the theme is ready + return {}; + } + + if (subclass == QQSK::Subclass::QQStyleKitReader) { + QQStyleKitDebug::groupBeingRead = group; + QQStyleKitReader *styleReader = controlProperties->asQQStyleKitReader(); + + if (s_isReadingProperty) { + if (!s_styleWarningsIssued) { + s_styleWarningsIssued = true; + qmlWarning(styleReader) << "The style property '" << property << "' was read " + << "before finishing the read of another style property. " + << "This is likely to cause a style glitch."; + } + } + QScopedValueRollback rollback(s_isReadingProperty, true); + + PropertyPathIds ids; + ids.property = pathId(group, property, PathId::ExcludeSubType); + ids.alternative = pathId(group, alternative, PathId::ExcludeSubType); + + if (subType != QQSK::PropertyGroup::NoGroup) { + ids.subTypeProperty = pathId(group, property, PathId::IncludeSubType); + ids.subTypeAlternative = pathId(group, alternative, PathId::IncludeSubType); + } else { + ids.subTypeProperty = PropertyPathId(); + ids.subTypeAlternative = PropertyPathId(); + } + + if (!pathFlags.testFlag(QQSK::PathFlag::StyleDirect)) { + /* A style reader can have a storage that contains local property overrides (that is, + * interpolated values from an ongoing transition). When searching for a property, we + * therefore need to check this storage first. The exception is if the property was read + * inside the 'global' group, which means that we should read the values directly + * from the style. */ + if (subType != QQSK::PropertyGroup::NoGroup) { + const QVariant value = readPropertyInStorageForState( + ids.subTypeProperty, ids.subTypeAlternative, styleReader, QQSK::StateFlag::Normal); + if (value.isValid()) + return value; + } + const QVariant value = readPropertyInStorageForState( + ids.property, ids.alternative, styleReader, QQSK::StateFlag::Normal); + if (value.isValid()) + return value; + } + + style->setPalette(styleReader->palette()); + cacheReaderState(styleReader->controlState()); + const QVariant value = readProperty(ids, styleReader, style); + return value; + } + + Q_UNREACHABLE(); + return {}; +} + +bool QQStyleKitPropertyResolver::writeStyleProperty( + const QQStyleKitPropertyGroup *group, + const QQSK::Property property, + const QVariant &value) +{ + // While readStyleProperty() takes propagation into account, writeStyleProperty() doesn't. + // Instead it writes \a value directly to the storage that the group belongs to. + Q_ASSERT(group); + auto [controlProperties, subType, pathFlags] = group->inspectGroupPath(); + const QQSK::Subclass subclass = controlProperties->subclass(); + const PropertyPathId propertyPathId = pathId(group, property, PathId::IncludeSubType); + // The subType is only used when reading a property using propagation + Q_UNUSED(subType); + + if (pathFlags.testFlag(QQSK::PathFlag::StyleDirect)) { + qmlWarning(controlProperties) << "Properties inside 'global' are read-only!"; + return false; + } + + if (subclass == QQSK::Subclass::QQStyleKitReader) { + // This is a write to a StyleKitReader, probably from an ongoing transition + QQStyleKitReader *reader = controlProperties->asQQStyleKitReader(); + const PropertyStorageId key = propertyPathId.storageId(QQSK::StateFlag::Normal); + const QVariant currentValue = reader->readStyleProperty(key); + const bool valueChanged = currentValue != value; + if (valueChanged) { + reader->writeStyleProperty(key, value); + QQStyleKitDebug::notifyPropertyWrite(group, property, reader, QQSK::StateFlag::Normal, key, value); + } + return valueChanged; + } + + if (subclass == QQSK::Subclass::QQStyleKitState) { + // This is a write to a control inside the StyleKit style + const auto [control, nestedState] = controlProperties->asQQStyleKitState()->controlAndState(); + const PropertyStorageId key = propertyPathId.storageId(nestedState); + const QVariant currentValue = control->readStyleProperty(key); + const bool valueChanged = currentValue != value; + if (valueChanged) { + control->m_writtenStates |= nestedState; + control->writeStyleProperty(key, value); + QQStyleKitDebug::notifyPropertyWrite(group, property, control, nestedState, key, value); + } + return valueChanged; + } + + Q_UNREACHABLE(); + return false; +} + +bool QQStyleKitPropertyResolver::hasLocalStyleProperty( + const QQStyleKitPropertyGroup *group, + const QQSK::Property property) +{ + Q_ASSERT(group); + auto [controlProperties, subType, pathFlags] = group->inspectGroupPath(); + const QQSK::Subclass subclass = controlProperties->subclass(); + const PropertyPathId propertyPathId = pathId(group, property, PathId::IncludeSubType); + Q_UNUSED(subType); + + if (pathFlags.testFlag(QQSK::PathFlag::StyleDirect)) + return false; + + if (subclass == QQSK::Subclass::QQStyleKitReader) { + const PropertyStorageId key = propertyPathId.storageId(QQSK::StateFlag::Normal); + return controlProperties->asQQStyleKitReader()->readStyleProperty(key).isValid(); + } + + if (subclass == QQSK::Subclass::QQStyleKitState) { + const auto [control, nestedState] = controlProperties->asQQStyleKitState()->controlAndState(); + const PropertyStorageId key = propertyPathId.storageId(nestedState); + return control->readStyleProperty(key).isValid(); + } + + Q_UNREACHABLE(); + return false; +} + +QT_END_NAMESPACE + +#include "moc_qqstylekitpropertyresolver_p.cpp" diff --git a/src/labs/stylekit/qqstylekitpropertyresolver_p.h b/src/labs/stylekit/qqstylekitpropertyresolver_p.h new file mode 100644 index 0000000000..fc88a843e7 --- /dev/null +++ b/src/labs/stylekit/qqstylekitpropertyresolver_p.h @@ -0,0 +1,114 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +// Qt-Security score:significant reason:default + +#ifndef QQSTYLEKITPROPERTYRESOLVER_P_H +#define QQSTYLEKITPROPERTYRESOLVER_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtQml/QtQml> + +#include "qqstylekitglobal_p.h" +#include "qqstylekitstorage_p.h" + +QT_BEGIN_NAMESPACE + +class QQStyleKitControl; +class QQStyleKitControls; +class QQStyleKitStyle; +class QQStyleKitReader; +class QQStyleKitPropertyGroup; + +class QQStyleKitPropertyResolver +{ + Q_GADGET + +public: + enum class PathId { + ExcludeSubType, + IncludeSubType + }; + Q_ENUM(PathId) + + struct PropertyPathIds { + PropertyPathId property; + PropertyPathId alternative; + PropertyPathId subTypeProperty; + PropertyPathId subTypeAlternative; + }; + + static QVariant readStyleProperty( + const QQStyleKitPropertyGroup *group, + const QQSK::Property property, + const QQSK::Property alternative = QQSK::Property::NoProperty); + + static bool writeStyleProperty( + const QQStyleKitPropertyGroup *group, + const QQSK::Property property, + const QVariant &value); + + static bool hasLocalStyleProperty( + const QQStyleKitPropertyGroup *group, + const QQSK::Property property); + +private: + static bool s_styleWarningsIssued; + static bool s_isReadingProperty; + static QQSK::State s_cachedState; + static QVarLengthArray<QQSK::StateFlag, 10> s_cachedStateList; + +private: + template <class T> + static QVariant readPropertyInStorageForState( + const PropertyPathId main, const PropertyPathId alternative, + const T *storageProvider, QQSK::State state); + + template <class INDICES_CONTAINER> + static QVariant readPropertyInControlForStates( + const PropertyPathId main, const PropertyPathId alternative, + const QQStyleKitControl *control, INDICES_CONTAINER &stateListIndices, + int startIndex, int recursionLevel); + + static QVariant readPropertyInControl( + const PropertyPathIds &ids, const QQStyleKitControl *control); + + static QVariant readPropertyInRelevantControls( + const QQStyleKitControls *controls, const PropertyPathIds &ids, + const int exactType, const QList<int> baseTypes); + + static QVariant readPropertyInStyle( + const PropertyPathIds &ids, const int exactType, + const QList<int> baseTypes, const QQStyleKitReader *styleReader, + const QQStyleKitStyle *style); + + static QVariant readProperty( + const PropertyPathIds &ids, + QQStyleKitReader *styleReader, + const QQStyleKitStyle *style); + + static PropertyPathId pathId( + const QQStyleKitPropertyGroup *group, + const QQSK::Property property, PathId flag); + + static const QList<int> baseTypesForType(int exactType); + + static void cacheReaderState(QQSK::State state); + static void rebuildParentChainForReader(QQStyleKitReader *styleReader); + static void rebuildVariationsForReader( + QQStyleKitReader *styleReader, + const QQStyleKitStyle *style); +}; + +QT_END_NAMESPACE + +#endif // QQSTYLEKITPROPERTYRESOLVER_P_H diff --git a/src/labs/stylekit/qqstylekitreader.cpp b/src/labs/stylekit/qqstylekitreader.cpp new file mode 100644 index 0000000000..eb5ecf76bf --- /dev/null +++ b/src/labs/stylekit/qqstylekitreader.cpp @@ -0,0 +1,526 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include <QtQuick/private/qquickpalette_p.h> +#include <QtQuick/private/qquickwindow_p.h> +#include <QtQuick/private/qquickstategroup_p.h> +#include <QtQuick/private/qquickpropertychanges_p.h> + +#include "qqstylekit_p.h" +#include "qqstylekitreader_p.h" +#include "qqstylekitpropertyresolver_p.h" +#include "qqstylekitstyle_p.h" + +QT_BEGIN_NAMESPACE + +using namespace Qt::StringLiterals; + +static const QString kAlternate1 = "A1"_L1; +static const QString kAlternate2 = "A2"_L1; + +QList<QQStyleKitReader *> QQStyleKitReader::s_allReaders; +QMap<QString, QQmlComponent *> QQStyleKitReader::s_propertyChangesComponents; + +QQStyleKitReader::QQStyleKitReader(QObject *parent) + : QQStyleKitControlProperties(QQSK::PropertyGroup::Control, parent) + , m_dontEmitChangedSignals(false) + , m_effectiveVariationsDirty(true) + , m_parentChainDirty(true) + , m_hierarchyHasVariations(true) + , m_global(QQStyleKitControlProperties(QQSK::PropertyGroup::globalFlag, this)) +{ + s_allReaders.append(this); +} + +QQStyleKitReader::~QQStyleKitReader() +{ + s_allReaders.removeOne(this); +} + +QQuickStateGroup *QQStyleKitReader::stateGroup() +{ + if (m_stateGroup) + return m_stateGroup; + + /* Lazy create a StyleKitReaderStateGroup as soon as we have delegates + * that needs to be "tracked". That is, the user of this StyleKitReader + * has read one or more properties, so we need to check and emit changes for + * those properties whenever our state changes. */ + const auto *stylePtr = style(); + Q_ASSERT(stylePtr); + + m_stateGroup = new QQuickStateGroup(this); + + // Add two states that we can alternate between + auto statesProp = m_stateGroup->statesProperty(); + QQuickState *alternate1 = new QQuickState(m_stateGroup); + QQuickState *alternate2 = new QQuickState(m_stateGroup); + alternate1->setName(kAlternate1); + alternate2->setName(kAlternate2); + m_stateGroup->statesProperty().append(&statesProp, alternate1); + m_stateGroup->statesProperty().append(&statesProp, alternate2); + + QQmlComponent *controlComp = createControlChangesComponent(); + instantiatePropertyChanges(controlComp); + + return m_stateGroup; +} + +QQmlComponent *QQStyleKitReader::createControlChangesComponent() const +{ + static const QLatin1String propertyName("control"_L1); + if (s_propertyChangesComponents.contains(propertyName)) + return s_propertyChangesComponents.value(propertyName); + + const QString qmlControlCode = QString::fromUtf8(R"( + import QtQuick + PropertyChanges { + spacing: global.spacing + padding: global.padding + leftPadding: global.leftPadding + rightPadding: global.rightPadding + topPadding: global.topPadding + bottomPadding: global.bottomPadding + text.color: global.text.color + text.alignment: global.text.alignment + } + )"); + + // TODO: cache propertyName to component! + QQmlComponent *component = new QQmlComponent(qmlEngine(style())); + component->setData(qmlControlCode.toUtf8(), QUrl()); + Q_ASSERT_X(!component->isError(), __FUNCTION__, component->errorString().toUtf8().constData()); + s_propertyChangesComponents.insert(propertyName, component); + return component; +} + +QQmlComponent *QQStyleKitReader::createDelegateChangesComponent(const QString &delegateName) const +{ + if (s_propertyChangesComponents.contains(delegateName)) + return s_propertyChangesComponents.value(delegateName); + + static const QString qmlTemplateCode = QString::fromUtf8(R"( + import QtQuick + PropertyChanges { $ { + implicitWidth: global.$.implicitWidth + implicitHeight: global.$.implicitHeight + visible: global.$.visible + color: global.$.color + gradient: global.$.gradient + radius: global.$.radius + topLeftRadius: global.$.topLeftRadius + topRightRadius: global.$.topRightRadius + bottomLeftRadius: global.$.bottomLeftRadius + bottomRightRadius: global.$.bottomRightRadius + margins: global.$.margins + alignment: global.$.alignment + leftMargin: global.$.leftMargin + rightMargin: global.$.rightMargin + topMargin: global.$.topMargin + bottomMargin: global.$.bottomMargin + scale: global.$.scale + rotation: global.$.rotation + opacity: global.$.opacity + border.color: global.$.border.color + border.width: global.$.border.width + shadow.color: global.$.shadow.color + shadow.scale: global.$.shadow.scale + shadow.blur: global.$.shadow.blur + shadow.visible: global.$.shadow.visible + shadow.opacity: global.$.shadow.opacity + shadow.verticalOffset: global.$.shadow.verticalOffset + shadow.horizontalOffset: global.$.shadow.horizontalOffset + shadow.delegate: global.$.shadow.delegate + image.source: global.$.image.source + image.color: global.$.image.color + image.fillMode: global.$.image.fillMode + delegate: global.$.delegate + data: global.$.data + }} + )"); + + QString substitutedCode = qmlTemplateCode; + substitutedCode.replace('$'_L1, delegateName); + QQmlComponent *component = new QQmlComponent(qmlEngine(style())); + component->setData(substitutedCode.toUtf8(), QUrl()); + Q_ASSERT_X(!component->isError(), __FUNCTION__, component->errorString().toUtf8().constData()); + s_propertyChangesComponents.insert(delegateName, component); + return component; +} + +void QQStyleKitReader::instantiatePropertyChanges(QQmlComponent *comp) +{ + QObject *obj = comp->create(qmlContext(this)); + auto *propertyChanges = qobject_cast<QQuickPropertyChanges *>(obj); + Q_ASSERT(propertyChanges); + + // setter for the "target" property is called setObject + propertyChanges->setObject(this); + /* set "explicit" to true, meaning that the StyleProperties shouldn't + * create bindings, but do one-off assignments. Bindings are not needed + * here since it's the state changes of this StyleKitReader that + * drives the property changes. The properties cannot change outside of + * a state change (or, if they do, it will trigger a full update equal + * to a theme change) */ + propertyChanges->setIsExplicit(true); + /* We don't need to ever restore the properties back to default, since + * the group state will never be reset back to an empty string. This will + * hopefully avoid the generation of restore structures inside the StateGroup. */ + propertyChanges->setRestoreEntryValues(false); + + /* Add the new PropertyChanges object to both states, A1 and A2 */ + for (QQuickState *state : stateGroup()->states()) { + auto changesProp = state->changes(); + changesProp.append(&changesProp, propertyChanges); + } +} + +void QQStyleKitReader::maybeTrackDelegates() +{ + forEachUsedDelegate( + [this](QQStyleKitDelegateProperties *delegate, QQSK::Delegate type, const QString &delegatePath){ + if (m_trackedDelegates.testFlag(type)) { + // We're already tracking the delegate. So nothing needs to be done. + return; + } + if (!delegate->visible()) { + /* As an optimization, if the delegate is hidden, we don't track it. Most + * controls set background.visible to false, for example. If this, for + * whatever reason, is not wanted, set opacity to 0 instead. */ + return; + } + /* Invariant: The application has read one or more properties for the given delegate + * from the Style, but we don't yet have a PropertyChanges object that can track + * changes to it (and run transitions). So we create one now. By lazy creating them this + * way, we avoid creating PropertyChanges for all the different delegates that a control + * _may_ have. Instead, we only track changes for the delegates it actually uses. */ + m_trackedDelegates.setFlag(type); + QQmlComponent *comp = createDelegateChangesComponent(delegatePath); + instantiatePropertyChanges(comp); + }); +} + +void QQStyleKitReader::updateControl() +{ + const QQStyleKitStyle *style = QQStyleKitStyle::current(); + if (!style || !style->loaded()) + return; + + /* Alternate between two states to trigger a state change. The state group + * will, upon changing state, take care of reading the updated property values, + * compare them against the current ones in the local storage, and emit changes + * (possibly using a transition) if changed. Since the new state might change the + * transition, we need to update it first before we do the state change, so that + * it takes effect. + * If we have skipped tracking some delegates because they are hidden, we need to + * check again if this is still the case for the current state. Otherwise, we now + * need to track them. Untracked delegates are not backed by PropertyChanges objects, + * and hence, will not update when we do a state swap below. + * Note that the first time this function is called after start-up, none of the + * delegates are yet tracked, and therefore will be created now. */ + + maybeTrackDelegates(); + + auto transitionProp = stateGroup()->transitionsProperty(); + const int transitionCountInStateGroup = transitionProp.count(&transitionProp); + const bool enabled = QQStyleKit::qmlAttachedProperties()->transitionsEnabled(); + QQuickTransition *transitionInStyle = enabled ? transition() : nullptr; + QQuickTransition *transitionInStateGroup = + transitionCountInStateGroup > 0 ? transitionProp.at(&transitionProp, 0) : nullptr; + if (transitionInStyle != transitionInStateGroup) { + transitionProp.clear(&transitionProp); + if (transitionInStyle) + transitionProp.append(&transitionProp, transitionInStyle); + } + + switch (m_alternateState) { + case AlternateState::Alternate1: + m_alternateState = AlternateState::Alternate2; + stateGroup()->setState(kAlternate2); + break; + case AlternateState::Alternate2: + m_alternateState = AlternateState::Alternate1; + stateGroup()->setState(kAlternate1); + break; + default: + Q_UNREACHABLE(); + } +} + +void QQStyleKitReader::resetAll() +{ + /* Tell all controls (or all QQuickStyleKitReaders inside all controls + * and elsewhere in the app, to be precise) to re-read all style properties. + * Optimization: do a pass of all the controls in the Normal state + * first, since we assume of those there will be many. Processing them + * first (that is, controls of the same state) means that we can limit + * the number of times we need to fully sync Style.target when + * doing property reads from QQStyleKitPropertyResolver. */ + for (QQStyleKitReader *reader : s_allReaders) { + reader->m_effectiveVariationsDirty = true; + reader->m_hierarchyHasVariations = true; + + /* Don't set m_parentChainDirty, since a theme or style change shouldn't affect + * the parent chain of any of the controls. + * TODO: add a public "StyleKit.forceLayout()" function that can be + * called whenever the app reparents controls in a way that can affect + * variations. This function should set m_parentChainDirty before calling this + * function. Parent changes could probably also be detected automatically, but + * its likely much more performant to put this burden on the app developer. */ + // m_parentChainDirty = true; + } + + for (QQStyleKitReader *reader : s_allReaders) { + if (reader->controlState() != QQSK::StateFlag::Normal) + continue; + reader->clearLocalStorage(); + reader->emitChangedForAllStyleProperties(); + } + + for (QQStyleKitReader *reader : s_allReaders) { + if (reader->controlState() == QQSK::StateFlag::Normal) + continue; + reader->clearLocalStorage(); + reader->emitChangedForAllStyleProperties(); + } +} + +void QQStyleKitReader::populateLocalStorage() +{ + if (!m_storage.isEmpty()) + return; + const auto *stylePtr = style(); + if (!stylePtr || !stylePtr->loaded()) + return; + + /* The local storage is empty, which is typically the case after an + * operation that should perform without a transition, such as a theme + * change or a change to a property value in the Style itself. + * Doing a transition in that case is unwanted and slow (since the + * operation typically affect all controls), so we short-cut the process by + * emitting changed signals directly for all the properties instead. + * Since that will render the local storage out-of-sync, we clear it at the + * same time to signal that it's 'dirty'. Which is why we need to sync it back + * up again now. + * Syncing the local storage before changing state (even if the values that + * end up in the storage should be exactly the same as those already + * showing), has the upshot that we can compare after the state change which + * properties has changed, which will limit the amount of changed signals we + * then need to emit. */ + m_dontEmitChangedSignals = true; + updateControl(); + m_dontEmitChangedSignals = false; +} + +void QQStyleKitReader::clearLocalStorage() +{ + /* Clear all the local property overrides that has been set on this reader. Such + * overrides are typically interpolated values set by a transition during a state + * change. By clearing them, the controls will end up reading the property values + * directly from the Style instead. */ + m_storage.clear(); +} + +QQSK::State QQStyleKitReader::controlState() const +{ + QQSK::State effectiveState = m_state; + + if (!enabled()) { + // Some states are not valid if the control is disabled + effectiveState &= ~(QQSK::StateFlag::Pressed | + QQSK::StateFlag::Hovered | + QQSK::StateFlag::Highlighted | + QQSK::StateFlag::Focused | + QQSK::StateFlag::Hovered); + } + + if (effectiveState == QQSK::StateFlag::NoState) + effectiveState.setFlag(QQSK::StateFlag::Normal); + + return effectiveState; +} + +QVariant QQStyleKitReader::readStyleProperty(PropertyStorageId key) const +{ + return m_storage.value(key); +} + +void QQStyleKitReader::writeStyleProperty(PropertyStorageId key, const QVariant &value) +{ + m_storage.insert(key, value); +} + +bool QQStyleKitReader::dontEmitChangedSignals() const +{ + return m_dontEmitChangedSignals; +} + +int QQStyleKitReader::type() const +{ + return m_type; +} + +void QQStyleKitReader::setType(int type) +{ + if (m_type == type) + return; + + m_type = type; + populateLocalStorage(); + emit typeChanged(); + updateControl(); +} + +#ifdef QT_DEBUG +QQStyleKitReader::ControlType QQStyleKitReader::typeAsControlType() const +{ + /* Note: m_type is of type int to support extending the list + * of possible types from the Style itself. This function + * is here to for debugging purposes */ + return ControlType(m_type); +} +#endif + +bool QQStyleKitReader::hovered() const +{ + return m_state.testFlag(QQSK::StateFlag::Hovered); +} + +void QQStyleKitReader::setHovered(bool hovered) +{ + if (hovered == QQStyleKitReader::hovered()) + return; + + populateLocalStorage(); + m_state.setFlag(QQSK::StateFlag::Hovered, hovered); + emit hoveredChanged(); + updateControl(); +} + +bool QQStyleKitReader::enabled() const +{ + return !m_state.testFlag(QQSK::StateFlag::Disabled); +} + +void QQStyleKitReader::setEnabled(bool enabled) +{ + if (enabled == QQStyleKitReader::enabled()) + return; + + populateLocalStorage(); + m_state.setFlag(QQSK::StateFlag::Disabled, !enabled); + emit enabledChanged(); + updateControl(); +} + +bool QQStyleKitReader::focused() const +{ + return m_state.testFlag(QQSK::StateFlag::Focused); +} + +void QQStyleKitReader::setFocused(bool focused) +{ + if (focused == QQStyleKitReader::focused()) + return; + + populateLocalStorage(); + m_state.setFlag(QQSK::StateFlag::Focused, focused); + emit focusedChanged(); + updateControl(); +} + +bool QQStyleKitReader::checked() const +{ + return m_state.testFlag(QQSK::StateFlag::Checked); +} + +void QQStyleKitReader::setChecked(bool checked) +{ + if (checked == QQStyleKitReader::checked()) + return; + + populateLocalStorage(); + m_state.setFlag(QQSK::StateFlag::Checked, checked); + emit checkedChanged(); + updateControl(); +} + +bool QQStyleKitReader::pressed() const +{ + return m_state.testFlag(QQSK::StateFlag::Pressed); +} + +void QQStyleKitReader::setPressed(bool pressed) +{ + if (pressed == QQStyleKitReader::pressed()) + return; + + populateLocalStorage(); + m_state.setFlag(QQSK::StateFlag::Pressed, pressed); + emit pressedChanged(); + updateControl(); +} + +bool QQStyleKitReader::vertical() const +{ + return m_state.testFlag(QQSK::StateFlag::Vertical); +} + +void QQStyleKitReader::setVertical(bool vertical) +{ + if (vertical == QQStyleKitReader::vertical()) + return; + + populateLocalStorage(); + m_state.setFlag(QQSK::StateFlag::Vertical, vertical); + emit verticalChanged(); + updateControl(); +} + +bool QQStyleKitReader::highlighted() const +{ + return m_state.testFlag(QQSK::StateFlag::Highlighted); +} + +void QQStyleKitReader::setHighlighted(bool highlighted) +{ + if (highlighted == QQStyleKitReader::highlighted()) + return; + + populateLocalStorage(); + m_state.setFlag(QQSK::StateFlag::Highlighted, highlighted); + emit highlightedChanged(); + updateControl(); +} + +QQuickPalette *QQStyleKitReader::palette() const +{ + return &const_cast<QQStyleKitReader *>(this)->m_palette; +} + +void QQStyleKitReader::setPalette(QQuickPalette *palette) +{ + if (palette && m_palette.toQPalette() == palette->toQPalette()) + return; + + m_palette.reset(); + if (palette) + m_palette.inheritPalette(palette->toQPalette()); + emit paletteChanged(); + + const auto *stylePtr = style(); + if (!stylePtr || !stylePtr->loaded()) + return; + + clearLocalStorage(); + emitChangedForAllStyleProperties(); +} + +QQStyleKitControlProperties *QQStyleKitReader::global() const +{ + return &const_cast<QQStyleKitReader *>(this)->m_global; +} + +QT_END_NAMESPACE + +#include "moc_qqstylekitreader_p.cpp" diff --git a/src/labs/stylekit/qqstylekitreader_p.h b/src/labs/stylekit/qqstylekitreader_p.h new file mode 100644 index 0000000000..f143a3cf53 --- /dev/null +++ b/src/labs/stylekit/qqstylekitreader_p.h @@ -0,0 +1,182 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +// Qt-Security score:significant reason:default + +#ifndef QQSTYLEKITREADER_P_H +#define QQSTYLEKITREADER_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtQml/QtQml> +#include <QtQuick/private/qquickpalette_p.h> + +#include "qqstylekitcontrolproperties_p.h" +#include "qqstylekitstorage_p.h" + +QT_BEGIN_NAMESPACE + +class QQuickPalette; +class QQStyleKitVariation; +class QQStyleKitPropertyResolver; + +class QQStyleKitReader : public QQStyleKitControlProperties +{ + Q_OBJECT + Q_PROPERTY(int type READ type WRITE setType NOTIFY typeChanged FINAL) + Q_PROPERTY(bool enabled READ enabled WRITE setEnabled NOTIFY enabledChanged FINAL) + Q_PROPERTY(bool focused READ focused WRITE setFocused NOTIFY focusedChanged FINAL) + Q_PROPERTY(bool checked READ checked WRITE setChecked NOTIFY checkedChanged FINAL) + Q_PROPERTY(bool hovered READ hovered WRITE setHovered NOTIFY hoveredChanged FINAL) + Q_PROPERTY(bool pressed READ pressed WRITE setPressed NOTIFY pressedChanged FINAL) + Q_PROPERTY(bool vertical READ vertical WRITE setVertical NOTIFY verticalChanged FINAL) + Q_PROPERTY(bool highlighted READ highlighted WRITE setHighlighted NOTIFY highlightedChanged FINAL) + Q_PROPERTY(QQuickPalette *palette READ palette WRITE setPalette NOTIFY paletteChanged FINAL) + Q_PROPERTY(QQStyleKitControlProperties *global READ global CONSTANT FINAL) + + QML_NAMED_ELEMENT(StyleKitReader) + +public: + enum ControlType { + Unknown = 100000, + Control, + AbstractButton, + Button, + CheckBox, + ComboBox, + FlatButton, + Slider, + SpinBox, + SwitchControl, + SearchField, + TextArea, + TextField, + TextInput, + RadioButton, + ItemDelegate, + Popup, + Menu, + Dialog, + Pane, + Page, + Frame + }; + Q_ENUM(ControlType) + + enum class AlternateState { + Alternate1, + Alternate2 + }; + Q_ENUM(AlternateState) + + QQStyleKitReader(QObject *parent = nullptr); + ~QQStyleKitReader(); + + int type() const; + void setType(int type); +#ifdef QT_DEBUG + ControlType typeAsControlType() const; +#endif + + bool hovered() const; + void setHovered(bool hovered); + + bool enabled() const; + void setEnabled(bool enabled); + + bool focused() const; + void setFocused(bool focused); + + bool checked() const; + void setChecked(bool checked); + + bool pressed() const; + void setPressed(bool pressed); + + QQuickPalette *palette() const; + void setPalette(QQuickPalette *palette); + + bool vertical() const; + void setVertical(bool vertical); + + bool highlighted() const; + void setHighlighted(bool highlighted); + + QQStyleKitControlProperties *global() const; + + QVariant readStyleProperty(PropertyStorageId key) const; + void writeStyleProperty(PropertyStorageId key, const QVariant &value); + void clearLocalStorage(); + + QQSK::State controlState() const; + + static void setTransitionEnabled(bool enabled); + static bool transitionEnabled(); + static void resetAll(); + + static QList<QQStyleKitReader *> s_allReaders; + +signals: + void typeChanged(); + void customTypeChanged(); + void propertiesChanged(); + void enabledChanged(); + void focusedChanged(); + void checkedChanged(); + void hoveredChanged(); + void pressedChanged(); + void paletteChanged(); + void verticalChanged(); + void highlightedChanged(); + +private: + void updateControl(); + void populateLocalStorage(); + bool dontEmitChangedSignals() const; + + QQuickStateGroup *stateGroup(); + QQmlComponent *createControlChangesComponent() const; + QQmlComponent *createDelegateChangesComponent(const QString &delegateName) const; + void instantiatePropertyChanges(QQmlComponent *comp); + void maybeTrackDelegates(); + +private: + Q_DISABLE_COPY(QQStyleKitReader) + + int m_type = ControlType::Unknown; + + bool m_dontEmitChangedSignals: 1; + bool m_effectiveVariationsDirty: 1; + bool m_parentChainDirty: 1; + bool m_hierarchyHasVariations: 1; + + QQuickPalette m_palette; + mutable QQStyleKitPropertyStorage m_storage; + AlternateState m_alternateState = AlternateState::Alternate1; + QQSK::State m_state = QQSK::StateFlag::NoState; + QQuickStateGroup *m_stateGroup = nullptr; + QQSK::Delegates m_trackedDelegates = QQSK::Delegate::NoDelegate; + + QPointer<QQStyleKitReader> m_parentReader; + QList<QQStyleKitVariation *> m_effectiveVariations; + + QQStyleKitControlProperties m_global; + + static QMap<QString, QQmlComponent *> s_propertyChangesComponents; + + friend class QQStyleKitControlProperties; + friend class QQStyleKitPropertyResolver; + friend class QQStyleKitPropertyGroup; +}; + +QT_END_NAMESPACE + +#endif // QQSTYLEKITREADER_P_H diff --git a/src/labs/stylekit/qqstylekitstorage.cpp b/src/labs/stylekit/qqstylekitstorage.cpp new file mode 100644 index 0000000000..6a62910573 --- /dev/null +++ b/src/labs/stylekit/qqstylekitstorage.cpp @@ -0,0 +1,21 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qqstylekitstorage_p.h" + +QT_BEGIN_NAMESPACE + +PropertyPathId::PropertyPathId( + const QQSK::Property property, PropertyPathId_t id, uint numberOfPropertiesInGroup) + : m_property(property), m_id(id), m_numberOfPropertiesInGroup(numberOfPropertiesInGroup) +{ +} + +PropertyStorageId PropertyPathId::storageId(QQSK::State state) const +{ + return m_id + (m_numberOfPropertiesInGroup * state); +} + +QT_END_NAMESPACE + +#include "moc_qqstylekitstorage_p.cpp" diff --git a/src/labs/stylekit/qqstylekitstorage_p.h b/src/labs/stylekit/qqstylekitstorage_p.h new file mode 100644 index 0000000000..9b49f2446c --- /dev/null +++ b/src/labs/stylekit/qqstylekitstorage_p.h @@ -0,0 +1,49 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +// Qt-Security score:significant reason:default + +#ifndef QQSTYLEKITSTORAGE_P_H +#define QQSTYLEKITSTORAGE_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtCore/QtCore> + +#include "qqstylekitglobal_p.h" + +QT_BEGIN_NAMESPACE + +using PropertyPathId_t = uint; +using PropertyStorageId = uint; +using QQStyleKitPropertyStorage = QMap<PropertyStorageId, QVariant>; + +class PropertyPathId { + Q_GADGET + +public: + PropertyPathId( + const QQSK::Property property = QQSK::Property::NoProperty, + PropertyPathId_t id = 0, uint numberOfPropertiesInGroup = 0); + + QQSK::Property property() const { return m_property; } + PropertyPathId_t pathId() const { return m_id; } + PropertyStorageId storageId(QQSK::State state) const; + +private: + QQSK::Property m_property; + PropertyPathId_t m_id; + uint m_numberOfPropertiesInGroup; +}; + +QT_END_NAMESPACE + +#endif // QQSTYLEKITSTORAGE_P_H diff --git a/src/labs/stylekit/qqstylekitstyle.cpp b/src/labs/stylekit/qqstylekitstyle.cpp new file mode 100644 index 0000000000..b0b3cedb6f --- /dev/null +++ b/src/labs/stylekit/qqstylekitstyle.cpp @@ -0,0 +1,304 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qqstylekit_p.h" +#include "qqstylekitstyle_p.h" +#include "qqstylekittheme_p.h" +#include "qqstylekitcustomtheme_p.h" +#include "qqstylekitcontrolproperties_p.h" +#include "qqstylekitpropertyresolver_p.h" + +#include <QtQuickTemplates2/private/qquickdeferredexecute_p_p.h> +#include <QtQml/private/qqmllist_p.h> + +#include <QtGui/QGuiApplication> +#include <QtGui/QStyleHints> + +QT_BEGIN_NAMESPACE + +static const QString kSystem = "System"_L1; +static const QString kLight = "Light"_L1; +static const QString kDark = "Dark"_L1; + +QQStyleKitStyle::QQStyleKitStyle(QObject *parent) + : QQStyleKitControls(parent) + , m_themeName(kSystem) +{ +} + +QQStyleKitStyle::~QQStyleKitStyle() +{ + if (m_theme) + m_theme->deleteLater(); +} + +QQuickPalette *QQStyleKitStyle::palette() +{ + if (!m_palette) { + static QQuickPalette placeholder; + return &placeholder; + } + return m_palette.get(); +} + +QQmlComponent *QQStyleKitStyle::light() const +{ + return m_light; +} + +QQStyleKitStyle *QQStyleKitStyle::fallbackStyle() const +{ + if (!m_fallbackStyle) { + auto *self = const_cast<QQStyleKitStyle *>(this); + self->executeFallbackStyle(); + } + return m_fallbackStyle; +} + +void QQStyleKitStyle::setFallbackStyle(QQStyleKitStyle *fallbackStyle) +{ + if (m_fallbackStyle == fallbackStyle) + return; + + m_fallbackStyle = fallbackStyle; + emit fallbackStyleChanged(); +} + +void QQStyleKitStyle::setLight(QQmlComponent *lightTheme) +{ + if (m_light == lightTheme) + return; + + m_light = lightTheme; + + emit lightChanged(); +} + +QQmlComponent *QQStyleKitStyle::dark() const +{ + return m_dark; +} + +void QQStyleKitStyle::setDark(QQmlComponent *darkTheme) +{ + if (m_dark == darkTheme) + return; + + m_dark = darkTheme; + + emit darkChanged(); +} + +QQStyleKitTheme *QQStyleKitStyle::theme() const +{ + return m_theme; +} + +QList<QObject *> QQStyleKitStyle::customThemesAsList() +{ + QList<QObject *> list; + for (auto *customTheme : customThemes()) + list.append(customTheme); + return list; +} + +QList<QQStyleKitCustomTheme *> QQStyleKitStyle::customThemes() const +{ + QList<QQStyleKitCustomTheme *> list; + for (auto *obj : children()) { + if (auto *customTheme = qobject_cast<QQStyleKitCustomTheme *>(obj)) + list.append(customTheme); + } + return list; +} + +void QQStyleKitStyle::parseThemes() +{ + m_themeNames = QStringList({kSystem, kLight, kDark}); + + for (auto *customTheme : customThemes()) { + const QString name = customTheme->name(); + if (name.isEmpty()) + continue; + m_themeNames << name; + m_customThemeNames << name; + } + + emit themeNamesChanged(); + emit customThemeNamesChanged(); +} + +QString QQStyleKitStyle::themeName() const +{ + return m_themeName; +} + +QStringList QQStyleKitStyle::themeNames() const +{ + return m_themeNames; +} + +QStringList QQStyleKitStyle::customThemeNames() const +{ + return m_customThemeNames; +} + +void QQStyleKitStyle::setThemeName(const QString &themeName) +{ + if (m_themeName == themeName) + return; + + m_themeName = themeName; + recreateTheme(); + + emit themeNameChanged(); +} + +void QQStyleKitStyle::recreateTheme() +{ + QString effectiveThemeName; + QQmlComponent *effectiveThemeComponent = nullptr; + + if (QString::compare(m_themeName, kSystem, Qt::CaseInsensitive) == 0) { + const auto scheme = QGuiApplication::styleHints()->colorScheme(); + if (scheme == Qt::ColorScheme::Light) { + effectiveThemeName = kLight; + effectiveThemeComponent = m_light; + } + else if (scheme == Qt::ColorScheme::Dark) { + effectiveThemeName =kDark; + effectiveThemeComponent = m_dark; + } + } else if (QString::compare(m_themeName, kLight, Qt::CaseInsensitive) == 0) { + effectiveThemeName = kLight; + effectiveThemeComponent = m_light; + } else if (QString::compare(m_themeName,kDark, Qt::CaseInsensitive) == 0) { + effectiveThemeName =kDark; + effectiveThemeComponent = m_dark; + } else if (!m_themeName.isEmpty()){ + for (auto *customTheme : customThemes()) { + if (QString::compare(m_themeName, customTheme->name(), Qt::CaseInsensitive) == 0) { + effectiveThemeName = customTheme->name(); + effectiveThemeComponent = customTheme->theme(); + break; + } + } + if (effectiveThemeName.isEmpty()) + qmlWarning(this) << "No theme found with name:" << m_themeName; + else if (!effectiveThemeComponent) + qmlWarning(this) << "Custom theme '" << effectiveThemeName << "' has no theme component set"; + } + + if (m_effectiveThemeName == effectiveThemeName) { + // Switching theme name from e.g "System" to "Light" might not + // actually change the currently effective theme. + emit themeNameChanged(); + return; + } + + if (m_theme) { + m_theme->deleteLater(); + m_theme = nullptr; + } + + m_currentThemeComponent = effectiveThemeComponent; + + if (effectiveThemeComponent) { + if (effectiveThemeComponent->status() != QQmlComponent::Ready) { + qmlWarning(this) << "failed to create theme '" << effectiveThemeName << "': " << effectiveThemeComponent->errorString(); + } else { + /* The 'createThemeInsideStyle' JS function is a work-around since we haven't found + * a way to instantiate a Theme inside the context of a Style from c++. Doing so is + * needed in order to allow custom style properties to be added as children of a + * Style, and at the same time, be able to access them from within a Theme. For + * this to work, the Style also needs to set 'pragma ComponentBehavior: Bound'. */ + QVariant themeAsVariant; + QMetaObject::invokeMethod(this, "createThemeInsideStyle", Qt::DirectConnection, + qReturnArg(themeAsVariant), QVariant::fromValue(effectiveThemeComponent)); + m_theme = qvariant_cast<QQStyleKitTheme *>(themeAsVariant); + + if (!m_theme || !effectiveThemeComponent->errorString().isEmpty()) { + qmlWarning(this) << "failed to create theme '" << effectiveThemeName << "': " << effectiveThemeComponent->errorString(); + } else { + m_theme->setParent(this); + } + } + } + + if (!m_theme) { + // We always require a theme, even if it's empty + m_theme = new QQStyleKitTheme(this); + m_theme->setObjectName("<empty theme>"_L1); + m_theme->m_completed = true; + } else { + m_theme->setParent(this); + } + + if (this == current()) { + m_theme->updateQuickTheme(); + QQStyleKitReader::resetAll(); + } + + emit themeChanged(); +} + +QQStyleKitStyle* QQStyleKitStyle::current() +{ + return QQStyleKit::qmlAttachedProperties()->style(); +} + +bool QQStyleKitStyle::loaded() const +{ + /* Before both the style and theme has completed loading + * we return false. This can be used to avoid unnecessary + * property reads when we anyway have to do a full update + * in the end. */ + if (!m_completed) + return false; + if (!m_theme || !m_theme->m_completed) + return false; + + return true; +} + +void QQStyleKitStyle::executeFallbackStyle(bool complete) +{ + if (m_fallbackStyle.wasExecuted()) + return; + + const QString name = "fallbackStyle"_L1; + if (!m_fallbackStyle || complete) + quickBeginDeferred(this, name, m_fallbackStyle); + if (complete) + quickCompleteDeferred(this, name, m_fallbackStyle); +} + +void QQStyleKitStyle::setPalette(QQuickPalette *palette) +{ + if (m_palette && palette && m_palette->toQPalette() == palette->toQPalette()) + return; + + m_palette = palette; + + QScopedValueRollback rollback(m_isUpdatingPalette, true); + emit paletteChanged(); +} + +void QQStyleKitStyle::componentComplete() +{ + QQStyleKitControls::componentComplete(); + + /* It's important to set m_completed before creating the theme, otherwise + * styleAndThemeFinishedLoading() will still be false, which will e.g cause + * property reads to return early from QQStyleKitPropertyResolver */ + m_completed = true; + + executeFallbackStyle(true); + parseThemes(); + recreateTheme(); +} + +QT_END_NAMESPACE + +#include "moc_qqstylekitstyle_p.cpp" + diff --git a/src/labs/stylekit/qqstylekitstyle_p.h b/src/labs/stylekit/qqstylekitstyle_p.h new file mode 100644 index 0000000000..b7deae4aff --- /dev/null +++ b/src/labs/stylekit/qqstylekitstyle_p.h @@ -0,0 +1,125 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +// Qt-Security score:significant reason:default + +#ifndef QQSTYLEKITSTYLE_P_H +#define QQSTYLEKITSTYLE_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qqstylekitreader_p.h" +#include "qqstylekitcontrols_p.h" +#include "qqstylekitcustomtheme_p.h" +#include "qqstylekitdebug_p.h" + +#include <QtQml/QtQml> +#include <QtQuickTemplates2/private/qquickdeferredpointer_p_p.h> + +QT_BEGIN_NAMESPACE + +class QQStyleKitTheme; +class QQStyleKitPropertyResolver; + +class QQStyleKitStyle : public QQStyleKitControls +{ + Q_OBJECT + Q_PROPERTY(QQuickPalette *palette READ palette NOTIFY paletteChanged FINAL) + Q_PROPERTY(QQStyleKitStyle *fallbackStyle READ fallbackStyle WRITE setFallbackStyle NOTIFY fallbackStyleChanged FINAL) + Q_PROPERTY(QQmlComponent *light READ light WRITE setLight NOTIFY lightChanged FINAL) + Q_PROPERTY(QQmlComponent *dark READ dark WRITE setDark NOTIFY darkChanged FINAL) + Q_PROPERTY(QString themeName READ themeName WRITE setThemeName NOTIFY themeNameChanged FINAL) + Q_PROPERTY(QStringList themeNames READ themeNames NOTIFY themeNamesChanged FINAL) + Q_PROPERTY(QStringList customThemeNames READ customThemeNames NOTIFY customThemeNamesChanged FINAL) + Q_PROPERTY(QQStyleKitTheme *theme READ theme NOTIFY themeChanged FINAL) + + Q_CLASSINFO("DeferredPropertyNames", "fallbackStyle") + QML_NAMED_ELEMENT(BaseStyle) + +public: + enum Contstants { + Stretch = -1, + }; + Q_ENUM(Contstants) + + QQStyleKitStyle(QObject *parent = nullptr); + ~QQStyleKitStyle(); + + QQuickPalette *palette(); + + QQStyleKitStyle *fallbackStyle() const; + void setFallbackStyle(QQStyleKitStyle *fallbackStyle); + + QQmlComponent *light() const; + void setLight(QQmlComponent *lightTheme); + + QQmlComponent *dark() const; + void setDark(QQmlComponent *darkTheme); + + QList<QQStyleKitCustomTheme *> customThemes() const; + QStringList themeNames() const; + QStringList customThemeNames() const; + + void setThemeName(const QString &themeName); + QString themeName() const; + QQStyleKitTheme *theme() const; + + bool loaded() const; + + static QQStyleKitStyle *current(); + + // For now, used by qqcontrolstowidgetstyle + Q_INVOKABLE QList<QObject *> customThemesAsList(); + +signals: + void paletteChanged(); + void fallbackStyleChanged(); + void lightChanged(); + void darkChanged(); + void themeChanged(); + void themeNameChanged(); + void themeNamesChanged(); + void customThemeNamesChanged(); + +protected: + void componentComplete() override; + +private: + void parseThemes(); + void recreateTheme(); + void executeFallbackStyle(bool complete = false); + void setPalette(QQuickPalette *palette); + +private: + Q_DISABLE_COPY(QQStyleKitStyle) + + bool m_completed = false; + bool m_isUpdatingPalette = false; + + QQuickDeferredPointer<QQStyleKitStyle> m_fallbackStyle; + QPointer<QQmlComponent> m_light; + QPointer<QQmlComponent> m_dark; + QPointer<QQStyleKitTheme> m_theme; + QPointer<QQmlComponent> m_currentThemeComponent; + QPointer<QQuickPalette> m_palette; + QString m_themeName; + QString m_effectiveThemeName; + QStringList m_themeNames; + QStringList m_customThemeNames; + + friend class QQStyleKitAttached; + friend class QQStyleKitPropertyGroup; + friend class QQStyleKitPropertyResolver; +}; + +QT_END_NAMESPACE + +#endif // QQSTYLEKITSTYLE_P_H diff --git a/src/labs/stylekit/qqstylekittheme.cpp b/src/labs/stylekit/qqstylekittheme.cpp new file mode 100644 index 0000000000..08cec449c9 --- /dev/null +++ b/src/labs/stylekit/qqstylekittheme.cpp @@ -0,0 +1,123 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include <QtQuickTemplates2/private/qquicktheme_p.h> +#include <QtQuick/private/qquickpalette_p.h> +#include <QtGui/private/qguiapplication_p.h> +#include <QtGui/qpa/qplatformtheme.h> + +#include "qqstylekittheme_p.h" +#include "qqstylekitstyle_p.h" + +QT_BEGIN_NAMESPACE + +QQStyleKitTheme::QQStyleKitTheme(QObject *parent) + : QQStyleKitControls(parent) +{ +} + +QQStyleKitStyle *QQStyleKitTheme::style() const +{ + QObject *parentObj = parent(); + if (!parentObj) + return nullptr; + Q_ASSERT(qobject_cast<QQStyleKitStyle *>(parentObj)); + return static_cast<QQStyleKitStyle *>(parentObj); +} + +QQStyleKitPalette *QQStyleKitTheme::palettes() +{ + return &m_palettes; +} + +// Copied from QQuickTheme +static QPlatformTheme::Palette toPlatformThemePalette(QQuickTheme::Scope scope) +{ + switch (scope) { + case QQuickTheme::Button: return QPlatformTheme::ButtonPalette; + case QQuickTheme::CheckBox: return QPlatformTheme::CheckBoxPalette; + case QQuickTheme::ComboBox: return QPlatformTheme::ComboBoxPalette; + case QQuickTheme::GroupBox: return QPlatformTheme::GroupBoxPalette; + case QQuickTheme::ItemView: return QPlatformTheme::ItemViewPalette; + case QQuickTheme::Label: return QPlatformTheme::LabelPalette; + case QQuickTheme::ListView: return QPlatformTheme::ItemViewPalette; + case QQuickTheme::Menu: return QPlatformTheme::MenuPalette; + case QQuickTheme::MenuBar: return QPlatformTheme::MenuBarPalette; + case QQuickTheme::RadioButton: return QPlatformTheme::RadioButtonPalette; + case QQuickTheme::SpinBox: return QPlatformTheme::TextLineEditPalette; + case QQuickTheme::Switch: return QPlatformTheme::CheckBoxPalette; + case QQuickTheme::TabBar: return QPlatformTheme::TabBarPalette; + case QQuickTheme::TextArea: return QPlatformTheme::TextEditPalette; + case QQuickTheme::TextField: return QPlatformTheme::TextLineEditPalette; + case QQuickTheme::ToolBar: return QPlatformTheme::ToolButtonPalette; + case QQuickTheme::ToolTip: return QPlatformTheme::ToolTipPalette; + case QQuickTheme::Tumbler: return QPlatformTheme::ItemViewPalette; + default: return QPlatformTheme::SystemPalette; + } +} + +void QQStyleKitTheme::updateThemePalette() +{ + auto *theme = QQuickTheme::instance(); + if (!theme) + return; + + // QQuickTheme currently offers only one default platform palette for all controls. + // This implementation will instead inherit the corresponding platform palette for each + // control type. Hence, for now, we don't use QQuickTheme::usePlatformPalette, but roll + // our own version instead. + theme->setUsePlatformPalette(false); + + const auto *platformTheme = QGuiApplicationPrivate::platformTheme(); + +#define SET_PALETTE(CONTROL, SCOPE) { \ + const QQuickPalette *controlPalette = m_palettes.CONTROL(); \ + const QPalette *platformPalette = platformTheme->palette(toPlatformThemePalette(SCOPE)); \ + if (controlPalette && platformPalette) { \ + QPalette resolved = controlPalette->toQPalette().resolve(*platformPalette); \ + theme->setPalette(SCOPE, resolved); \ + } else if (platformPalette) { \ + theme->setPalette(SCOPE, *platformPalette); \ + } else if (controlPalette) { \ + theme->setPalette(SCOPE, controlPalette->toQPalette()); \ + } \ +} + + SET_PALETTE(system, QQuickTheme::System); + SET_PALETTE(button, QQuickTheme::Button); + SET_PALETTE(checkBox, QQuickTheme::CheckBox); + SET_PALETTE(comboBox, QQuickTheme::ComboBox); + SET_PALETTE(groupBox, QQuickTheme::GroupBox); + SET_PALETTE(itemView, QQuickTheme::ItemView); + SET_PALETTE(label, QQuickTheme::Label); + SET_PALETTE(listView, QQuickTheme::ListView); + SET_PALETTE(menu, QQuickTheme::Menu); + SET_PALETTE(menuBar, QQuickTheme::MenuBar); + SET_PALETTE(radioButton, QQuickTheme::RadioButton); + SET_PALETTE(spinBox, QQuickTheme::SpinBox); + SET_PALETTE(switchControl, QQuickTheme::Switch); + SET_PALETTE(tabBar, QQuickTheme::TabBar); + SET_PALETTE(textArea, QQuickTheme::TextArea); + SET_PALETTE(textField, QQuickTheme::TextField); + SET_PALETTE(toolBar, QQuickTheme::ToolBar); + SET_PALETTE(toolTip, QQuickTheme::ToolTip); + SET_PALETTE(tumbler, QQuickTheme::Tumbler); + + QEvent event(QEvent::ApplicationPaletteChange); + QGuiApplication::sendEvent(qGuiApp, &event); +} + +void QQStyleKitTheme::updateQuickTheme() +{ + updateThemePalette(); +} + +void QQStyleKitTheme::componentComplete() +{ + QQStyleKitControls::componentComplete(); + m_completed = true; +} + +QT_END_NAMESPACE + +#include "moc_qqstylekittheme_p.cpp" diff --git a/src/labs/stylekit/qqstylekittheme_p.h b/src/labs/stylekit/qqstylekittheme_p.h new file mode 100644 index 0000000000..8096e9935b --- /dev/null +++ b/src/labs/stylekit/qqstylekittheme_p.h @@ -0,0 +1,65 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +// Qt-Security score:significant reason:default + +#ifndef QQSTYLEKITTHEME_P_H +#define QQSTYLEKITTHEME_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtQml/QtQml> + +#include "qqstylekitcontrols_p.h" +#include "qqstylekitpalette_p.h" + +QT_BEGIN_NAMESPACE + +class QQStyleKitControls; +class QQStyleKitPropertyResolver; + +class QQStyleKitTheme : public QQStyleKitControls +{ + Q_OBJECT + + Q_PROPERTY(QQStyleKitPalette *palettes READ palettes NOTIFY palettesChanged FINAL) + + QML_NAMED_ELEMENT(Theme) + +public: + QQStyleKitTheme(QObject *parent = nullptr); + + QQStyleKitStyle *style() const; + QQStyleKitPalette *palettes(); + +signals: + void targetChanged(); + void palettesChanged(); + +protected: + void componentComplete() override; + +private: + Q_DISABLE_COPY(QQStyleKitTheme) + + bool m_completed = false; + QQStyleKitPalette m_palettes; + + void updateThemePalette(); + void updateQuickTheme(); + + friend class QQStyleKitAttached; + friend class QQStyleKitStyle; +}; + +QT_END_NAMESPACE + +#endif // QQSTYLEKITTHEME_P_H diff --git a/src/labs/stylekit/qqstylekitthemeproperties.cpp b/src/labs/stylekit/qqstylekitthemeproperties.cpp new file mode 100644 index 0000000000..7d342c563b --- /dev/null +++ b/src/labs/stylekit/qqstylekitthemeproperties.cpp @@ -0,0 +1,22 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qqstylekitthemeproperties_p.h" + +QT_BEGIN_NAMESPACE + +QQStyleKitThemeProperties::QQStyleKitThemeProperties(QObject *parent) + : QQStyleKitControls(parent) +{ +} + +QQStyleKitFont *QQStyleKitThemeProperties::fonts() +{ + return &m_fonts; +} + +QT_END_NAMESPACE + +#include "moc_qqstylekitthemeproperties_p.cpp" + + diff --git a/src/labs/stylekit/qqstylekitthemeproperties_p.h b/src/labs/stylekit/qqstylekitthemeproperties_p.h new file mode 100644 index 0000000000..5d8985441b --- /dev/null +++ b/src/labs/stylekit/qqstylekitthemeproperties_p.h @@ -0,0 +1,49 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +// Qt-Security score:significant reason:default + +#ifndef QQSTYLEKITTHEMEPROPERTIES_P_H +#define QQSTYLEKITTHEMEPROPERTIES_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtQml/QtQml> +#include "qqstylekitcontrols_p.h" +#include "qqstylekitfont_p.h" + +QT_BEGIN_NAMESPACE + +class QQStyleKitThemeProperties : public QQStyleKitControls +{ + Q_OBJECT + Q_PROPERTY(QQStyleKitFont *fonts READ fonts NOTIFY fontsChanged FINAL) + //TODO: Move palettes property here as well + QML_UNCREATABLE("This component is abstract, and cannot be instantiated") + QML_NAMED_ELEMENT(ThemeProperties) + +public: + QQStyleKitThemeProperties(QObject *parent = nullptr); + + QQStyleKitFont *fonts(); + +signals: + void fontsChanged(); + +private: + Q_DISABLE_COPY(QQStyleKitThemeProperties) + + QQStyleKitFont m_fonts; +}; + +QT_END_NAMESPACE + +#endif // QQSTYLEKITTHEMEPROPERTIES_P_H diff --git a/src/labs/stylekit/qqstylekitvariation.cpp b/src/labs/stylekit/qqstylekitvariation.cpp new file mode 100644 index 0000000000..4c224c2fb7 --- /dev/null +++ b/src/labs/stylekit/qqstylekitvariation.cpp @@ -0,0 +1,15 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qqstylekitvariation_p.h" + +QT_BEGIN_NAMESPACE + +QQStyleKitVariation::QQStyleKitVariation(QObject *parent) + : QQStyleKitControls(parent) +{ +} + +QT_END_NAMESPACE + +#include "moc_qqstylekitvariation_p.cpp" diff --git a/src/labs/stylekit/qqstylekitvariation_p.h b/src/labs/stylekit/qqstylekitvariation_p.h new file mode 100644 index 0000000000..07fe440b84 --- /dev/null +++ b/src/labs/stylekit/qqstylekitvariation_p.h @@ -0,0 +1,38 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QQSTYLEKITVARIATION_P_H +#define QQSTYLEKITVARIATION_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtQml/QtQml> + +#include "qqstylekitcontrols_p.h" + +QT_BEGIN_NAMESPACE + +class QQStyleKitVariation : public QQStyleKitControls +{ + Q_OBJECT + QML_NAMED_ELEMENT(Variation) + +public: + QQStyleKitVariation(QObject *parent = nullptr); + +private: + Q_DISABLE_COPY(QQStyleKitVariation) +}; + +QT_END_NAMESPACE + +#endif // QQSTYLEKITVARIATION_P_H diff --git a/src/quickcontrols/configure.cmake b/src/quickcontrols/configure.cmake index 62da4f2fb7..75e1e440c1 100644 --- a/src/quickcontrols/configure.cmake +++ b/src/quickcontrols/configure.cmake @@ -50,6 +50,12 @@ qt_feature("quickcontrols2-fluentwinui3" PRIVATE PURPOSE "Provides a style based on the Fluent design and Windows UI 3 style." CONDITION QT_FEATURE_quickcontrols2_fusion ) +qt_feature("quickcontrols2-stylekit" PRIVATE + SECTION "Quick Controls 2" + LABEL "StyleKit" + PURPOSE "Provides a styling framework." + CONDITION QT_FEATURE_quickcontrols2_basic +) qt_feature("quickcontrols2-macos" PRIVATE SECTION "Quick Controls 2" LABEL "macOS" @@ -74,4 +80,5 @@ qt_configure_add_summary_entry( ARGS "quickcontrols2-basic quickcontrols2-fusion quickcontrols2-fluentwinui3 quickcontrols2-imagine quickcontrols2-ios quickcontrols2-material quickcontrols2-universal quickcontrols2-macos quickcontrols2-windows" MESSAGE "Styles" ) +qt_configure_add_summary_entry(ARGS "quickcontrols2-stylekit") qt_configure_end_summary_section() # end of "Qt Quick Controls 2" section |
