Skip to content

Commit 5c972ee

Browse files
[ui/core] Add Validators paradigm
1 parent 0ee8481 commit 5c972ee

15 files changed

Lines changed: 542 additions & 207 deletions

File tree

meshroom/common/PySignal.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,10 @@ class ClassSignal:
158158
"""
159159
_map = {}
160160

161+
def __init__(self, *args, **kwargs):
162+
self._args = args
163+
self._kwargs = kwargs
164+
161165
def __get__(self, instance, owner):
162166
if instance is None:
163167
# When we access ClassSignal element on the class object without any instance,

meshroom/core/attribute.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from collections.abc import Iterable, Sequence
1313
from string import Template
1414
from meshroom.common import BaseObject, Property, Variant, Signal, ListModel, DictModel, Slot
15+
from meshroom.core.desc.validators import NotEmptyValidator
1516
from meshroom.core import desc, hashValue
1617
from meshroom.core.keyValues import KeyValues
1718
from meshroom.core.exception import InvalidEdgeError
@@ -84,6 +85,7 @@ def __init__(self, node, attributeDesc: desc.Attribute, isOutput: bool, root=Non
8485
self._enabled: bool = True
8586
self._depth: int = root.depth + 1 if root is not None else 0
8687
self._exposed: bool = root.exposed if root is not None else attributeDesc.exposed
88+
self._description: str = attributeDesc.description
8789
self._invalidate = False if self._isOutput else attributeDesc.invalidate
8890
self._invalidationValue = "" # invalidation value for output attributes
8991
self._value = None
@@ -235,6 +237,15 @@ def _setValue(self, value):
235237
self.requestNodeUpdate()
236238
self.valueChanged.emit()
237239

240+
def _get_description(self):
241+
return self._description
242+
243+
def _set_description(self, desc):
244+
if self._description == desc:
245+
return
246+
self._description = desc
247+
self.descriptionChanged.emit()
248+
238249
def _getKeyValues(self):
239250
"""
240251
Return the per-key values object of the attribute or of the linked attribute.
@@ -487,6 +498,43 @@ def updateInternals(self):
487498
# Emit if the enable status has changed
488499
self._setEnabled(self._getEnabled())
489500

501+
def getErrorMessages(self) -> list[str]:
502+
""" Execute the validators and aggregate the eventual error messages"""
503+
504+
result = []
505+
506+
for validator in self.desc.validators:
507+
isValid, errorMessages = validator(self.node, self)
508+
509+
if isValid:
510+
continue
511+
512+
for errorMessage in errorMessages:
513+
result.append(errorMessage)
514+
515+
return result
516+
517+
def _isValid(self) -> bool:
518+
""" Check the validation and return False if any validator return (False, erorrs)
519+
"""
520+
521+
for validator in self.desc.validators:
522+
isValid, _ = validator(self.node, self)
523+
524+
if not isValid:
525+
return False
526+
527+
return True
528+
529+
def _isMandatory(self) -> bool:
530+
""" An attribute is considered as mandatory it contain a NotEmptyValidator """
531+
532+
for validator in self.desc.validators:
533+
if isinstance(validator, NotEmptyValidator):
534+
return True
535+
536+
return False
537+
490538
def _getEnabled(self) -> bool:
491539
if callable(self._desc.enabled):
492540
try:
@@ -742,6 +790,11 @@ def validateIncomingConnection(self, connectingAttribute: Attribute) -> bool:
742790

743791
expressionApplied = Signal()
744792

793+
errorMessageChanged = Signal()
794+
errorMessages = Property(Variant, lambda self: self.getErrorMessages(), notify=errorMessageChanged)
795+
isMandatory = Property(bool, _isMandatory, constant=True )
796+
isValid = Property(bool, _isValid, constant=True)
797+
745798

