From 33e53037784f46e7f60050314e2bcd58d87c9a9c Mon Sep 17 00:00:00 2001 From: Patrick O'Leary Date: Thu, 30 Apr 2026 15:56:41 -0500 Subject: [PATCH] "Disable view interaction, add zoom buttons with tooltips to layout toolbar - Remove lock icon and space-key shortcut for toggling view lock - Disable pointer events and interaction on all view panels - Add zoom-in/zoom-out buttons (magnify icons) to viewport layout toolbar - Save/restore zoom level (camera ParallelScale) in state file - Add get_zoom/set_zoom/zoom_in/zoom_out to both ViewManagers - Add tooltips to all animation toolbar buttons" --- src/e3sm_quickview/app.py | 12 ++++++--- src/e3sm_quickview/components/toolbars.py | 21 ++++++++++++++- src/e3sm_quickview/view_manager.py | 33 +++++++++++++++++------ src/e3sm_quickview/view_manager2.py | 29 ++++++++++++++------ 4 files changed, 74 insertions(+), 21 deletions(-) diff --git a/src/e3sm_quickview/app.py b/src/e3sm_quickview/app.py index d5fbc25..3d90b2e 100644 --- a/src/e3sm_quickview/app.py +++ b/src/e3sm_quickview/app.py @@ -167,7 +167,6 @@ def _build_ui(self, **_): ProjectionEquidistant="projection = ['Cyl. Equidistant']", ProjectionRobinson="projection = ['Robinson']", ProjectionMollweide="projection = ['Mollweide']", - ToggleViewLock="lock_views = !lock_views", FileOpen=(self.toggle_toolbar, "['load-data']"), SaveState="trigger('download_state_dialog')", UploadState="utils.get('document').querySelector('#fileUpload').click()", @@ -199,8 +198,6 @@ def _build_ui(self, **_): mt.bind("v", "ToggleVariableSelection") - mt.bind("space", "ToggleViewLock", stop_propagation=True) - mt.bind("esc", "RemoveAllToolbars") # Native Dialogs @@ -225,7 +222,11 @@ def _build_ui(self, **_): # Fixed overlay for toolbars with html.Div(style=css.TOOLBARS_FIXED_OVERLAY): - toolbars.Layout(apply_size=self.view_manager.apply_size) + toolbars.Layout( + apply_size=self.view_manager.apply_size, + zoom_in=self.view_manager.zoom_in, + zoom_out=self.view_manager.zoom_out, + ) toolbars.Cropping() toolbars.DataSelection() toolbars.Animation() @@ -306,6 +307,7 @@ def download_state(self): "active": self.state.active_layout, "tools": self.state.active_tools, "help": not self.state.compact_drawer, + "zoom": self.view_manager.get_zoom(), } data_selection = { k: self.state[k] @@ -413,6 +415,8 @@ async def _import_state(self, state_content): self.state.active_layout = state_content["layout"]["active"] self.state.active_tools = state_content["layout"]["tools"] self.state.compact_drawer = not state_content["layout"]["help"] + if "zoom" in state_content["layout"]: + self.view_manager.set_zoom(state_content["layout"]["zoom"]) # Update filebrowser state with self.state: diff --git a/src/e3sm_quickview/components/toolbars.py b/src/e3sm_quickview/components/toolbars.py index 20acbba..b9c4099 100644 --- a/src/e3sm_quickview/components/toolbars.py +++ b/src/e3sm_quickview/components/toolbars.py @@ -38,7 +38,7 @@ def to_kwargs(value): class Layout(v3.VToolbar): - def __init__(self, apply_size=None): + def __init__(self, apply_size=None, zoom_in=None, zoom_out=None): super().__init__(**to_kwargs("adjust-layout")) with self: @@ -46,6 +46,19 @@ def __init__(self, apply_size=None): v3.VLabel("Viewport layout", classes="text-subtitle-2") v3.VSpacer() + v3.VIconBtn( + v_tooltip_bottom="'Zoom in'", + icon="mdi-magnify-plus-outline", + flat=True, + click=zoom_in, + ) + v3.VIconBtn( + v_tooltip_bottom="'Zoom out'", + icon="mdi-magnify-minus-outline", + flat=True, + click=zoom_out, + ) + v3.VSlider( v_model=("aspect_ratio", 0.5), prepend_icon="mdi-arrow-expand-vertical", @@ -457,24 +470,28 @@ def __init__(self): ) v3.VDivider(vertical=True, classes="mx-2") v3.VIconBtn( + v_tooltip_bottom="'First step'", icon="mdi-page-first", flat=True, disabled=("animation_step === 0",), click="animation_step = 0", ) v3.VIconBtn( + v_tooltip_bottom="'Previous step'", icon="mdi-chevron-left", flat=True, disabled=("animation_step === 0",), click="animation_step = Math.max(0, animation_step - 1)", ) v3.VIconBtn( + v_tooltip_bottom="'Next step'", icon="mdi-chevron-right", flat=True, disabled=("animation_step === animation_step_max",), click="animation_step = Math.min(animation_step_max, animation_step + 1)", ) v3.VIconBtn( + v_tooltip_bottom="'Last step'", icon="mdi-page-last", disabled=("animation_step === animation_step_max",), flat=True, @@ -482,6 +499,7 @@ def __init__(self): ) v3.VDivider(vertical=True, classes="mx-2") v3.VIconBtn( + v_tooltip_bottom="'Play reverse'", icon=( "animation_play && animation_direction === 'reverse' ? 'mdi-stop' : 'mdi-play'", ), @@ -493,6 +511,7 @@ def __init__(self): style="transform: scaleX(-1);", ) v3.VIconBtn( + v_tooltip_bottom="'Play forward'", icon=( "animation_play && animation_direction === 'forward' ? 'mdi-stop' : 'mdi-play'", ), diff --git a/src/e3sm_quickview/view_manager.py b/src/e3sm_quickview/view_manager.py index b42b5ec..0282a41 100644 --- a/src/e3sm_quickview/view_manager.py +++ b/src/e3sm_quickview/view_manager.py @@ -912,13 +912,6 @@ def _build_ui(self): ), ) - v3.VIcon( - "mdi-lock-outline", - size="x-small", - v_show=("lock_views", True), - style="transform: scale(0.75);", - ) - v3.VSpacer() html.Div( "t = {{ time_idx }}", @@ -958,7 +951,7 @@ def _build_ui(self): { aspectRatio: active_layout === 'auto_layout' ? (1.0 / aspect_ratio) : null, height: active_layout !== 'auto_layout' ? 'calc(100% - 2.4rem)' : null, - pointerEvents: lock_views ? 'none': null, + pointerEvents: 'none', } """, ), @@ -1013,6 +1006,30 @@ def reset_camera(self): for view in views: view.disable_render = False + def zoom_in(self): + for view in list(self._var2view.values()): + cam = view.camera + cam.SetParallelScale(cam.GetParallelScale() / 1.2) + view.render() + + def zoom_out(self): + for view in list(self._var2view.values()): + cam = view.camera + cam.SetParallelScale(cam.GetParallelScale() * 1.2) + view.render() + + def get_zoom(self): + for view in list(self._var2view.values()): + return view.camera.GetParallelScale() + return None + + def set_zoom(self, scale): + if scale is None: + return + for view in list(self._var2view.values()): + view.camera.SetParallelScale(scale) + view.render() + def render(self): for view in list(self._var2view.values()): view.render() diff --git a/src/e3sm_quickview/view_manager2.py b/src/e3sm_quickview/view_manager2.py index 630cfca..815dd23 100644 --- a/src/e3sm_quickview/view_manager2.py +++ b/src/e3sm_quickview/view_manager2.py @@ -918,12 +918,6 @@ def _build_ui(self): click=f"utils.quickview.capturePanel('{self.variable_name}')", style="transform: scale(0.75);", ) - v3.VIcon( - "mdi-lock-outline", - size="x-small", - v_show=("lock_views", True), - style="transform: scale(0.75);", - ) v3.VSpacer() html.Div( @@ -957,13 +951,13 @@ def _build_ui(self): { aspectRatio: active_layout === 'auto_layout' ? (1.0 / aspect_ratio) : null, height: active_layout !== 'auto_layout' ? 'calc(100% - 2.4rem)' : null, - pointerEvents: lock_views ? 'none': null, + pointerEvents: 'none', } """, ), ): rca.ImageRegion( - enable_interaction=True, + enable_interaction=False, bounds=(self._bounds_key, (0, 0, 1, 1)), size=(self.update_size, "[$event]"), ) @@ -1083,6 +1077,25 @@ def reset_camera(self, render=True): if render and view_to_reset: self.render() + def zoom_in(self): + scale = self._camera.GetParallelScale() + self._camera.SetParallelScale(scale / 1.2) + self.render() + + def zoom_out(self): + scale = self._camera.GetParallelScale() + self._camera.SetParallelScale(scale * 1.2) + self.render() + + def get_zoom(self): + return self._camera.GetParallelScale() + + def set_zoom(self, scale): + if scale is None: + return + self._camera.SetParallelScale(scale) + self.render() + @controller.set("size_update") def on_size_update(self): if not self.layout_dirty or not self.pending_render: