aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorHenning Gruendl <henning.gruendl@qt.io>2022-05-08 23:43:46 +0200
committerHenning Gründl <henning.gruendl@qt.io>2022-05-13 07:09:42 +0000
commite703ee97d6c9949fac3928655e7ea42b4f5c30b9 (patch)
tree6b822f830ca28f6e21769157e92d83532608ae27
parentd027c5855b3c2733e87a063b3d2971b4124dfbf1 (diff)
QmlDesigner: Add FilterComboBox
* Add FilterComboBox and SortFilterModel * Use FilterComboBox in UrlChooser * Add group attribute to UrlChooser model in order to sort according to groups (default items) and alphabetically * Add escape to cancel modification after editing text * Fix accepted and activated signal endpoints Task-number: QDS-6397 Change-Id: I8fd1371d01d86fbbf5fc74ca9f20677d4ea49587 Reviewed-by: <github-actions-qt-creator@cristianadam.eu> Reviewed-by: Thomas Hartmann <thomas.hartmann@qt.io>
-rw-r--r--share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/HelperWidgets/UrlChooser.qml137
-rw-r--r--share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioControls/FilterComboBox.qml754
-rw-r--r--share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioControls/SortFilterModel.qml86
-rw-r--r--share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioControls/qmldir2
4 files changed, 917 insertions, 62 deletions
diff --git a/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/HelperWidgets/UrlChooser.qml b/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/HelperWidgets/UrlChooser.qml
index dd4a11d9cf..ce2584c4be 100644
--- a/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/HelperWidgets/UrlChooser.qml
+++ b/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/HelperWidgets/UrlChooser.qml
@@ -32,7 +32,7 @@ import StudioTheme 1.0 as StudioTheme
import QtQuickDesignerTheme 1.0
Row {
- id: urlChooser
+ id: root
property variant backendValue
property color textColor: colorLogic.highlight ? colorLogic.textColor
@@ -47,22 +47,24 @@ Row {
FileResourcesModel {
id: fileModel
modelNodeBackendProperty: modelNodeBackend
- filter: urlChooser.filter
+ filter: root.filter
}
ColorLogic {
id: colorLogic
- backendValue: urlChooser.backendValue
+ backendValue: root.backendValue
}
- StudioControls.ComboBox {
+ StudioControls.FilterComboBox {
id: comboBox
- property ListModel items: ListModel {}
+ property ListModel listModel: ListModel {}
implicitWidth: StudioTheme.Values.singleControlColumnWidth
+ StudioTheme.Values.actionIndicatorWidth
width: implicitWidth
+ allowUserInput: true
+
// Note: highlightedIndex property isn't used because it has no setter and it doesn't reset
// when the combobox is closed by focusing on some other control.
property int hoverIndex: -1
@@ -70,7 +72,7 @@ Row {
ToolTip {
id: toolTip
visible: comboBox.hover && toolTip.text !== ""
- text: urlChooser.backendValue.valueToString
+ text: root.backendValue.valueToString
delay: StudioTheme.Values.toolTipDelay
height: StudioTheme.Values.toolTipHeight
background: Rectangle {
@@ -88,27 +90,39 @@ Row {
delegate: ItemDelegate {
required property string fullPath
required property string name
+ required property int group
required property int index
- id: delegateItem
- width: parent.width
+ id: delegateRoot
+ width: comboBox.popup.width - comboBox.popup.leftPadding - comboBox.popup.rightPadding
+ - (comboBox.popupScrollBar.visible ? comboBox.popupScrollBar.contentItem.implicitWidth + 2
+ : 0) // TODO Magic number
height: StudioTheme.Values.height - 2 * StudioTheme.Values.border
padding: 0
- highlighted: comboBox.highlightedIndex === index
+ hoverEnabled: true
+ highlighted: comboBox.highlightedIndex === delegateRoot.DelegateModel.itemsIndex
+
+ onHoveredChanged: {
+ if (delegateRoot.hovered && !comboBox.popupMouseArea.active)
+ comboBox.setHighlightedIndexItems(delegateRoot.DelegateModel.itemsIndex)
+ }
+
+ onClicked: comboBox.selectItem(delegateRoot.DelegateModel.itemsIndex)
indicator: Item {
id: itemDelegateIconArea
- width: delegateItem.height
- height: delegateItem.height
+ width: delegateRoot.height
+ height: delegateRoot.height
Label {
id: itemDelegateIcon
text: StudioTheme.Constants.tickIcon
- color: delegateItem.highlighted ? StudioTheme.Values.themeTextSelectedTextColor
+ color: delegateRoot.highlighted ? StudioTheme.Values.themeTextSelectedTextColor
: StudioTheme.Values.themeTextColor
font.family: StudioTheme.Constants.iconFont.family
font.pixelSize: StudioTheme.Values.spinControlIconSizeMulti
- visible: comboBox.currentIndex === index ? true : false
+ visible: comboBox.currentIndex === delegateRoot.DelegateModel.itemsIndex ? true
+ : false
anchors.fill: parent
renderType: Text.NativeRendering
horizontalAlignment: Text.AlignHCenter
@@ -119,7 +133,7 @@ Row {
contentItem: Text {
leftPadding: itemDelegateIconArea.width
text: name
- color: delegateItem.highlighted ? StudioTheme.Values.themeTextSelectedTextColor
+ color: delegateRoot.highlighted ? StudioTheme.Values.themeTextSelectedTextColor
: StudioTheme.Values.themeTextColor
font: comboBox.font
elide: Text.ElideRight
@@ -127,17 +141,17 @@ Row {
}
background: Rectangle {
- id: itemDelegateBackground
x: 0
y: 0
- width: delegateItem.width
- height: delegateItem.height
- color: delegateItem.highlighted ? StudioTheme.Values.themeInteraction : "transparent"
+ width: delegateRoot.width
+ height: delegateRoot.height
+ color: delegateRoot.highlighted ? StudioTheme.Values.themeInteraction
+ : "transparent"
}
ToolTip {
id: itemToolTip
- visible: delegateItem.hovered && comboBox.highlightedIndex === index
+ visible: delegateRoot.hovered && comboBox.highlightedIndex === index
text: fullPath
delay: StudioTheme.Values.toolTipDelay
height: StudioTheme.Values.toolTipHeight
@@ -161,7 +175,7 @@ Row {
ExtendedFunctionLogic {
id: extFuncLogic
- backendValue: urlChooser.backendValue
+ backendValue: root.backendValue
onReseted: comboBox.editText = ""
}
@@ -181,20 +195,15 @@ Row {
// Takes into account applied bindings
property string textValue: {
- if (urlChooser.backendValue.isBound)
- return urlChooser.backendValue.expression
+ if (root.backendValue.isBound)
+ return root.backendValue.expression
- var fullPath = urlChooser.backendValue.valueToString
+ var fullPath = root.backendValue.valueToString
return fullPath.substr(fullPath.lastIndexOf('/') + 1)
}
onTextValueChanged: comboBox.setCurrentText(comboBox.textValue)
- editable: true
- textRole: "name"
- valueRole: "fullPath"
- model: comboBox.items
-
onModelChanged: {
if (!comboBox.isComplete)
return
@@ -206,20 +215,14 @@ Row {
if (!comboBox.isComplete)
return
- var inputValue = comboBox.editText
+ let inputValue = comboBox.editText
// Check if value set by user matches with a name in the model then pick the full path
- var index = comboBox.find(inputValue)
+ let index = comboBox.find(inputValue)
if (index !== -1)
- inputValue = comboBox.items.get(index).fullPath
+ inputValue = comboBox.items.get(index).model.fullPath
- // Get the currently assigned backend value, extract its file name and compare it to the
- // input value. If they differ the new value needs to be set.
- var currentValue = urlChooser.backendValue.value
- var fileName = currentValue.substr(currentValue.lastIndexOf('/') + 1);
-
- if (fileName !== inputValue)
- urlChooser.backendValue.value = inputValue
+ root.backendValue.value = inputValue
comboBox.dirty = false
}
@@ -234,14 +237,16 @@ Row {
}
function handleActivate(index) {
- if (urlChooser.backendValue === undefined || !comboBox.isComplete)
+ if (root.backendValue === undefined || !comboBox.isComplete)
return
- if (index === -1) // select first item if index is invalid
- index = 0
+ let inputValue = comboBox.editText
+
+ if (index >= 0)
+ inputValue = comboBox.items.get(index).model.fullPath
- if (urlChooser.backendValue.value !== comboBox.items.get(index).fullPath)
- urlChooser.backendValue.value = comboBox.items.get(index).fullPath
+ if (root.backendValue.value !== inputValue)
+ root.backendValue.value = inputValue
comboBox.dirty = false
}
@@ -250,7 +255,7 @@ Row {
// Hack to style the text input
for (var i = 0; i < comboBox.children.length; i++) {
if (comboBox.children[i].text !== undefined) {
- comboBox.children[i].color = urlChooser.textColor
+ comboBox.children[i].color = root.textColor
comboBox.children[i].anchors.rightMargin = 34
}
}
@@ -261,36 +266,44 @@ Row {
function createModel() {
// Build the combobox model
- comboBox.items.clear()
-
- if (urlChooser.defaultItems !== undefined) {
- for (var i = 0; i < urlChooser.defaultItems.length; ++i) {
- comboBox.items.append({
- fullPath: urlChooser.defaultItems[i],
- name: urlChooser.defaultItems[i]
+ comboBox.listModel.clear()
+ // While adding items to the model this binding needs to be interrupted, otherwise the
+ // update function of the SortFilterModel is triggered every time on append() which makes
+ // QtDS very slow. This will happen when selecting different items in the scene.
+ comboBox.model = {}
+
+ if (root.defaultItems !== undefined) {
+ for (var i = 0; i < root.defaultItems.length; ++i) {
+ comboBox.listModel.append({
+ fullPath: root.defaultItems[i],
+ name: root.defaultItems[i],
+ group: 0
})
}
}
for (var j = 0; j < fileModel.fullPathModel.length; ++j) {
- comboBox.items.append({
+ comboBox.listModel.append({
fullPath: fileModel.fullPathModel[j],
- name: fileModel.fileNameModel[j]
+ name: fileModel.fileNameModel[j],
+ group: 1
})
}
+
+ comboBox.model = Qt.binding(function() { return comboBox.listModel })
}
Connections {
target: fileModel
function onFullPathModelChanged() {
- urlChooser.createModel()
+ root.createModel()
comboBox.setCurrentText(comboBox.textValue)
}
}
- onDefaultItemsChanged: urlChooser.createModel()
+ onDefaultItemsChanged: root.createModel()
- Component.onCompleted: urlChooser.createModel()
+ Component.onCompleted: root.createModel()
function indexOf(model, criteria) {
for (var i = 0; i < model.count; ++i) {
@@ -305,16 +318,16 @@ Row {
function onStateChanged(state) {
// update currentIndex when the popup opens to override the default behavior in super classes
// that selects currentIndex based on values in the combo box.
- if (comboBox.popup.opened && !urlChooser.backendValue.isBound) {
- var index = urlChooser.indexOf(comboBox.items,
+ if (comboBox.popup.opened && !root.backendValue.isBound) {
+ var index = root.indexOf(comboBox.items,
function(item) {
- return item.fullPath === urlChooser.backendValue.value
+ return item.fullPath === root.backendValue.value
})
if (index !== -1) {
comboBox.currentIndex = index
comboBox.hoverIndex = index
- comboBox.editText = comboBox.items.get(index).name
+ comboBox.editText = comboBox.items.get(index).model.name
}
}
}
@@ -324,11 +337,11 @@ Row {
IconIndicator {
icon: StudioTheme.Constants.addFile
- iconColor: urlChooser.textColor
+ iconColor: root.textColor
onClicked: {
fileModel.openFileDialog()
if (fileModel.fileName !== "")
- urlChooser.backendValue.value = fileModel.fileName
+ root.backendValue.value = fileModel.fileName
}
}
}
diff --git a/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioControls/FilterComboBox.qml b/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioControls/FilterComboBox.qml
new file mode 100644
index 0000000000..bdd3cb5aef
--- /dev/null
+++ b/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioControls/FilterComboBox.qml
@@ -0,0 +1,754 @@
+/****************************************************************************
+**
+** Copyright (C) 2022 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of Qt Quick 3D.
+**
+** $QT_BEGIN_LICENSE:GPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 or (at your option) any later version
+** approved by the KDE Free Qt Foundation. The licenses are as published by
+** the Free Software Foundation and appearing in the file LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+import QtQuick
+import QtQuick.Templates as T
+import StudioTheme 1.0 as StudioTheme
+
+Item {
+ id: root
+
+ enum Interaction { None, TextEdit, Key }
+
+ property int currentInteraction: FilterComboBox.Interaction.None
+
+ property alias model: sortFilterModel.model
+ property alias items: sortFilterModel.items
+ property alias delegate: sortFilterModel.delegate
+
+ property alias font: textInput.font
+
+ // This indicates if the value was committed or the user is still editing
+ property bool editing: false
+
+ // This is the actual filter that is applied on the model
+ property string filter: ""
+ property bool filterActive: root.filter !== ""
+
+ // Accept arbitrary input or only items from the model
+ property bool allowUserInput: false
+
+ property alias editText: textInput.text
+ property int highlightedIndex: -1 // items index
+ property int currentIndex: -1 // items index
+
+ property string autocompleteString: ""
+
+ property bool __isCompleted: false
+
+ property alias actionIndicator: actionIndicator
+
+ // This property is used to indicate the global hover state
+ property bool hover: actionIndicator.hover || textInput.hover || checkIndicator.hover
+ property alias edit: textInput.edit
+ property alias open: popup.visible
+
+ property alias actionIndicatorVisible: actionIndicator.visible
+ property real __actionIndicatorWidth: StudioTheme.Values.actionIndicatorWidth
+ property real __actionIndicatorHeight: StudioTheme.Values.actionIndicatorHeight
+
+ property bool dirty: false // user modification flag
+
+ property bool escapePressed: false
+
+ signal accepted()
+ signal activated(int index)
+ signal compressedActivated(int index, int reason)
+
+ enum ActivatedReason { EditingFinished, Other }
+
+ property alias popup: popup
+ property alias popupScrollBar: popupScrollBar
+ property alias popupMouseArea: popupMouseArea
+
+ width: StudioTheme.Values.defaultControlWidth
+ height: StudioTheme.Values.defaultControlHeight
+ implicitHeight: StudioTheme.Values.defaultControlHeight
+
+ function selectItem(itemsIndex) {
+ textInput.text = sortFilterModel.items.get(itemsIndex).model.name
+ root.currentIndex = itemsIndex
+ root.finishEditing()
+ root.activated(itemsIndex)
+ }
+
+ function submitValue() {
+ if (!root.allowUserInput) {
+ // If input isn't according to any item in the model, don't finish editing
+ if (root.highlightedIndex === -1)
+ return
+
+ root.selectItem(root.highlightedIndex)
+ } else {
+ root.currentIndex = -1
+
+ // Only trigger the signal, if the value was modified
+ if (root.dirty) {
+ myTimer.stop()
+ root.dirty = false
+ root.editText = root.editText.trim()
+ //root.compressedActivated(root.find(root.editText),
+ // ComboBox.ActivatedReason.EditingFinished)
+ }
+
+ root.finishEditing()
+ root.accepted()
+ }
+ }
+
+ function finishEditing() {
+ root.editing = false
+ root.filter = ""
+ root.autocompleteString = ""
+ textInput.focus = false // Remove focus from text field
+ popup.close()
+ }
+
+ function increaseVisibleIndex() {
+ let numItems = sortFilterModel.visibleGroup.count
+ if (!numItems)
+ return
+
+ if (root.highlightedIndex === -1) // Nothing is selected
+ root.setHighlightedIndexVisible(0)
+ else {
+ let currentVisibleIndex = sortFilterModel.items.get(root.highlightedIndex).visibleIndex
+ ++currentVisibleIndex
+
+ if (currentVisibleIndex > numItems - 1)
+ currentVisibleIndex = 0
+
+ root.setHighlightedIndexVisible(currentVisibleIndex)
+ }
+ }
+
+ function decreaseVisibleIndex() {
+ let numItems = sortFilterModel.visibleGroup.count
+ if (!numItems)
+ return
+
+ if (root.highlightedIndex === -1) // Nothing is selected
+ root.setHighlightedIndexVisible(numItems - 1)
+ else {
+ let currentVisibleIndex = sortFilterModel.items.get(root.highlightedIndex).visibleIndex
+ --currentVisibleIndex
+
+ if (currentVisibleIndex < 0)
+ currentVisibleIndex = numItems - 1
+
+ root.setHighlightedIndexVisible(currentVisibleIndex)
+ }
+ }
+
+ function updateHighlightedIndex() {
+ // Check if current index is still part of the filtered list, if not set it to 0
+ if (root.highlightedIndex !== -1 && !sortFilterModel.items.get(root.highlightedIndex).inVisible) {
+ root.setHighlightedIndexVisible(0)
+ } else {
+ // Needs to be set in order for ListView to keep its currenIndex up to date, so
+ // scroll position gets updated according to the higlighted item
+ root.setHighlightedIndexItems(root.highlightedIndex)
+ }
+ }
+
+ function setHighlightedIndexItems(itemsIndex) { // items group index
+ root.highlightedIndex = itemsIndex
+
+ if (itemsIndex === -1)
+ listView.currentIndex = -1
+ else
+ listView.currentIndex = sortFilterModel.items.get(itemsIndex).visibleIndex
+ }
+
+ function setHighlightedIndexVisible(visibleIndex) { // visible group index
+ if (visibleIndex === -1)
+ root.highlightedIndex = -1
+ else
+ root.highlightedIndex = sortFilterModel.visibleGroup.get(visibleIndex).itemsIndex
+
+ listView.currentIndex = visibleIndex
+ }
+
+ function updateAutocomplete() {
+ if (root.highlightedIndex === -1)
+ root.autocompleteString = ""
+ else {
+ let suggestion = sortFilterModel.items.get(root.highlightedIndex).model.name
+ root.autocompleteString = suggestion.substring(textInput.text.length)
+ }
+ }
+
+ // TODO is this already case insensitiv?!
+ function find(text) {
+ for (let i = 0; i < sortFilterModel.items.count; ++i)
+ if (sortFilterModel.items.get(i).model.name === text)
+ return i
+
+ return -1
+ }
+
+ Timer {
+ id: myTimer
+ property int activatedIndex
+ repeat: false
+ running: false
+ interval: 100
+ onTriggered: root.compressedActivated(myTimer.activatedIndex,
+ ComboBox.ActivatedReason.Other)
+ }
+
+ onActivated: function(index) {
+ myTimer.activatedIndex = index
+ myTimer.restart()
+ }
+
+ onHighlightedIndexChanged: {
+ if (root.editing || (root.editText === "" && root.allowUserInput))
+ root.updateAutocomplete()
+ }
+
+ DelegateModel {
+ id: noMatchesModel
+
+ model: ListModel {
+ ListElement { name: "No matches" }
+ }
+
+ delegate: ItemDelegate {
+ id: noMatchesDelegate
+ width: popup.width - popup.leftPadding - popup.rightPadding
+ - (popupScrollBar.visible ? popupScrollBar.contentItem.implicitWidth + 2
+ : 0) // TODO Magic number
+ height: StudioTheme.Values.height - 2 * StudioTheme.Values.border
+ padding: 0
+
+ contentItem: Text {
+ leftPadding: StudioTheme.Values.inputHorizontalPadding
+ text: name
+ font.italic: true
+ color: StudioTheme.Values.themeTextColor
+ elide: Text.ElideRight
+ verticalAlignment: Text.AlignVCenter
+ }
+
+ background: Rectangle {
+ x: 0
+ y: 0
+ width: noMatchesDelegate.width
+ height: noMatchesDelegate.height
+ color: "transparent"
+ }
+ }
+ }
+
+ SortFilterModel {
+ id: sortFilterModel
+
+ filterAcceptsItem: function(item) {
+ return item.name.toLowerCase().startsWith(root.filter.toLowerCase())
+ }
+
+ lessThan: function(left, right) {
+ if (left.group === right.group) {
+ return left.name.toLowerCase().localeCompare(right.name.toLowerCase())
+ }
+
+ return left.group - right.group
+ }
+
+ delegate: ItemDelegate {
+ id: delegateRoot
+ width: popup.width - popup.leftPadding - popup.rightPadding
+ - (popupScrollBar.visible ? popupScrollBar.contentItem.implicitWidth + 2
+ : 0) // TODO Magic number
+ height: StudioTheme.Values.height - 2 * StudioTheme.Values.border
+ padding: 0
+ hoverEnabled: true
+ highlighted: root.highlightedIndex === delegateRoot.DelegateModel.itemsIndex
+
+ onHoveredChanged: {
+ if (delegateRoot.hovered && !popupMouseArea.active)
+ root.setHighlightedIndexItems(delegateRoot.DelegateModel.itemsIndex)
+ }
+
+ onClicked: root.selectItem(delegateRoot.DelegateModel.itemsIndex)
+
+ indicator: Item {
+ id: itemDelegateIconArea
+ width: delegateRoot.height
+ height: delegateRoot.height
+
+ T.Label {
+ id: itemDelegateIcon
+ text: StudioTheme.Constants.tickIcon
+ color: delegateRoot.highlighted ? StudioTheme.Values.themeTextSelectedTextColor
+ : StudioTheme.Values.themeTextColor
+ font.family: StudioTheme.Constants.iconFont.family
+ font.pixelSize: StudioTheme.Values.spinControlIconSizeMulti
+ visible: root.currentIndex === delegateRoot.DelegateModel.itemsIndex ? true
+ : false
+ anchors.fill: parent
+ renderType: Text.NativeRendering
+ horizontalAlignment: Text.AlignHCenter
+ verticalAlignment: Text.AlignVCenter
+ }
+ }
+
+ contentItem: Text {
+ leftPadding: itemDelegateIconArea.width
+ text: name
+ color: delegateRoot.highlighted ? StudioTheme.Values.themeTextSelectedTextColor
+ : StudioTheme.Values.themeTextColor
+ font: textInput.font
+ elide: Text.ElideRight
+ verticalAlignment: Text.AlignVCenter
+ }
+
+ background: Rectangle {
+ x: 0
+ y: 0
+ width: delegateRoot.width
+ height: delegateRoot.height
+ color: delegateRoot.highlighted ? StudioTheme.Values.themeInteraction
+ : "transparent"
+ }
+ }
+
+ onUpdated: {
+ if (!root.__isCompleted)
+ return
+
+ if (sortFilterModel.count === 0)
+ root.setHighlightedIndexVisible(-1)
+ else {
+ if (root.highlightedIndex === -1 && !root.allowUserInput)
+ root.setHighlightedIndexVisible(0)
+ }
+ }
+ }
+
+ Row {
+ ActionIndicator {
+ id: actionIndicator
+ myControl: root
+ x: 0
+ y: 0
+ width: actionIndicator.visible ? root.__actionIndicatorWidth : 0
+ height: actionIndicator.visible ? root.__actionIndicatorHeight : 0
+ }
+
+ TextInput {
+ id: textInput
+
+ property bool hover: textInputMouseArea.containsMouse && textInput.enabled
+ property bool edit: textInput.activeFocus
+ property string preFocusText: ""
+
+ x: 0
+ y: 0
+ z: 2
+ width: root.width - actionIndicator.width
+ height: root.height
+ leftPadding: StudioTheme.Values.inputHorizontalPadding
+ rightPadding: StudioTheme.Values.inputHorizontalPadding + checkIndicator.width
+ + StudioTheme.Values.border
+ horizontalAlignment: Qt.AlignLeft
+ verticalAlignment: Qt.AlignVCenter
+ color: StudioTheme.Values.themeTextColor
+ selectionColor: StudioTheme.Values.themeTextSelectionColor
+ selectedTextColor: StudioTheme.Values.themeTextSelectedTextColor
+ selectByMouse: true
+ clip: true
+
+ Rectangle {
+ id: textInputBackground
+ z: -1
+ width: textInput.width
+ height: textInput.height
+ color: StudioTheme.Values.themeControlBackground
+ border.color: StudioTheme.Values.themeControlOutline
+ border.width: StudioTheme.Values.border
+ }
+
+ MouseArea {
+ id: textInputMouseArea
+ anchors.fill: parent
+ enabled: true
+ hoverEnabled: true
+ propagateComposedEvents: true
+ acceptedButtons: Qt.LeftButton
+ cursorShape: Qt.PointingHandCursor
+ onPressed: function(mouse) {
+ textInput.forceActiveFocus()
+ mouse.accepted = false
+ }
+
+ // Stop scrollable views from scrolling while ComboBox is in edit mode and the mouse
+ // pointer is on top of it. We might add wheel selection in the future.
+ onWheel: function(wheel) {
+ wheel.accepted = root.edit
+ }
+ }
+
+ onEditingFinished: {
+ if (root.escapePressed) {
+ root.escapePressed = false
+ root.editText = textInput.preFocusText
+ } else {
+ if (root.currentInteraction === FilterComboBox.Interaction.TextEdit) {
+ if (root.dirty)
+ root.submitValue()
+ } else if (root.currentInteraction === FilterComboBox.Interaction.Key) {
+ root.selectItem(root.highlightedIndex)
+ }
+ }
+
+ sortFilterModel.update()
+ }
+
+ onTextEdited: {
+ root.currentInteraction = FilterComboBox.Interaction.TextEdit
+ root.editing = true
+ popupMouseArea.active = true
+ root.dirty = true
+
+ if (textInput.text !== "")
+ root.filter = textInput.text
+ else {
+ root.filter = ""
+ root.autocompleteString = ""
+ }
+
+ if (!popup.visible)
+ popup.open()
+
+ sortFilterModel.update()
+
+ if (!root.allowUserInput)
+ root.updateHighlightedIndex()
+ else
+ root.setHighlightedIndexVisible(-1)
+
+ root.updateAutocomplete()
+ }
+
+ onActiveFocusChanged: {
+ if (textInput.activeFocus) {
+ popup.open()
+ textInput.preFocusText = textInput.text
+ } else
+ popup.close()
+ }
+
+ states: [
+ State {
+ name: "default"
+ when: root.enabled && !textInput.edit && !root.hover && !root.open
+ PropertyChanges {
+ target: textInputBackground
+ color: StudioTheme.Values.themeControlBackground
+ }
+ PropertyChanges {
+ target: textInputMouseArea
+ cursorShape: Qt.PointingHandCursor
+ acceptedButtons: Qt.LeftButton
+ }
+ },
+ State {
+ name: "globalHover"
+ when: root.hover && !textInput.hover && !textInput.edit && !root.open
+ PropertyChanges {
+ target: textInputBackground
+ color: StudioTheme.Values.themeControlBackgroundGlobalHover
+ }
+ },
+ State {
+ name: "hover"
+ when: textInput.hover && root.hover && !textInput.edit
+ PropertyChanges {
+ target: textInputBackground
+ color: StudioTheme.Values.themeControlBackgroundHover
+ }
+ },
+ State {
+ name: "edit"
+ when: root.edit
+ PropertyChanges {
+ target: textInputBackground
+ color: StudioTheme.Values.themeControlBackgroundInteraction
+ border.color: StudioTheme.Values.themeControlOutlineInteraction
+ }
+ PropertyChanges {
+ target: textInputMouseArea
+ cursorShape: Qt.IBeamCursor
+ acceptedButtons: Qt.NoButton
+ }
+ },
+ State {
+ name: "disable"
+ when: !root.enabled
+ PropertyChanges {
+ target: textInputBackground
+ color: StudioTheme.Values.themeControlBackgroundDisabled
+ }
+ PropertyChanges {
+ target: textInput
+ color: StudioTheme.Values.themeTextColorDisabled
+ }
+ }
+ ]
+
+ Text {
+ visible: root.autocompleteString !== ""
+ text: root.autocompleteString
+ x: textInput.leftPadding + textMetrics.advanceWidth
+ y: (textInput.height - Math.ceil(textMetrics.height)) / 2
+ color: "gray" // TODO proper color value
+ font: textInput.font
+ renderType: textInput.renderType
+ }
+
+ TextMetrics {
+ id: textMetrics
+ font: textInput.font
+ text: textInput.text
+ }
+
+ Rectangle {
+ id: checkIndicator
+
+ property bool hover: checkIndicatorMouseArea.containsMouse && checkIndicator.enabled
+ property bool pressed: checkIndicatorMouseArea.containsPress
+ property bool checked: popup.visible
+
+ x: textInput.width - checkIndicator.width - StudioTheme.Values.border
+ y: StudioTheme.Values.border
+ width: StudioTheme.Values.height - StudioTheme.Values.border
+ height: textInput.height - (StudioTheme.Values.border * 2)
+ color: StudioTheme.Values.themeControlBackground
+ border.width: 0
+
+ MouseArea {
+ id: checkIndicatorMouseArea
+ anchors.fill: parent
+ hoverEnabled: true
+ onClicked: {
+ if (popup.visible)
+ popup.close()
+ else
+ popup.open()
+
+ if (!textInput.activeFocus) {
+ textInput.forceActiveFocus()
+ textInput.selectAll()
+ }
+ }
+ }
+
+ T.Label {
+ id: checkIndicatorIcon
+ anchors.fill: parent
+ color: StudioTheme.Values.themeTextColor
+ text: StudioTheme.Constants.upDownSquare2
+ horizontalAlignment: Text.AlignHCenter
+ verticalAlignment: Text.AlignVCenter
+ font.pixelSize: StudioTheme.Values.sliderControlSizeMulti
+ font.family: StudioTheme.Constants.iconFont.family
+ }
+
+ states: [
+ State {
+ name: "default"
+ when: root.enabled && checkIndicator.enabled && !root.edit
+ && !checkIndicator.hover && !root.hover
+ && !checkIndicator.checked
+ PropertyChanges {
+ target: checkIndicator
+ color: StudioTheme.Values.themeControlBackground
+ }
+ },
+ State {
+ name: "globalHover"
+ when: root.enabled && checkIndicator.enabled
+ && !checkIndicator.hover && root.hover && !root.edit
+ && !checkIndicator.checked
+ PropertyChanges {
+ target: checkIndicator
+ color: StudioTheme.Values.themeControlBackgroundGlobalHover
+ }
+ },
+ State {
+ name: "hover"
+ when: root.enabled && checkIndicator.enabled
+ && checkIndicator.hover && root.hover && !checkIndicator.pressed
+ && !checkIndicator.checked
+ PropertyChanges {
+ target: checkIndicator
+ color: StudioTheme.Values.themeControlBackgroundHover
+ }
+ },
+ State {
+ name: "check"
+ when: checkIndicator.checked
+ PropertyChanges {
+ target: checkIndicatorIcon
+ color: StudioTheme.Values.themeIconColor
+ }
+ PropertyChanges {
+ target: checkIndicator
+ color: StudioTheme.Values.themeInteraction
+ }
+ },
+ State {
+ name: "press"
+ when: root.enabled && checkIndicator.enabled
+ && checkIndicator.pressed
+ PropertyChanges {
+ target: checkIndicatorIcon
+ color: StudioTheme.Values.themeIconColor
+ }
+ PropertyChanges {
+ target: checkIndicator
+ color: StudioTheme.Values.themeInteraction
+ }
+ },
+ State {
+ name: "disable"
+ when: !root.enabled
+ PropertyChanges {
+ target: checkIndicator
+ color: StudioTheme.Values.themeControlBackgroundDisabled
+ }
+ PropertyChanges {
+ target: checkIndicatorIcon
+ color: StudioTheme.Values.themeTextColorDisabled
+ }
+ }
+ ]
+ }
+ }
+ }
+
+ T.Popup {
+ id: popup
+ x: textInput.x + StudioTheme.Values.border
+ y: textInput.height
+ width: textInput.width - (StudioTheme.Values.border * 2)
+ height: Math.min(popup.contentItem.implicitHeight + popup.topPadding + popup.bottomPadding,
+ root.Window.height - popup.topMargin - popup.bottomMargin,
+ StudioTheme.Values.maxComboBoxPopupHeight)
+ padding: StudioTheme.Values.border
+ margins: 0 // If not defined margin will be -1
+ closePolicy: T.Popup.NoAutoClose
+
+ contentItem: ListView {
+ id: listView
+ clip: true
+ implicitHeight: listView.contentHeight
+ highlightMoveVelocity: -1
+ boundsBehavior: Flickable.StopAtBounds
+ flickDeceleration: 10000
+
+ model: {
+ if (popup.visible)
+ return sortFilterModel.count ? sortFilterModel : noMatchesModel
+
+ return null
+ }
+
+ ScrollBar.vertical: ScrollBar {
+ id: popupScrollBar
+ visible: listView.height < listView.contentHeight
+ }
+ }
+
+ background: Rectangle {
+ color: StudioTheme.Values.themePopupBackground
+ border.width: 0
+ }
+
+ onOpened: {
+ // Reset the highlightedIndex of ListView as binding with condition didn't work
+ if (root.highlightedIndex !== -1)
+ root.setHighlightedIndexItems(root.highlightedIndex)
+ }
+
+ onAboutToShow: {
+ // Select first item in list
+ if (root.highlightedIndex === -1 && sortFilterModel.count && !root.allowUserInput)
+ root.setHighlightedIndexVisible(0)
+ }
+
+ MouseArea {
+ // This is MouseArea is intended to block the hovered property of an ItemDelegate
+ // when the ListView changes due to Key interaction.
+
+ id: popupMouseArea
+ property bool active: true
+
+ anchors.fill: parent
+ enabled: popup.visible && popupMouseArea.active
+ hoverEnabled: true
+ onPositionChanged: { popupMouseArea.active = false }
+ }
+ }
+
+ Keys.onDownPressed: {
+ if (!sortFilterModel.visibleGroup.count)
+ return
+
+ root.currentInteraction = FilterComboBox.Interaction.Key
+ root.increaseVisibleIndex()
+
+ popupMouseArea.active = true
+ }
+
+ Keys.onUpPressed: {
+ if (!sortFilterModel.visibleGroup.count)
+ return
+
+ root.currentInteraction = FilterComboBox.Interaction.Key
+ root.decreaseVisibleIndex()
+
+ popupMouseArea.active = true
+ }
+
+ Keys.onEscapePressed: {
+ root.escapePressed = true
+ root.finishEditing()
+ }
+
+ Component.onCompleted: {
+ let index = root.find(root.editText)
+ root.currentIndex = index
+ root.highlightedIndex = index // TODO might not be intended
+
+ root.__isCompleted = true
+ }
+}
diff --git a/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioControls/SortFilterModel.qml b/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioControls/SortFilterModel.qml
new file mode 100644
index 0000000000..e70b0093e3
--- /dev/null
+++ b/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioControls/SortFilterModel.qml
@@ -0,0 +1,86 @@
+/****************************************************************************
+**
+** Copyright (C) 2022 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of Qt Quick 3D.
+**
+** $QT_BEGIN_LICENSE:GPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 or (at your option) any later version
+** approved by the KDE Free Qt Foundation. The licenses are as published by
+** the Free Software Foundation and appearing in the file LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+import QtQuick
+import QtQml.Models
+
+DelegateModel {
+ id: delegateModel
+
+ property var visibleGroup: visibleItems
+
+ property var lessThan: function(left, right) { return true }
+ property var filterAcceptsItem: function(item) { return true }
+
+ signal updated()
+
+ function update() {
+ if (delegateModel.items.count > 0) {
+ delegateModel.items.setGroups(0, delegateModel.items.count, "items")
+ }
+
+ // Filter items
+ var visible = []
+ for (var i = 0; i < delegateModel.items.count; ++i) {
+ var item = delegateModel.items.get(i)
+ if (delegateModel.filterAcceptsItem(item.model)) {
+ visible.push(item)
+ }
+ }
+
+ // Sort the list of visible items
+ visible.sort(function(a, b) {
+ return delegateModel.lessThan(a.model, b.model);
+ });
+
+ // Add all items to the visible group
+ for (i = 0; i < visible.length; ++i) {
+ item = visible[i]
+ item.inVisible = true
+ if (item.visibleIndex !== i) {
+ visibleItems.move(item.visibleIndex, i, 1)
+ }
+ }
+
+ delegateModel.updated()
+ }
+
+ items.onChanged: delegateModel.update()
+ onLessThanChanged: delegateModel.update()
+ onFilterAcceptsItemChanged: delegateModel.update()
+
+ groups: DelegateModelGroup {
+ id: visibleItems
+
+ name: "visible"
+ includeByDefault: false
+ }
+
+ filterOnGroup: "visible"
+}
diff --git a/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioControls/qmldir b/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioControls/qmldir
index b5f8c7a4e3..25be4d0acf 100644
--- a/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioControls/qmldir
+++ b/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioControls/qmldir
@@ -8,6 +8,7 @@ CheckIndicator 1.0 CheckIndicator.qml
ComboBox 1.0 ComboBox.qml
ComboBoxInput 1.0 ComboBoxInput.qml
ContextMenu 1.0 ContextMenu.qml
+FilterComboBox 1.0 FilterComboBox.qml
InfinityLoopIndicator 1.0 InfinityLoopIndicator.qml
ItemDelegate 1.0 ItemDelegate.qml
LinkIndicator2D 1.0 LinkIndicator2D.qml
@@ -30,6 +31,7 @@ SectionLabel 1.0 SectionLabel.qml
SectionLayout 1.0 SectionLayout.qml
Slider 1.0 Slider.qml
SliderPopup 1.0 SliderPopup.qml
+SortFilterModel 1.0 SortFilterModel.qml
SpinBox 1.0 SpinBox.qml
SpinBoxIndicator 1.0 SpinBoxIndicator.qml
SpinBoxInput 1.0 SpinBoxInput.qml