From 2d41a724aa711decceba1a1df3fa167c585fbdaa Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Tue, 14 Apr 2020 11:02:04 +0200 Subject: [PATCH] Simplify pan/zoom toggling. - Store current active tool just in the .mode attribute, rather than both in .mode and ._active; use a "StrEnum" (like an IntEnum, but for strs...) for backcompat. - Connect a single handler which will dispatch to the correct sub-handler depending on the active tool, rather than having to disconnect and reconnect handlers every time a tool is changed. --- lib/matplotlib/backend_bases.py | 102 ++++++++++-------------- lib/matplotlib/backends/backend_gtk3.py | 2 +- lib/matplotlib/backends/backend_qt5.py | 4 +- lib/matplotlib/backends/backend_wx.py | 4 +- 4 files changed, 48 insertions(+), 64 deletions(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 93842a40165b..e8f525820c40 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -29,7 +29,7 @@ """ from contextlib import contextmanager -from enum import IntEnum +from enum import Enum, IntEnum import functools import importlib import io @@ -2651,6 +2651,15 @@ def set_window_title(self, title): cursors = tools.cursors +class _Mode(str, Enum): + NONE = "" + PAN = "pan/zoom" + ZOOM = "zoom rect" + + def __str__(self): + return self.value + + class NavigationToolbar2: """ Base class for the navigation cursor, version 2 @@ -2715,19 +2724,20 @@ def __init__(self, canvas): canvas.toolbar = self self._nav_stack = cbook.Stack() self._xypress = None # location and axis info at the time of the press - self._idPress = None - self._idRelease = None - self._active = None # This cursor will be set after the initial draw. self._lastCursor = cursors.POINTER self._init_toolbar() + self._id_press = self.canvas.mpl_connect( + 'button_press_event', self._zoom_pan_handler) + self._id_release = self.canvas.mpl_connect( + 'button_release_event', self._zoom_pan_handler) self._id_drag = self.canvas.mpl_connect( 'motion_notify_event', self.mouse_move) self._zoom_info = None self._button_pressed = None # determined by button pressed at start - self.mode = '' # a mode string for the status bar + self.mode = _Mode.NONE # a mode string for the status bar self.set_history_buttons() def set_message(self, s): @@ -2805,17 +2815,17 @@ def _update_cursor(self, event): """ Update the cursor after a mouse move event or a tool (de)activation. """ - if not event.inaxes or not self._active: + if not event.inaxes or not self.mode: if self._lastCursor != cursors.POINTER: self.set_cursor(cursors.POINTER) self._lastCursor = cursors.POINTER else: - if (self._active == 'ZOOM' + if (self.mode == _Mode.ZOOM and self._lastCursor != cursors.SELECT_REGION): self.set_cursor(cursors.SELECT_REGION) self._lastCursor = cursors.SELECT_REGION - elif (self._active == 'PAN' and - self._lastCursor != cursors.MOVE): + elif (self.mode == _Mode.PAN + and self._lastCursor != cursors.MOVE): self.set_cursor(cursors.MOVE) self._lastCursor = cursors.MOVE @@ -2870,40 +2880,32 @@ def mouse_move(self, event): else: self.set_message(self.mode) + def _zoom_pan_handler(self, event): + if self.mode == _Mode.PAN: + if event.name == "button_press_event": + self.press_pan(event) + elif event.name == "button_release_event": + self.release_pan(event) + if self.mode == _Mode.ZOOM: + if event.name == "button_press_event": + self.press_zoom(event) + elif event.name == "button_release_event": + self.release_zoom(event) + def pan(self, *args): """ - Activate the pan/zoom tool. + Toggle the pan/zoom tool. Pan with left button, zoom with right. """ - # set the pointer icon and button press funcs to the - # appropriate callbacks - - if self._active == 'PAN': - self._active = None + if self.mode == _Mode.PAN: + self.mode = _Mode.NONE + self.canvas.widgetlock.release(self) else: - self._active = 'PAN' - if self._idPress is not None: - self._idPress = self.canvas.mpl_disconnect(self._idPress) - self.mode = '' - - if self._idRelease is not None: - self._idRelease = self.canvas.mpl_disconnect(self._idRelease) - self.mode = '' - - if self._active: - self._idPress = self.canvas.mpl_connect( - 'button_press_event', self.press_pan) - self._idRelease = self.canvas.mpl_connect( - 'button_release_event', self.release_pan) - self.mode = 'pan/zoom' + self.mode = _Mode.PAN self.canvas.widgetlock(self) - else: - self.canvas.widgetlock.release(self) - for a in self.canvas.figure.get_axes(): - a.set_navigate_mode(self._active) - + a.set_navigate_mode(self.mode) self.set_message(self.mode) def press(self, event): @@ -3101,33 +3103,15 @@ def update(self): self.set_history_buttons() def zoom(self, *args): - """Activate zoom to rect mode.""" - if self._active == 'ZOOM': - self._active = None + """Toggle zoom to rect mode.""" + if self.mode == _Mode.ZOOM: + self.mode = _Mode.NONE + self.canvas.widgetlock.release(self) else: - self._active = 'ZOOM' - - if self._idPress is not None: - self._idPress = self.canvas.mpl_disconnect(self._idPress) - self.mode = '' - - if self._idRelease is not None: - self._idRelease = self.canvas.mpl_disconnect(self._idRelease) - self.mode = '' - - if self._active: - self._idPress = self.canvas.mpl_connect('button_press_event', - self.press_zoom) - self._idRelease = self.canvas.mpl_connect('button_release_event', - self.release_zoom) - self.mode = 'zoom rect' + self.mode = _Mode.ZOOM self.canvas.widgetlock(self) - else: - self.canvas.widgetlock.release(self) - for a in self.canvas.figure.get_axes(): - a.set_navigate_mode(self._active) - + a.set_navigate_mode(self.mode) self.set_message(self.mode) def set_history_buttons(self): diff --git a/lib/matplotlib/backends/backend_gtk3.py b/lib/matplotlib/backends/backend_gtk3.py index 60537cf6443a..d565818e07e5 100644 --- a/lib/matplotlib/backends/backend_gtk3.py +++ b/lib/matplotlib/backends/backend_gtk3.py @@ -528,7 +528,7 @@ def _update_buttons_checked(self): button = self._gtk_ids.get(name) if button: with button.handler_block(button._signal_handler): - button.set_active(self._active == active) + button.set_active(self.mode.name == active) def pan(self, *args): super().pan(*args) diff --git a/lib/matplotlib/backends/backend_qt5.py b/lib/matplotlib/backends/backend_qt5.py index a2006188897c..64a127994aec 100644 --- a/lib/matplotlib/backends/backend_qt5.py +++ b/lib/matplotlib/backends/backend_qt5.py @@ -745,9 +745,9 @@ def edit_parameters(self): def _update_buttons_checked(self): # sync button checkstates to match active mode if 'pan' in self._actions: - self._actions['pan'].setChecked(self._active == 'PAN') + self._actions['pan'].setChecked(self.mode.name == 'PAN') if 'zoom' in self._actions: - self._actions['zoom'].setChecked(self._active == 'ZOOM') + self._actions['zoom'].setChecked(self.mode.name == 'ZOOM') def pan(self, *args): super().pan(*args) diff --git a/lib/matplotlib/backends/backend_wx.py b/lib/matplotlib/backends/backend_wx.py index b400efa7cb7d..4114be06d16e 100644 --- a/lib/matplotlib/backends/backend_wx.py +++ b/lib/matplotlib/backends/backend_wx.py @@ -1209,7 +1209,7 @@ def set_cursor(self, cursor): self.canvas.Update() def press(self, event): - if self._active == 'ZOOM': + if self.mode.name == 'ZOOM': if not self.retinaFix: self.wxoverlay = wx.Overlay() else: @@ -1221,7 +1221,7 @@ def press(self, event): self.zoomAxes = event.inaxes def release(self, event): - if self._active == 'ZOOM': + if self.mode.name == 'ZOOM': # When the mouse is released we reset the overlay and it # restores the former content to the window. if not self.retinaFix: