Skip to content

Commit 845936d

Browse files
[ui] Attribute: Attribute check validators when user edit, and show orange icon if an error exists. Mandatory atribute have a '*'
1 parent 09b4797 commit 845936d

File tree

4 files changed

+244
-120
lines changed

4 files changed

+244
-120
lines changed

meshroom/core/attribute.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from collections.abc import Iterable, Sequence
1010
from string import Template
1111
from meshroom.common import BaseObject, Property, Variant, Signal, ListModel, DictModel, Slot
12+
from meshroom.core.desc.validators import NotEmptyValidator
1213
from meshroom.core import desc, hashValue
1314

1415
from typing import TYPE_CHECKING
@@ -470,6 +471,31 @@ def _is2D(self) -> bool:
470471

471472
return next((imageSemantic for imageSemantic in Attribute.VALID_IMAGE_SEMANTICS if self.desc.semantic == imageSemantic), None) is not None
472473

474+
def getErrorMessages(self) -> list[str]:
475+
""" Execute the validators and aggregate the eventual error messages"""
476+
477+
result = []
478+
479+
for validator in self.desc._validators:
480+
isValid, errorMessages = validator(self.node, self)
481+
482+
if isValid:
483+
continue
484+
485+
for errorMessage in errorMessages:
486+
result.append(errorMessage)
487+
488+
return result
489+
490+
def _isMandatory(self) -> bool:
491+
""" An attribute is considered as mandatory it contain a NotEmptyValidator """
492+
493+
for validator in self.desc.validators:
494+
if isinstance(validator, NotEmptyValidator):
495+
return True
496+
497+
return False
498+
473499
name = Property(str, getName, constant=True)
474500
fullName = Property(str, getFullName, constant=True)
475501
fullNameToNode = Property(str, getFullNameToNode, constant=True)
@@ -516,6 +542,10 @@ def _is2D(self) -> bool:
516542
validValue = Property(bool, getValidValue, setValidValue, notify=validValueChanged)
517543
root = Property(BaseObject, root.fget, constant=True)
518544

545+
errorMessageChanged = Signal()
546+
errorMessages = Property(Variant, lambda self: self.getErrorMessages(), notify=errorMessageChanged)
547+
isMandatory = Property(bool, _isMandatory, constant=True )
548+
519549

520550
def raiseIfLink(func):
521551
""" If Attribute instance is a link, raise a RuntimeError."""

meshroom/core/desc/attribute.py

Lines changed: 44 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,26 @@
66

77
from meshroom.common import BaseObject, JSValue, Property, Variant, VariantList
88

9+
from typing import TYPE_CHECKING
10+
11+
if TYPE_CHECKING:
12+
from meshroom.core.desc.validators import AttributeValidator
13+
914

1015
class Attribute(BaseObject):
1116
"""
1217
"""
1318

14-
def __init__(self, name, label, description, value, advanced, semantic, group, enabled, invalidate=True,
15-
uidIgnoreValue=None, validValue=True, errorMessage="", visible=True, exposed=False):
19+
def __init__(self, name, label, description, value, advanced, semantic, group, enabled,
20+
invalidate=True,
21+
uidIgnoreValue=None,
22+
validValue=True,
23+
errorMessage="",
24+
visible=True,
25+
exposed=False,
26+
validators:list["AttributeValidator"]=None
27+
):
28+
1629
super(Attribute, self).__init__()
1730
self._name = name
1831
self._label = label
@@ -33,6 +46,8 @@ def __init__(self, name, label, description, value, advanced, semantic, group, e
3346
self._isDynamicValue = (self._value is None)
3447
self._valueType = None
3548

49+
self._validators = validators if isinstance(validators, (list, tuple)) else []
50+
3651
def getInstanceType(self):
3752
""" Return the correct Attribute instance corresponding to the description. """
3853
# Import within the method to prevent cyclic dependencies
@@ -70,7 +85,11 @@ def matchDescription(self, value, strict=True):
7085
except ValueError:
7186
return False
7287
return True
73-
88+
89+
@property
90+
def validators(self):
91+
return self._validators
92+
7493
name = Property(str, lambda self: self._name, constant=True)
7594
label = Property(str, lambda self: self._label, constant=True)
7695
description = Property(str, lambda self: self._description, constant=True)
@@ -108,15 +127,15 @@ def matchDescription(self, value, strict=True):
108127
class ListAttribute(Attribute):
109128
""" A list of Attributes """
110129
def __init__(self, elementDesc, name, label, description, group="allParams", advanced=False, semantic="",
111-
enabled=True, joinChar=" ", visible=True, exposed=False):
130+
enabled=True, joinChar=" ", visible=True, exposed=False, validators=None):
112131
"""
113132
:param elementDesc: the Attribute description of elements to store in that list
114133
"""
115134
self._elementDesc = elementDesc
116135
self._joinChar = joinChar
117136
super(ListAttribute, self).__init__(name=name, label=label, description=description, value=[],
118137
invalidate=False, group=group, advanced=advanced, semantic=semantic,
119-
enabled=enabled, visible=visible, exposed=exposed)
138+
enabled=enabled, visible=visible, exposed=exposed, validators=validators)
120139

