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