forked from qt-creator/qt-creator
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:
committed by
Henning Gründl
parent
d027c5855b
commit
e703ee97d6
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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
|
||||||
|
}
|
||||||
|
}
|
@@ -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"
|
||||||
|
}
|
@@ -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
|
||||||
|
Reference in New Issue
Block a user