121140
def getInstanceType(self):
122141
# Import within the method to prevent cyclic dependencies
@@ -160,7 +179,7 @@ def matchDescription(self, value, strict=True):
160179
class GroupAttribute(Attribute):
161180
""" A macro Attribute composed of several Attributes """
162181
def __init__(self, groupDesc, name, label, description, group="allParams", advanced=False, semantic="",
163-
enabled=True, joinChar=" ", brackets=None, visible=True, exposed=False):
182+
enabled=True, joinChar=" ", brackets=None, visible=True, exposed=False, validators=None):
164183
"""
165184
:param groupDesc: the description of the Attributes composing this group
166185
"""
@@ -169,7 +188,7 @@ def __init__(self, groupDesc, name, label, description, group="allParams", advan
169188
self._brackets = brackets
170189
super(GroupAttribute, self).__init__(name=name, label=label, description=description, value={},
171190
group=group, advanced=advanced, invalidate=False, semantic=semantic,
172-
enabled=enabled, visible=visible, exposed=exposed)
191+
enabled=enabled, visible=visible, exposed=exposed, validators=validators)
173192

174193
def getInstanceType(self):
175194
# Import within the method to prevent cyclic dependencies
@@ -267,21 +286,21 @@ class Param(Attribute):
267286
"""
268287
"""
269288
def __init__(self, name, label, description, value, group, advanced, semantic, enabled, invalidate=True,
270-
uidIgnoreValue=None, validValue=True, errorMessage="", visible=True, exposed=False):
289+
uidIgnoreValue=None, validValue=True, errorMessage="", visible=True, exposed=False, validators=None):
271290
super(Param, self).__init__(name=name, label=label, description=description, value=value,
272291
group=group, advanced=advanced, enabled=enabled, invalidate=invalidate,
273292
semantic=semantic, uidIgnoreValue=uidIgnoreValue, validValue=validValue,
274-
errorMessage=errorMessage, visible=visible, exposed=exposed)
293+
errorMessage=errorMessage, visible=visible, exposed=exposed, validators=validators)
275294

276295

277296
class File(Attribute):
278297
"""
279298
"""
280299
def __init__(self, name, label, description, value, group="allParams", advanced=False, invalidate=True,
281-
semantic="", enabled=True, visible=True, exposed=True):
300+
semantic="", enabled=True, visible=True, exposed=True, validators=None):
282301
super(File, self).__init__(name=name, label=label, description=description, value=value, group=group,
283302
advanced=advanced, enabled=enabled, invalidate=invalidate, semantic=semantic,
284-
visible=visible, exposed=exposed)
303+
visible=visible, exposed=exposed, validators=validators)
285304
self._valueType = str
286305

287306
def validateValue(self, value):
@@ -304,10 +323,10 @@ class BoolParam(Param):
304323
"""
305324
"""
306325
def __init__(self, name, label, description, value, group="allParams", advanced=False, enabled=True,
307-
invalidate=True, semantic="", visible=True, exposed=False):
326+
invalidate=True, semantic="", visible=True, exposed=False, validators=None):
308327
super(BoolParam, self).__init__(name=name, label=label, description=description, value=value,
309328
group=group, advanced=advanced, enabled=enabled, invalidate=invalidate,
310-
semantic=semantic, visible=visible, exposed=exposed)
329+
semantic=semantic, visible=visible, exposed=exposed, validators=validators)
311330
self._valueType = bool
312331

313332
def validateValue(self, value):
@@ -332,12 +351,12 @@ class IntParam(Param):
332351
"""
333352
"""
334353
def __init__(self, name, label, description, value, range=None, group="allParams", advanced=False, enabled=True,
335-
invalidate=True, semantic="", validValue=True, errorMessage="", visible=True, exposed=False):
354+
invalidate=True, semantic="", validValue=True, errorMessage="", visible=True, exposed=False, validators=None):
336355
self._range = range
337356
super(IntParam, self).__init__(name=name, label=label, description=description, value=value,
338357
group=group, advanced=advanced, enabled=enabled, invalidate=invalidate,
339358
semantic=semantic, validValue=validValue, errorMessage=errorMessage,
340-
visible=visible, exposed=exposed)
359+
visible=visible, exposed=exposed, validators=validators)
341360
self._valueType = int
342361

343362
def validateValue(self, value):
@@ -362,12 +381,12 @@ class FloatParam(Param):
362381
"""
363382
"""
364383
def __init__(self, name, label, description, value, range=None, group="allParams", advanced=False, enabled=True,
365-
invalidate=True, semantic="", validValue=True, errorMessage="", visible=True, exposed=False):
384+
invalidate=True, semantic="", validValue=True, errorMessage="", visible=True, exposed=False, validators=None):
366385
self._range = range
367386
super(FloatParam, self).__init__(name=name, label=label, description=description, value=value,
368387
group=group, advanced=advanced, enabled=enabled, invalidate=invalidate,
369388
semantic=semantic, validValue=validValue, errorMessage=errorMessage,
370-
visible=visible, exposed=exposed)
389+
visible=visible, exposed=exposed, validators=validators)
371390
self._valueType = float
372391

