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
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
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
|
||||
|
Reference in New Issue
Block a user