From 9e1c4a1287b224b039f6838b74aa6668b9bb4ef3 Mon Sep 17 00:00:00 2001
From: David Hammer <dhammer@mailbox.org>
Date: Thu, 28 Oct 2021 12:45:51 +0200
Subject: [PATCH] Much better device scene

---
 src/calng/base_correction.py |   8 +-
 src/calng/scenes.py          | 541 ++++++++++++++++++++++++-----------
 2 files changed, 373 insertions(+), 176 deletions(-)

diff --git a/src/calng/base_correction.py b/src/calng/base_correction.py
index 36fabc31..a6bb0069 100644
--- a/src/calng/base_correction.py
+++ b/src/calng/base_correction.py
@@ -222,7 +222,7 @@ class BaseCorrection(PythonDevice):
             .key("availableScenes")
             .setSpecialDisplayType(DT_SCENES)
             .readOnly()
-            .initialValue(["overview", "constants"])
+            .initialValue(["overview"])
             .commit(),
         )
 
@@ -590,14 +590,8 @@ class BaseCorrection(PythonDevice):
         payload["success"] = True
         if scene_name == "overview":
             payload["data"] = scenes.correction_device_overview_scene(
-                device_id=self.getInstanceId()
-            )
-        elif scene_name == "constants":
-            payload["data"] = scenes.correction_device_constants_scene(
                 device_id=self.getInstanceId(),
                 schema=self.getFullSchema(),
-                param_prefix="constantParameters",
-                status_prefix="foundConstants",
             )
         else:
             payload["success"] = False
diff --git a/src/calng/scenes.py b/src/calng/scenes.py
index aae1e480..a2f3fcf4 100644
--- a/src/calng/scenes.py
+++ b/src/calng/scenes.py
@@ -1,215 +1,418 @@
-from karabo.bound import Types
+import textwrap
+
+import karathon
 from karabo.common.scenemodel.api import (
+    ColorBoolModel,
     DisplayCommandModel,
     DisplayLabelModel,
     DisplayStateColorModel,
     DisplayTextLogModel,
     DoubleLineEditModel,
+    EvaluatorModel,
     IntLineEditModel,
+    LineEditModel,
     LabelModel,
     SceneModel,
     write_scene,
+    RectangleModel,
 )
 
 BASE_INC = 30
 PADDING = 10
 RECONFIGURABLE = 4  # TODO: look up proper enum
 
+_type_to_line_editable = {
+    "DOUBLE": (DoubleLineEditModel, {}),
+    "FLOAT": (DoubleLineEditModel, {}),
+    "INT32": (IntLineEditModel, {}),
+    "UINT32": (IntLineEditModel, {}),
+    "INT64": (IntLineEditModel, {}),
+    "UINT64": (IntLineEditModel, {}),
+    "STRING": (LineEditModel, {"klass": "EditableLineEdit"}),
+}
 
-def correction_device_constants_scene(device_id, schema, param_prefix, status_prefix):
-    # TODO: are there layout models in scene model somewhere? like gridbox?
-    subscenes = []
-    max_height = 0
-    # first column: parameters
-    row_offset = PADDING
-    col_offset = PADDING
-    subscenes.append(
-        LabelModel(
-            text="Parameters used for CalCat queries",
-            width=12 * BASE_INC,
-            x=col_offset,
-            y=row_offset,
-        )
-    )
-    row_offset += BASE_INC
-    for key in schema.getKeys(param_prefix):
-        key_path = f"{param_prefix}.{key}"
-        subscenes.append(
-            LabelModel(
-                text=schema.getDisplayedName(key_path)
-                if schema.hasDisplayedName(key_path)
-                else key,
-                width=5 * BASE_INC,
-                height=BASE_INC,
-                x=col_offset,
-                y=row_offset,
-            )
-        )
-        subscenes.append(
-            DisplayLabelModel(
-                keys=[f"{device_id}.{param_prefix}.{key}"],
-                width=7 * BASE_INC,
-                height=BASE_INC,
-                x=col_offset + 5 * BASE_INC,
-                y=row_offset,
+
+def titled(title, width=8):
+    def actual_decorator(component_class):
+        # should this create subclass instead of mutating?
+        orig_render = component_class.render
+
+        def new_render(self, x, y, *args, **kwargs):
+            return [
+                LabelModel(
+                    frame_width=1,
+                    text=title,
+                    width=width * BASE_INC,
+                    height=BASE_INC,
+                    x=x,
+                    y=y,
+                )
+            ] + orig_render(self, x, y + BASE_INC, *args, **kwargs)
+
+        component_class.render = new_render
+
+        orig_height = component_class.height
+
+        def new_height(self):
+            return orig_height(self) + BASE_INC
+
+        component_class.height = new_height
+
+        return component_class
+
+    return actual_decorator
+
+
+def boxed(component_class):
+    # should this create subclass instead of mutating?
+    orig_render = component_class.render
+    orig_height = component_class.height
+    orig_width = component_class.width
+
+    def new_render(self, x, y, *args, **kwargs):
+        return [
+            RectangleModel(
+                x=x,
+                y=y,
+                width=orig_width(self) + 2 * PADDING,
+                height=orig_height(self) + 2 * PADDING,
+                stroke="#000000",
             )
+        ] + orig_render(self, x + PADDING, y + PADDING, *args, **kwargs)
+
+    component_class.render = new_render
+
+    def new_height(self):
+        return orig_height(self) + 2 * PADDING
+
+    component_class.height = new_height
+
+    def new_width(self):
+        return orig_width(self) + 2 * PADDING
+
+    component_class.width = new_width
+
+    return component_class
+
+
+class MaybeEditableRow:
+    def __init__(
+        self,
+        device_id,
+        schema_hash,
+        key_path,
+        label_width=7,
+        display_width=5,
+        edit_width=5,
+    ):
+        self.label_width = label_width * BASE_INC
+        self.display_width = display_width * BASE_INC
+        self.edit_width = edit_width * BASE_INC
+
+        key_attr = schema_hash.getAttributes(key_path)
+        label_text = textwrap.shorten(
+            key_attr["displayedName"]
+            if "displayedName" in key_attr
+            else key_path.split(".")[-1],
+            24,
         )
-        if schema.getAccessMode(key_path) == RECONFIGURABLE:
-            value_type = schema.getValueType(key_path)
-            if value_type in (Types.DOUBLE, Types.FLOAT):
-                subscenes.append(
-                    DoubleLineEditModel(
-                        keys=[f"{device_id}.{param_prefix}.{key}"],
-                        width=7 * BASE_INC,
-                        height=BASE_INC,
-                        x=col_offset + 12 * BASE_INC,
-                        y=row_offset,
-                    )
-                )
-            elif value_type in (Types.INT32, Types.UINT32, Types.INT64, Types.UINT64):
-                subscenes.append(
-                    IntLineEditModel(
-                        keys=[f"{device_id}.{param_prefix}.{key}"],
-                        width=7 * BASE_INC,
-                        height=BASE_INC,
-                        x=col_offset + 12 * BASE_INC,
-                        y=row_offset,
-                    )
-                )
-            else:
-                subscenes.append(
-                    LabelModel(
-                        text=f"Not yet supported: editing {key} of type {value_type}",
-                        width=7 * BASE_INC,
-                        height=BASE_INC,
-                        x=col_offset + 12 * BASE_INC,
-                        y=row_offset,
-                    )
-                )
 
-        row_offset += BASE_INC
-    max_height = max(max_height, row_offset)
-
-    # second column: constants as currently loaded
-    row_offset = PADDING
-    col_offset += 19 * BASE_INC  # width of first column
-    col_offset += PADDING  # and padding
-    subscenes.append(
-        DisplayCommandModel(
-            keys=[f"{device_id}.loadMostRecentConstants"],
-            x=col_offset,
-            y=row_offset,
-            width=10 * BASE_INC,
+        self.label = LabelModel(
+            text=label_text,
+            width=self.label_width,
             height=BASE_INC,
         )
-    )
-    row_offset += BASE_INC
-    subscenes.append(
-        LabelModel(
-            text="Constants found and loaded",
-            width=10 * BASE_INC,
+        self.value_display = DisplayLabelModel(
+            keys=[f"{device_id}.{key_path}"],
+            width=self.display_width,
             height=BASE_INC,
-            x=col_offset,
-            y=row_offset,
         )
-    )
-    row_offset += BASE_INC
-    for constant_name in schema.getKeys(status_prefix):
-        constant_path = f"{status_prefix}.{constant_name}"
-        subscenes.append(
-            LabelModel(
-                text=constant_name,
-                width=10 * BASE_INC,
-                height=BASE_INC,
-                x=col_offset,
-                y=row_offset,
-            )
-        )
-        row_offset += BASE_INC
-        for field in ("found", "createdAt"):
-            subscenes.append(
-                LabelModel(
-                    text=field,  # TODO: displayedName
-                    width=5 * BASE_INC,
+        if key_attr["accessMode"] == RECONFIGURABLE:
+            value_type = key_attr["valueType"]
+            if value_type in _type_to_line_editable:
+                line_editable_class, extra_args = _type_to_line_editable[value_type]
+                self.value_edit = line_editable_class(
+                    keys=[f"{device_id}.{key_path}"],
+                    width=self.edit_width,
                     height=BASE_INC,
-                    x=col_offset,
-                    y=row_offset,
+                    **extra_args,
                 )
-            )
-            subscenes.append(
-                DisplayLabelModel(
-                    keys=[f"{device_id}.{constant_path}.{field}"],
-                    width=7 * BASE_INC,
+            else:
+                self.value_edit = LabelModel(
+                    text=f"Not yet supported: editing {key_path} of type {value_type}",
+                    width=self.edit_width,
                     height=BASE_INC,
-                    x=col_offset + 5 * BASE_INC,
-                    y=row_offset,
                 )
+
+    def height(self):
+        return BASE_INC
+
+    def width(self):
+        return self.label_width + self.display_width + self.edit_width
+
+    def render(self, x=None, y=None):
+        self.label.x = x
+        self.label.y = y
+        self.value_display.x = x + self.label_width
+        self.value_display.y = y
+        if hasattr(self, "value_edit"):
+            self.value_edit.x = x + self.label_width + self.display_width
+            self.value_edit.y = y
+            return [
+                self.label,
+                self.value_display,
+                self.value_edit,
+            ]
+        else:
+            return [
+                self.label,
+                self.value_display,
+            ]
+
+
+@titled("Parameters used for CalCat queries", width=10)
+@boxed
+class ConstantParameterColumn:
+    def __init__(self, device_id, schema_hash, prefix="constantParameters"):
+        self.device_id = device_id
+        self.rows = [
+            MaybeEditableRow(device_id, schema_hash, f"{prefix}.{key}")
+            for key in schema_hash.get(prefix).getKeys()
+        ]
+
+    def render(self, x, y):
+        res = []
+        y_offset = 0
+        for row in self.rows:
+            res.extend(row.render(x=x, y=y + y_offset))
+            y_offset += row.height()
+
+        res.append(
+            DisplayCommandModel(
+                keys=[f"{self.device_id}.loadMostRecentConstants"],
+                x=x + 7 * BASE_INC,
+                y=y + y_offset,
+                width=10 * BASE_INC,
+                height=BASE_INC,
             )
-            row_offset += BASE_INC
-    max_height = max(max_height, row_offset)
+        )
 
-    scene = SceneModel(
-        height=max_height + 2 * PADDING,
-        width=col_offset + 12 * BASE_INC + PADDING,
-        children=subscenes,
-    )
-    return write_scene(scene)
+        return res
 
+    def height(self):
+        return sum(row.height() for row in self.rows) + BASE_INC
 
-def correction_device_overview_scene(device_id):
-    subscenes = []
-    row_offset = 0
-    # device class and name
-    subscenes.append(
-        LabelModel(
-            text="Correction device",
-            width=5 * BASE_INC,
+    def width(self):
+        return max(row.width() for row in self.rows)
+
+
+class ConstantNode:
+    def __init__(self, device_id, constant_path):
+        self.title = LabelModel(
+            text=constant_path.split(".")[-1],
+            width=7 * BASE_INC,
             height=BASE_INC,
-            x=0,
-            y=row_offset,
         )
-    )
-    subscenes.append(
-        DisplayLabelModel(
+        self.ampel = ColorBoolModel(
+            height=BASE_INC, width=BASE_INC, keys=[f"{device_id}.{constant_path}.found"]
+        )
+
+    def render(self, x, y):
+        self.title.x = x
+        self.title.y = y
+        self.ampel.x = x + 7 * BASE_INC
+        self.ampel.y = y
+        y += BASE_INC
+        return [self.title, self.ampel]
+
+    def height(self):
+        return BASE_INC
+
+    def width(self):
+        return 8 * BASE_INC
+
+
+@titled("Found constants", width=6)
+@boxed
+class FoundConstantsColumn:
+    def __init__(self, device_id, schema_hash, prefix="foundConstants"):
+        self.device_id = device_id
+        self.rows = [
+            ConstantNode(device_id, f"{prefix}.{constant_name}")
+            for constant_name in schema_hash.get(prefix).getKeys()
+        ]
+
+    def render(self, x, y):
+        res = []
+        y_offset = 0
+        for row in self.rows:
+            res.extend(row.render(x, y + y_offset))
+            y_offset += row.height()
+        return res
+
+    def height(self):
+        return sum(row.height() for row in self.rows)
+
+    def width(self):
+        return max(row.width() for row in self.rows)
+
+
+class ConstantLoadedAmpeln:
+    def __init__(self, device_id, schema_hash, prefix="foundConstants"):
+        self.keys = [
+            f"{device_id}.{prefix}.{key}.found"
+            for key in schema_hash.get(prefix).getKeys()
+        ]
+
+    def render(self, x, y):
+        return [
+            ColorBoolModel(
+                x=x + i * BASE_INC,
+                y=y,
+                height=BASE_INC,
+                width=BASE_INC,
+                keys=[key],
+            )
+            for i, key in enumerate(self.keys)
+        ]
+
+    def width(self):
+        return BASE_INC * len(self.keys)
+
+    def height(self):
+        return BASE_INC
+
+
+@titled("Device status", width=6)
+@boxed
+class CorrectionDeviceStatus:
+    def __init__(self, device_id):
+        self.name = DisplayLabelModel(
             keys=[f"{device_id}.deviceId"],
-            width=10 * BASE_INC,
+            width=14 * BASE_INC,
             height=BASE_INC,
-            x=5 * BASE_INC,
-            y=row_offset,
         )
-    )
-    row_offset += BASE_INC
-    # device state
-    subscenes.append(
-        DisplayStateColorModel(
+        self.state = DisplayStateColorModel(
+            show_string=True,
             keys=[f"{device_id}.state"],
-            width=5 * BASE_INC,
+            width=7 * BASE_INC,
             height=BASE_INC,
-            # parent_component="DisplayComponent",
-            x=0,
-            y=row_offset,
         )
-    )
-    subscenes.append(
-        DisplayLabelModel(
-            keys=[f"{device_id}.state"],
-            width=5 * BASE_INC,
+        self.rate = EvaluatorModel(
+            expression="f'{x:.02f}'",
+            keys=[f"{device_id}.performance.rate"],
+            width=7 * BASE_INC,
             height=BASE_INC,
-            x=0,
-            y=row_offset,
         )
-    )
-    row_offset += BASE_INC
-    # log of status
-    subscenes.append(
-        DisplayTextLogModel(
+        self.processing_time = EvaluatorModel(
+            expression="f'{x:.02f}'",
+            keys=[f"{device_id}.performance.processingDuration"],
+            width=7 * BASE_INC,
+            height=BASE_INC,
+        )
+        self.tid = DisplayLabelModel(
+            keys=[f"{device_id}.trainId"],
+            width=7 * BASE_INC,
+            height=BASE_INC,
+        )
+        self.status_log = DisplayTextLogModel(
             keys=[f"{device_id}.status"],
-            width=10 * BASE_INC,
-            height=10 * BASE_INC,
-            x=0,
-            y=row_offset,
+            width=14 * BASE_INC,
+            height=20 * BASE_INC,
+        )
+
+    def render(self, x, y):
+        self.name.x = x
+        self.name.y = y
+        y += BASE_INC
+        self.state.x = x
+        self.state.y = y
+        self.tid.x = x + 7 * BASE_INC
+        self.tid.y = y
+        y += BASE_INC
+        self.rate.x = x
+        self.rate.y = y
+        self.processing_time.x = x + 7 * BASE_INC
+        self.processing_time.y = y
+        y += BASE_INC
+        self.status_log.x = x
+        self.status_log.y = y
+        return [
+            self.name,
+            self.state,
+            self.rate,
+            self.processing_time,
+            self.tid,
+            self.status_log,
+        ]
+
+    def width(self):
+        return 14 * BASE_INC
+
+    def height(self):
+        return 23 * BASE_INC
+
+
+class CompactCorrectionDeviceOverview:
+    def __init__(self, device_id, schema_hash):
+        self.status = DisplayStateColorModel(
+            show_string=True,
+            keys=[f"{device_id}.state"],
+            width=6 * BASE_INC,
+            height=BASE_INC,
         )
+        self.rate = EvaluatorModel(
+            expression="f'{x:.02f}'",
+            keys=[f"{device_id}.performance.rate"],
+            width=4 * BASE_INC,
+            height=BASE_INC,
+        )
+        self.tid = DisplayLabelModel(
+            keys=[f"{device_id}.trainId"],
+            width=4 * BASE_INC,
+            height=BASE_INC,
+        )
+        self.ampeln = ConstantLoadedAmpeln(device_id, schema_hash)
+
+    def render(self, x, y):
+        self.status.x = x
+        self.status.y = y
+        x += self.status.width
+        self.rate.x = x
+        self.rate.y = y
+        x += self.rate.width
+        self.tid.x = x
+        self.tid.y = y
+        x += self.tid.width
+        return [self.status, self.rate, self.tid] + self.ampeln.render(x, y)
+
+
+def correction_device_overview_scene(device_id, schema):
+    if isinstance(schema, karathon.Schema):
+        schema_hash = schema.getParameterHash()
+    else:
+        schema_hash = schema.hash
+
+    status_overview = CorrectionDeviceStatus(device_id)
+    cpc = ConstantParameterColumn(device_id, schema_hash)
+    fcc = FoundConstantsColumn(device_id, schema_hash)
+
+    subscenes = []
+    x = PADDING
+    y = PADDING
+    subscenes.extend(status_overview.render(x, y))
+    x += status_overview.width() + BASE_INC
+    subscenes.extend(cpc.render(x, y))
+    x += cpc.width() + BASE_INC
+    subscenes.extend(fcc.render(x, y))
+
+    scene = SceneModel(
+        height=max(status_overview.height(), cpc.height()) + 2 * PADDING,
+        width=2 * PADDING
+        + 2 * BASE_INC
+        + status_overview.width()
+        + cpc.width()
+        + fcc.width(),
+        children=subscenes,
     )
-    row_offset += BASE_INC * 10
-    scene = SceneModel(height=845.0, width=742.0, children=subscenes)
     return write_scene(scene)
-- 
GitLab