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>
This commit is contained in:
Henning Gruendl
2022-05-08 23:43:46 +02:00
committed by Henning Gründl
parent d027c5855b
commit e703ee97d6
4 changed files with 916 additions and 61 deletions

View File

@@ -32,7 +32,7 @@ import StudioTheme 1.0 as StudioTheme
import QtQuickDesignerTheme 1.0 import QtQuickDesignerTheme 1.0
Row { Row {
id: urlChooser id: root
property variant backendValue property variant backendValue
property color textColor: colorLogic.highlight ? colorLogic.textColor property color textColor: colorLogic.highlight ? colorLogic.textColor
@@ -47,22 +47,24 @@ Row {
FileResourcesModel { FileResourcesModel {
id: fileModel id: fileModel
modelNodeBackendProperty: modelNodeBackend modelNodeBackendProperty: modelNodeBackend
filter: urlChooser.filter filter: root.filter
} }
ColorLogic { ColorLogic {
id: colorLogic id: colorLogic
backendValue: urlChooser.backendValue backendValue: root.backendValue
} }
StudioControls.ComboBox { StudioControls.FilterComboBox {
id: comboBox id: comboBox
property ListModel items: ListModel {} property ListModel listModel: ListModel {}
implicitWidth: StudioTheme.Values.singleControlColumnWidth implicitWidth: StudioTheme.Values.singleControlColumnWidth
+ StudioTheme.Values.actionIndicatorWidth + StudioTheme.Values.actionIndicatorWidth
width: implicitWidth width: implicitWidth
allowUserInput: true
// Note: highlightedIndex property isn't used because it has no setter and it doesn't reset // 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. // when the combobox is closed by focusing on some other control.
property int hoverIndex: -1 property int hoverIndex: -1
@@ -70,7 +72,7 @@ Row {
ToolTip { ToolTip {
id: toolTip id: toolTip
visible: comboBox.hover && toolTip.text !== "" visible: comboBox.hover && toolTip.text !== ""
text: urlChooser.backendValue.valueToString text: root.backendValue.valueToString
delay: StudioTheme.Values.toolTipDelay delay: StudioTheme.Values.toolTipDelay
height: StudioTheme.Values.toolTipHeight height: StudioTheme.Values.toolTipHeight
background: Rectangle { background: Rectangle {
@@ -88,27 +90,39 @@ Row {
delegate: ItemDelegate { delegate: ItemDelegate {
required property string fullPath required property string fullPath
required property string name required property string name
required property int group
required property int index required property int index
id: delegateItem id: delegateRoot
width: parent.width 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 height: StudioTheme.Values.height - 2 * StudioTheme.Values.border
padding: 0 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 { indicator: Item {
id: itemDelegateIconArea id: itemDelegateIconArea
width: delegateItem.height width: delegateRoot.height
height: delegateItem.height height: delegateRoot.height
Label { Label {
id: itemDelegateIcon id: itemDelegateIcon
text: StudioTheme.Constants.tickIcon text: StudioTheme.Constants.tickIcon
color: delegateItem.highlighted ? StudioTheme.Values.themeTextSelectedTextColor color: delegateRoot.highlighted ? StudioTheme.Values.themeTextSelectedTextColor
: StudioTheme.Values.themeTextColor : StudioTheme.Values.themeTextColor
font.family: StudioTheme.Constants.iconFont.family font.family: StudioTheme.Constants.iconFont.family
font.pixelSize: StudioTheme.Values.spinControlIconSizeMulti font.pixelSize: StudioTheme.Values.spinControlIconSizeMulti
visible: comboBox.currentIndex === index ? true : false visible: comboBox.currentIndex === delegateRoot.DelegateModel.itemsIndex ? true
: false
anchors.fill: parent anchors.fill: parent
renderType: Text.NativeRendering renderType: Text.NativeRendering
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
@@ -119,7 +133,7 @@ Row {
contentItem: Text { contentItem: Text {
leftPadding: itemDelegateIconArea.width leftPadding: itemDelegateIconArea.width
text: name text: name
color: delegateItem.highlighted ? StudioTheme.Values.themeTextSelectedTextColor color: delegateRoot.highlighted ? StudioTheme.Values.themeTextSelectedTextColor
: StudioTheme.Values.themeTextColor : StudioTheme.Values.themeTextColor
font: comboBox.font font: comboBox.font
elide: Text.ElideRight elide: Text.ElideRight
@@ -127,17 +141,17 @@ Row {
} }
background: Rectangle { background: Rectangle {
id: itemDelegateBackground
x: 0 x: 0
y: 0 y: 0
width: delegateItem.width width: delegateRoot.width
height: delegateItem.height height: delegateRoot.height
color: delegateItem.highlighted ? StudioTheme.Values.themeInteraction : "transparent" color: delegateRoot.highlighted ? StudioTheme.Values.themeInteraction
: "transparent"
} }
ToolTip { ToolTip {
id: itemToolTip id: itemToolTip
visible: delegateItem.hovered && comboBox.highlightedIndex === index visible: delegateRoot.hovered && comboBox.highlightedIndex === index
text: fullPath text: fullPath
delay: StudioTheme.Values.toolTipDelay delay: StudioTheme.Values.toolTipDelay
height: StudioTheme.Values.toolTipHeight height: StudioTheme.Values.toolTipHeight
@@ -161,7 +175,7 @@ Row {
ExtendedFunctionLogic { ExtendedFunctionLogic {
id: extFuncLogic id: extFuncLogic
backendValue: urlChooser.backendValue backendValue: root.backendValue
onReseted: comboBox.editText = "" onReseted: comboBox.editText = ""
} }
@@ -181,20 +195,15 @@ Row {
// Takes into account applied bindings // Takes into account applied bindings
property string textValue: { property string textValue: {
if (urlChooser.backendValue.isBound) if (root.backendValue.isBound)
return urlChooser.backendValue.expression return root.backendValue.expression
var fullPath = urlChooser.backendValue.valueToString var fullPath = root.backendValue.valueToString
return fullPath.substr(fullPath.lastIndexOf('/') + 1) return fullPath.substr(fullPath.lastIndexOf('/') + 1)
} }
onTextValueChanged: comboBox.setCurrentText(comboBox.textValue) onTextValueChanged: comboBox.setCurrentText(comboBox.textValue)
editable: true
textRole: "name"
valueRole: "fullPath"
model: comboBox.items
onModelChanged: { onModelChanged: {
if (!comboBox.isComplete) if (!comboBox.isComplete)
return return
@@ -206,20 +215,14 @@ Row {
if (!comboBox.isComplete) if (!comboBox.isComplete)
return 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 // 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) 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 root.backendValue.value = inputValue
// 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
comboBox.dirty = false comboBox.dirty = false
} }
@@ -234,14 +237,16 @@ Row {
} }
function handleActivate(index) { function handleActivate(index) {
if (urlChooser.backendValue === undefined || !comboBox.isComplete) if (root.backendValue === undefined || !comboBox.isComplete)
return return
if (index === -1) // select first item if index is invalid let inputValue = comboBox.editText
index = 0
if (urlChooser.backendValue.value !== comboBox.items.get(index).fullPath) if (index >= 0)
urlChooser.backendValue.value = comboBox.items.get(index).fullPath inputValue = comboBox.items.get(index).model.fullPath
if (root.backendValue.value !== inputValue)
root.backendValue.value = inputValue
comboBox.dirty = false comboBox.dirty = false
} }
@@ -250,7 +255,7 @@ Row {
// Hack to style the text input // Hack to style the text input
for (var i = 0; i < comboBox.children.length; i++) { for (var i = 0; i < comboBox.children.length; i++) {
if (comboBox.children[i].text !== undefined) { if (comboBox.children[i].text !== undefined) {
comboBox.children[i].color = urlChooser.textColor comboBox.children[i].color = root.textColor
comboBox.children[i].anchors.rightMargin = 34 comboBox.children[i].anchors.rightMargin = 34
} }
} }
@@ -261,36 +266,44 @@ Row {
function createModel() { function createModel() {
// Build the combobox model // Build the combobox model
comboBox.items.clear() 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 (urlChooser.defaultItems !== undefined) { if (root.defaultItems !== undefined) {
for (var i = 0; i < urlChooser.defaultItems.length; ++i) { for (var i = 0; i < root.defaultItems.length; ++i) {
comboBox.items.append({ comboBox.listModel.append({
fullPath: urlChooser.defaultItems[i], fullPath: root.defaultItems[i],
name: urlChooser.defaultItems[i] name: root.defaultItems[i],
group: 0
}) })
} }
} }
for (var j = 0; j < fileModel.fullPathModel.length; ++j) { for (var j = 0; j < fileModel.fullPathModel.length; ++j) {
comboBox.items.append({ comboBox.listModel.append({
fullPath: fileModel.fullPathModel[j], fullPath: fileModel.fullPathModel[j],
name: fileModel.fileNameModel[j] name: fileModel.fileNameModel[j],
group: 1
}) })
} }
comboBox.model = Qt.binding(function() { return comboBox.listModel })
} }
Connections { Connections {
target: fileModel target: fileModel
function onFullPathModelChanged() { function onFullPathModelChanged() {
urlChooser.createModel() root.createModel()
comboBox.setCurrentText(comboBox.textValue) comboBox.setCurrentText(comboBox.textValue)
} }
} }
onDefaultItemsChanged: urlChooser.createModel() onDefaultItemsChanged: root.createModel()
Component.onCompleted: urlChooser.createModel() Component.onCompleted: root.createModel()
function indexOf(model, criteria) { function indexOf(model, criteria) {
for (var i = 0; i < model.count; ++i) { for (var i = 0; i < model.count; ++i) {
@@ -305,16 +318,16 @@ Row {
function onStateChanged(state) { function onStateChanged(state) {
// update currentIndex when the popup opens to override the default behavior in super classes // update currentIndex when the popup opens to override the default behavior in super classes
// that selects currentIndex based on values in the combo box. // that selects currentIndex based on values in the combo box.
if (comboBox.popup.opened && !urlChooser.backendValue.isBound) { if (comboBox.popup.opened && !root.backendValue.isBound) {
var index = urlChooser.indexOf(comboBox.items, var index = root.indexOf(comboBox.items,
function(item) { function(item) {
return item.fullPath === urlChooser.backendValue.value return item.fullPath === root.backendValue.value
}) })
if (index !== -1) { if (index !== -1) {
comboBox.currentIndex = index comboBox.currentIndex = index
comboBox.hoverIndex = 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 { IconIndicator {
icon: StudioTheme.Constants.addFile icon: StudioTheme.Constants.addFile
iconColor: urlChooser.textColor iconColor: root.textColor
onClicked: { onClicked: {
fileModel.openFileDialog() fileModel.openFileDialog()
if (fileModel.fileName !== "") if (fileModel.fileName !== "")
urlChooser.backendValue.value = fileModel.fileName root.backendValue.value = fileModel.fileName
} }
} }
} }

View File

@@ -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
}
}

View File

@@ -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"
}

View File

@@ -8,6 +8,7 @@ CheckIndicator 1.0 CheckIndicator.qml
ComboBox 1.0 ComboBox.qml ComboBox 1.0 ComboBox.qml
ComboBoxInput 1.0 ComboBoxInput.qml ComboBoxInput 1.0 ComboBoxInput.qml
ContextMenu 1.0 ContextMenu.qml ContextMenu 1.0 ContextMenu.qml
FilterComboBox 1.0 FilterComboBox.qml
InfinityLoopIndicator 1.0 InfinityLoopIndicator.qml InfinityLoopIndicator 1.0 InfinityLoopIndicator.qml
ItemDelegate 1.0 ItemDelegate.qml ItemDelegate 1.0 ItemDelegate.qml
LinkIndicator2D 1.0 LinkIndicator2D.qml LinkIndicator2D 1.0 LinkIndicator2D.qml
@@ -30,6 +31,7 @@ SectionLabel 1.0 SectionLabel.qml
SectionLayout 1.0 SectionLayout.qml SectionLayout 1.0 SectionLayout.qml
Slider 1.0 Slider.qml Slider 1.0 Slider.qml
SliderPopup 1.0 SliderPopup.qml SliderPopup 1.0 SliderPopup.qml
SortFilterModel 1.0 SortFilterModel.qml
SpinBox 1.0 SpinBox.qml SpinBox 1.0 SpinBox.qml
SpinBoxIndicator 1.0 SpinBoxIndicator.qml SpinBoxIndicator 1.0 SpinBoxIndicator.qml
SpinBoxInput 1.0 SpinBoxInput.qml SpinBoxInput 1.0 SpinBoxInput.qml