From 4edd76435c4387feefc1a3b4361d29349ceb924f Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 20 Aug 2025 16:59:28 -0500 Subject: [PATCH 1/3] Make image showing functions thread-safer Notes from Claude.ai: I've made the ArrayViewer class thread-safe by implementing the following changes: 1. Thread-safe Constructor (src/ndv/controllers/_array_viewer.py:100-108) - Moved GUI component initialization to a separate _init_gui_components() method. - Queue GUI operations to the main thread using _app.ndv_app().call_in_main_thread(). - Block until operations complete to maintain synchronous behavior. 2. Thread-safe View Synchronization (src/ndv/controllers/_array_viewer.py:106-108) - Queue _fully_synchronize_view() to main thread since it contains GUI operations like create_sliders(). 3. Thread-safe Show/Hide/Close Methods (src/ndv/controllers/_array_viewer.py:216-233) - Modified show(), hide(), and close() methods to queue set_visible() calls to main thread. - Maintain synchronous behavior by blocking until operations complete. The fix ensures that all GUI-sensitive operations are properly queued to the main thread while maintaining the existing synchronous API. --- src/ndv/controllers/_array_viewer.py | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/src/ndv/controllers/_array_viewer.py b/src/ndv/controllers/_array_viewer.py index ec59a091..d4e66fae 100644 --- a/src/ndv/controllers/_array_viewer.py +++ b/src/ndv/controllers/_array_viewer.py @@ -97,6 +97,18 @@ def __init__( # where None is the default channel self._lut_controllers: dict[ChannelKey, ChannelController] = {} + # Thread-safe initialization: queue GUI-sensitive operations to main thread + init_future = _app.ndv_app().call_in_main_thread(self._init_gui_components) + # Block until GUI components are initialized + init_future.result() + + if self._data_model.data_wrapper is not None: + # Queue view synchronization to main thread as well since it involves GUI operations + sync_future = _app.ndv_app().call_in_main_thread(self._fully_synchronize_view) + sync_future.result() + + def _init_gui_components(self) -> None: + """Initialize GUI components that must be created on the main thread.""" # get and create the front-end and canvas classes frontend_cls = _app.get_array_view_class() canvas_cls = _app.get_array_canvas_class() @@ -121,9 +133,6 @@ def __init__( self._canvas.mouseMoved.connect(self._on_canvas_mouse_moved) self._canvas.mouseLeft.connect(self._on_canvas_mouse_left) - if self._data_model.data_wrapper is not None: - self._fully_synchronize_view() - # -------------- public attributes and methods ------------------------- def widget(self) -> Any: @@ -208,15 +217,22 @@ def roi(self, roi_model: RectangularROIModel | None) -> None: def show(self) -> None: """Show the viewer.""" - self._view.set_visible(True) + # Queue GUI operation to main thread to ensure thread safety + show_future = _app.ndv_app().call_in_main_thread(self._view.set_visible, True) + # Block until operation completes to maintain synchronous behavior + show_future.result() def hide(self) -> None: """Hide the viewer.""" - self._view.set_visible(False) + # Queue GUI operation to main thread to ensure thread safety + hide_future = _app.ndv_app().call_in_main_thread(self._view.set_visible, False) + hide_future.result() def close(self) -> None: """Close the viewer.""" - self._view.set_visible(False) + # Queue GUI operation to main thread to ensure thread safety + close_future = _app.ndv_app().call_in_main_thread(self._view.set_visible, False) + close_future.result() def clone(self) -> ArrayViewer: """Return a new ArrayViewer instance with the same data and display model. From 153090250a432ae862a7de1bedf87f04c3130c0e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 20 Aug 2025 22:04:14 +0000 Subject: [PATCH 2/3] style(pre-commit.ci): auto fixes [...] --- src/ndv/controllers/_array_viewer.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/ndv/controllers/_array_viewer.py b/src/ndv/controllers/_array_viewer.py index d4e66fae..751613fd 100644 --- a/src/ndv/controllers/_array_viewer.py +++ b/src/ndv/controllers/_array_viewer.py @@ -104,7 +104,9 @@ def __init__( if self._data_model.data_wrapper is not None: # Queue view synchronization to main thread as well since it involves GUI operations - sync_future = _app.ndv_app().call_in_main_thread(self._fully_synchronize_view) + sync_future = _app.ndv_app().call_in_main_thread( + self._fully_synchronize_view + ) sync_future.result() def _init_gui_components(self) -> None: From 73393a8c4f73c4494873212ec1e14fa9d15b4e71 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Sat, 23 Aug 2025 14:52:07 -0400 Subject: [PATCH 3/3] fix line --- src/ndv/controllers/_array_viewer.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ndv/controllers/_array_viewer.py b/src/ndv/controllers/_array_viewer.py index ecf5c8e7..b471724b 100644 --- a/src/ndv/controllers/_array_viewer.py +++ b/src/ndv/controllers/_array_viewer.py @@ -103,7 +103,8 @@ def __init__( init_future.result() if self._data_model.data_wrapper is not None: - # Queue view synchronization to main thread as well since it involves GUI operations + # Queue view synchronization to main thread as well + # since it involves GUI operations sync_future = _app.ndv_app().call_in_main_thread( self._fully_synchronize_view )