373392
def validateValue(self, value):
@@ -391,10 +410,10 @@ class PushButtonParam(Param):
391410
"""
392411
"""
393412
def __init__(self, name, label, description, group="allParams", advanced=False, enabled=True,
394-
invalidate=True, semantic="", visible=True, exposed=False):
413+
invalidate=True, semantic="", visible=True, exposed=False, validators=None):
395414
super(PushButtonParam, self).__init__(name=name, label=label, description=description, value=None,
396415
group=group, advanced=advanced, enabled=enabled, invalidate=invalidate,
397-
semantic=semantic, visible=visible, exposed=exposed)
416+
semantic=semantic, visible=visible, exposed=exposed, validators=validators)
398417
self._valueType = None
399418

400419
def getInstanceType(self):
@@ -430,12 +449,12 @@ class ChoiceParam(Param):
430449
def __init__(self, name: str, label: str, description: str, value, values, exclusive=True, saveValuesOverride=False,
431450
group="allParams", joinChar=" ", advanced=False, enabled=True, invalidate=True, semantic="",
432451
validValue=True, errorMessage="",
433-
visible=True, exposed=False):
452+
visible=True, exposed=False, validators=None):
434453

435454
super(ChoiceParam, self).__init__(name=name, label=label, description=description, value=value,
436455
group=group, advanced=advanced, enabled=enabled, invalidate=invalidate,
437456
semantic=semantic, validValue=validValue, errorMessage=errorMessage,
438-
visible=visible, exposed=exposed)
457+
visible=visible, exposed=exposed, validators=validators)
439458
self._values = values
440459
self._saveValuesOverride = saveValuesOverride
441460
self._exclusive = exclusive
@@ -509,11 +528,11 @@ class StringParam(Param):
509528
"""
510529
def __init__(self, name, label, description, value, group="allParams", advanced=False, enabled=True,
511530
invalidate=True, semantic="", uidIgnoreValue=None, validValue=True, errorMessage="", visible=True,
512-
exposed=False):
531+
exposed=False, validators=None):
513532
super(StringParam, self).__init__(name=name, label=label, description=description, value=value,
514533
group=group, advanced=advanced, enabled=enabled, invalidate=invalidate,
515534
semantic=semantic, uidIgnoreValue=uidIgnoreValue, validValue=validValue,
516-
errorMessage=errorMessage, visible=visible, exposed=exposed)
535+
errorMessage=errorMessage, visible=visible, exposed=exposed, validators=validators)
517536
self._valueType = str
518537

519538
def validateValue(self, value):
@@ -534,10 +553,10 @@ class ColorParam(Param):
534553
"""
535554
"""
536555
def __init__(self, name, label, description, value, group="allParams", advanced=False, enabled=True,
537-
invalidate=True, semantic="", visible=True, exposed=False):
556+
invalidate=True, semantic="", visible=True, exposed=False, validators=None):
538557
super(ColorParam, self).__init__(name=name, label=label, description=description, value=value,
539558
group=group, advanced=advanced, enabled=enabled, invalidate=invalidate,
540-
semantic=semantic, visible=visible, exposed=exposed)
559+
semantic=semantic, visible=visible, exposed=exposed, validators=validators)
541560
self._valueType = str
542561

543562
def validateValue(self, value):

meshroom/core/desc/validators.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
from typing import TYPE_CHECKING, TypeVar
2+
3+
if TYPE_CHECKING:
4+
from meshroom.core.attribute import Attribute
5+
from meshroom.core.node import Node
6+
7+
8+
SuccessResponse = (True, [])
9+
Number = TypeVar("Number", int, float)
10+
11+
12+
class AttributeValidator(object):
13+
14+
def __call__(self, node: "Node", attribute: "Attribute") -> tuple[bool, list[str]]: raise NotImplementedError()
15+
16+
17+
class NotEmptyValidator(AttributeValidator):
18+
19+
def __call__(self, node: "Node", attribute: "Attribute") -> tuple[bool, list[str]]:
20+
21+
if attribute.value is None or attribute.value == "":
22+
return (False, ["Empty value are not allowed"])
23+
24+
return SuccessResponse
25+
26+
27+
class RangeValidator(AttributeValidator):
28+
29+
def __init__(self, min:Number, max:Number):
30+
self._min = min
31+
self._max = max
32+
33+
def __call__(self, node:"Node", attribute: "Attribute") -> tuple[bool, list[str]]:
34+
35+
if not isinstance(attribute, Number):
36+
return (False, ["Attribute value should be a number"])
37+
38+
39+
if attribute.value < self._min or attribute.value > self._max:
40+
return (False, [f"Value should be greater than {self._min} and less than {self._max}",
41+
f"({self._min} < {attribute.value} < {self._max})"])
42+
43+
return SuccessResponse

0 commit comments

Comments
 (0)