From 74951c5fa217f9012a641af344cbe2f42946630b Mon Sep 17 00:00:00 2001 From: Peter Sobolewski Date: Fri, 16 Aug 2024 22:45:17 -0400 Subject: [PATCH 1/7] Add zoom to VIew menu --- napari/_app_model/constants/_menus.py | 2 + napari/_qt/_qapp_model/qactions/_view.py | 52 ++++++++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/napari/_app_model/constants/_menus.py b/napari/_app_model/constants/_menus.py index f1491ae953c..bdc3ce455ef 100644 --- a/napari/_app_model/constants/_menus.py +++ b/napari/_app_model/constants/_menus.py @@ -90,6 +90,8 @@ def contributables(cls) -> set['MenuId']: class MenuGroup: NAVIGATION = 'navigation' # always the first group in any menu RENDER = '1_render' + # View menu + ZOOM = 'zoom' # Plugins menubar PLUGINS = '1_plugins' PLUGIN_MULTI_SUBMENU = '2_plugin_multi_submenu' diff --git a/napari/_qt/_qapp_model/qactions/_view.py b/napari/_qt/_qapp_model/qactions/_view.py index a800aac9279..d7d55e5bef9 100644 --- a/napari/_qt/_qapp_model/qactions/_view.py +++ b/napari/_qt/_qapp_model/qactions/_view.py @@ -17,6 +17,7 @@ from napari._qt.qt_viewer import QtViewer from napari.settings import get_settings from napari.utils.translations import trans +from napari.viewer import Viewer # View submenus VIEW_SUBMENUS = [ @@ -61,6 +62,18 @@ def _get_current_tooltip_visibility() -> bool: return get_settings().appearance.layer_tooltip_visibility +def _reset_zoom(viewer: Viewer): + viewer.reset_view() + + +def _zoom_in(viewer: Viewer): + viewer.camera.zoom *= 1.5 + + +def _zoom_out(viewer: Viewer): + viewer.camera.zoom /= 1.5 + + Q_VIEW_ACTIONS: list[Action] = [ Action( id='napari.window.view.toggle_fullscreen', @@ -113,6 +126,45 @@ def _get_current_tooltip_visibility() -> bool: keybindings=[{'primary': KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KeyP}], toggled=ToggleRule(get_current=_get_current_play_status), ), + Action( + id='napari.viewer.reset_view', + title=trans._('Reset Zoom'), + menus=[ + { + 'id': MenuId.MENUBAR_VIEW, + 'group': MenuGroup.ZOOM, + 'order': 1, + } + ], + callback=_reset_zoom, + keybindings=[{'primary': KeyMod.CtrlCmd | KeyCode.Digit0}], + ), + Action( + id='napari.viewer.camera.zoom_in', + title=trans._('Zoom In'), + menus=[ + { + 'id': MenuId.MENUBAR_VIEW, + 'group': MenuGroup.ZOOM, + 'order': 1, + } + ], + callback=_zoom_in, + keybindings=[StandardKeyBinding.ZoomIn], + ), + Action( + id='napari.viewer.camera.zoom_out', + title=trans._('Zoom Out'), + menus=[ + { + 'id': MenuId.MENUBAR_VIEW, + 'group': MenuGroup.ZOOM, + 'order': 1, + } + ], + callback=_zoom_out, + keybindings=[StandardKeyBinding.ZoomOut], + ), Action( id='napari.window.view.toggle_activity_dock', title=trans._('Toggle Activity Dock'), From 0956fdd1a3f27762489348e0eaf11b6f215f588f Mon Sep 17 00:00:00 2001 From: Peter Sobolewski Date: Fri, 16 Aug 2024 23:16:49 -0400 Subject: [PATCH 2/7] Add test --- .../_qt/_qapp_model/_tests/test_view_menu.py | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/napari/_qt/_qapp_model/_tests/test_view_menu.py b/napari/_qt/_qapp_model/_tests/test_view_menu.py index 00119d54d4b..36734eae037 100644 --- a/napari/_qt/_qapp_model/_tests/test_view_menu.py +++ b/napari/_qt/_qapp_model/_tests/test_view_menu.py @@ -189,3 +189,27 @@ def test_toggle_layer_tooltips(make_napari_viewer, qtbot): # Restore layer tooltip visibility app.commands.execute_command(action_id) assert not _get_current_tooltip_visibility() + + +def test_zoom_actions(make_napari_viewer): + """Test zoom actions""" + viewer = make_napari_viewer() + app = get_app() + + viewer.add_image(np.ones((10, 10))) + + # get initial zoom state + initial_zoom = viewer.camera.zoom + + # Check zoom in action + app.commands.execute_command('napari.viewer.camera.zoom_in') + assert viewer.camera.zoom == pytest.approx(1.5 * initial_zoom) + + # Check zoom out action + app.commands.execute_command('napari.viewer.camera.zoom_out') + assert viewer.camera.zoom == pytest.approx(initial_zoom) + + viewer.camera.zoom = 2 + # Check reset zoom action + app.commands.execute_command('napari.viewer.reset_view') + assert viewer.camera.zoom == pytest.approx(initial_zoom) From 3d0c2f52ebc8a4e5fbf004d6d4a2f34815c81398 Mon Sep 17 00:00:00 2001 From: Peter Sobolewski Date: Wed, 18 Sep 2024 18:58:18 -0400 Subject: [PATCH 3/7] Use app-model StandardKeyBinding.OriginalSize --- napari/_qt/_qapp_model/qactions/_view.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/napari/_qt/_qapp_model/qactions/_view.py b/napari/_qt/_qapp_model/qactions/_view.py index d7d55e5bef9..552590767d7 100644 --- a/napari/_qt/_qapp_model/qactions/_view.py +++ b/napari/_qt/_qapp_model/qactions/_view.py @@ -137,7 +137,7 @@ def _zoom_out(viewer: Viewer): } ], callback=_reset_zoom, - keybindings=[{'primary': KeyMod.CtrlCmd | KeyCode.Digit0}], + keybindings=[StandardKeyBinding.OriginalSize], ), Action( id='napari.viewer.camera.zoom_in', From fcb1aa42692c08c81522948c39b7b4d94b998b5e Mon Sep 17 00:00:00 2001 From: Peter Sobolewski Date: Wed, 18 Sep 2024 18:59:07 -0400 Subject: [PATCH 4/7] update min app-model --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a38f5b6773e..cbf5ac67366 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,7 +37,7 @@ classifiers = [ requires-python = ">=3.9" dependencies = [ "appdirs>=1.4.4", - "app-model>=0.2.8,<0.3.0", + "app-model>=0.3.0,<0.4.0", "cachey>=0.2.1", "certifi>=2018.1.18", "dask[array]>=2021.10.0", From f5e79f522a3889f440fd35841f4d094f28efe65e Mon Sep 17 00:00:00 2001 From: Peter Sobolewski Date: Wed, 18 Sep 2024 19:00:06 -0400 Subject: [PATCH 5/7] update constraints --- resources/constraints/constraints_py3.10.txt | 2 +- resources/constraints/constraints_py3.10_docs.txt | 2 +- resources/constraints/constraints_py3.10_pydantic_1.txt | 2 +- resources/constraints/constraints_py3.10_windows.txt | 2 +- resources/constraints/constraints_py3.11.txt | 2 +- resources/constraints/constraints_py3.11_docs.txt | 2 +- resources/constraints/constraints_py3.11_pydantic_1.txt | 2 +- resources/constraints/constraints_py3.11_windows.txt | 2 +- resources/constraints/constraints_py3.12.txt | 2 +- resources/constraints/constraints_py3.12_pydantic_1.txt | 2 +- resources/constraints/constraints_py3.12_windows.txt | 2 +- resources/constraints/constraints_py3.9.txt | 2 +- resources/constraints/constraints_py3.9_examples.txt | 2 +- resources/constraints/constraints_py3.9_pydantic_1.txt | 2 +- resources/constraints/constraints_py3.9_windows.txt | 2 +- 15 files changed, 15 insertions(+), 15 deletions(-) diff --git a/resources/constraints/constraints_py3.10.txt b/resources/constraints/constraints_py3.10.txt index e6d8337f3b3..b2e8f2856e1 100644 --- a/resources/constraints/constraints_py3.10.txt +++ b/resources/constraints/constraints_py3.10.txt @@ -4,7 +4,7 @@ alabaster==1.0.0 # via sphinx annotated-types==0.7.0 # via pydantic -app-model==0.2.8 +app-model==0.3.0 # via napari (napari_repo/pyproject.toml) appdirs==1.4.4 # via diff --git a/resources/constraints/constraints_py3.10_docs.txt b/resources/constraints/constraints_py3.10_docs.txt index d2fe1010e44..cc6dae2c29e 100644 --- a/resources/constraints/constraints_py3.10_docs.txt +++ b/resources/constraints/constraints_py3.10_docs.txt @@ -8,7 +8,7 @@ anyio==4.4.0 # via # starlette # watchfiles -app-model==0.2.8 +app-model==0.3.0 # via napari (napari_repo/pyproject.toml) appdirs==1.4.4 # via diff --git a/resources/constraints/constraints_py3.10_pydantic_1.txt b/resources/constraints/constraints_py3.10_pydantic_1.txt index 80c17d5cb68..c62e4bb4c92 100644 --- a/resources/constraints/constraints_py3.10_pydantic_1.txt +++ b/resources/constraints/constraints_py3.10_pydantic_1.txt @@ -2,7 +2,7 @@ # uv pip compile --python-version 3.10 --output-file napari_repo/resources/constraints/constraints_py3.10_pydantic_1.txt napari_repo/pyproject.toml napari_repo/resources/constraints/version_denylist.txt napari_repo/resources/constraints/pydantic_le_2.txt --extra pyqt5 --extra pyqt6 --extra pyside2 --extra pyside6_experimental --extra testing --extra testing_extra --extra optional alabaster==1.0.0 # via sphinx -app-model==0.2.8 +app-model==0.3.0 # via napari (napari_repo/pyproject.toml) appdirs==1.4.4 # via diff --git a/resources/constraints/constraints_py3.10_windows.txt b/resources/constraints/constraints_py3.10_windows.txt index ea80b494a48..bfc7e3bae0e 100644 --- a/resources/constraints/constraints_py3.10_windows.txt +++ b/resources/constraints/constraints_py3.10_windows.txt @@ -4,7 +4,7 @@ alabaster==1.0.0 # via sphinx annotated-types==0.7.0 # via pydantic -app-model==0.2.8 +app-model==0.3.0 # via napari (napari_repo/pyproject.toml) appdirs==1.4.4 # via diff --git a/resources/constraints/constraints_py3.11.txt b/resources/constraints/constraints_py3.11.txt index 7270c7d55b5..b787ee8997c 100644 --- a/resources/constraints/constraints_py3.11.txt +++ b/resources/constraints/constraints_py3.11.txt @@ -4,7 +4,7 @@ alabaster==1.0.0 # via sphinx annotated-types==0.7.0 # via pydantic -app-model==0.2.8 +app-model==0.3.0 # via napari (napari_repo/pyproject.toml) appdirs==1.4.4 # via diff --git a/resources/constraints/constraints_py3.11_docs.txt b/resources/constraints/constraints_py3.11_docs.txt index a566e7b99a8..2ec41cd708c 100644 --- a/resources/constraints/constraints_py3.11_docs.txt +++ b/resources/constraints/constraints_py3.11_docs.txt @@ -8,7 +8,7 @@ anyio==4.4.0 # via # starlette # watchfiles -app-model==0.2.7 +app-model==0.3.0 # via napari (pyproject.toml) appdirs==1.4.4 # via diff --git a/resources/constraints/constraints_py3.11_pydantic_1.txt b/resources/constraints/constraints_py3.11_pydantic_1.txt index 58fd9732fcb..ca0bb4e5b1b 100644 --- a/resources/constraints/constraints_py3.11_pydantic_1.txt +++ b/resources/constraints/constraints_py3.11_pydantic_1.txt @@ -2,7 +2,7 @@ # uv pip compile --python-version 3.11 --output-file napari_repo/resources/constraints/constraints_py3.11_pydantic_1.txt napari_repo/pyproject.toml napari_repo/resources/constraints/version_denylist.txt napari_repo/resources/constraints/pydantic_le_2.txt --extra pyqt5 --extra pyqt6 --extra pyside2 --extra pyside6_experimental --extra testing --extra testing_extra --extra optional alabaster==1.0.0 # via sphinx -app-model==0.2.8 +app-model==0.3.0 # via napari (napari_repo/pyproject.toml) appdirs==1.4.4 # via diff --git a/resources/constraints/constraints_py3.11_windows.txt b/resources/constraints/constraints_py3.11_windows.txt index 70e1c8fdbe2..4a0fd72591f 100644 --- a/resources/constraints/constraints_py3.11_windows.txt +++ b/resources/constraints/constraints_py3.11_windows.txt @@ -4,7 +4,7 @@ alabaster==1.0.0 # via sphinx annotated-types==0.7.0 # via pydantic -app-model==0.2.8 +app-model==0.3.0 # via napari (napari_repo/pyproject.toml) appdirs==1.4.4 # via diff --git a/resources/constraints/constraints_py3.12.txt b/resources/constraints/constraints_py3.12.txt index dc08e96cec0..99fcc50fc19 100644 --- a/resources/constraints/constraints_py3.12.txt +++ b/resources/constraints/constraints_py3.12.txt @@ -4,7 +4,7 @@ alabaster==1.0.0 # via sphinx annotated-types==0.7.0 # via pydantic -app-model==0.2.8 +app-model==0.3.0 # via napari (napari_repo/pyproject.toml) appdirs==1.4.4 # via diff --git a/resources/constraints/constraints_py3.12_pydantic_1.txt b/resources/constraints/constraints_py3.12_pydantic_1.txt index a2e8ee36b59..fc157d76878 100644 --- a/resources/constraints/constraints_py3.12_pydantic_1.txt +++ b/resources/constraints/constraints_py3.12_pydantic_1.txt @@ -2,7 +2,7 @@ # uv pip compile --python-version 3.12 --output-file napari_repo/resources/constraints/constraints_py3.12_pydantic_1.txt napari_repo/pyproject.toml napari_repo/resources/constraints/version_denylist.txt napari_repo/resources/constraints/pydantic_le_2.txt --extra pyqt5 --extra pyqt6 --extra pyside2 --extra pyside6_experimental --extra testing --extra testing_extra --extra optional alabaster==1.0.0 # via sphinx -app-model==0.2.8 +app-model==0.3.0 # via napari (napari_repo/pyproject.toml) appdirs==1.4.4 # via diff --git a/resources/constraints/constraints_py3.12_windows.txt b/resources/constraints/constraints_py3.12_windows.txt index e2737ab25d9..139cc798343 100644 --- a/resources/constraints/constraints_py3.12_windows.txt +++ b/resources/constraints/constraints_py3.12_windows.txt @@ -4,7 +4,7 @@ alabaster==1.0.0 # via sphinx annotated-types==0.7.0 # via pydantic -app-model==0.2.8 +app-model==0.3.0 # via napari (napari_repo/pyproject.toml) appdirs==1.4.4 # via diff --git a/resources/constraints/constraints_py3.9.txt b/resources/constraints/constraints_py3.9.txt index 3670caaa0a4..2f1728e0704 100644 --- a/resources/constraints/constraints_py3.9.txt +++ b/resources/constraints/constraints_py3.9.txt @@ -4,7 +4,7 @@ alabaster==0.7.16 # via sphinx annotated-types==0.7.0 # via pydantic -app-model==0.2.8 +app-model==0.3.0 # via napari (napari_repo/pyproject.toml) appdirs==1.4.4 # via diff --git a/resources/constraints/constraints_py3.9_examples.txt b/resources/constraints/constraints_py3.9_examples.txt index 9aa79c49e20..a02a4282144 100644 --- a/resources/constraints/constraints_py3.9_examples.txt +++ b/resources/constraints/constraints_py3.9_examples.txt @@ -4,7 +4,7 @@ alabaster==0.7.16 # via sphinx annotated-types==0.7.0 # via pydantic -app-model==0.2.8 +app-model==0.3.0 # via napari (napari_repo/pyproject.toml) appdirs==1.4.4 # via diff --git a/resources/constraints/constraints_py3.9_pydantic_1.txt b/resources/constraints/constraints_py3.9_pydantic_1.txt index 719dfff1d04..05f254ce81c 100644 --- a/resources/constraints/constraints_py3.9_pydantic_1.txt +++ b/resources/constraints/constraints_py3.9_pydantic_1.txt @@ -2,7 +2,7 @@ # uv pip compile --python-version 3.9 --output-file napari_repo/resources/constraints/constraints_py3.9_pydantic_1.txt napari_repo/pyproject.toml napari_repo/resources/constraints/version_denylist.txt napari_repo/resources/constraints/pydantic_le_2.txt --extra pyqt5 --extra pyqt6 --extra pyside2 --extra pyside6_experimental --extra testing --extra testing_extra --extra optional alabaster==0.7.16 # via sphinx -app-model==0.2.8 +app-model==0.3.0 # via napari (napari_repo/pyproject.toml) appdirs==1.4.4 # via diff --git a/resources/constraints/constraints_py3.9_windows.txt b/resources/constraints/constraints_py3.9_windows.txt index b6cd2aa3f91..2d51c05c1e6 100644 --- a/resources/constraints/constraints_py3.9_windows.txt +++ b/resources/constraints/constraints_py3.9_windows.txt @@ -4,7 +4,7 @@ alabaster==0.7.16 # via sphinx annotated-types==0.7.0 # via pydantic -app-model==0.2.8 +app-model==0.3.0 # via napari (napari_repo/pyproject.toml) appdirs==1.4.4 # via From 93dc487bea47fab9931b86bc85737646999c125e Mon Sep 17 00:00:00 2001 From: Peter Sobolewski <76622105+psobolewskiPhD@users.noreply.github.com> Date: Fri, 20 Sep 2024 22:26:57 -0400 Subject: [PATCH 6/7] Update napari/_qt/_qapp_model/_tests/test_view_menu.py Co-authored-by: Grzegorz Bokota --- napari/_qt/_qapp_model/_tests/test_view_menu.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/napari/_qt/_qapp_model/_tests/test_view_menu.py b/napari/_qt/_qapp_model/_tests/test_view_menu.py index 2cc70cc6760..8439c5d38df 100644 --- a/napari/_qt/_qapp_model/_tests/test_view_menu.py +++ b/napari/_qt/_qapp_model/_tests/test_view_menu.py @@ -194,7 +194,7 @@ def test_toggle_layer_tooltips(make_napari_viewer, qtbot): def test_zoom_actions(make_napari_viewer): """Test zoom actions""" viewer = make_napari_viewer() - app = get_app() + app = get_app_model() viewer.add_image(np.ones((10, 10))) From 632289cd25d795e2d67128e9acfce634b8032641 Mon Sep 17 00:00:00 2001 From: Peter Sobolewski Date: Sat, 21 Sep 2024 11:43:35 -0400 Subject: [PATCH 7/7] Change to fit_to_view --- napari/_qt/_qapp_model/_tests/test_view_menu.py | 13 +++++++++++-- napari/_qt/_qapp_model/qactions/_view.py | 10 +++++----- napari/components/viewer_model.py | 7 +++++-- 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/napari/_qt/_qapp_model/_tests/test_view_menu.py b/napari/_qt/_qapp_model/_tests/test_view_menu.py index 8439c5d38df..320d9b11109 100644 --- a/napari/_qt/_qapp_model/_tests/test_view_menu.py +++ b/napari/_qt/_qapp_model/_tests/test_view_menu.py @@ -196,7 +196,7 @@ def test_zoom_actions(make_napari_viewer): viewer = make_napari_viewer() app = get_app_model() - viewer.add_image(np.ones((10, 10))) + viewer.add_image(np.ones((10, 10, 10))) # get initial zoom state initial_zoom = viewer.camera.zoom @@ -211,5 +211,14 @@ def test_zoom_actions(make_napari_viewer): viewer.camera.zoom = 2 # Check reset zoom action - app.commands.execute_command('napari.viewer.reset_view') + app.commands.execute_command('napari.viewer.fit_to_view') assert viewer.camera.zoom == pytest.approx(initial_zoom) + + # Check that angle is preserved + viewer.dims.ndisplay = 3 + viewer.camera.angles = (90, 0, 0) + viewer.camera.zoom = 2 + app.commands.execute_command('napari.viewer.fit_to_view') + # Zoom should be reset, but angle unchanged + assert viewer.camera.zoom == pytest.approx(initial_zoom) + assert viewer.camera.angles == (90, 0, 0) diff --git a/napari/_qt/_qapp_model/qactions/_view.py b/napari/_qt/_qapp_model/qactions/_view.py index 552590767d7..d0846ba682c 100644 --- a/napari/_qt/_qapp_model/qactions/_view.py +++ b/napari/_qt/_qapp_model/qactions/_view.py @@ -62,8 +62,8 @@ def _get_current_tooltip_visibility() -> bool: return get_settings().appearance.layer_tooltip_visibility -def _reset_zoom(viewer: Viewer): - viewer.reset_view() +def _fit_to_view(viewer: Viewer): + viewer.reset_view(reset_camera_angle=False) def _zoom_in(viewer: Viewer): @@ -127,8 +127,8 @@ def _zoom_out(viewer: Viewer): toggled=ToggleRule(get_current=_get_current_play_status), ), Action( - id='napari.viewer.reset_view', - title=trans._('Reset Zoom'), + id='napari.viewer.fit_to_view', + title=trans._('Fit to View'), menus=[ { 'id': MenuId.MENUBAR_VIEW, @@ -136,7 +136,7 @@ def _zoom_out(viewer: Viewer): 'order': 1, } ], - callback=_reset_zoom, + callback=_fit_to_view, keybindings=[StandardKeyBinding.OriginalSize], ), Action( diff --git a/napari/components/viewer_model.py b/napari/components/viewer_model.py index 456855585a3..8f91b71973f 100644 --- a/napari/components/viewer_model.py +++ b/napari/components/viewer_model.py @@ -375,7 +375,9 @@ def _sliced_extent_world_augmented(self) -> np.ndarray: ) return self.layers._extent_world_augmented[:, self.dims.displayed] - def reset_view(self, *, margin: float = 0.05) -> None: + def reset_view( + self, *, margin: float = 0.05, reset_camera_angle: bool = True + ) -> None: """Reset the camera view. Parameters @@ -426,7 +428,8 @@ def reset_view(self, *, margin: float = 0.05) -> None: self.camera.zoom = scale_factor * np.min( np.array(self._canvas_size) / scale ) - self.camera.angles = (0, 0, 90) + if reset_camera_angle: + self.camera.angles = (0, 0, 90) # Emit a reset view event, which is no longer used internally, but # which maybe useful for building on napari.