Skip to content

Commit 6ee3b01

Browse files
authored
Merge pull request #2656 from alicevision/dev/choiceParamUI
UI: Redesign ChoiceParam UI component
2 parents 5710bf3 + ccc77c3 commit 6ee3b01

File tree

4 files changed

+213
-193
lines changed

4 files changed

+213
-193
lines changed
Lines changed: 115 additions & 139 deletions
Original file line numberDiff line numberDiff line change
@@ -1,177 +1,153 @@
11
import QtQuick
22
import QtQuick.Controls
3-
3+
import QtQuick.Layouts
44
import Utils 1.0
55

6+
import MaterialIcons
7+
68
/**
7-
* ComboBox with filter text area
8-
*
9-
* @param inputModel - model to filter
10-
* @param editingFinished - signal emitted when editing is finished
11-
* @alias filterText - text to filter the model
9+
* ComboBox with filtering capabilities and support for custom values (i.e: outside the source model).
1210
*/
1311

1412
ComboBox {
15-
id: combo
13+
id: root
14+
15+
// Model to populate the combobox.
16+
required property var sourceModel
17+
// Input value to use as the current combobox value.
18+
property var inputValue
19+
// The text to filter the combobox model when the choices are displayed.
20+
property alias filterText: filterTextArea.text
21+
// Whether the current input value is within the source model.
22+
readonly property bool validValue: sourceModel.includes(inputValue)
23+
24+
25+
QtObject {
26+
id: m
27+
readonly property int delegateModelCount: root.delegateModel.count
28+
29+
// Ensure the highlighted index is always within the range of delegates whenever the
30+
// combobox model changes, for combobox validation to always considers a valid item.
31+
onDelegateModelCountChanged: {
32+
if(delegateModelCount > 0 && root.highlightedIndex >= delegateModelCount) {
33+
while(root.highlightedIndex > 0 && root.highlightedIndex >= delegateModelCount) {
34+
// highlightIndex is read-only, this method has to be used to change it programmatically.
35+
root.decrementCurrentIndex();
36+
}
37+
}
38+
}
39+
}
1640

17-
property var inputModel
1841
signal editingFinished(var value)
1942

20-
property alias filterText: filterTextArea
21-
property bool validValue: true
22-
23-
enabled: root.editable
24-
model: {
25-
var filteredData = inputModel.filter(condition => {
26-
if (filterTextArea.text.length > 0) return condition.toString().toLowerCase().includes(filterTextArea.text.toLowerCase())
27-
return true
28-
})
29-
if (filteredData.length > 0) {
30-
filterTextArea.background.color = Qt.lighter(palette.base, 2)
31-
validValue = true
32-
33-
// order filtered data by relevance (results that start with the filter text come first)
34-
filteredData.sort((a, b) => {
35-
const nameA = a.toString().toLowerCase();
36-
const nameB = b.toString().toLowerCase();
37-
const filterText = filterTextArea.text.toLowerCase()
38-
if (nameA.startsWith(filterText) && !nameB.startsWith(filterText))
39-
return -1
40-
if (!nameA.startsWith(filterText) && nameB.startsWith(filterText))
41-
return 1
42-
return 0
43-
})
44-
} else {
45-
filterTextArea.background.color = Colors.red
46-
validValue = false
47-
}
43+
function clearFilter() {
44+
filterText = "";
45+
}
4846

49-
if (filteredData.length == 0 || filterTextArea.length == 0) {
50-
filteredData = inputModel
51-
}
47+
// Re-computing current index when source values are set.
48+
Component.onCompleted: _updateCurrentIndex()
49+
onInputValueChanged: _updateCurrentIndex()
50+
onModelChanged: _updateCurrentIndex()
5251

53-
return filteredData
52+
function _updateCurrentIndex() {
53+
currentIndex = find(inputValue);
5454
}
5555

56-
background: Rectangle {
57-
implicitHeight: root.implicitHeight
58-
color: {
59-
if (validValue) {
60-
return palette.mid
61-
} else {
62-
return Colors.red
63-
}
64-
}
65-
border.color: palette.base
66-
}
56+
displayText: inputValue
6757

68-
popup: Popup {
69-
width: combo.width
70-
implicitHeight: contentItem.implicitHeight
58+
model: {
59+
return sourceModel.filter(item => {
60+
return item.toString().toLowerCase().includes(filterText.toLowerCase());
61+
});
62+
}
7163

72-
onAboutToShow: {
73-
filterTextArea.forceActiveFocus()
64+
popup.onOpened: {
65+
filterTextArea.forceActiveFocus();
66+
}
7467

75-
if (mapToGlobal(popup.x, popup.y).y + root.implicitHeight * (model.length + 1) > _window.contentItem.height) {
76-
y = -((combo.height * (combo.model.length + 1) > _window.contentItem.height) ? _window.contentItem.height*2/3 : combo.height * (combo.model.length + 1))
77-
} else {
78-
y = 0
79-
}
68+
popup.onClosed: clearFilter()
69+
70+
onActivated: (index) => {
71+
const isValidEntry = model.length > 0;
72+
if (!isValidEntry) {
73+
return;
8074
}
75+
editingFinished(model[index]);
76+
}
8177

82-
contentItem: Item {
83-
anchors.fill: parent
84-
TextArea {
85-
id: filterTextArea
86-
leftPadding: 12
87-
anchors.left: parent.left
88-
anchors.right: parent.right
89-
anchors.top: parent.top
90-
91-
selectByMouse: true
92-
hoverEnabled: true
93-
wrapMode: TextEdit.WrapAnywhere
94-
placeholderText: "Filter"
95-
background: Rectangle {}
96-
97-
onEditingFinished: {
98-
combo.popup.close()
99-
combo.editingFinished(displayText)
78+
StateGroup {
79+
id: filterState
80+
// Override properties depending on filter text status.
81+
states: [
82+
State {
83+
name: "Invalid"
84+
when: m.delegateModelCount === 0
85+
PropertyChanges {
86+
target: filterTextArea
87+
color: Colors.orange
88+
// Prevent ComboBox validation when there are no entries in the model.
89+
Keys.forwardTo: []
10090
}
91+
}
92+
]
93+
}
10194

102-
Keys.onEnterPressed: {
103-
if (!validValue) {
104-
displayText = filterTextArea.text
105-
} else {
106-
displayText = currentText
107-
}
108-
editingFinished()
109-
}
95+
popup.contentItem: ColumnLayout {
96+
width: parent.width
97+
spacing: 0
11098

111-
Keys.onReturnPressed: {
112-
if (!validValue) {
113-
displayText = filterTextArea.text
114-
} else {
115-
displayText = currentText
116-
}
117-
editingFinished()
118-
}
99+
RowLayout {
100+
Layout.fillWidth: true
101+
spacing: 2
119102

120-
Keys.onUpPressed: {
121-
// if the current index is 0, the user wants to go to the last item
122-
if (combo.currentIndex == 0) {
123-
combo.currentIndex = combo.model.length - 1
124-
} else {
125-
combo.currentIndex--
103+
TextField {
104+
id: filterTextArea
105+
placeholderText: "Type to filter..."
106+
Layout.fillWidth: true
107+
leftPadding: 18
108+
Keys.forwardTo: [root]
109+
110+
background: Item {
111+
MaterialLabel {
112+
anchors.verticalCenter: parent.verticalCenter
113+
anchors.left: parent.left
114+
anchors.leftMargin: 2
115+
text: MaterialIcons.search
126116
}
127117
}
118+
}
128119

129-
Keys.onDownPressed: {
130-
// if the current index is the last one, the user wants to go to the first item
131-
if (combo.currentIndex == combo.model.length - 1) {
132-
combo.currentIndex = 0
133-
} else {
134-
combo.currentIndex++
135-
}
120+
MaterialToolButton {
121+
enabled: root.filterText !== ""
122+
text: MaterialIcons.add_task
123+
ToolTip.text: "Force custom value"
124+
onClicked: {
125+
editingFinished(root.filterText);
126+
root.popup.close();
136127
}
137128
}
138129
}
139130

140-
ListView {
141-
id: listView
142-
clip: true
143-
anchors.left: parent.left
144-
anchors.right: parent.right
145-
anchors.top: filterTextArea.bottom
146-
147-
implicitHeight: (combo.height * (combo.model.length + 1) > _window.contentItem.height) ? _window.contentItem.height*2/3 : contentHeight
148-
model: combo.popup.visible ? combo.delegateModel : null
149-
150-
ScrollBar.vertical: MScrollBar {}
131+
Rectangle {
132+
height: 1
133+
Layout.fillWidth: true
134+
color: Colors.sysPalette.mid
151135
}
152-
}
153136

154-
delegate: ItemDelegate {
155-
width: combo.width
156-
height: combo.height
137+
ScrollView {
138+
Layout.fillWidth: true
139+
Layout.fillHeight: true
140+
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
157141

158-
contentItem: Text {
159-
text: modelData
160-
color: palette.text
161-
}
142+
ListView {
143+
implicitHeight: contentHeight
144+
clip: true
162145

163-
highlighted: validValue ? combo.currentIndex === index : false
164-
165-
hoverEnabled: true
166-
}
167-
168-
onHighlightedIndexChanged: {
169-
if (highlightedIndex >= 0) {
170-
combo.currentIndex = highlightedIndex
146+
model: root.delegateModel
147+
highlightRangeMode: ListView.ApplyRange
148+
currentIndex: root.highlightedIndex
149+
ScrollBar.vertical: ScrollBar {}
150+
}
171151
}
172152
}
173-
174-
onCurrentTextChanged: {
175-
displayText = currentText
176-
}
177153
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import QtQuick
2+
import QtQuick.Controls
3+
import QtQuick.Layouts
4+
5+
import MaterialIcons
6+
import Controls
7+
8+
/**
9+
* A combobox-type control with a single current `value` and a list of possible `values`.
10+
* Provides filtering capabilities and support for custom values (i.e: `value` not in `values`).
11+
*/
12+
RowLayout {
13+
id: root
14+
15+
required property var value
16+
required property var values
17+
18+
signal editingFinished(var value)
19+
20+
FilterComboBox {
21+
id: comboBox
22+
23+
Layout.fillWidth: true
24+
sourceModel: root.values
25+
inputValue: root.value
26+
onEditingFinished: value => root.editingFinished(value)
27+
}
28+
29+
MaterialLabel {
30+
visible: !comboBox.validValue
31+
text: MaterialIcons.warning
32+
ToolTip.text: "Custom value detected"
33+
}
34+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import QtQuick
2+
import QtQuick.Controls
3+
import Controls
4+
5+
/**
6+
* A multi-checkboxes control with a current `value` (list of 0-N elements) and a list of possible `values`.
7+
* Provides support for custom values (`value` elements not in `values`).
8+
*/
9+
Flow {
10+
id: root
11+
12+
required property var value
13+
required property var values
14+
property color customValueColor: "orange"
15+
16+
signal toggled(var value, var checked)
17+
18+
// Predefined possible values.
19+
Repeater {
20+
model: root.values
21+
delegate: CheckBox {
22+
text: modelData
23+
checked: root.value.includes(modelData)
24+
onToggled: root.toggled(modelData, checked)
25+
}
26+
}
27+
28+
// Custom elements outside the predefined possible values.
29+
Repeater {
30+
model: root.value.filter(v => !root.values.includes(v))
31+
delegate: CheckBox {
32+
text: modelData
33+
palette.text: root.customValueColor
34+
font.italic: true
35+
checked: true
36+
ToolTip.text: "Custom value"
37+
ToolTip.visible: hovered
38+
onToggled: root.toggled(modelData, checked)
39+
}
40+
}
41+
}

0 commit comments

Comments
 (0)