diff options
author | Henning Gruendl <henning.gruendl@qt.io> | 2022-05-08 23:43:46 +0200 |
---|---|---|
committer | Henning Gründl <henning.gruendl@qt.io> | 2022-05-13 07:09:42 +0000 |
commit | e703ee97d6c9949fac3928655e7ea42b4f5c30b9 (patch) | |
tree | 6b822f830ca28f6e21769157e92d83532608ae27 | |
parent | d027c5855b3c2733e87a063b3d2971b4124dfbf1 (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>
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 |