From c73a6987bbaf29f44f9bf6c8ea3768ab95223bf6 Mon Sep 17 00:00:00 2001 From: David Hammer <dhammer@mailbox.org> Date: Wed, 10 Nov 2021 13:53:00 +0100 Subject: [PATCH] Improve scene generation code --- src/calng/CalibrationManager.py | 1 + src/calng/scenes.py | 303 +++++++++++++++++++++----------- 2 files changed, 203 insertions(+), 101 deletions(-) diff --git a/src/calng/CalibrationManager.py b/src/calng/CalibrationManager.py index 874f48d1..ae009f0e 100644 --- a/src/calng/CalibrationManager.py +++ b/src/calng/CalibrationManager.py @@ -318,6 +318,7 @@ class CalibrationManager(DeviceClientBase, Device): # Assumes there are correction devices known to manager scene_data = scenes.manager_device_overview_scene( self.deviceId, + self.getDeviceSchema(), self._correction_device_schema, sorted(self._correction_device_ids)) payload = Hash('success', True, 'name', name, 'data', scene_data) diff --git a/src/calng/scenes.py b/src/calng/scenes.py index efc14e04..19759af3 100644 --- a/src/calng/scenes.py +++ b/src/calng/scenes.py @@ -1,3 +1,4 @@ +import enum import textwrap import karathon @@ -34,6 +35,14 @@ _type_to_line_editable = { } +class Align(enum.Enum): + CENTER = enum.auto() + TOP = enum.auto() + BOTTOM = enum.auto() + LEFT = enum.auto() + RIGHT = enum.auto() + + def titled(title, width=8): def actual_decorator(component_class): # should this create subclass instead of mutating? @@ -56,9 +65,9 @@ def titled(title, width=8): orig_height = component_class.height def new_height(self): - return orig_height(self) + BASE_INC + return orig_height.fget(self) + BASE_INC - component_class.height = new_height + component_class.height = property(fget=new_height) return component_class @@ -68,16 +77,16 @@ def titled(title, width=8): 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 + orig_height = component_class.height 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, + width=orig_width.fget(self) + 2 * PADDING, + height=orig_height.fget(self) + 2 * PADDING, stroke="#000000", ) ] + orig_render(self, x + PADDING, y + PADDING, *args, **kwargs) @@ -85,18 +94,87 @@ def boxed(component_class): component_class.render = new_render def new_height(self): - return orig_height(self) + 2 * PADDING + return orig_height.fget(self) + 2 * PADDING - component_class.height = new_height + component_class.height = property(fget=new_height) def new_width(self): - return orig_width(self) + 2 * PADDING + return orig_width.fget(self) + 2 * PADDING - component_class.width = new_width + component_class.width = property(fget=new_width) return component_class +class HorizontalLayout: + def __init__(self, children=None, padding=PADDING): + if children is None: + self.children = [] + else: + self.children = children + self.padding = padding + + def render(self, x, y, align=Align.TOP): + if align is not Align.TOP: + height = self.height + res = [] + for child in self.children: + if align is Align.TOP: + res.extend(child.render(x, y)) + elif align is Align.CENTER: + res.extend(child.render(x, y + (height - child.height) / 2)) + elif align is Align.BOTTOM: + res.extend(child.render(x, y + (height - child.height))) + else: + raise ValueError(f"Invalid align {align} for HorizontalLayout") + x += child.width + self.padding + return res + + @property + def width(self): + if not self.children: + return 0 + return self.padding * (len(self.children) - 1) + sum( + c.width for c in self.children + ) + + @property + def height(self): + if not self.children: + return 0 + return max(c.height for c in self.children) + + +class VerticalLayout: + def __init__(self, children=None, padding=PADDING): + if children is None: + self.children = [] + else: + self.children = children + self.padding = padding + + def render(self, x, y): + res = [] + for child in self.children: + res.extend(child.render(x, y)) + y += child.height + self.padding + return res + + @property + def width(self): + if not self.children: + return 0 + return max(c.width for c in self.children) + + @property + def height(self): + if not self.children: + return 0 + return self.padding * (len(self.children) - 1) + sum( + c.height for c in self.children + ) + + class MaybeEditableRow: def __init__( self, @@ -146,13 +224,15 @@ class MaybeEditableRow: height=BASE_INC, ) - def height(self): - return BASE_INC - + @property def width(self): return self.label_width + self.display_width + self.edit_width - def render(self, x=None, y=None): + @property + def height(self): + return BASE_INC + + def render(self, x, y): self.label.x = x self.label.y = y self.value_display.x = x + self.label_width @@ -176,36 +256,44 @@ class MaybeEditableRow: @boxed class ConstantParameterColumn: def __init__(self, device_id, schema_hash, prefix="constantParameters"): + if "." in prefix: + self.extra_path_prefix = prefix[: prefix.rfind(".") + 1] + else: + self.extra_path_prefix = "" self.device_id = device_id self.rows = [ - MaybeEditableRow(device_id, schema_hash, f"{prefix}.{key}") + MaybeEditableRow( + device_id, + schema_hash, + f"{prefix}.{key}", + ) for key in schema_hash.get(prefix).getKeys() ] + self.load_button = DisplayCommandModel( + keys=[f"{self.device_id}.{self.extra_path_prefix}loadMostRecentConstants"], + width=10 * BASE_INC, + height=BASE_INC, + ) 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, - ) - ) + y_offset += row.height + self.load_button.x = x + 7 * BASE_INC + self.load_button.y = y + y_offset + res.append(self.load_button) return res - def height(self): - return sum(row.height() for row in self.rows) + BASE_INC - + @property def width(self): - return max(row.width() for row in self.rows) + return max(row.width for row in self.rows) + + @property + def height(self): + return sum(row.height for row in self.rows) + BASE_INC class ConstantNode: @@ -227,12 +315,14 @@ class ConstantNode: y += BASE_INC return [self.title, self.ampel] - def height(self): - return BASE_INC - + @property def width(self): return 8 * BASE_INC + @property + def height(self): + return BASE_INC + @titled("Found constants", width=6) @boxed @@ -248,14 +338,16 @@ class FoundConstantsColumn: y_offset = 0 for row in self.rows: res.extend(row.render(x, y + y_offset)) - y_offset += row.height() + y_offset += row.height return res - def height(self): - return sum(row.height() for row in self.rows) - + @property def width(self): - return max(row.width() for row in self.rows) + return max(row.width for row in self.rows) + + @property + def height(self): + return sum(row.height for row in self.rows) class CorrectionStepNode: @@ -265,14 +357,14 @@ class CorrectionStepNode: ) self.checkbox_main = CheckBoxModel( keys=[f"{device_id}.{step_path}.enable"], - height=BASE_INC, width=BASE_INC, + height=BASE_INC, klass="EditableCheckBox", ) self.checkbox_preview = CheckBoxModel( keys=[f"{device_id}.{step_path}.preview"], - height=BASE_INC, width=BASE_INC, + height=BASE_INC, klass="EditableCheckBox", ) @@ -287,23 +379,19 @@ class CorrectionStepNode: self.checkbox_preview.y = y return [self.label, self.checkbox_main, self.checkbox_preview] - def height(self): - return BASE_INC - + @property def width(self): return 10 * BASE_INC + @property + def height(self): + return BASE_INC + @titled("Correction steps", width=6) @boxed class CorrectionStepsColumn: - def __init__( - self, device_id, schema_hash, prefix="corrections", control_prefix=None - ): - # separate control prefix allows iterating device schema, but setting on manager - if control_prefix is None: - control_prefix = prefix - + def __init__(self, device_id, schema_hash, prefix="corrections"): self.header_main = LabelModel( text="enable", width=3 * BASE_INC, height=BASE_INC ) @@ -311,7 +399,7 @@ class CorrectionStepsColumn: text="preview", width=3 * BASE_INC, height=BASE_INC ) self.steps = [ - CorrectionStepNode(device_id, f"{control_prefix}.{step_path}") + CorrectionStepNode(device_id, f"{prefix}.{step_path}") for step_path in schema_hash.get(prefix).getKeys() ] @@ -326,14 +414,16 @@ class CorrectionStepsColumn: step.x = x step.y = y res.extend(step.render(x, y)) - y += step.height() + y += step.height return res + @property def width(self): - return max(step.width() for step in self.steps) + return max(step.width for step in self.steps) + @property def height(self): - return sum(step.height() for step in self.steps) + BASE_INC + return sum(step.height for step in self.steps) + BASE_INC class ConstantLoadedAmpeln: @@ -355,9 +445,11 @@ class ConstantLoadedAmpeln: for i, key in enumerate(self.keys) ] + @property def width(self): return BASE_INC * len(self.keys) + @property def height(self): return BASE_INC @@ -425,9 +517,11 @@ class CorrectionDeviceStatus: self.status_log, ] + @property def width(self): return 14 * BASE_INC + @property def height(self): return 23 * BASE_INC @@ -475,9 +569,11 @@ class CompactCorrectionDeviceOverview: x += self.tid.width return [self.name, self.status, self.rate, self.tid] + self.ampeln.render(x, y) + @property def width(self): - return 19 * BASE_INC + self.ampeln.width() + return 19 * BASE_INC + self.ampeln.width + @property def height(self): return BASE_INC @@ -488,63 +584,68 @@ def correction_device_overview_scene(device_id, schema): else: schema_hash = schema.hash - status_overview = CorrectionDeviceStatus(device_id) - cpc = ConstantParameterColumn(device_id, schema_hash) - fcc = FoundConstantsColumn(device_id, schema_hash) - csc = CorrectionStepsColumn(device_id, schema_hash) - - subscenes = [] - x = PADDING - y = PADDING - subscenes.extend(status_overview.render(x, y)) - x += status_overview.width() + PADDING - subscenes.extend(cpc.render(x, y)) - x += cpc.width() + PADDING - subscenes.extend(fcc.render(x, y)) - y += fcc.height() + PADDING - subscenes.extend(csc.render(x, y)) + content = HorizontalLayout( + children=[ + CorrectionDeviceStatus(device_id), + ConstantParameterColumn(device_id, schema_hash), + VerticalLayout( + children=[ + FoundConstantsColumn(device_id, schema_hash), + CorrectionStepsColumn(device_id, schema_hash), + ] + ), + ] + ) scene = SceneModel( - height=max( - status_overview.height(), - cpc.height(), - fcc.height() + PADDING + csc.height(), - ) - + 2 * PADDING, - width=2 * PADDING - + 2 * PADDING - + status_overview.width() - + cpc.width() - + csc.width(), - children=subscenes, + children=content.render(PADDING, PADDING), + width=content.width + 2 * PADDING, + height=content.height + 2 * PADDING, ) return write_scene(scene) def manager_device_overview_scene( - manager_device_id, correction_device_schema, correction_device_ids + manager_device_id, + manager_device_schema, + correction_device_schema, + correction_device_ids, ): - if isinstance(correction_device_schema, karathon.Schema): - schema_hash = correction_device_schema.getParameterHash() - else: - schema_hash = correction_device_schema.hash - - x = PADDING - y = PADDING - subscenes = [] - correction_steps = CorrectionStepsColumn( - manager_device_id, schema_hash, control_prefix="managed.corrections" + mds_hash = schema_to_hash(manager_device_schema) + cds_hash = schema_to_hash(correction_device_schema) + + content = VerticalLayout( + children=[ + HorizontalLayout( + children=[ + ConstantParameterColumn( + manager_device_id, mds_hash, prefix="managed.constantParameters" + ), + CorrectionStepsColumn( + manager_device_id, mds_hash, prefix="managed.corrections" + ), + ] + ), + VerticalLayout( + children=[ + CompactCorrectionDeviceOverview(device_id, cds_hash) + for device_id in correction_device_ids + ], + padding=0, + ), + ] ) - subscenes.extend(correction_steps.render(x, y)) - y += correction_steps.height() + PADDING - for device_id in correction_device_ids: - ccdo = CompactCorrectionDeviceOverview(device_id, schema_hash) - subscenes.extend(ccdo.render(x, y)) - y += ccdo.height() scene = SceneModel( - height=y + PADDING, - width=ccdo.width() + 2 * PADDING, - children=subscenes, + width=content.width + 2 * PADDING, + height=content.height + 2 * PADDING, + children=content.render(PADDING, PADDING), ) return write_scene(scene) + + +def schema_to_hash(schema): + if isinstance(schema, karathon.Schema): + return schema.getParameterHash() + else: + return schema.hash -- GitLab