@@ -539,6 +539,8 @@ def matchText(self, text: str) -> bool:
539539 is2dDisplayable = Property (bool , _is2dDisplayable , constant = True )
540540 # Whether the attribute value is displayable in 3d.
541541 is3dDisplayable = Property (bool , _is3dDisplayable , constant = True )
542+ # Whether the attribute is a shape or a shape list, managed by the ShapeEditor and ShapeViewer.
543+ hasDisplayableShape = Property (bool , lambda self : False , constant = True )
542544
543545 # Attribute link properties and signals
544546 inputLinksChanged = Signal ()
@@ -999,3 +1001,261 @@ def matchText(self, text: str) -> bool:
9991001 # Override value property
10001002 value = Property (Variant , Attribute ._getValue , _setValue , notify = Attribute .valueChanged )
10011003 isDefault = Property (bool , lambda self : all (v .isDefault for v in self .value ), notify = Attribute .valueChanged )
1004+
1005+
1006+ class ShapeAttribute (GroupAttribute ):
1007+ """
1008+ GroupAttribute subtype tailored for shape-specific handling.
1009+ """
1010+
1011+ def __init__ (self , node , attributeDesc : desc .Shape , isOutput : bool ,
1012+ root = None , parent = None ):
1013+ self ._visible = True
1014+ self ._color = "#2A82DA" # default shape color
1015+ super ().__init__ (node , attributeDesc , isOutput , root , parent )
1016+
1017+ # Override
1018+ # Signal observationsChanged should be emitted.
1019+ def _setValue (self , exportedValue ):
1020+ super ()._setValue (exportedValue )
1021+ self .observationsChanged .emit ()
1022+
1023+ # Override
1024+ # Signal observationsChanged should be emitted.
1025+ def resetToDefaultValue (self ):
1026+ super ().resetToDefaultValue ()
1027+ self .observationsChanged .emit ()
1028+
1029+ # Override
1030+ # Signal observationsChanged should be emitted.
1031+ def upgradeValue (self , exportedValue ):
1032+ super ().upgradeValue (exportedValue )
1033+ self .observationsChanged .emit ()
1034+
1035+ # Override
1036+ # Fix missing link expression serialization.
1037+ # Should be remove if link expression serialization is added in GroupAttribute.
1038+ def getSerializedValue (self ):
1039+ if self .isLink :
1040+ return self ._getInputLink ().asLinkExpr ()
1041+ return {key : attr .getSerializedValue () for key , attr in self ._value .objects .items ()}
1042+
1043+ def getValueAsDict (self ) -> dict :
1044+ """
1045+ Return the shape attribute value as dict.
1046+ For not keyable shape, this is the same as getSerializedValue().
1047+ For keyable shape, the dict is indexed by key.
1048+ """
1049+ from collections import defaultdict
1050+ outValue = defaultdict (dict )
1051+ if self .isLink :
1052+ return self ._getInputLink ().asLinkExpr ()
1053+ if not self .shapeKeyable :
1054+ return super ().getSerializedValue ()
1055+ for attribute in self .value :
1056+ if isinstance (attribute , ShapeAttribute ):
1057+ attributeDict = attribute .getValueAsDict ()
1058+ if attributeDict :
1059+ for key , value in attributeDict .items ():
1060+ outValue [key ][attribute .name ] = value
1061+ else :
1062+ for pair in attribute .keyValues .pairs :
1063+ outValue [str (pair .key )][attribute .name ] = pair .value
1064+ return dict (outValue )
1065+
1066+ def _getVisible (self ) -> bool :
1067+ """
1068+ Return whether the shape attribute is visible for display.
1069+ """
1070+ return self ._visible
1071+
1072+ def _setVisible (self , visible :bool ):
1073+ """
1074+ Set the shape attribute visibility for display.
1075+ """
1076+ self ._visible = visible
1077+ self .shapeChanged .emit ()
1078+
1079+ def _getColor (self ) -> str :
1080+ """
1081+ Return the shape attribute color for display.
1082+ """
1083+ if self .isLink :
1084+ return self .inputLink .shapeColor
1085+ return self ._color
1086+
1087+ @raiseIfLink
1088+ def _setColor (self , color : str ):
1089+ """
1090+ Set the shape attribute color for display.
1091+ """
1092+ self ._color = color
1093+ self .shapeChanged .emit ()
1094+
1095+ def _hasKeyableChilds (self ) -> bool :
1096+ """
1097+ Whether all child attributes are keyable.
1098+ """
1099+ return all ((isinstance (attribute , ShapeAttribute ) and attribute .shapeKeyable ) or
1100+ attribute .keyable for attribute in self .value )
1101+
1102+ def _getNbObservations (self ) -> int :
1103+ """
1104+ Return the shape attribute number of observations.
1105+ Note: Observation is a value defined across all child attributes for a specific key.
1106+ """
1107+ if self .shapeKeyable :
1108+ firstAttribute = next (iter (self .value .values ()))
1109+ if isinstance (firstAttribute , ShapeAttribute ):
1110+ return firstAttribute .nbObservations
1111+ return len (firstAttribute .keyValues .pairs )
1112+ return 1
1113+
1114+ def _getObservationKeys (self ) -> list :
1115+ """
1116+ Return the shape attribute list of observation keys.
1117+ Note: Observation is a value defined across all child attributes for a specific key.
1118+ """
1119+ if not self .shapeKeyable :
1120+ return []
1121+ firstAttribute = next (iter (self .value .values ()))
1122+ if isinstance (firstAttribute , ShapeAttribute ):
1123+ return firstAttribute .observationKeys
1124+ return firstAttribute .keyValues .getKeys ()
1125+
1126+ @Slot (str , result = bool )
1127+ def hasObservation (self , key : str ) -> bool :
1128+ """
1129+ Whether the shape attribute has an observation for the given key.
1130+ Note: Observation is a value defined across all child attributes for a specific key.
1131+ """
1132+ if not self .shapeKeyable :
1133+ return True
1134+ return all ((isinstance (attribute , ShapeAttribute ) and attribute .hasObservation (key )) or
1135+ (not isinstance (attribute , ShapeAttribute ) and attribute .keyValues .hasKey (key ))
1136+ for attribute in self .value )
1137+
1138+ @raiseIfLink
1139+ def removeObservation (self , key : str ):
1140+ """
1141+ Remove the shape attribute observation for the given key.
1142+ Note: Observation is a value defined across all child attributes for a specific key.
1143+ """
1144+ for attribute in self .value :
1145+ if isinstance (attribute , ShapeAttribute ):
1146+ attribute .removeObservation (key )
1147+ else :
1148+ if attribute .keyable :
1149+ attribute .keyValues .remove (key )
1150+ else :
1151+ attribute .resetToDefaultValue ()
1152+ self .observationsChanged .emit ()
1153+
1154+ @raiseIfLink
1155+ def setObservation (self , key : str , observation : Variant ):
1156+ """
1157+ Set the shape attribute observation for the given key with the given observation.
1158+ Note: Observation is a value defined across all child attributes for a specific key.
1159+ """
1160+ for attributeStr , value in observation .items ():
1161+ attribute = self .childAttribute (attributeStr )
1162+ if attribute is None :
1163+ raise RuntimeError (f"Cannot set shape observation for attribute { self ._getFullName ()} \
1164+ observation is incorrect." )
1165+ if isinstance (attribute , ShapeAttribute ):
1166+ attribute .setObservation (key , value )
1167+ else :
1168+ if attribute .keyable :
1169+ attribute .keyValues .add (key , value )
1170+ else :
1171+ attribute .value = value
1172+ self .observationsChanged .emit ()
1173+
1174+ @Slot (str , result = Variant )
1175+ def getObservation (self , key : str ) -> Variant :
1176+ """
1177+ Return the shape attribute observation for the given key.
1178+ Note: Observation is a value defined across all child attributes for a specific key.
1179+ """
1180+ observation = {}
1181+ for attribute in self .value :
1182+ if isinstance (attribute , ShapeAttribute ):
1183+ shapeObservation = attribute .getObservation (key )
1184+ if shapeObservation is None :
1185+ return None
1186+ else :
1187+ observation [attribute .name ] = shapeObservation
1188+ else :
1189+ if attribute .keyable :
1190+ if attribute .keyValues .hasKey (key ):
1191+ observation [attribute .name ] = attribute .keyValues .getValueAtKeyOrDefault (key )
1192+ else :
1193+ return None
1194+ else :
1195+ observation [attribute .name ] = attribute .value
1196+ return observation
1197+
1198+ # Properties and signals
1199+ # Emitted when a shape related property changed (color, visibility).
1200+ shapeChanged = Signal ()
1201+ # Emitted when a shape observation changed.
1202+ observationsChanged = Signal ()
1203+ # Whether the shape is displayable.
1204+ isVisible = Property (bool , _getVisible , _setVisible , notify = shapeChanged )
1205+ # The shape color for display.
1206+ shapeColor = Property (str , _getColor , _setColor , notify = shapeChanged )
1207+ # The shape list of observation keys.
1208+ observationKeys = Property (Variant , _getObservationKeys , notify = observationsChanged )
1209+ # The number of observation defined.
1210+ nbObservations = Property (int , _getNbObservations , notify = observationsChanged )
1211+ # Whether the shape attribute childs are keyable.
1212+ shapeKeyable = Property (bool ,_hasKeyableChilds , constant = True )
1213+ # Override hasDisplayableShape property.
1214+ hasDisplayableShape = Property (bool , lambda self : True , constant = True )
1215+ # Override value property.
1216+ value = Property (Variant , Attribute ._getValue , _setValue , notify = Attribute .valueChanged )
1217+
1218+ class ShapeListAttribute (ListAttribute ):
1219+ """
1220+ ListAttribute subtype tailored for shape-specific handling.
1221+ """
1222+
1223+ def __init__ (self , node , attributeDesc : desc .ShapeList , isOutput : bool ,
1224+ root = None , parent = None ):
1225+ self ._visible = True
1226+ super ().__init__ (node , attributeDesc , isOutput , root , parent )
1227+
1228+ def getShapesAsDicts (self ):
1229+ """
1230+ Return the shape list attribute value as dict.
1231+ """
1232+ return [shapeAttribute .getValueAsDict () for shapeAttribute in self .value ]
1233+
1234+ def _getVisible (self ) -> bool :
1235+ """
1236+ Return whether the shape list is visible for display.
1237+ """
1238+ if self .isLink :
1239+ return self .inputLink .isVisible
1240+ return self ._visible
1241+
1242+ def _setVisible (self , visible :bool ):
1243+ """
1244+ Set the shape visibility for display.
1245+ """
1246+ if self .isLink :
1247+ self .inputLink .isVisible = visible
1248+ else :
1249+ self ._visible = visible
1250+ for attribute in self .value :
1251+ if isinstance (attribute , ShapeAttribute ):
1252+ attribute .isVisible = visible
1253+ self .shapeListChanged .emit ()
1254+
1255+ # Properties and signals
1256+ # Emitted when a shape list related property changed.
1257+ shapeListChanged = Signal ()
1258+ # Whether the shape list is displayable.
1259+ isVisible = Property (bool , _getVisible , _setVisible , notify = shapeListChanged )
1260+ # Override hasDisplayableShape property.
1261+ hasDisplayableShape = Property (bool , lambda self : True , constant = True )
0 commit comments