Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UI: Redesign exclusive ChoiceParam UI component #2656

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
237 changes: 97 additions & 140 deletions meshroom/ui/qml/Controls/FilterComboBox.qml
Original file line number Diff line number Diff line change
@@ -1,177 +1,134 @@
import QtQuick
import QtQuick.Controls

import QtQuick.Layouts
import Utils 1.0

import MaterialIcons

/**
* ComboBox with filter text area
*
* @param inputModel - model to filter
* @param editingFinished - signal emitted when editing is finished
* @alias filterText - text to filter the model
* ComboBox with filtering capabilities and support for custom values (i.e: outside the source model).
*/

ComboBox {
id: combo
id: root

property var inputModel
signal editingFinished(var value)
// Model to populate the combobox.
required property var sourceModel
// Input value to use as the current combobox value.
property var inputValue
// The text to filter the combobox model when the choices are displayed.
property alias filterText: filterTextArea.text
// Whether the current input value is within the source model.
readonly property bool validValue: sourceModel.includes(inputValue)

property alias filterText: filterTextArea
property bool validValue: true
signal editingFinished(var value)

enabled: root.editable
model: {
var filteredData = inputModel.filter(condition => {
if (filterTextArea.text.length > 0) return condition.toString().toLowerCase().includes(filterTextArea.text.toLowerCase())
return true
})
if (filteredData.length > 0) {
filterTextArea.background.color = Qt.lighter(palette.base, 2)
validValue = true

// order filtered data by relevance (results that start with the filter text come first)
filteredData.sort((a, b) => {
const nameA = a.toString().toLowerCase();
const nameB = b.toString().toLowerCase();
const filterText = filterTextArea.text.toLowerCase()
if (nameA.startsWith(filterText) && !nameB.startsWith(filterText))
return -1
if (!nameA.startsWith(filterText) && nameB.startsWith(filterText))
return 1
return 0
})
} else {
filterTextArea.background.color = Colors.red
validValue = false
}
function clearFilter() {
filterText = "";
}

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

return filteredData
function _updateCurrentIndex() {
currentIndex = find(inputValue);
}

background: Rectangle {
implicitHeight: root.implicitHeight
color: {
if (validValue) {
return palette.mid
} else {
return Colors.red
}
}
border.color: palette.base
displayText: inputValue

model: {
return sourceModel.filter(item => {
return item.toString().toLowerCase().includes(filterText.toLowerCase());
});
}

popup: Popup {
width: combo.width
implicitHeight: contentItem.implicitHeight
popup.onClosed: clearFilter()

onAboutToShow: {
filterTextArea.forceActiveFocus()
// Allows typing into the filter text area while the combobox has focus.
Keys.forwardTo: [filterTextArea]

if (mapToGlobal(popup.x, popup.y).y + root.implicitHeight * (model.length + 1) > _window.contentItem.height) {
y = -((combo.height * (combo.model.length + 1) > _window.contentItem.height) ? _window.contentItem.height*2/3 : combo.height * (combo.model.length + 1))
} else {
y = 0
}
onActivated: index => {
const isValidEntry = model.length > 0;
if (!isValidEntry) {
return;
}
editingFinished(model[index]);
}

contentItem: Item {
anchors.fill: parent
TextArea {
id: filterTextArea
leftPadding: 12
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top

selectByMouse: true
hoverEnabled: true
wrapMode: TextEdit.WrapAnywhere
placeholderText: "Filter"
background: Rectangle {}

onEditingFinished: {
combo.popup.close()
combo.editingFinished(displayText)
StateGroup {
id: filterState
// Override properties depending on filter text status.
states: [
State {
name: "Invalid"
when: root.delegateModel.count === 0
PropertyChanges {
target: filterTextArea
color: Colors.orange
}
}
]
}

Keys.onEnterPressed: {
if (!validValue) {
displayText = filterTextArea.text
} else {
displayText = currentText
}
editingFinished()
}
popup.contentItem: ColumnLayout {
width: parent.width
Layout.maximumHeight: root.Window.height
spacing: 0

Keys.onReturnPressed: {
if (!validValue) {
displayText = filterTextArea.text
} else {
displayText = currentText
}
editingFinished()
}
RowLayout {
Layout.fillWidth: true
spacing: 2

Keys.onUpPressed: {
// if the current index is 0, the user wants to go to the last item
if (combo.currentIndex == 0) {
combo.currentIndex = combo.model.length - 1
} else {
combo.currentIndex--
}
TextField {
id: filterTextArea
placeholderText: "Type to filter..."
Layout.fillWidth: true
leftPadding: 18
MouseArea {
// Prevent textfield from stealing combobox's active focus, without disabling it.
anchors.fill: parent
}

Keys.onDownPressed: {
// if the current index is the last one, the user wants to go to the first item
if (combo.currentIndex == combo.model.length - 1) {
combo.currentIndex = 0
} else {
combo.currentIndex++
background: Item {
MaterialLabel {
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: 2
text: MaterialIcons.search
}
}
}
}

ListView {
id: listView
clip: true
anchors.left: parent.left
anchors.right: parent.right
anchors.top: filterTextArea.bottom

implicitHeight: (combo.height * (combo.model.length + 1) > _window.contentItem.height) ? _window.contentItem.height*2/3 : contentHeight
model: combo.popup.visible ? combo.delegateModel : null

ScrollBar.vertical: MScrollBar {}
MaterialToolButton {
enabled: root.filterText !== ""
text: MaterialIcons.add_task
ToolTip.text: "Force custom value"
onClicked: {
editingFinished(root.filterText);
root.popup.close();
}
}
}
}

delegate: ItemDelegate {
width: combo.width
height: combo.height

contentItem: Text {
text: modelData
color: palette.text
Rectangle {
height: 1
Layout.fillWidth: true
color: Colors.sysPalette.mid
}

highlighted: validValue ? combo.currentIndex === index : false

hoverEnabled: true
}

onHighlightedIndexChanged: {
if (highlightedIndex >= 0) {
combo.currentIndex = highlightedIndex
ScrollView {
Layout.fillWidth: true
Layout.fillHeight: true
ListView {
implicitHeight: contentHeight
clip: true

model: root.delegateModel
highlightRangeMode: ListView.ApplyRange
currentIndex: root.highlightedIndex
ScrollBar.vertical: ScrollBar {}
}
}
}

onCurrentTextChanged: {
displayText = currentText
}
}
47 changes: 15 additions & 32 deletions meshroom/ui/qml/GraphEditor/AttributeItemDelegate.qml
Original file line number Diff line number Diff line change
Expand Up @@ -471,42 +471,25 @@ RowLayout {
Component {
id: comboBoxComponent

FilterComboBox {
inputModel: attribute.values

Component.onCompleted: {
// If value not in list, override the text and precise it is not valid
var idx = find(attribute.value)
if (idx === -1) {
displayText = attribute.value
validValue = false
} else {
currentIndex = idx
}
}
RowLayout {
FilterComboBox {
id: comboBox

onEditingFinished: function(value) {
_reconstruction.setAttribute(attribute, value)
}
Layout.fillWidth: true

Connections {
target: attribute
function onValueChanged() {
// When reset, clear and find the current index
// but if only reopen the combo box, keep the current value

// Convert all values of desc values as string
var valuesAsString = attribute.values.map(function(value) {
return value.toString()
})
if (valuesAsString.includes(attribute.value) || attribute.value === attribute.desc.value) {
filterText.clear()
validValue = true
displayText = currentText
currentIndex = find(attribute.value)
}
enabled: root.editable
sourceModel: attribute.values
inputValue: attribute.value

onEditingFinished: (value) => {
_reconstruction.setAttribute(attribute, value)
}
}
MaterialLabel {
visible: !comboBox.validValue
text: MaterialIcons.warning
ToolTip.text: "Custom value detected"
}
}
}

Expand Down
Loading