746799
def raiseIfLink(func):
747800
"""

meshroom/core/desc/attribute.py

Lines changed: 69 additions & 57 deletions
Large diffs are not rendered by default.

meshroom/core/desc/validators.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
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+
def success() -> tuple[bool, list[str]]:
9+
return (True, [])
10+
11+
def error(*messages: str) -> tuple[bool, list[str]]:
12+
return (False, messages)
13+
14+
class AttributeValidator(object):
15+
""" Interface for an attribute validation
16+
You can inherit from this class and override the __call__ methods to implement your own attribute validation logic
17+
18+
Because it's a callable class, you can also create your own validators on the fly
19+
20+
.. code-block: python
21+
22+
lambda node, attribute: success() if attribute.value and attribute.value != "" else error("attribute have no value")
23+
"""
24+
25+
def __call__(self, node: "Node", attribute: "Attribute") -> tuple[bool, list[str]]:
26+
"""
27+
Override this method to implement your custom validation logic.
28+
You can use the success() and error() helpers that encapsulate the returning response.
29+
30+
:param node: The node that holds the attribute to validate
31+
:param attribute: The atribute to validate
32+
33+
:returns: The validtion response: (True, []) if it's valid, (False, [errorMessage1, errorMessage2, ...]) if error exists
34+
35+
"""
36+
raise NotImplementedError()
37+
38+
39+
class NotEmptyValidator(AttributeValidator):
40+
""" The attribute value should not be empty
41+
This class is used to determine if an attribute label should be considered as mandatory/required
42+
"""
43+
44+
def __call__(self, node: "Node", attribute: "Attribute") -> tuple[bool, list[str]]:
45+
46+
if attribute.value is None or attribute.value == "":
47+
return error("Empty value are not allowed")
48+
49+
return success()
50+
51+
52+
class RangeValidator(AttributeValidator):
53+
""" Check if the attribute value is in the given range
54+
"""
55+
56+
def __init__(self, min, max):
57+
self._min = min
58+
self._max = max
59+
60+
def __call__(self, node:"Node", attribute: "Attribute") -> tuple[bool, list[str]]:
61+
62+
if attribute.value < self._min or attribute.value > self._max:
63+
return error(f"Value should be greater than {self._min} and less than {self._max}",
64+
f"({self._min} < {attribute.value} < {self._max})")
65+
66+
return success()

meshroom/core/graph.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1710,6 +1710,7 @@ def setVerbose(self, v):
17101710
statusUpdated = Signal()
17111711
canComputeLeavesChanged = Signal()
17121712
canComputeLeaves = Property(bool, lambda self: self._canComputeLeaves, notify=canComputeLeavesChanged)
1713+
attributeValueChanged = Signal(Attribute)
17131714

17141715

17151716
def loadGraph(filepath, strictCompatibility: bool = False) -> Graph:

meshroom/core/node.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1522,6 +1522,8 @@ def _onAttributeChanged(self, attr: Attribute):
15221522
if callback:
15231523
callback(self)
15241524

1525+
self.hasInvalidAttributeChanged.emit()
1526+
15251527
if self.graph:
15261528
# If we are in a graph, propagate the notification to the connected output attributes
15271529
for edge in self.graph.outEdges(attr):
@@ -1792,7 +1794,7 @@ def loadOutputAttr(self):
17921794
# This does not apply to non dynamic output
17931795
if not self.nodeDesc.hasDynamicOutputAttribute:
17941796
return
1795-
1797+
17961798
# Check existence of values.json file
17971799
valuesFile = self.valuesFile
17981800
if not os.path.exists(valuesFile):
@@ -2157,6 +2159,14 @@ def hasTextOutputAttribute(self) -> bool:
21572159
"""
21582160
return next((attr for attr in self._attributes if attr.enabled and attr.isOutput and attr.isTextDisplayable), None) is not None
21592161

2162+
def _hasInvalidAttribute(self):
2163+
for attribute in self._attributes:
2164+
if len(attribute.errorMessages) > 0:
2165+
return True
2166+
return False
2167+
2168+
name = Property(str, getName, constant=True)
2169+
21602170
def _hasDisplayableShape(self):
21612171
"""
21622172
Return True if at least one attribute is a ShapeAttribute, a ShapeListAttribute or a shape File.
@@ -2236,6 +2246,9 @@ def _hasDisplayableShape(self):
22362246
# Whether the node contains a ShapeAttribute, a ShapeListAttribute or a shape File.
22372247
hasDisplayableShape = Property(bool, _hasDisplayableShape, constant=True)
22382248

2249+
hasInvalidAttributeChanged = Signal()
2250+
hasInvalidAttribute = Property(bool, _hasInvalidAttribute, notify=hasInvalidAttributeChanged)
2251+
22392252

22402253
class Node(BaseNode):
22412254
"""

meshroom/ui/commands.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -314,16 +314,21 @@ def redoImpl(self):
314314
if self.value == self.oldValue:
315315
return False
316316
if self.graph.attribute(self.attrName) is not None:
317-
self.graph.attribute(self.attrName).value = self.value
317+
attribute = self.graph.attribute(self.attrName)
318318
else:
319-
self.graph.internalAttribute(self.attrName).value = self.value
319+
attribute = self.graph.internalAttribute(self.attrName)
320+
321+
attribute.value = self.value
322+
320323
return True
321324

