QmlDesigner: Add a property search feature in the property view

- add the PropertySearchBar control responsible for all property search logic
- add the PropertySearchBar control to the PropertyEditorPane
- add properties that support searching for items in the hierarchy
- add properties/states that support changing visibility
- add resetView function call in property editor view

Task-number: QDS-14709
Fixes: QDS-14806
Fixes: QDS-14807
Fixes: QDS-14808
Change-Id: I2e2477c95e592c7e49e13c25f0bb204de6bdb38d
Reviewed-by: Miikka Heikkinen <miikka.heikkinen@qt.io>
Reviewed-by: Mahmoud Badri <mahmoud.badri@qt.io>
This commit is contained in:
Rafal Andrusieczko
2025-02-13 17:00:46 +01:00
parent 247c8cef5d
commit 9fb0dc74ff
10 changed files with 221 additions and 2 deletions

View File

@@ -186,6 +186,8 @@ Section {
spacing: 1 spacing: 1
Section { Section {
readonly property bool __isInEffectsSection: true // used by property search logic
sectionHeight: 37 sectionHeight: 37
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
@@ -232,6 +234,8 @@ Section {
} }
Section { Section {
readonly property bool __isInEffectsSection: true // used by property search logic
sectionHeight: 37 sectionHeight: 37
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
@@ -305,6 +309,8 @@ Section {
Section { Section {
id: delegate id: delegate
readonly property bool __isInEffectsSection: true // used by property search logic
property QtObject wrapper: modelNodeBackend.registerSubSelectionWrapper(modelData) property QtObject wrapper: modelNodeBackend.registerSubSelectionWrapper(modelData)
property bool wasExpanded: false property bool wasExpanded: false

View File

@@ -90,9 +90,17 @@ PropertyEditorPane {
StudioControls.TabButton { StudioControls.TabButton {
text: backendValues.__classNamePrivateInternal.value text: backendValues.__classNamePrivateInternal.value
onClicked: () => {
if (itemPane.searchBar.hasDoneSearch)
itemPane.searchBar.search();
}
} }
StudioControls.TabButton { StudioControls.TabButton {
text: qsTr("Layout") text: qsTr("Layout")
onClicked: () => {
if (itemPane.searchBar.hasDoneSearch)
itemPane.searchBar.search();
}
} }
} }

View File

@@ -13,6 +13,11 @@ Rectangle {
height: 400 height: 400
color: StudioTheme.Values.themePanelBackground color: StudioTheme.Values.themePanelBackground
// Called from C++ to clear the search when the selected node changes
function clearSearch() {
// The function is empty, because it is a placeholder to match other panes
}
ColumnLayout { ColumnLayout {
id: mainColumn id: mainColumn
anchors.fill: parent anchors.fill: parent

View File

@@ -507,6 +507,8 @@ Section {
} }
PropertyLabel { PropertyLabel {
readonly property bool __inDynamicPropertiesSection: true
text: propertyName text: propertyName
tooltip: propertyType tooltip: propertyType
Layout.alignment: Qt.AlignTop Layout.alignment: Qt.AlignTop

View File

@@ -18,6 +18,7 @@ Rectangle {
default property alias content: mainColumn.children default property alias content: mainColumn.children
property alias scrollView: mainScrollView property alias scrollView: mainScrollView
property alias searchBar: propertySearchBar
property bool headerDocked: false property bool headerDocked: false
readonly property Item headerItem: headerDocked ? dockedHeaderLoader.item : undockedHeaderLoader.item readonly property Item headerItem: headerDocked ? dockedHeaderLoader.item : undockedHeaderLoader.item
@@ -29,10 +30,23 @@ Rectangle {
Controller.closeContextMenu() Controller.closeContextMenu()
} }
// Called from C++ to clear the search when the selected node changes
function clearSearch() {
propertySearchBar.clear();
}
PropertySearchBar {
id: propertySearchBar
contentItem: mainColumn
width: parent.width
z: parent.z + 1
}
Loader { Loader {
id: dockedHeaderLoader id: dockedHeaderLoader
anchors.top: itemPane.top anchors.top: propertySearchBar.bottom
z: parent.z + 1 z: parent.z + 1
height: item ? item.implicitHeight : 0 height: item ? item.implicitHeight : 0
width: parent.width width: parent.width
@@ -123,6 +137,16 @@ Rectangle {
HeaderBackground{} HeaderBackground{}
} }
Label {
Layout.fillWidth: true
Layout.leftMargin: 10
visible: propertySearchBar.hasDoneSearch && !propertySearchBar.hasMatchSearch
text: qsTr("No match found.")
color: StudioTheme.Values.themeTextColor
font.pixelSize: StudioTheme.Values.baseFont
}
Column { Column {
id: mainColumn id: mainColumn

View File

@@ -9,10 +9,13 @@ import StudioTheme 1.0 as StudioTheme
T.Label { T.Label {
id: label id: label
readonly property bool __isPropertyLabel: true // used by property search logic
property alias tooltip: toolTipArea.tooltip property alias tooltip: toolTipArea.tooltip
property bool blockedByContext: false property bool blockedByContext: false
property bool blockedByTemplate: false // MCU property bool blockedByTemplate: false // MCU
property bool searchNoMatch: false
width: StudioTheme.Values.propertyLabelWidth width: StudioTheme.Values.propertyLabelWidth
color: StudioTheme.Values.themeTextColor color: StudioTheme.Values.themeTextColor
@@ -34,6 +37,14 @@ T.Label {
} }
states: [ states: [
State {
name: "searchNoMatch"
when: searchNoMatch
PropertyChanges {
target: label
visible: false
}
},
State { State {
name: "disabled" name: "disabled"
when: !label.enabled && !(label.blockedByContext || label.blockedByTemplate) when: !label.enabled && !(label.blockedByContext || label.blockedByTemplate)

View File

@@ -0,0 +1,135 @@
// Copyright (C) 2025 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
import QtQuick
import StudioControls as StudioControls
import StudioTheme as StudioTheme
Rectangle {
id: root
readonly property alias hasMatchSearch: internal.matched
readonly property alias hasDoneSearch: internal.searched
property Item contentItem: null
color: StudioTheme.Values.themeToolbarBackground
height: StudioTheme.Values.toolbarHeight
function clear() {
internal.clear();
searchBox.text = "";
internal.timer.stop();
}
function search() {
internal.search();
}
StudioControls.SearchBox {
id: searchBox
anchors.fill: parent
anchors.bottomMargin: StudioTheme.Values.toolbarVerticalMargin
anchors.topMargin: StudioTheme.Values.toolbarVerticalMargin
anchors.leftMargin: StudioTheme.Values.toolbarHorizontalMargin
anchors.rightMargin: StudioTheme.Values.toolbarHorizontalMargin
style: StudioTheme.Values.searchControlStyle
onSearchChanged: internal.timer.restart()
}
QtObject {
id: internal
readonly property var reverts: []
readonly property Timer timer: Timer {
interval: 300
repeat: false
onTriggered: internal.search()
}
property bool matched: false
property bool searched: false
function clear() {
internal.reverts.forEach(revert => revert());
internal.reverts.length = 0;
internal.matched = false;
internal.searched = false;
}
function search() {
internal.clear();
const searchText = searchBox.text.toLowerCase();
if (searchText.length > 0) {
internal.traverse(root.contentItem, searchText);
internal.searched = true;
}
}
function disableSearchNoMatchAction(item) {
item.searchNoMatch = true;
internal.reverts.push(() => {
item.searchNoMatch = false;
});
}
function disableVisibleAction(item) {
item.visible = false;
internal.reverts.push(() => {
item.visible = true;
});
}
function enableSearchHideAction(item) {
item.searchHide = true;
internal.reverts.push(() => {
item.searchHide = false;
});
}
function expandSectionAction(item) {
internal.matched = true;
const prevValue = item.expanded;
item.expanded = true;
internal.reverts.push(() => {
item.expanded = prevValue;
});
}
function traverse(item, searchText) {
let hideSection = true;
let hideParentSection = true;
item.children.forEach((child, index, arr) => {
if (!child.visible)
return;
if (child.__isPropertyLabel) {
const propertyLabel = child;
const text = propertyLabel.text.toLowerCase();
if (!text.includes(searchText)) {
internal.disableSearchNoMatchAction(propertyLabel);
const action = propertyLabel.__inDynamicPropertiesSection ? internal.disableVisibleAction
: internal.disableSearchNoMatchAction;
const nextItem = arr[index + 1];
action(nextItem);
} else {
hideSection = false;
}
}
hideSection &= internal.traverse(child, searchText);
if (child.__isSection) {
const action = hideSection ? internal.enableSearchHideAction
: internal.expandSectionAction;
action(child);
if (child.__isInEffectsSection && !hideSection)
hideParentSection = false;
hideSection = true;
}
});
return hideParentSection && hideSection;
}
}
}

View File

@@ -5,6 +5,21 @@ import QtQuick 2.15
import QtQuick.Layouts 1.15 import QtQuick.Layouts 1.15
RowLayout { RowLayout {
id: root
property bool searchNoMatch: false
Layout.fillWidth: true Layout.fillWidth: true
spacing: 0 spacing: 0
states: [
State {
name: "searchNoMatch"
when: searchNoMatch
PropertyChanges {
target: root
visible: false
}
}
]
} }

View File

@@ -10,6 +10,8 @@ import StudioTheme as StudioTheme
Item { Item {
id: section id: section
readonly property bool __isSection: true // used by property search logic
property string caption: "Title" property string caption: "Title"
property color labelColor: StudioTheme.Values.themeTextColor property color labelColor: StudioTheme.Values.themeTextColor
property int labelCapitalization: Font.AllUppercase property int labelCapitalization: Font.AllUppercase
@@ -58,6 +60,7 @@ Item {
property bool dropEnabled: false property bool dropEnabled: false
property bool highlight: false property bool highlight: false
property bool eyeEnabled: true // eye button enabled (on) property bool eyeEnabled: true // eye button enabled (on)
property bool searchHide: false
property bool useDefaulContextMenu: true property bool useDefaulContextMenu: true
@@ -343,6 +346,14 @@ Item {
} }
states: [ states: [
State {
name: "Hide"
when: section.searchHide
PropertyChanges {
target: section
visible: false
}
},
State { State {
name: "Collapsed" name: "Collapsed"
when: !section.expanded when: !section.expanded

View File

@@ -446,8 +446,10 @@ void PropertyEditorView::resetView()
setupQmlBackend(); setupQmlBackend();
if (m_qmlBackEndForCurrentType) if (m_qmlBackEndForCurrentType) {
m_qmlBackEndForCurrentType->emitSelectionChanged(); m_qmlBackEndForCurrentType->emitSelectionChanged();
QMetaObject::invokeMethod(m_qmlBackEndForCurrentType->widget()->rootObject(), "clearSearch");
}
m_locked = false; m_locked = false;