diff --git a/csaxs_bec/bec_ipython_client/plugins/omny/AI_docs/OPEN_ISSUES.md b/csaxs_bec/bec_ipython_client/plugins/omny/AI_docs/OPEN_ISSUES.md index ee91ca86..6dd51135 100644 --- a/csaxs_bec/bec_ipython_client/plugins/omny/AI_docs/OPEN_ISSUES.md +++ b/csaxs_bec/bec_ipython_client/plugins/omny/AI_docs/OPEN_ISSUES.md @@ -105,6 +105,20 @@ Still completely untested (not attempted yet, not known-broken — just unexplor yet — out of scope for the sim-validation work, in scope for the eventual macro- standardization pass. +## gui_tools.py progress fields (deferred on purpose) + +`gui_tools.py` was modernized to flomni's current single-level `.new()` / `_is_deleted()`-guard +pattern (window lifecycle, xray-eye-align GUI, cameras + hard-stop console, `RingProgressBar`). +One piece was deliberately **not** ported: flomni's `self.progress` dict carries +`tomo_start_time`/`heartbeat`/`estimated_remaining_time`/`estimated_finish_time` plus a +`_describe_active_hook()` method feeding an ETA/heartbeat line in `_flomnigui_update_progress()`. +omny's `self.progress` (set up in `OMNY.__init__`, `omny.py`) still only has the original 7 fields +(`subtomo`, `subtomo_projection`, `subtomo_total_projections`, `projection`, +`total_projections`, `angle`, `tomo_type`) and `_omnygui_update_progress()` was written against +just those. Mirko's call: keep it simple for this pass, revisit later. Not a crash risk — +`omny_webpage_generator.py` already reads the ETA-ish fields defensively via `.get()`, degrading +to "N/A" when absent, and that file is unwired/dead code anyway (see below). + ## Environment note (not code, but will bite again if forgotten) Both `csaxs_bec` and `bec_widgets` were pip-installed editable pointing at pre-repo- diff --git a/csaxs_bec/bec_ipython_client/plugins/omny/gui_tools.py b/csaxs_bec/bec_ipython_client/plugins/omny/gui_tools.py index 89ef27f5..d2a41535 100644 --- a/csaxs_bec/bec_ipython_client/plugins/omny/gui_tools.py +++ b/csaxs_bec/bec_ipython_client/plugins/omny/gui_tools.py @@ -1,12 +1,16 @@ import builtins +import time -# from csaxs_bec.bec_ipython_client.plugins.cSAXS import epics_get, epics_put, fshopen, fshclose +from bec_lib import bec_logger + +logger = bec_logger.logger if builtins.__dict__.get("bec") is not None: bec = builtins.__dict__.get("bec") dev = builtins.__dict__.get("dev") scans = builtins.__dict__.get("scans") + def umv(*args): return scans.umv(*args, relative=False) @@ -16,76 +20,184 @@ class OMNYGuiToolsError(Exception): class OMNYGuiTools: + # Screen assumed 2560x1440. Window is right-aligned with a small margin + # from the top, matching flomni's gui_tools.py convention. + _SCREEN_WIDTH = 2560 + _WINDOW_WIDTH = 1500 + _WINDOW_HEIGHT = 850 + _WINDOW_TOP_MARGIN = 50 - def __init__(self, client): - self.gui = getattr(client, "gui", None) - # No "main" BEC-widgets window exists yet in --nogui sessions, or before - # a GUI has actually been opened; fall back to None like flomni's - # GuiTools does (self.flomni_window, lazily populated by - # flomnigui_show_gui()), instead of crashing OMNY's construction. - try: - self.gui_window = self.gui.windows["main"].widget - except (KeyError, AttributeError): - self.gui_window = None + def __init__(self): + self.omny_window = None + self.text_box = None + self.progressbar = None + self.xeyegui = None + self.idle_text_box = None self.fig200 = None self.fig201 = None self.fig202 = None self.fig203 = None - self.progressbar = None - self.text_box = None - self.idle_text_box = None + self.console = None + + def set_client(self, client): + self.client = client + self.gui = getattr(client, "gui", None) def omnygui_show_gui(self): - self.gui_window.show() + if "omny" in self.gui.windows: + self.omny_window = self.gui.windows["omny"] + self.gui.omny.raise_window() + else: + # geometry: (pos_x, pos_y, w, h) + pos_x = self._SCREEN_WIDTH - self._WINDOW_WIDTH + geometry = (pos_x, self._WINDOW_TOP_MARGIN, self._WINDOW_WIDTH, self._WINDOW_HEIGHT) + self.omny_window = self.gui.new("omny", geometry=geometry) + time.sleep(1) def omnygui_stop_gui(self): - self.gui_window.hide() + self.gui.omny.hide() + + def omnygui_raise(self): + self.gui.omny.raise_window() + + def _omnygui_is_missing(self, attribute_name): + widget = getattr(self, attribute_name, None) + if widget is None: + return True + if hasattr(widget, "_is_deleted") and widget._is_deleted(): + return True + return False + + def _omnycam_check_device_exists(self, device): + try: + device + except: + return False + else: + return True + + def omnygui_show_xeyealign(self): + self.omnygui_show_gui() + if self._omnygui_is_missing("xeyegui"): + self.omnygui_remove_all_docks() + self.xeyegui = self.gui.omny.new("OMNY_XRayEye", object_name="xrayeye") + # cam_xeye is not (yet) part of omny's device config -- guard rather + # than assume it exists, unlike flomni's unconditional version. + if "cam_xeye" in dev and not dev.cam_xeye.live_mode_enabled.get(): + dev.cam_xeye.live_mode_enabled.put(True) + self.xeyegui.switch_tab("alignment") + + def omnygui_show_xeyealign_fittab(self): + self.omnygui_show_gui() + if self._omnygui_is_missing("xeyegui"): + self.omnygui_remove_all_docks() + self.xeyegui = self.gui.omny.new("OMNY_XRayEye", object_name="xrayeye") + self.xeyegui.switch_tab("fit") def _omnycam_parking(self): self.omnygui_show_omnycam_parking() def omnygui_show_omnycam_parking(self): self.omnygui_show_gui() - if self.fig200 is None: - self._omnycam_clear() - self.fig200 = self.gui_window.add_dock(name="omnycam200").add_widget("BECImageWidget") + if self._omnygui_is_missing("fig200") or self._omnygui_is_missing("fig203"): + self.omnygui_remove_all_docks() + self.fig200 = self.gui.omny.new("BECImageWidget") if self._omnycam_check_device_exists(dev.cam200): fig = self.fig200.image("cam200") fig.set_rotation(deg_90=3) self.fig200.lock_aspect_ratio(True) else: print("Cannot open cam200. Device does not exist.") - self.fig203 = self.gui_window.add_dock(name="omnycam203").add_widget("BECImageWidget") + self.fig203 = self.gui.omny.new("BECImageWidget") if self._omnycam_check_device_exists(dev.cam203): fig = self.fig203.image("cam203") fig.set_rotation(deg_90=3) self.fig203.lock_aspect_ratio(True) else: print("Cannot open cam203. Device does not exist.") - try: - self.gui_window.remove_dock(name="default_figure") - except: - pass + + def _omnycam_samplestage(self): + self.omnygui_show_omnycam_samplestage() + + def omnygui_show_omnycam_samplestage(self): + self.omnygui_show_gui() + if self._omnygui_is_missing("fig201") or self._omnygui_is_missing("fig202"): + self.omnygui_remove_all_docks() + self.fig201 = self.gui.omny.new("BECImageWidget") + if self._omnycam_check_device_exists(dev.cam201): + fig = self.fig201.image("cam201") + fig.set_rotation(deg_90=3) + self.fig201.lock_aspect_ratio(True) + else: + print("Cannot open cam201. Device does not exist.") + self.fig202 = self.gui.omny.new("BECImageWidget") + if self._omnycam_check_device_exists(dev.cam202): + fig = self.fig202.image("cam202") + fig.set_rotation(deg_90=3) + self.fig202.lock_aspect_ratio(True) + else: + print("Cannot open cam202. Device does not exist.") + + def omnygui_show_cameras(self): + """General camera view + hard-stop console, the omny analog of + flomni's flomnigui_show_cameras(). Distinct from + omnygui_show_omnycam_parking()/omnygui_show_omnycam_samplestage() + above, which are omny's own pre-existing, narrower parking/ + samplestage-specific views -- left untouched. + """ + self.omnygui_show_gui() + if ( + self._omnygui_is_missing("fig200") + or self._omnygui_is_missing("fig203") + or self._omnygui_is_missing("console") + ): + self.omnygui_remove_all_docks() + self.fig200 = self.gui.omny.new("BECImageWidget") + if self._omnycam_check_device_exists(dev.cam200): + fig = self.fig200.image("cam200") + fig.set_rotation(deg_90=3) + self.fig200.lock_aspect_ratio(True) + else: + print("Cannot open cam200. Device does not exist.") + self.fig203 = self.gui.omny.new("BECImageWidget") + if self._omnycam_check_device_exists(dev.cam203): + fig = self.fig203.image("cam203") + fig.set_rotation(deg_90=3) + self.fig203.lock_aspect_ratio(True) + else: + print("Cannot open cam203. Device does not exist.") + + # Hard-stop console, docked below the cameras. otransy is the + # closest omny analog to flomni's ftransy (both the transfer-Y + # axis) -- no secondary/extra stop device wired yet (Mirko's + # call, revisit once a second candidate is confirmed). + self.console = self.gui.omny.new( + "z_ConsoleButtonsWidget", + object_name="console", + where="bottom", + hard_stop_device_name="otransy", + hard_stop_label="OMNY Motion Stop", + ) + self.gui.omny.set_layout_ratios(vertical=[5, 1]) def omnygui_remove_all_docks(self): - self.gui_window.clear_all() + if hasattr(self.gui, "omny"): + self.gui.omny.delete_all() + self.progressbar = None + self.text_box = None + self.xeyegui = None + self.idle_text_box = None self.fig200 = None self.fig201 = None self.fig202 = None self.fig203 = None - self.progressbar = None - self.text_box = None - self.idle_text_box = None + self.console = None def omnygui_idle(self): self.omnygui_show_gui() - if self.idle_text_box is None: + if self._omnygui_is_missing("idle_text_box"): self.omnygui_remove_all_docks() - self.idle_text_box = self.gui_window.add_dock(name="idle_text").add_widget("TextBox") - try: - self.gui_window.remove_dock(name="default_figure") - except: - pass + self.idle_text_box = self.gui.omny.new("TextBox") text = ( "
"
+ "██████╗ ███████╗ ██████╗ ██████╗ ███╗ ███╗███╗ ██╗██╗ ██╗\n"
@@ -98,79 +210,50 @@ class OMNYGuiTools:
)
self.idle_text_box.set_html_text(text)
- def _omnycam_clear(self):
- self.omnygui_remove_all_docks()
-
- def _omnycam_check_device_exists(self, device):
- try:
- device
- except:
- return False
- else:
- return True
-
- def _omnycam_samplestage(self):
- self.omnygui_show_omnycam_samplestage()
-
- def omnygui_show_omnycam_samplestage(self):
- self.omnygui_show_gui()
- if self.fig201 is None:
- self.omnygui_remove_all_docks()
- self.fig201 = self.gui_window.add_dock(name="omnycam201").add_widget("BECImageWidget")
- if self._omnycam_check_device_exists(dev.cam201):
- fig = self.fig201.image("cam201")
- fig.set_rotation(deg_90=3)
- self.fig201.lock_aspect_ratio(True)
- else:
- print("Cannot open cam201. Device does not exist.")
- self.fig202 = self.gui_window.add_dock(name="omnycam202").add_widget("BECImageWidget")
- if self._omnycam_check_device_exists(dev.cam202):
- fig = self.fig202.image("cam202")
- fig.set_rotation(deg_90=3)
- self.fig202.lock_aspect_ratio(True)
- else:
- print("Cannot open cam202. Device does not exist.")
- try:
- self.gui_window.remove_dock(name="default_figure")
- except:
- pass
-
def omnygui_show_progress(self):
- self.omnygui_show_gui()
- if self.progressbar is None:
- self.omnygui_remove_all_docks()
- # Add a new dock with a RingProgressBar widget
- self.progressbar = self.gui_window.add_dock(name="progress").add_widget(
- "RingProgressBar"
- )
- # Customize the size of the progress ring
- self.progressbar.set_line_widths(20)
- # Disable automatic updates and manually set the self.progressbar value
- self.progressbar.enable_auto_updates(False)
- # Set precision for the self.progressbar display
- self.progressbar.set_precision(1) # Display self.progressbar with one decimal places
- # Setting multiple rigns with different values
- self.progressbar.set_number_of_bars(3)
- self.progressbar.rings[2].set_update("scan")
- # Set the values of the rings to 50, 75, and 25 from outer to inner ring
- # self.progressbar.set_value([50, 75])
- # Add a new dock with a TextBox widget
- self.text_box = self.gui_window.add_dock(name="progress_text").add_widget("TextBox")
- try:
- self.gui_window.remove_dock(name="default_figure")
- except:
- pass
- self._omnygui_update_progress()
+ try:
+ self.omnygui_show_gui()
+ if self._omnygui_is_missing("progressbar"):
+ self.omnygui_remove_all_docks()
+ self.progressbar = self.gui.omny.new("RingProgressBar")
+ # Two manually-updated rings: overall tomogram progress and
+ # subtomo progress. Unlike flomni's third, auto-updating
+ # "scan"-linked ring, and unlike flomni's ETA/heartbeat/
+ # active-hook text, omny's progress dict doesn't carry that
+ # data yet (see omny/AI_docs/OPEN_ISSUES.md) -- kept simple
+ # here on purpose, revisit once that's ported.
+ self.progressbar.add_ring().set_update("manual")
+ self.progressbar.add_ring().set_update("manual")
+ self._omnygui_update_progress()
+ except Exception as e:
+ logger.warning(f"Error in omnygui_show_progress: {e}")
def _omnygui_update_progress(self):
- if self.progressbar is not None:
+ try:
+ if self.progressbar is None:
+ logger.warning("OMNY progress GUI update skipped: progressbar is not available.")
+ return
+ main_progress_ring = self.progressbar.rings[0]
+ subtomo_progress_ring = self.progressbar.rings[1]
progress = self.progress["projection"] / self.progress["total_projections"] * 100
- subotmo_progress = (
+ subtomo_progress = (
self.progress["subtomo_projection"]
/ self.progress["subtomo_total_projections"]
* 100
)
- self.progressbar.set_value([progress, subotmo_progress])
+ main_progress_ring.set_value(progress)
+ subtomo_progress_ring.set_value(subtomo_progress)
- text = f"Progress report:\n Tomo type: ....................... {self.progress['tomo_type']}\n Projection: ...................... {self.progress['projection']:.0f}\n Total projections expected ....... {self.progress['total_projections']}\n Angle: ........................... {self.progress['angle']}\n Current subtomo: ................. {self.progress['subtomo']}\n Current projection within subtomo: {self.progress['subtomo_projection']}\n Total projections per subtomo: ... {self.progress['subtomo_total_projections']}"
- self.text_box.set_plain_text(text)
+ text = (
+ f"Progress report:\n"
+ f" Tomo type: {self.progress['tomo_type']}\n"
+ f" Projection: {self.progress['projection']:.0f}\n"
+ f" Total projections expected {self.progress['total_projections']:.1f}\n"
+ f" Angle: {self.progress['angle']:.1f}\n"
+ f" Current subtomo: {self.progress['subtomo']}\n"
+ f" Current projection within subtomo: {self.progress['subtomo_projection']}\n"
+ f" Total projections per subtomo: {int(self.progress['subtomo_total_projections'])}"
+ )
+ self.progressbar.set_center_label(text)
+ except Exception as exc:
+ logger.warning(f"OMNY progress GUI update skipped: {exc}")
diff --git a/csaxs_bec/bec_ipython_client/plugins/omny/omny.py b/csaxs_bec/bec_ipython_client/plugins/omny/omny.py
index b8530a5b..b0df048f 100644
--- a/csaxs_bec/bec_ipython_client/plugins/omny/omny.py
+++ b/csaxs_bec/bec_ipython_client/plugins/omny/omny.py
@@ -596,7 +596,8 @@ class OMNY(
self.OMNYTools = OMNYTools(self.client)
OMNY_rt_client.__init__(self)
self.align = XrayEyeAlign(self.client, self)
- OMNYGuiTools.__init__(self, self.client)
+ OMNYGuiTools.__init__(self)
+ self.set_client(self.client)
def start_x_ray_eye_alignment(self):
if self.OMNYTools.yesno(
diff --git a/csaxs_bec/bec_ipython_client/plugins/omny/x_ray_eye_align.py b/csaxs_bec/bec_ipython_client/plugins/omny/x_ray_eye_align.py
index 868d5c6f..55a1ee29 100644
--- a/csaxs_bec/bec_ipython_client/plugins/omny/x_ray_eye_align.py
+++ b/csaxs_bec/bec_ipython_client/plugins/omny/x_ray_eye_align.py
@@ -79,7 +79,7 @@ class XrayEyeAlign:
# ------------------------------------------------------------------
def _reset_init_values(self):
- self.shift_xy = [0.0, 0.0] # base shift to bring sample to beam centre [µm]
+ self.shift_xy = [0.0, 0.0] # base shift to bring sample to beam centre [µm]
self._xray_fov_xy = [0.0, 0.0]
def tomo_rotate(self, val: float):
@@ -161,7 +161,7 @@ class XrayEyeAlign:
Compute x/y offsets, push to GUI fit tab, wait for DAP fit,
then load fit parameters into the global variable store.
"""
- self.lamni.lamnigui_show_xeyealign()
+ self.lamni.omnygui_show_xeyealign()
self.send_message("Getting things ready. Please wait...")
self.gui.enable_submit_button(False)
@@ -188,14 +188,8 @@ class XrayEyeAlign:
k = 0
while True:
if dev.omny_xray_gui.submit.get() == 1:
- val_x = (
- getattr(dev.omny_xray_gui, f"xval_x_{k}").get()
- * self.PIXEL_CALIBRATION
- )
- val_y = (
- getattr(dev.omny_xray_gui, f"yval_y_{k}").get()
- * self.PIXEL_CALIBRATION
- )
+ val_x = getattr(dev.omny_xray_gui, f"xval_x_{k}").get() * self.PIXEL_CALIBRATION
+ val_y = getattr(dev.omny_xray_gui, f"yval_y_{k}").get() * self.PIXEL_CALIBRATION
self.alignment_values[k] = [val_x, val_y]
print(
f"Clicked position {k}: "
@@ -242,15 +236,11 @@ class XrayEyeAlign:
self.gui.enable_submit_button(False)
self.scans.lamni_move_to_scan_center(
- self.shift_xy[0] / 1000,
- self.shift_xy[1] / 1000,
- self.get_tomo_angle(),
+ self.shift_xy[0] / 1000, self.shift_xy[1] / 1000, self.get_tomo_angle()
).wait()
time.sleep(1)
self.scans.lamni_move_to_scan_center(
- self.shift_xy[0] / 1000,
- self.shift_xy[1] / 1000,
- self.get_tomo_angle(),
+ self.shift_xy[0] / 1000, self.shift_xy[1] / 1000, self.get_tomo_angle()
).wait()
dev.omny_xray_gui.angle.set(self.get_tomo_angle())
@@ -274,16 +264,12 @@ class XrayEyeAlign:
self._disable_rt_feedback()
self.tomo_rotate(approach_angle)
self.scans.lamni_move_to_scan_center(
- self.shift_xy[0] / 1000,
- self.shift_xy[1] / 1000,
- self.get_tomo_angle(),
+ self.shift_xy[0] / 1000, self.shift_xy[1] / 1000, self.get_tomo_angle()
).wait()
self._disable_rt_feedback()
self.tomo_rotate(target_angle)
self.scans.lamni_move_to_scan_center(
- self.shift_xy[0] / 1000,
- self.shift_xy[1] / 1000,
- self.get_tomo_angle(),
+ self.shift_xy[0] / 1000, self.shift_xy[1] / 1000, self.get_tomo_angle()
).wait()
dev.omny_xray_gui.angle.set(self.get_tomo_angle())
@@ -317,9 +303,7 @@ class XrayEyeAlign:
self.shift_xy[0] += _mvx
self.shift_xy[1] += _mvy
self.scans.lamni_move_to_scan_center(
- self.shift_xy[0] / 1000,
- self.shift_xy[1] / 1000,
- self.get_tomo_angle(),
+ self.shift_xy[0] / 1000, self.shift_xy[1] / 1000, self.get_tomo_angle()
).wait()
print(
f"Current centre: "
@@ -350,7 +334,7 @@ class XrayEyeAlign:
self.client.set_global_var("tomo_fov_offset", self.shift_xy)
# Switch GUI to fit tab and wait for DAP to finish fitting
- self.lamni.lamnigui_show_xeyealign_fittab()
+ self.lamni.omnygui_show_xeyealign_fittab()
print("Waiting 5 s for DAP sinusoidal fit to converge...")
time.sleep(5)
@@ -399,12 +383,8 @@ class XrayEyeAlign:
f.write("angle\thorizontal\tvertical\n")
for k in range(1, 9):
angle_deg = LAMNI_ALIGNMENT_ANGLES[k - 1]
- x_off = (
- self.alignment_values[1][0] - self.alignment_values[k][0]
- ) * 1000
- y_off = (
- self.alignment_values[k][1] - self.alignment_values[1][1]
- ) * 1000
+ x_off = (self.alignment_values[1][0] - self.alignment_values[k][0]) * 1000
+ y_off = (self.alignment_values[k][1] - self.alignment_values[1][1]) * 1000
f.write(f"{angle_deg}\t{x_off:.4f}\t{y_off:.4f}\n")
print(
f" Angle {angle_deg:3d} deg: "
@@ -428,4 +408,4 @@ class XrayEyeAlign:
# Push to XRayEye widget: feeds waveform_x (row 1) and waveform_y (row 2)
self.gui.submit_fit_array(data)
- print(f"Fit data submitted with shape {data.shape}:\n{data}")
\ No newline at end of file
+ print(f"Fit data submitted with shape {data.shape}:\n{data}")
diff --git a/tests/tests_bec_ipython_client/test_omny_gui_tools.py b/tests/tests_bec_ipython_client/test_omny_gui_tools.py
new file mode 100644
index 00000000..4786e180
--- /dev/null
+++ b/tests/tests_bec_ipython_client/test_omny_gui_tools.py
@@ -0,0 +1,106 @@
+"""Unit tests for OMNYGuiTools, ported to flomni's current set_client()/
+_is_deleted()-guard pattern. Focused on the pure-logic pieces that don't
+require a real bec_widgets/Qt GUI: the dock-reuse guard and the
+cam_xeye existence guard in omnygui_show_xeyealign() (omny's device config
+has no cam_xeye device, unlike flomni's)."""
+
+from unittest import mock
+
+from csaxs_bec.bec_ipython_client.plugins.omny.gui_tools import OMNYGuiTools
+
+# pylint: disable=protected-access
+
+GUI_TOOLS = "csaxs_bec.bec_ipython_client.plugins.omny.gui_tools"
+
+
+def _make_gui_tools():
+ tools = OMNYGuiTools()
+ client = mock.MagicMock()
+ tools.set_client(client)
+ return tools, client
+
+
+def test_omnygui_is_missing_true_when_never_set():
+ tools, _ = _make_gui_tools()
+ assert tools._omnygui_is_missing("xeyegui") is True
+
+
+def test_omnygui_is_missing_true_when_deleted():
+ tools, _ = _make_gui_tools()
+ widget = mock.MagicMock()
+ widget._is_deleted.return_value = True
+ tools.xeyegui = widget
+ assert tools._omnygui_is_missing("xeyegui") is True
+
+
+def test_omnygui_is_missing_false_for_live_widget():
+ tools, _ = _make_gui_tools()
+ widget = mock.MagicMock()
+ widget._is_deleted.return_value = False
+ tools.xeyegui = widget
+ assert tools._omnygui_is_missing("xeyegui") is False
+
+
+def test_omnygui_is_missing_false_when_no_is_deleted_method():
+ tools, _ = _make_gui_tools()
+ tools.xeyegui = object()
+ assert tools._omnygui_is_missing("xeyegui") is False
+
+
+def test_omnygui_show_xeyealign_skips_cam_xeye_when_not_configured():
+ """omny has no cam_xeye device configured -- must not raise when dev
+ lacks it, unlike flomni's unconditional live_mode_enabled toggle."""
+ tools, client = _make_gui_tools()
+ client.gui.windows = {"omny": mock.MagicMock()}
+ new_widget = mock.MagicMock()
+ client.gui.omny.new.return_value = new_widget
+
+ dev_mock = mock.MagicMock()
+ dev_mock.__contains__ = mock.Mock(return_value=False)
+
+ with mock.patch(f"{GUI_TOOLS}.dev", dev_mock, create=True):
+ tools.omnygui_show_xeyealign()
+
+ new_widget.switch_tab.assert_called_once_with("alignment")
+
+
+def test_omnygui_show_xeyealign_enables_live_mode_when_cam_xeye_present():
+ tools, client = _make_gui_tools()
+ client.gui.windows = {"omny": mock.MagicMock()}
+ new_widget = mock.MagicMock()
+ client.gui.omny.new.return_value = new_widget
+
+ dev_mock = mock.MagicMock()
+ dev_mock.__contains__ = mock.Mock(return_value=True)
+ dev_mock.cam_xeye.live_mode_enabled.get.return_value = False
+
+ with mock.patch(f"{GUI_TOOLS}.dev", dev_mock, create=True):
+ tools.omnygui_show_xeyealign()
+
+ dev_mock.cam_xeye.live_mode_enabled.put.assert_called_once_with(True)
+ new_widget.switch_tab.assert_called_once_with("alignment")
+
+
+def test_omnygui_show_gui_reuses_existing_window():
+ tools, client = _make_gui_tools()
+ existing_window = mock.MagicMock()
+ client.gui.windows = {"omny": existing_window}
+
+ tools.omnygui_show_gui()
+
+ client.gui.new.assert_not_called()
+ client.gui.omny.raise_window.assert_called_once()
+ assert tools.omny_window is existing_window
+
+
+def test_omnygui_show_gui_creates_new_window_when_absent():
+ tools, client = _make_gui_tools()
+ client.gui.windows = {}
+ new_window = mock.MagicMock()
+ client.gui.new.return_value = new_window
+
+ with mock.patch(f"{GUI_TOOLS}.time.sleep"):
+ tools.omnygui_show_gui()
+
+ client.gui.new.assert_called_once()
+ assert tools.omny_window is new_window