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
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 (urlChooser.backendValue.value !== comboBox.items.get(index).fullPath)
urlChooser.backendValue.value = comboBox.items.get(index).fullPath
if (index >= 0)
inputValue = comboBox.items.get(index).model.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()
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) {
for (var i = 0; i < urlChooser.defaultItems.length; ++i) {
comboBox.items.append({
fullPath: urlChooser.defaultItems[i],
name: urlChooser.defaultItems[i]
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
}
}
}

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