forked from qt-creator/qt-creator
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:
@@ -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
|
||||||
|
|
||||||
|
@@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
|
||||||
|
@@ -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)
|
||||||
|
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
@@ -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
|
||||||
|
@@ -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;
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user