From 20f08b0aeb84001016cf7c46aebcb72643928543 Mon Sep 17 00:00:00 2001 From: David Hammer <dhammer@mailbox.org> Date: Thu, 18 Nov 2021 00:20:59 +0100 Subject: [PATCH] Very fancy scene generation --- src/calng/CalibrationManager.py | 22 +- src/calng/base_correction.py | 16 +- src/calng/scenes.py | 444 +++++++++++++++++++------------- 3 files changed, 296 insertions(+), 186 deletions(-) diff --git a/src/calng/CalibrationManager.py b/src/calng/CalibrationManager.py index b57da967..68158430 100644 --- a/src/calng/CalibrationManager.py +++ b/src/calng/CalibrationManager.py @@ -308,7 +308,7 @@ class CalibrationManager(DeviceClientBase, Device): displayType='Scenes', requiredAccessLevel=AccessLevel.OBSERVER, accessMode=AccessMode.READONLY, - defaultValue=['overview'], + defaultValue=['overview', 'managed_keys'], daqPolicy=DaqPolicy.OMIT) @slot @@ -324,9 +324,23 @@ class CalibrationManager(DeviceClientBase, Device): self._domain_device_ids, ) payload = Hash('success', True, 'name', name, 'data', scene_data) - return Hash('type', 'deviceScene', - 'origin', self.deviceId, - 'payload', payload) + elif name.startswith('browse_schema'): + if ':' in name: + prefix = name[len('browse_schema:'):] + else: + prefix = 'managed' + scene_data = scenes.recursive_subschema_scene( + self.deviceId, + self.getDeviceSchema(), + prefix, + ) + payload = Hash('success', True, 'name', name, 'data', scene_data) + else: + payload = Hash('success', False, 'name', name) + + return Hash('type', 'deviceScene', + 'origin', self.deviceId, + 'payload', payload) detectorType = String( displayedName='Detector type', diff --git a/src/calng/base_correction.py b/src/calng/base_correction.py index 47f695f0..0a4f1eae 100644 --- a/src/calng/base_correction.py +++ b/src/calng/base_correction.py @@ -608,14 +608,24 @@ class BaseCorrection(PythonDevice): def requestScene(self, params): payload = Hash() - scene_name = params.get("name", default="") - payload["name"] = scene_name + name = params.get("name", default="") + payload["name"] = name payload["success"] = True - if scene_name == "overview": + if name == "overview": payload["data"] = scenes.correction_device_overview_scene( device_id=self.getInstanceId(), schema=self.getFullSchema(), ) + elif name.startswith("browse_schema"): + if ":" in name: + prefix = name[len("browse_schema:") :] + else: + prefix = "managed" + payload["data"] = scenes.recursive_subschema_scene( + self.getInstanceId(), + self.getFullSchema(), + prefix, + ) else: payload["success"] = False response = Hash() diff --git a/src/calng/scenes.py b/src/calng/scenes.py index c5e38373..12f3136e 100644 --- a/src/calng/scenes.py +++ b/src/calng/scenes.py @@ -20,12 +20,16 @@ from karabo.common.scenemodel.api import ( write_scene, ) +# section: common setup + BASE_INC = 25 NARROW_INC = 20 PADDING = 5 RECONFIGURABLE = 4 # TODO: look up proper enum +NODE_TYPE_NODE = 1 _type_to_line_editable = { + "BOOL": (CheckBoxModel, {"klass": "EditableCheckBox"}), "DOUBLE": (DoubleLineEditModel, {}), "FLOAT": (DoubleLineEditModel, {}), "INT32": (IntLineEditModel, {}), @@ -53,6 +57,9 @@ class Align(enum.Enum): RIGHT = enum.auto() +# section: nice component decorators + + def titled(title, width=8 * NARROW_INC): def actual_decorator(component_class): class new_class(component_class): @@ -105,6 +112,18 @@ def boxed(component_class): return new_class +# section: useful layout and utility classes + + +class Space: + def __init__(self, width, height): + self.width = width + self.height = height + + def render(self, x, y): + return [] + + class HorizontalLayout: def __init__(self, children=None, padding=PADDING): if children is None: @@ -126,7 +145,7 @@ class HorizontalLayout: y_ = y + (height - child.height) else: raise ValueError(f"Invalid align {align} for HorizontalLayout") - res.extend(safe_render(child, x, y)) + res.extend(safe_render(child, x, y_)) x += child.width + self.padding return res @@ -208,6 +227,8 @@ class MaybeEditableRow(HorizontalLayout): ] ) if key_attr["accessMode"] == RECONFIGURABLE: + if "valueType" not in key_attr: + return value_type = key_attr["valueType"] if value_type in _type_to_line_editable: line_editable_class, extra_args = _type_to_line_editable[value_type] @@ -221,7 +242,7 @@ class MaybeEditableRow(HorizontalLayout): ) else: self.children.append( - value_edit=LabelModel( + LabelModel( text=f"Not yet supported: editing {key_path} of type {value_type}", width=edit_width, height=height, @@ -229,51 +250,7 @@ class MaybeEditableRow(HorizontalLayout): ) -@titled("Parameters used for CalCat queries", width=10 * NARROW_INC) -@boxed -class ConstantParameterColumn(VerticalLayout): - def __init__(self, device_id, schema_hash, prefix="constantParameters"): - super().__init__(padding=0) - if "." in prefix: - extra_path_prefix = prefix[: prefix.rfind(".") + 1] - else: - extra_path_prefix = "" - self.children.extend( - [ - MaybeEditableRow( - device_id, - schema_hash, - f"{prefix}.{key}", - ) - for key in schema_hash.get(prefix).getKeys() - ] - ) - self.children.append( - DisplayCommandModel( - keys=[f"{device_id}.{extra_path_prefix}loadMostRecentConstants"], - width=10 * BASE_INC, - height=BASE_INC, - ) - ) - - -class ConstantNode(HorizontalLayout): - def __init__(self, device_id, constant_path): - super().__init__(padding=0) - self.children.extend( - [ - LabelModel( - text=constant_path.split(".")[-1], - width=7 * NARROW_INC, - height=NARROW_INC, - ), - ColorBoolModel( - width=NARROW_INC, - height=NARROW_INC, - keys=[f"{device_id}.{constant_path}.found"], - ), - ] - ) +# section: specific handcrafted components for device classes @titled("Found constants", width=6 * NARROW_INC) @@ -283,69 +260,31 @@ class FoundConstantsColumn(VerticalLayout): super().__init__(padding=0) self.children.extend( [ - ConstantNode(device_id, f"{prefix}.{constant_name}") + HorizontalLayout( + children=[ + LabelModel( + text=constant_name, + width=6 * NARROW_INC, + height=NARROW_INC, + ), + ColorBoolModel( + width=NARROW_INC, + height=NARROW_INC, + keys=[f"{device_id}.{prefix}.{constant_name}.found"], + ), + DisplayLabelModel( + keys=[f"{device_id}.{prefix}.{constant_name}.validFrom"], + width=8 * BASE_INC, + height=BASE_INC, + ), + ], + padding=0, + ) for constant_name in schema_hash.get(prefix).getKeys() ] ) -class Space: - def __init__(self, width, height): - self.width = width - self.height = height - - def render(self, x, y): - return [] - - -class CorrectionStepNode(HorizontalLayout): - def __init__(self, device_id, step_path): - super().__init__(padding=0) - self.children.extend( - [ - LabelModel( - text=step_path.split(".")[-1], width=7 * BASE_INC, height=BASE_INC - ), - CheckBoxModel( - keys=[f"{device_id}.{step_path}.enable"], - width=BASE_INC, - height=BASE_INC, - klass="EditableCheckBox", - ), - CheckBoxModel( - keys=[f"{device_id}.{step_path}.preview"], - width=BASE_INC, - height=BASE_INC, - klass="EditableCheckBox", - ), - ] - ) - - -@titled("Correction steps", width=6 * NARROW_INC) -@boxed -class CorrectionStepsColumn(VerticalLayout): - def __init__(self, device_id, schema_hash, prefix="corrections"): - super().__init__(padding=0) - self.children.append( - HorizontalLayout( - children=[ - Space(width=7 * BASE_INC, height=BASE_INC), - LabelModel( - text="enable/preview", width=6 * BASE_INC, height=BASE_INC - ), - ], - padding=0, - ) - ) - self.children.extend( - [ - CorrectionStepNode(device_id, f"{prefix}.{step_path}") - for step_path in schema_hash.get(prefix).getKeys() - ] - ) - - class ConstantLoadedAmpeln(HorizontalLayout): def __init__(self, device_id, schema_hash, prefix="foundConstants"): super().__init__(padding=0) @@ -366,52 +305,60 @@ class ConstantLoadedAmpeln(HorizontalLayout): class ManagerDeviceStatus(VerticalLayout): def __init__(self, device_id): super().__init__(padding=0) - self.name = DisplayLabelModel( + name = DisplayLabelModel( keys=[f"{device_id}.deviceId"], width=14 * BASE_INC, height=BASE_INC, ) - self.state = DisplayStateColorModel( + state = DisplayStateColorModel( show_string=True, keys=[f"{device_id}.state"], width=7 * BASE_INC, height=BASE_INC, ) - self.restart_button = DisplayCommandModel( + restart_button = DisplayCommandModel( keys=[f"{device_id}.restartServers"], width=7 * BASE_INC, height=BASE_INC, ) - self.instantiate_button = DisplayCommandModel( + instantiate_button = DisplayCommandModel( keys=[f"{device_id}.startInstantiate"], width=7 * BASE_INC, height=BASE_INC, ) - self.apply_button = DisplayCommandModel( + apply_button = DisplayCommandModel( keys=[f"{device_id}.applyManagedValues"], width=7 * BASE_INC, height=BASE_INC, ) - self.status_log = DisplayTextLogModel( + status_log = DisplayTextLogModel( keys=[f"{device_id}.status"], width=14 * BASE_INC, height=14 * BASE_INC, ) self.children.extend( [ - self.name, + name, HorizontalLayout( children=[ - self.state, - self.restart_button, + state, + restart_button, ], padding=0, ), HorizontalLayout( - children=[self.instantiate_button, self.apply_button], + children=[instantiate_button, apply_button], padding=0, ), - self.status_log, + DeviceSceneLinkModel( + text="All managed properties", + keys=[f"{device_id}.availableScenes"], + target="browse_schema", + target_window=SceneTargetWindow.Dialog, + width=7 * BASE_INC, + height=BASE_INC, + ), + status_log, ] ) @@ -421,57 +368,57 @@ class ManagerDeviceStatus(VerticalLayout): class CorrectionDeviceStatus(VerticalLayout): def __init__(self, device_id): super().__init__(padding=0) - self.name = DisplayLabelModel( + name = DisplayLabelModel( keys=[f"{device_id}.deviceId"], width=14 * BASE_INC, height=BASE_INC, ) - self.state = DisplayStateColorModel( + state = DisplayStateColorModel( show_string=True, keys=[f"{device_id}.state"], width=7 * BASE_INC, height=BASE_INC, ) - self.rate = EvaluatorModel( + rate = EvaluatorModel( expression="f'{x:.02f}'", keys=[f"{device_id}.performance.rate"], width=7 * BASE_INC, height=BASE_INC, ) - self.processing_time = EvaluatorModel( + processing_time = EvaluatorModel( expression="f'{x:.02f}'", - keys=[f"{device_id}.performance.processingDuration"], + keys=[f"{device_id}.performance.processingTime"], width=7 * BASE_INC, height=BASE_INC, ) - self.tid = DisplayLabelModel( + tid = DisplayLabelModel( keys=[f"{device_id}.trainId"], width=7 * BASE_INC, height=BASE_INC, ) - self.status_log = DisplayTextLogModel( + status_log = DisplayTextLogModel( keys=[f"{device_id}.status"], width=14 * BASE_INC, height=14 * BASE_INC, ) self.children.extend( [ - self.name, + name, HorizontalLayout( children=[ - self.state, - self.tid, + state, + tid, ], padding=0, ), HorizontalLayout( children=[ - self.rate, - self.processing_time, + rate, + processing_time, ], padding=0, ), - self.status_log, + status_log, ] ) @@ -511,33 +458,95 @@ class CompactCorrectionDeviceOverview(HorizontalLayout): ) -def correction_device_overview_scene(device_id, schema): +@titled("Other devices managed") +@boxed +class CompactDeviceLinkList(VerticalLayout): + def __init__(self, device_ids): + super().__init__() + self.children.extend( + [ + HorizontalLayout( + children=[ + DeviceSceneLinkModel( + text=device_id.split("/")[-1], + keys=[f"{device_id}.availableScenes"], + width=7 * BASE_INC, + height=BASE_INC, + ), + DisplayStateColorModel( + show_string=True, + keys=[f"{device_id}.state"], + width=7 * BASE_INC, + height=BASE_INC, + ), + ], + padding=0, + ) + for device_id in device_ids + ] + ) + + +# section: generating actual scenes + + +def schema_to_hash(schema): if isinstance(schema, karathon.Schema): - schema_hash = schema.getParameterHash() + return schema.getParameterHash() else: - schema_hash = schema.hash + return schema.hash + + +def scene_generator(fun): + # TODO: pretty decorator + def aux(*args, **kwargs): + content = fun(*args, **kwargs) + + scene = SceneModel( + children=content.render(PADDING, PADDING), + width=content.width + 2 * PADDING, + height=content.height + 2 * PADDING, + ) + return write_scene(scene) + + return aux - content = HorizontalLayout( + +@scene_generator +def correction_device_overview_scene(device_id, schema): + schema_hash = schema_to_hash(schema) + + return HorizontalLayout( children=[ CorrectionDeviceStatus(device_id), - ConstantParameterColumn(device_id, schema_hash), VerticalLayout( children=[ - FoundConstantsColumn(device_id, schema_hash), - CorrectionStepsColumn(device_id, schema_hash), + recursive_maybe_editable( + device_id, + schema_hash, + "constantParameters", + title="Parameters used for CalCat queries", + ), + DisplayCommandModel( + keys=[f"{device_id}.loadMostRecentConstants"], + width=10 * BASE_INC, + height=BASE_INC, + ), ] ), + FoundConstantsColumn(device_id, schema_hash), + recursive_maybe_editable( + device_id, + schema_hash, + "corrections", + max_depth=2, + title="Correction steps", + ), ] ) - scene = SceneModel( - children=content.render(PADDING, PADDING), - width=content.width + 2 * PADDING, - height=content.height + 2 * PADDING, - ) - return write_scene(scene) - +@scene_generator def manager_device_overview_scene( manager_device_id, manager_device_schema, @@ -548,62 +557,139 @@ def manager_device_overview_scene( mds_hash = schema_to_hash(manager_device_schema) cds_hash = schema_to_hash(correction_device_schema) - content = VerticalLayout( + return VerticalLayout( children=[ HorizontalLayout( children=[ ManagerDeviceStatus(manager_device_id), VerticalLayout( children=[ - ConstantParameterColumn( + recursive_maybe_editable( manager_device_id, mds_hash, - prefix="managed.constantParameters", + "managed.constantParameters", + title="Parameters used for CalCat queries", ), - CorrectionStepsColumn( - manager_device_id, - mds_hash, - prefix="managed.corrections", + DisplayCommandModel( + keys=[ + f"{manager_device_id}.managed.loadMostRecentConstants" + ], + width=10 * BASE_INC, + height=BASE_INC, ), ] ), - titled("Other devices managed")(boxed(VerticalLayout))( - children=[ - DeviceSceneLinkModel( - text=device_id.split("/")[-1], - keys=[f"{device_id}.availableScenes"], - width=14 * BASE_INC, - height=BASE_INC, - ) - for device_id in sorted( - set(domain_device_ids) - - set(correction_device_ids) - - {manager_device_id} - ) - ] + recursive_maybe_editable( + manager_device_id, + mds_hash, + "managed.corrections", + max_depth=2, ), ], ), - titled("Correction devices", width=8 * NARROW_INC)(boxed(VerticalLayout))( + HorizontalLayout( children=[ - CompactCorrectionDeviceOverview(device_id, cds_hash) - for device_id in sorted(correction_device_ids) - ], - padding=0, + titled("Correction devices", width=8 * NARROW_INC)( + boxed(VerticalLayout) + )( + children=[ + CompactCorrectionDeviceOverview(device_id, cds_hash) + for device_id in sorted(correction_device_ids) + ], + padding=0, + ), + CompactDeviceLinkList( + sorted( + set(domain_device_ids) + - set(correction_device_ids) + - {manager_device_id} + ) + ), + ] ), ] ) - scene = SceneModel( - width=content.width + 2 * PADDING, - height=content.height + 2 * PADDING, - children=content.render(PADDING, PADDING), - ) - return write_scene(scene) +# section: here be monsters -def schema_to_hash(schema): - if isinstance(schema, karathon.Schema): - return schema.getParameterHash() + +def recursive_maybe_editable( + device_id, schema_hash, prefix, depth=1, max_depth=3, title=None +): + if title is None: + title = prefix.split(".")[-1] + # note: not just using sets because that loses ordering + node_keys = [] + value_keys = [] + slot_keys = [] + for key in schema_hash.get(prefix).getKeys(): + attrs = schema_hash.getAttributes(f"{prefix}.{key}") + if attrs.get("nodeType") == NODE_TYPE_NODE: + if "classId" in attrs and attrs.get("classId") == "Slot": + slot_keys.append(key) + else: + node_keys.append(key) + else: + value_keys.append(key) + res = titled(title)(boxed(VerticalLayout))( + padding=0, + children=[ + MaybeEditableRow(device_id, schema_hash, f"{prefix}.{key}") + for key in value_keys + ] + + [ + DisplayCommandModel( + keys=[f"{device_id}.{prefix}.{key}"], + width=10 * BASE_INC, + height=BASE_INC, + ) + for key in slot_keys + ], + ) + if depth < max_depth: + res.children.extend( + [ + VerticalLayout( + children=[ + recursive_maybe_editable( + device_id, + schema_hash, + f"{prefix}.{key}", + depth=depth + 1, + max_depth=max_depth, + ) + for key in node_keys + ], + ) + ] + ) else: - return schema.hash + res.children.extend( + [ + VerticalLayout( + children=[ + DeviceSceneLinkModel( + text=key, + keys=[f"{device_id}.availableScenes"], + target=f"browse_schema:{prefix}.{key}", + target_window=SceneTargetWindow.Dialog, + width=5 * BASE_INC, + height=BASE_INC, + ), + ] + ) + for key in node_keys + ] + ) + return res + + +@scene_generator +def recursive_subschema_scene( + device_id, + device_schema, + prefix="managed", +): + mds_hash = schema_to_hash(device_schema) + return recursive_maybe_editable(device_id, mds_hash, prefix) -- GitLab