322325
def undoImpl(self):
323326
if self.graph.attribute(self.attrName) is not None:
324-
self.graph.attribute(self.attrName).value = self.oldValue
327+
attribute = self.graph.attribute(self.attrName)
325328
else:
326-
self.graph.internalAttribute(self.attrName).value = self.oldValue
329+
attribute = self.graph.internalAttribute(self.attrName)
330+
331+
attribute.value = self.oldValue
327332

328333
class AddAttributeKeyValueCommand(GraphCommand):
329334
def __init__(self, graph, attribute, key, value, parent=None):

meshroom/ui/qml/Application.qml

Lines changed: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ Page {
3939
property alias showImageGallery: imageGalleryVisibilityCB.checked
4040
property alias showTextViewer: textViewerVisibilityCB.checked
4141
}
42-
42+
4343
Settings {
4444
id: nodeActionsSettings
4545
category: "NodeActions"
@@ -135,6 +135,14 @@ Page {
135135
return true;
136136
}
137137

138+
function getAllNodes() {
139+
const nodes = []
140+
for(let i=0; i<graphEditor.graph.nodes.count; i++) {
141+
nodes.push(graphEditor.graph.nodes.at(i))
142+
}
143+
return nodes
144+
}
145+
138146
// File dialogs
139147
Platform.FileDialog {
140148
id: saveFileDialog
@@ -267,14 +275,24 @@ Page {
267275
function submit(nodes) {
268276
if (!canSubmit) {
269277
unsavedSubmitDialog.open()
270-
} else {
278+
}
279+
else {
271280
try {
272-
_currentScene.submit(nodes)
281+
if(nodes == null) {
282+
nodes = getAllNodes()
283+
}
284+
if ( nodes && nodes.find(node => node.hasInvalidAttribute) ) {
285+
submitWithWarningDialog.nodes = nodes
286+
submitWithWarningDialog.open()
287+
} else {
288+
_currentScene.submit(nodes)
289+
}
273290
}
274-
catch (error) {
291+
catch (error) {
275292
const data = ErrorHandler.analyseError(error)
276-
if (data.context === "SUBMITTING")
293+
if (data.context === "SUBMITTING") {
277294
computeSubmitErrorDialog.openError(data.type, data.msg, nodes)
295+
}
278296
}
279297
}
280298
}
@@ -400,6 +418,26 @@ Page {
400418
onAccepted: saveAsAction.trigger()
401419
}
402420

421+
MessageDialog {
422+
id: submitWithWarningDialog
423+
424+
canCopy: false
425+
icon.text: MaterialIcons.warning
426+
parent: Overlay.overlay
427+
preset: "Warning"
428+
title: "Nodes Containing Warnings"
429+
text: "Some nodes contain warnings. Are you sure you want to submit?"
430+
helperText: "Submit even if some nodes have warnings"
431+
standardButtons: Dialog.Cancel | Dialog.Yes
432+
433+
property var nodes: []
434+
435+
onDiscarded: close()
436+
onAccepted: {
437+
_reconstruction.submit(nodes)
438+
}
439+
}
440+
403441
MessageDialog {
404442
id: fileModifiedDialog
405443

@@ -733,7 +771,7 @@ Page {
733771
model: MeshroomApp.recentProjectFiles
734772
MenuItem {
735773
enabled: modelData["status"] != 0
736-
774+
737775
onTriggered: ensureSaved(function() {
738776
openRecentMenu.dismiss()
739777
if (_currentScene.load(modelData["path"])) {
@@ -887,7 +925,7 @@ Page {
887925
id: nodeActionsSettingsMenu
888926
title: "NodeActions Settings"
889927
implicitWidth: 250
890-
928+
891929
MenuItem {
892930
id: nodeActionsConfirmDelete
893931
checkable: true
@@ -1504,7 +1542,7 @@ Page {
15041542
SplitView.minimumWidth: 350
15051543

15061544
node: _currentScene ? _currentScene.selectedNode : null
1507-
property bool computing: _currentScene ? _currentScene.computing : false
1545+
property bool computing: _currentScene ? _currentScene.computing : false
15081546
property var currentAttributes: []
15091547

15101548
// Make NodeEditor readOnly when computing

0 commit comments

Comments
 (0)