Skip to content

Commit 8029cd7

Browse files
committed
[ui] ExpressionTextField : fix issues with undostack, error status, number parsing, ...
1 parent 7c9e85b commit 8029cd7

File tree

8 files changed

+164
-71
lines changed

8 files changed

+164
-71
lines changed

meshroom/core/desc/attribute.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import os
44
import types
55
from collections.abc import Iterable
6-
import math
76

87
from meshroom.common import BaseObject, JSValue, Property, Variant, VariantList
98

@@ -342,7 +341,7 @@ def __init__(self, name, label, description, value, range=None, group="allParams
342341
self._valueType = int
343342

344343
def validateValue(self, value):
345-
if value is None or math.isnan(value):
344+
if value is None or isinstance(value, str):
346345
return value
347346
# Handle unsigned int values that are translated to int by shiboken and may overflow
348347
try:
@@ -372,7 +371,7 @@ def __init__(self, name, label, description, value, range=None, group="allParams
372371
self._valueType = float
373372

374373
def validateValue(self, value):
375-
if value is None:
374+
if value is None or isinstance(value, str):
376375
return value
377376
try:
378377
return float(value)

meshroom/ui/qml/Controls/ExpressionTextField.qml

Lines changed: 0 additions & 52 deletions
This file was deleted.

meshroom/ui/qml/Controls/qmldir

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
module Controls
22

3-
MathEvaluator 1.0 mathEvaluator.js
43
ColorChart 1.0 ColorChart.qml
54
ColorSelector 1.0 ColorSelector.qml
65
ExpandableGroup 1.0 ExpandableGroup.qml
@@ -22,4 +21,3 @@ SelectionBox 1.0 SelectionBox.qml
2221
SelectionLine 1.0 SelectionLine.qml
2322
DelegateSelectionBox 1.0 DelegateSelectionBox.qml
2423
DelegateSelectionLine 1.0 DelegateSelectionLine.qml
25-
ExpressionTextField 1.0 ExpressionTextField.qml

meshroom/ui/qml/GraphEditor/AttributeItemDelegate.qml

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,9 @@ RowLayout {
259259
switch (attribute.type) {
260260
case "IntParam":
261261
case "FloatParam":
262-
_reconstruction.setAttribute(root.attribute, Number(value))
262+
// We don't set a number because we want to keep the invalid expression
263+
// _reconstruction.setAttribute(root.attribute, Number(value))
264+
_reconstruction.setAttribute(root.attribute, value)
263265
updateAttributeLabel()
264266
break
265267
case "File":
@@ -606,6 +608,7 @@ RowLayout {
606608
id: sliderComponent
607609
RowLayout {
608610
ExpressionTextField {
611+
id: expressionTextField
609612
implicitWidth: 100
610613
Layout.fillWidth: !slider.active
611614
enabled: root.editable
@@ -618,17 +621,30 @@ RowLayout {
618621
// of the number. When we are editing (item is in focus), the content should follow the editing.
619622
autoScroll: activeFocus
620623
isInt: attribute.type === "FloatParam" ? false : true
621-
onEditingFinished: setTextFieldAttribute(text)
624+
625+
onEditingFinished: {
626+
if (hasExprError)
627+
setTextFieldAttribute(expressionTextField.text) // On the undo stack we keep the expression
628+
else
629+
setTextFieldAttribute(expressionTextField.evaluatedValue)
630+
}
622631
onAccepted: {
623-
setTextFieldAttribute(text)
624-
632+
if (hasExprError)
633+
setTextFieldAttribute(expressionTextField.text) // On the undo stack we keep the expression
634+
else
635+
setTextFieldAttribute(expressionTextField.evaluatedValue)
625636
// When the text is too long, display the left part
626637
// (with the most important values and cut the floating point details)
627638
ensureVisible(0)
628639
}
640+
629641
Component.onDestruction: {
630-
if (activeFocus)
631-
setTextFieldAttribute(text)
642+
if (activeFocus) {
643+
if (hasExprError)
644+
setTextFieldAttribute(expressionTextField.text)
645+
else
646+
setTextFieldAttribute(expressionTextField.evaluatedValue)
647+
}
632648
}
633649
Component.onCompleted: {
634650
// When the text is too long, display the left part
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import QtQuick
2+
import QtQuick.Controls
3+
4+
TextField {
5+
id: root
6+
7+
// evaluated numeric value (NaN if invalid)
8+
// It helps keeping the connection that text has so that we don't loose ability to undo/reset
9+
property real evaluatedValue: 0
10+
11+
property bool hasExprError: false
12+
property bool isInt: false
13+
property int decimals: 2
14+
15+
// Overlay for error state (red border on top of default background)
16+
Rectangle {
17+
anchors.fill: parent
18+
radius: 4
19+
border.color: "red"
20+
color: "transparent"
21+
visible: root.hasExprError
22+
z: 1
23+
}
24+
25+
function raiseError() {
26+
hasExprError = true
27+
}
28+
29+
function clearError() {
30+
hasExprError = false
31+
}
32+
33+
function reset(_value) {
34+
clearError()
35+
evaluatedValue = _value
36+
if (isInt) {
37+
root.text = _value.toFixed(0)
38+
} else {
39+
root.text = _value.toFixed(decimals)
40+
}
41+
}
42+
43+
function getEvalExpression(_text) {
44+
try {
45+
var result = MathEvaluator.eval(_text)
46+
if (isInt)
47+
result = Math.round(result)
48+
else
49+
result = result.toFixed(decimals)
50+
return result
51+
} catch (err) {
52+
console.error("Error evaluating expression (", _text, "):", err)
53+
return NaN
54+
}
55+
}
56+
57+
function refreshStatus() {
58+
if (isNaN(getEvalExpression(root.text))) {
59+
raiseError()
60+
} else {
61+
clearError()
62+
}
63+
}
64+
65+
function updateExpression() {
66+
var result = getEvalExpression(root.text)
67+
if (!isNaN(result)) {
68+
evaluatedValue = result
69+
clearError()
70+
return result
71+
} else {
72+
evaluatedValue = NaN
73+
raiseError()
74+
return NaN
75+
}
76+
}
77+
78+
// When user commits input, evaluate but do NOT overwrite text
79+
onAccepted: {
80+
updateExpression()
81+
}
82+
83+
onEditingFinished: {
84+
updateExpression()
85+
}
86+
87+
onTextChanged: {
88+
if (!activeFocus) {
89+
refreshStatus()
90+
}
91+
}
92+
93+
Component.onCompleted: {
94+
refreshStatus()
95+
}
96+
}

meshroom/ui/qml/Controls/mathEvaluator.js renamed to meshroom/ui/qml/Utils/mathEvaluator.js

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,21 +20,34 @@ var functions = {
2020
};
2121

2222

23+
function removeLeadingZeros(str) {
24+
return str.replace(/\b0*(\d+(?:\.\d*)?)/g, (match, number) => {
25+
// If the number starts with a decimal, add a leading zero
26+
if (number.startsWith('.')) {
27+
return '0' + number;
28+
}
29+
// Otherwise just return the number without leading zeros
30+
return number;
31+
});
32+
}
33+
34+
2335
/**
2436
* Evaluate an expression
2537
*
2638
* @param {*} expr Math expression
2739
* @returns float or int
2840
*/
2941
function eval(expr) {
30-
// Additionally replace the "," to "."
31-
expr = expr.replace(",", ".").replace(" ", "")
42+
// Warning : commas are for separating function args and dot to indicate decimals
3243

3344
// Only allow numbers, operators, parentheses, and function names
3445
if (!/^[0-9+\-*/^()e.,\s]*$/.test(expr.replace(/\b[a-zA-Z]+\b/g, ""))) {
3546
throw "Invalid characters in expression";
3647
}
3748

49+
expr = removeLeadingZeros(expr);
50+
3851
// Replace symbols and functions
3952
for (var symbol in symbols) {
4053
expr = expr.replace(new RegExp("\\b" + symbol + "\\b", "g"), symbols[symbol]);

meshroom/ui/qml/Utils/qmldir

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,5 @@ singleton ExifOrientation 1.0 ExifOrientation.qml
1111
# singleton Filepath 1.0 Filepath.qml
1212
# singleton Scene3DHelper 1.0 Scene3DHelper.qml
1313
# singleton Transformations3DHelper 1.0 Transformations3DHelper.qml
14+
MathEvaluator 1.0 mathEvaluator.js
15+
ExpressionTextField 1.0 ExpressionTextField.qml

meshroom/ui/qml/Viewer/HdrImageToolbar.qml

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import QtQuick.Controls
33
import QtQuick.Layouts
44

55
import Controls 1.0
6+
import Utils 1.0
67

78
FloatingPane {
89
id: root
@@ -16,8 +17,8 @@ FloatingPane {
1617

1718

1819
property real slidersPowerValue: 4.0
19-
property real gainValue: Math.pow(gainCtrl.value, slidersPowerValue)
20-
property real gammaValue: Math.pow(gammaCtrl.value, slidersPowerValue)
20+
property real gainValue: Math.pow(gainCtrl.value, slidersPowerValue).toFixed(2)
21+
property real gammaValue: Math.pow(gammaCtrl.value, slidersPowerValue).toFixed(2)
2122
property alias channelModeValue: channelsCtrl.value
2223
property variant colorRGBA: null
2324
property variant mousePosition: ({x:0, y:0})
@@ -112,6 +113,7 @@ FloatingPane {
112113

113114
onClicked: {
114115
gainCtrl.value = gainDefaultValue
116+
gainLabel.reset(gainValue)
115117
}
116118
}
117119
ExpressionTextField {
@@ -121,11 +123,19 @@ FloatingPane {
121123
ToolTip.delay: 100
122124
ToolTip.text: "Color Gain (in linear colorspace)"
123125

124-
text: gainValue.toFixed(2)
126+
text: gainValue
127+
decimals: 2
125128
Layout.preferredWidth: textMetrics_gainValue.width
126129
selectByMouse: true
127130
onAccepted: {
128-
gainCtrl.value = Math.pow(Number(gainLabel.text), 1.0 / slidersPowerValue)
131+
if (!gainLabel.hasExprError) {
132+
if (gainLabel.evaluatedValue <= 0) {
133+
gainLabel.evaluatedValue = 0
134+
gainCtrl.value = gainLabel.evaluatedValue
135+
} else {
136+
gainCtrl.value = Math.pow(Number(gainLabel.evaluatedValue), 1.0 / slidersPowerValue)
137+
}
138+
}
129139
}
130140
}
131141
Slider {
@@ -135,6 +145,7 @@ FloatingPane {
135145
to: 2
136146
value: gainDefaultValue
137147
stepSize: 0.01
148+
onMoved: gainLabel.reset(Math.pow(value, slidersPowerValue))
138149
}
139150
}
140151

@@ -151,6 +162,7 @@ FloatingPane {
151162

152163
onClicked: {
153164
gammaCtrl.value = gammaDefaultValue;
165+
gammaLabel.reset(gammaValue)
154166
}
155167
}
156168
ExpressionTextField {
@@ -160,11 +172,19 @@ FloatingPane {
160172
ToolTip.delay: 100
161173
ToolTip.text: "Apply Gamma (after Gain and in linear colorspace)"
162174

163-
text: gammaValue.toFixed(2)
175+
text: gammaValue
176+
decimals: 2
164177
Layout.preferredWidth: textMetrics_gainValue.width
165178
selectByMouse: true
166179
onAccepted: {
167-
gammaCtrl.value = Math.pow(Number(gammaLabel.text), 1.0 / slidersPowerValue)
180+
if (!gammaLabel.hasExprError) {
181+
if (gammaLabel.evaluatedValue <= 0) {
182+
gammaLabel.evaluatedValue = 0
183+
gammaCtrl.value = gammaLabel.evaluatedValue
184+
} else {
185+
gammaCtrl.value = Math.pow(Number(gammaLabel.evaluatedValue), 1.0 / slidersPowerValue)
186+
}
187+
}
168188
}
169189
}
170190
Slider {
@@ -174,6 +194,7 @@ FloatingPane {
174194
to: 2
175195
value: gammaDefaultValue
176196
stepSize: 0.01
197+
onMoved: gammaLabel.reset(Math.pow(value, slidersPowerValue))
177198
}
178199
}
179200

0 commit comments

Comments
 (0)