"""Acceptance for the panel layout, on its own. ``ViewLayoutController`` owns what a *view* is: which panels are on screen, in what arrangement, and which preset is active. It drives the panels through their own small interfaces and borrows a handful of window-level operations through ``LayoutHost``, so both can be stood in for — which is the point of the split, and what these tests use. """ from __future__ import annotations import pytest from orisys_studio.view_layout import ViewLayoutController VIEW_PRESETS = ("custom", "live", "replay", "calibration") VIEW_SHOWS_3D = ("custom", "live", "replay") VIEW_SPLITTER_SIZES = {"live": (0, 360, 1080, 0), "replay": (0, 360, 1080, 0), "calibration": (0, 0, 560, 880)} DEFAULT_SPLITTER_SIZES = (300, 360, 680, 0) MIN_PANEL_WIDTHS = {"mesh3d": 260, "arrow": 300, "plots": 340, "cal": 300} class _Action: def __init__(self, checked: bool = False) -> None: self.checked = bool(checked) self.enabled: bool | None = None self.blocked = 0 self.toggled = _Signal() def isChecked(self) -> bool: # noqa: N802 return self.checked def setChecked(self, value: bool) -> None: # noqa: N802 self.checked = bool(value) def setEnabled(self, value: bool) -> None: # noqa: N802 self.enabled = bool(value) def blockSignals(self, flag: bool) -> None: # noqa: N802 self.blocked += 1 class _Signal: def __init__(self) -> None: self.slots: list = [] def connect(self, slot) -> None: self.slots.append(slot) class _Panel: """A panel as the layout treats it: visibility, a minimum width, a stretch.""" def __init__(self) -> None: self.visible: bool | None = None self.minimum_width: int | None = None self.views = 0 def setVisible(self, visible: bool) -> None: # noqa: N802 self.visible = bool(visible) def setMinimumWidth(self, width: int) -> None: # noqa: N802 self.minimum_width = int(width) def ensure_view(self): self.views += 1 def set_enabled(self, enabled: bool) -> None: self.enabled = bool(enabled) def set_source_kind(self, kind: str) -> None: self.kind = str(kind) class _Splitter: def __init__(self, widgets) -> None: self._widgets = list(widgets) self.stretches: list[tuple[int, int]] = [] self.sizes: list = [] def count(self) -> int: return len(self._widgets) def widget(self, index: int): return self._widgets[index] def setStretchFactor(self, index: int, value: int) -> None: # noqa: N802 self.stretches.append((index, value)) def setSizes(self, sizes) -> None: # noqa: N802 self.sizes = list(sizes) class _Picker: def __init__(self, sensor_type: str = "box") -> None: self._sensor = sensor_type def sensor_type(self) -> str: return self._sensor class _Toolbar: def __init__(self, *, expanded: bool = False) -> None: self._expanded = expanded self.source_views: list[str] = [] self.run_roles: list[str] = [] self.reset_roles: list[str] = [] def set_source_view(self, view: str) -> None: self.source_views.append(view) def set_run_role(self, role: str) -> None: self.run_roles.append(role) def set_reset_role(self, role: str) -> None: self.reset_roles.append(role) def settings_expanded(self) -> bool: return self._expanded def set_settings_expanded(self, expanded: bool) -> None: self._expanded = bool(expanded) class _BrandBar: def __init__(self) -> None: self.active: list = [] def set_active_view(self, preset) -> None: self.active.append(preset) class _Replay: def __init__(self) -> None: self.paused = 0 self.sources: list[bool] = [] self.recomputed: list[bool] = [] self.refreshed = 0 self.synced = 0 def pause_playback(self) -> None: self.paused += 1 def apply_source(self, active: bool) -> None: self.sources.append(bool(active)) def on_recompute_toggled(self, checked: bool) -> None: self.recomputed.append(bool(checked)) def refresh_state(self) -> None: self.refreshed += 1 def sync_actions(self) -> None: self.synced += 1 class _Host: def __init__(self, prefs: dict) -> None: self.prefs = dict(prefs) self.persisted = 0 self.run_updates = 0 self.rail_updates = 0 self.source_kind = "live" def _persist_prefs(self) -> None: self.persisted += 1 def _update_run_enabled(self) -> None: self.run_updates += 1 def _refresh_status_rail(self) -> None: self.rail_updates += 1 def _current_source_kind(self) -> str: return self.source_kind def _device_geometry_provider(self): return None def _controller(*, prefs=None, sensor_type="box", settings_expanded=False): prefs = dict(prefs or {}) host = _Host(prefs) mesh, image, charts, cal = _Panel(), _Panel(), _Panel(), _Panel() splitter = _Splitter([mesh, image, charts, cal]) toolbar = _Toolbar(expanded=settings_expanded) brand = _BrandBar() replay = _Replay() controller = ViewLayoutController( host=host, splitter=splitter, mesh_panel=mesh, image_panel=image, charts=charts, cal_panel=cal, sensor_picker=_Picker(sensor_type), brand_bar=brand, toolbar=toolbar, replay=replay, presets=VIEW_PRESETS, shows_3d_in=VIEW_SHOWS_3D, splitter_sizes=VIEW_SPLITTER_SIZES, default_splitter_sizes=DEFAULT_SPLITTER_SIZES, min_panel_widths=MIN_PANEL_WIDTHS, default_3d_pane_width=340, ) actions = { "cal_panel": _Action(), "finger_3d": _Action(), "settings": _Action(), } controller.bind_actions(**actions) parts = { "host": host, "mesh": mesh, "image": image, "charts": charts, "cal": cal, "splitter": splitter, "toolbar": toolbar, "brand": brand, "replay": replay, "actions": actions, } return controller, parts # --------------------------------------------------------------------- tests def test_a_preset_decides_which_panels_a_view_shows(): controller, parts = _controller(prefs={"view_preset": "live"}) controller.apply_preset("calibration") assert controller.view_preset == "calibration" assert parts["cal"].visible is True assert parts["image"].visible is False, "the 2D panel does not belong to calibration" assert parts["charts"].visible is True assert parts["host"].prefs["view_preset"] == "calibration" assert parts["toolbar"].source_views[-1] == "live" assert parts["toolbar"].reset_roles[-1] == "reset" assert parts["host"].persisted == 1 controller.apply_preset("replay") assert parts["image"].visible is True assert parts["toolbar"].source_views[-1] == "replay" assert parts["toolbar"].run_roles[-1] == "playback" assert parts["toolbar"].reset_roles[-1] == "stop" # The charts follow the recording in this view, and the replay's own entries are # re-synced with it. (Calibration did neither of those: it has no replay source.) assert parts["replay"].sources == [True] assert parts["replay"].refreshed == 1 assert parts["replay"].synced == 2 assert parts["brand"].active[-1] == "replay" def test_an_unknown_preset_is_a_custom_layout_not_a_crash(): controller, parts = _controller() controller.apply_preset("nonsense") assert controller.view_preset == "custom" assert parts["host"].prefs["view_preset"] == "custom" assert parts["brand"].active[-1] is None def test_leaving_the_replay_view_stops_the_transport_and_drops_the_pass_label(): controller, parts = _controller(prefs={"view_preset": "replay"}) controller.apply_preset("live") assert parts["replay"].paused == 1, "the transport kept running across a view switch" assert parts["replay"].sources[-1] is False, "the charts still read the recording" assert parts["replay"].recomputed == [False], "the pass label survived the view" def test_the_3d_pane_needs_a_finger_sensor_and_its_own_switch(): controller, parts = _controller(prefs={"view_preset": "live"}, sensor_type="box") assert not controller.shows_3d() # A finger sensor alone is not enough either: the switch has to be on. controller._picker = _Picker("finger") controller.show_finger_3d = False assert not controller.shows_3d() controller.show_finger_3d = True assert controller.shows_3d() # ...and the calibration view never shows it. controller.apply_preset("calibration") assert not controller.shows_3d() assert parts["mesh"].visible is False assert parts["mesh"].views == 0, "a hidden pane must not build a GL view" def test_a_preset_that_gives_no_width_to_the_3d_pane_takes_it_from_the_widest(): controller, parts = _controller(prefs={"view_preset": "live"}, sensor_type="finger") controller.show_finger_3d = True sizes = controller.splitter_sizes_for("live") assert sizes[0] == 340, sizes assert sum(sizes) == sum(VIEW_SPLITTER_SIZES["live"]), "the layout changed width" # Without the pane the table is used as it stands. controller.show_finger_3d = False assert controller.splitter_sizes_for("live") == list(VIEW_SPLITTER_SIZES["live"]) def test_the_shipped_live_layout_makes_the_3d_pane_the_widest_panel(): """The 3D view is the subject when it is on, not an inset beside the charts.""" from orisys_studio.app_window import DEFAULT_SPLITTER_SIZES, VIEW_SPLITTER_SIZES as LIVE mesh, image, charts = DEFAULT_SPLITTER_SIZES assert mesh > image, DEFAULT_SPLITTER_SIZES assert mesh > charts, DEFAULT_SPLITTER_SIZES live_mesh, live_image, live_charts, _cal_live = LIVE["live"] assert live_mesh > live_image and live_mesh > live_charts, LIVE["live"] assert LIVE["replay"] == LIVE["live"], ( "the replay view is the live layout with another source" ) def test_the_calibration_panel_toggle_is_the_calibration_view_switch(): controller, parts = _controller(prefs={"view_preset": "live"}) controller.on_cal_panel_toggled(True) assert controller.view_preset == "calibration" assert controller.show_cal is True controller.on_cal_panel_toggled(False) assert controller.view_preset == "live" assert controller.show_cal is False assert parts["host"].prefs["show_cal_panel"] is False assert parts["brand"].active[-1] == "live" assert controller.show_cal is False def test_the_3d_pane_switch_does_not_drive_itself(): controller, parts = _controller(prefs={"view_preset": "live"}, sensor_type="finger") controller.show_finger_3d = True before = len(parts["mesh"].__dict__) # The pane is already on: the toggle says so again and nothing is written. controller.on_3d_pane_toggled(True) assert "show_finger_3d" not in parts["host"].prefs, ( "a no-op toggle still persisted a preference" ) controller.on_3d_pane_toggled(False) assert controller.show_finger_3d is False assert parts["host"].prefs["show_finger_3d"] is False assert parts["mesh"].visible is False assert len(parts["mesh"].__dict__) >= before # sanity: the panel was reused def test_the_view_menu_entries_follow_what_is_on_screen(): controller, parts = _controller(prefs={"view_preset": "live"}, sensor_type="box") controller.sync_view_checks() assert parts["actions"]["finger_3d"].enabled is False, "a box sensor has no scene" assert parts["actions"]["finger_3d"].checked is False assert parts["actions"]["cal_panel"].checked is False controller.on_cal_panel_toggled(True) assert parts["actions"]["cal_panel"].checked is True assert parts["actions"]["cal_panel"].blocked > 0, ( "the check was set without blocking its own signal" ) def test_the_settings_drawer_keeps_the_menu_entry_in_step(): controller, parts = _controller(prefs={"view_preset": "live"}) controller.set_settings_expanded(True) assert parts["toolbar"].settings_expanded() is True assert parts["actions"]["settings"].checked is True assert parts["host"].prefs["settings_expanded"] is True assert parts["host"].persisted == 1 controller.set_settings_expanded(False, persist=False) assert parts["actions"]["settings"].checked is False assert parts["host"].persisted == 1, "persist=False still wrote the preferences" def test_the_3d_pane_gets_the_keys_and_the_sensor_it_should_read(): controller, parts = _controller(prefs={"view_preset": "live"}, sensor_type="finger") seen = {} parts["mesh"].set_keys = lambda keys: seen.setdefault("keys", keys) parts["mesh"].set_sensor = lambda *a, **k: seen.setdefault("sensor", (a, k)) controller.sync_3d_source() assert seen["sensor"][0] == ("finger",), seen["sensor"] assert "keys" in seen