mirror of
https://github.com/alicevision/Meshroom.git
synced 2025-04-28 17:57:16 +02:00
[core] Introduce DynamicChoiceParam attribute
Add a new ChoiceParam-like type that supports the override and serialization of the list of possible values.
This commit is contained in:
parent
d51dba12ad
commit
a8c54a40db
4 changed files with 215 additions and 3 deletions
|
@ -362,7 +362,10 @@ class Attribute(BaseObject):
|
|||
If it is a list with one empty string element, it will returns 2 quotes.
|
||||
'''
|
||||
# ChoiceParam with multiple values should be combined
|
||||
if isinstance(self.attributeDesc, desc.ChoiceParam) and not self.attributeDesc.exclusive:
|
||||
if (
|
||||
isinstance(self.attributeDesc, (desc.ChoiceParam, desc.DynamicChoiceParam))
|
||||
and not self.attributeDesc.exclusive
|
||||
):
|
||||
# Ensure value is a list as expected
|
||||
assert (isinstance(self.value, Sequence) and not isinstance(self.value, str))
|
||||
v = self.attributeDesc.joinChar.join(self.getEvalValue())
|
||||
|
@ -370,7 +373,10 @@ class Attribute(BaseObject):
|
|||
return '"{}"'.format(v)
|
||||
return v
|
||||
# String, File, single value Choice are based on strings and should includes quotes to deal with spaces
|
||||
if withQuotes and isinstance(self.attributeDesc, (desc.StringParam, desc.File, desc.ChoiceParam)):
|
||||
if withQuotes and isinstance(
|
||||
self.attributeDesc,
|
||||
(desc.StringParam, desc.File, desc.ChoiceParam, desc.DynamicChoiceParam),
|
||||
):
|
||||
return '"{}"'.format(self.getEvalValue())
|
||||
return str(self.getEvalValue())
|
||||
|
||||
|
@ -797,3 +803,42 @@ class GroupAttribute(Attribute):
|
|||
# Override value property
|
||||
value = Property(Variant, Attribute._get_value, _set_value, notify=Attribute.valueChanged)
|
||||
isDefault = Property(bool, _isDefault, notify=Attribute.valueChanged)
|
||||
|
||||
|
||||
class DynamicChoiceParam(GroupAttribute):
|
||||
def __init__(self, node, attributeDesc, isOutput, root=None, parent=None):
|
||||
super().__init__(node, attributeDesc, isOutput, root, parent)
|
||||
# Granularity (and performance) could be improved by using the 'valueChanged' signals of sub-attributes.
|
||||
# But as there are situations where:
|
||||
# * the whole GroupAttribute is 'changed' (eg: connection/disconnection)
|
||||
# * the sub-attributes are re-created (eg: resetToDefaultValue)
|
||||
# it is simpler to use the GroupAttribute's 'valueChanged' signal as the main trigger for updates.
|
||||
self.valueChanged.connect(self.choiceValueChanged)
|
||||
self.valueChanged.connect(self.choiceValuesChanged)
|
||||
|
||||
def _get_value(self):
|
||||
if self.isLink:
|
||||
return super()._get_value()
|
||||
return self.choiceValue.value
|
||||
|
||||
def _set_value(self, value):
|
||||
if isinstance(value, dict) or Attribute.isLinkExpression(value):
|
||||
super()._set_value(value)
|
||||
else:
|
||||
self.choiceValue.value = value
|
||||
|
||||
def getValues(self):
|
||||
if self.isLink:
|
||||
return self.linkParam.getValues()
|
||||
return self.choiceValues.getExportValue() or self.desc.values
|
||||
|
||||
def setValues(self, values):
|
||||
self.choiceValues.value = values
|
||||
|
||||
def getValueStr(self, withQuotes=True):
|
||||
return Attribute.getValueStr(self, withQuotes)
|
||||
|
||||
choiceValueChanged = Signal()
|
||||
value = Property(Variant, _get_value, _set_value, notify=choiceValueChanged)
|
||||
choiceValuesChanged = Signal()
|
||||
values = Property(Variant, getValues, setValues, notify=choiceValuesChanged)
|
||||
|
|
|
@ -3,6 +3,7 @@ from .attribute import (
|
|||
BoolParam,
|
||||
ChoiceParam,
|
||||
ColorParam,
|
||||
DynamicChoiceParam,
|
||||
File,
|
||||
FloatParam,
|
||||
GroupAttribute,
|
||||
|
@ -33,6 +34,7 @@ __all__ = [
|
|||
"BoolParam",
|
||||
"ChoiceParam",
|
||||
"ColorParam",
|
||||
"DynamicChoiceParam",
|
||||
"File",
|
||||
"FloatParam",
|
||||
"GroupAttribute",
|
||||
|
|
|
@ -2,7 +2,8 @@ import ast
|
|||
import distutils.util
|
||||
import os
|
||||
import types
|
||||
from collections.abc import Iterable
|
||||
from collections.abc import Iterable, Sequence
|
||||
from typing import Union
|
||||
|
||||
from meshroom.common import BaseObject, JSValue, Property, Variant, VariantList
|
||||
|
||||
|
@ -526,3 +527,100 @@ class ColorParam(Param):
|
|||
'color code (param: {}, value: {}, type: {})'.format(self.name, value, type(value)))
|
||||
return value
|
||||
|
||||
|
||||
class DynamicChoiceParam(GroupAttribute):
|
||||
"""
|
||||
Attribute supporting a single or multiple values, providing a list of predefined options that can be
|
||||
modified at runtime and serialized.
|
||||
"""
|
||||
|
||||
_PYTHON_BUILTIN_TO_PARAM_TYPE = {
|
||||
str: StringParam,
|
||||
int: IntParam,
|
||||
}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
label: str,
|
||||
description: str,
|
||||
value: Union[str, int, Sequence[Union[str, int]]],
|
||||
values: Union[Sequence[str], Sequence[int]],
|
||||
exclusive: bool=True,
|
||||
group: str="allParams",
|
||||
joinChar: str=" ",
|
||||
advanced: bool=False,
|
||||
enabled: bool=True,
|
||||
invalidate: bool=True,
|
||||
semantic: str="",
|
||||
validValue: bool=True,
|
||||
errorMessage: str="",
|
||||
visible: bool=True,
|
||||
exposed: bool=False,
|
||||
):
|
||||
# DynamicChoiceParam is a composed of:
|
||||
# - a child ChoiceParam to hold the attribute value and as a backend to expose a ChoiceParam-compliant API
|
||||
# - a child ListAttribute to hold the list of possible values
|
||||
|
||||
self._valueParam = ChoiceParam(
|
||||
name="choiceValue",
|
||||
label="Value",
|
||||
description="",
|
||||
value=value,
|
||||
# Initialize the list of possible values to pass description validation.
|
||||
values=values,
|
||||
exclusive=exclusive,
|
||||
group="",
|
||||
joinChar=joinChar,
|
||||
advanced=advanced,
|
||||
enabled=enabled,
|
||||
invalidate=invalidate,
|
||||
semantic=semantic,
|
||||
validValue=validValue,
|
||||
errorMessage=errorMessage,
|
||||
visible=visible,
|
||||
exposed=exposed,
|
||||
)
|
||||
|
||||
valueType: type = self._valueParam._valueType
|
||||
paramType = DynamicChoiceParam._PYTHON_BUILTIN_TO_PARAM_TYPE[valueType]
|
||||
|
||||
self._valuesParam = ListAttribute(
|
||||
name="choiceValues",
|
||||
label="Values",
|
||||
elementDesc=paramType(
|
||||
name="choiceEntry",
|
||||
label="Choice entry",
|
||||
description="A possible choice value",
|
||||
invalidate=False,
|
||||
value=valueType(),
|
||||
),
|
||||
description="List of possible choice values",
|
||||
group="",
|
||||
advanced=True,
|
||||
visible=False,
|
||||
exposed=False,
|
||||
)
|
||||
self._valuesParam._value = values
|
||||
|
||||
super().__init__(
|
||||
name=name,
|
||||
label=label,
|
||||
description=description,
|
||||
group=group,
|
||||
groupDesc=[self._valueParam, self._valuesParam],
|
||||
advanced=advanced,
|
||||
semantic=semantic,
|
||||
enabled=enabled,
|
||||
visible=visible,
|
||||
exposed=exposed,
|
||||
)
|
||||
|
||||
def getInstanceType(self):
|
||||
from meshroom.core.attribute import DynamicChoiceParam
|
||||
|
||||
return DynamicChoiceParam
|
||||
|
||||
values = Property(VariantList, lambda self: self._valuesParam._value, constant=True)
|
||||
exclusive = Property(bool, lambda self: self._valueParam.exclusive, constant=True)
|
||||
joinChar = Property(str, lambda self: self._valueParam.joinChar, constant=True)
|
||||
|
|
|
@ -15,6 +15,19 @@ class NodeWithStaticChoiceParam(desc.Node):
|
|||
),
|
||||
]
|
||||
|
||||
class NodeWithDynamicChoiceParam(desc.Node):
|
||||
inputs = [
|
||||
desc.DynamicChoiceParam(
|
||||
name="dynChoice",
|
||||
label="Dynamic Choice",
|
||||
description="A dynamic choice parameter",
|
||||
value="A",
|
||||
values=["A", "B", "C"],
|
||||
exclusive=True,
|
||||
exposed=True,
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
class TestStaticChoiceParam:
|
||||
@classmethod
|
||||
|
@ -47,3 +60,57 @@ class TestStaticChoiceParam:
|
|||
loadedNode = loadedGraph.node(node.name)
|
||||
|
||||
assert loadedNode.choice.value == "CustomValue"
|
||||
|
||||
|
||||
class TestDynamicChoiceParam:
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
registerNodeType(NodeWithDynamicChoiceParam)
|
||||
|
||||
@classmethod
|
||||
def teardown_class(cls):
|
||||
unregisterNodeType(NodeWithDynamicChoiceParam)
|
||||
|
||||
def test_resetDefaultValues(self, graphSavedOnDisk):
|
||||
graph: Graph = graphSavedOnDisk
|
||||
|
||||
node = graph.addNewNode(NodeWithDynamicChoiceParam.__name__)
|
||||
node.dynChoice.values = ["D", "E", "F"]
|
||||
node.dynChoice.value = "D"
|
||||
node.dynChoice.resetToDefaultValue()
|
||||
assert node.dynChoice.values == ["A", "B", "C"]
|
||||
assert node.dynChoice.value == "A"
|
||||
|
||||
def test_customValueIsSerialized(self, graphSavedOnDisk):
|
||||
graph: Graph = graphSavedOnDisk
|
||||
|
||||
node = graph.addNewNode(NodeWithDynamicChoiceParam.__name__)
|
||||
node.dynChoice.value = "CustomValue"
|
||||
graph.save()
|
||||
|
||||
loadedGraph = loadGraph(graph.filepath)
|
||||
loadedNode = loadedGraph.node(node.name)
|
||||
|
||||
assert loadedNode.dynChoice.value == "CustomValue"
|
||||
|
||||
def test_customValuesAreSerialized(self, graphSavedOnDisk):
|
||||
graph: Graph = graphSavedOnDisk
|
||||
|
||||
node = graph.addNewNode(NodeWithDynamicChoiceParam.__name__)
|
||||
node.dynChoice.values = ["D", "E", "F"]
|
||||
|
||||
graph.save()
|
||||
loadedGraph = loadGraph(graph.filepath)
|
||||
loadedNode = loadedGraph.node(node.name)
|
||||
|
||||
assert loadedNode.dynChoice.values == ["D", "E", "F"]
|
||||
|
||||
def test_duplicateNodeWithGroupAttributeDerivedAttribute(self):
|
||||
graph = Graph("")
|
||||
node = graph.addNewNode(NodeWithDynamicChoiceParam.__name__)
|
||||
node.dynChoice.values = ["D", "E", "F"]
|
||||
node.dynChoice.value = "G"
|
||||
duplicates = graph.duplicateNodes([node])
|
||||
duplicate = duplicates[node][0]
|
||||
assert duplicate.dynChoice.value == node.dynChoice.value
|
||||
assert duplicate.dynChoice.values == node.dynChoice.values
|
||||
|
|
Loading…
Add table
Reference in a new issue