From dba7f8d148af656bd2aa960ca650f4d7238c545c Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 9 Jul 2021 18:17:48 -0400 Subject: [PATCH 1/3] Move set_cursor from the toolbar to FigureCanvas. While the toolbar does need to track what it wants to set the cursor to based on the active tool, actually setting it is an act on the canvas (or sometimes the window, but this is toolkit dependent, so we won't worry about that.) --- lib/matplotlib/backend_bases.py | 42 +++++++++++++------ lib/matplotlib/backend_tools.py | 6 +-- lib/matplotlib/backends/backend_gtk3.py | 19 +++++---- lib/matplotlib/backends/backend_macosx.py | 7 ++-- lib/matplotlib/backends/backend_qt5.py | 7 ++-- .../backends/backend_webagg_core.py | 31 +++++++------- lib/matplotlib/backends/backend_wx.py | 11 ++--- 7 files changed, 73 insertions(+), 50 deletions(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 8a5ba25a704f..cbd807c0bcfb 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -2011,6 +2011,24 @@ def release_mouse(self, ax): if self.mouse_grabber is ax: self.mouse_grabber = None + def set_cursor(self, cursor): + """ + Set the current cursor. + + This may have no effect if the backend does not display anything. + + If required by the backend, this method should trigger an update in + the backend event loop after the cursor is set, as this method may be + called e.g. before a long-running task during which the GUI is not + updated. + + Parameters + ---------- + cursor : `.Cursors` + The cursor to dispay over the canvas. Note: some backends may + change the cursor for the entire window. + """ + def draw(self, *args, **kwargs): """ Render the `.Figure`. @@ -2914,7 +2932,7 @@ def __init__(self, canvas): canvas.toolbar = self self._nav_stack = cbook.Stack() # This cursor will be set after the initial draw. - self._lastCursor = cursors.POINTER + self._lastCursor = tools.Cursors.POINTER self._id_press = self.canvas.mpl_connect( 'button_press_event', self._zoom_pan_handler) @@ -2983,16 +3001,16 @@ def _update_cursor(self, event): """ if self.mode and event.inaxes and event.inaxes.get_navigate(): if (self.mode == _Mode.ZOOM - and self._lastCursor != cursors.SELECT_REGION): - self.set_cursor(cursors.SELECT_REGION) - self._lastCursor = cursors.SELECT_REGION + and self._lastCursor != tools.Cursors.SELECT_REGION): + self.canvas.set_cursor(tools.Cursors.SELECT_REGION) + self._lastCursor = tools.Cursors.SELECT_REGION elif (self.mode == _Mode.PAN - and self._lastCursor != cursors.MOVE): - self.set_cursor(cursors.MOVE) - self._lastCursor = cursors.MOVE - elif self._lastCursor != cursors.POINTER: - self.set_cursor(cursors.POINTER) - self._lastCursor = cursors.POINTER + and self._lastCursor != tools.Cursors.MOVE): + self.canvas.set_cursor(tools.Cursors.MOVE) + self._lastCursor = tools.Cursors.MOVE + elif self._lastCursor != tools.Cursors.POINTER: + self.canvas.set_cursor(tools.Cursors.POINTER) + self._lastCursor = tools.Cursors.POINTER @contextmanager def _wait_cursor_for_draw_cm(self): @@ -3009,10 +3027,10 @@ def _wait_cursor_for_draw_cm(self): time.time(), getattr(self, "_draw_time", -np.inf)) if self._draw_time - last_draw_time > 1: try: - self.set_cursor(cursors.WAIT) + self.canvas.set_cursor(tools.Cursors.WAIT) yield finally: - self.set_cursor(self._lastCursor) + self.canvas.set_cursor(self._lastCursor) else: yield diff --git a/lib/matplotlib/backend_tools.py b/lib/matplotlib/backend_tools.py index 15654cce2073..6bdde8fd4519 100644 --- a/lib/matplotlib/backend_tools.py +++ b/lib/matplotlib/backend_tools.py @@ -266,15 +266,15 @@ def _add_tool_cbk(self, event): self._add_tool(event.tool) def _set_cursor_cbk(self, event): - if not event: + if not event or not self.canvas: return if (self._current_tool and getattr(event, "inaxes", None) and event.inaxes.get_navigate()): if self._last_cursor != self._current_tool.cursor: - self.set_cursor(self._current_tool.cursor) + self.canvas.set_cursor(self._current_tool.cursor) self._last_cursor = self._current_tool.cursor elif self._last_cursor != self._default_cursor: - self.set_cursor(self._default_cursor) + self.canvas.set_cursor(self._default_cursor) self._last_cursor = self._default_cursor def set_cursor(self, cursor): diff --git a/lib/matplotlib/backends/backend_gtk3.py b/lib/matplotlib/backends/backend_gtk3.py index 5747e6ced397..442e53b2bd43 100644 --- a/lib/matplotlib/backends/backend_gtk3.py +++ b/lib/matplotlib/backends/backend_gtk3.py @@ -90,7 +90,7 @@ def _create_application(): @functools.lru_cache() def _mpl_to_gtk_cursor(mpl_cursor): - name = { + name = _api.check_getitem({ Cursors.MOVE: "move", Cursors.HAND: "pointer", Cursors.POINTER: "default", @@ -98,7 +98,7 @@ def _mpl_to_gtk_cursor(mpl_cursor): Cursors.WAIT: "wait", Cursors.RESIZE_HORIZONTAL: "ew-resize", Cursors.RESIZE_VERTICAL: "ns-resize", - }[mpl_cursor] + }, cursor=mpl_cursor) return Gdk.Cursor.new_from_name(Gdk.Display.get_default(), name) @@ -188,6 +188,14 @@ def __init__(self, figure=None): def destroy(self): self.close_event() + def set_cursor(self, cursor): + # docstring inherited + window = self.get_property("window") + if window is not None: + window.set_cursor(_mpl_to_gtk_cursor(cursor)) + context = GLib.MainContext.default() + context.iteration(True) + def scroll_event(self, widget, event): x = event.x # flipy so y=0 is bottom of canvas @@ -533,13 +541,6 @@ def set_message(self, s): escaped = GLib.markup_escape_text(s) self.message.set_markup(f'{escaped}') - def set_cursor(self, cursor): - window = self.canvas.get_property("window") - if window is not None: - window.set_cursor(_mpl_to_gtk_cursor(cursor)) - context = GLib.MainContext.default() - context.iteration(True) - def draw_rubberband(self, event, x0, y0, x1, y1): height = self.canvas.figure.bbox.height y1 = height - y1 diff --git a/lib/matplotlib/backends/backend_macosx.py b/lib/matplotlib/backends/backend_macosx.py index c84087f3b209..d4fca882bd81 100644 --- a/lib/matplotlib/backends/backend_macosx.py +++ b/lib/matplotlib/backends/backend_macosx.py @@ -39,6 +39,10 @@ def _set_device_scale(self, value): self._dpi_ratio, old_value = value, self._dpi_ratio self.figure.dpi = self.figure.dpi / old_value * self._dpi_ratio + def set_cursor(self, cursor): + # docstring inherited + _macosx.set_cursor(cursor) + def _draw(self): renderer = self.get_renderer(cleared=self.figure.stale) if self.figure.stale: @@ -108,9 +112,6 @@ def release_zoom(self, event): super().release_zoom(event) self.canvas.remove_rubberband() - def set_cursor(self, cursor): - _macosx.set_cursor(cursor) - def save_figure(self, *args): filename = _macosx.choose_save_file('Save the figure', self.canvas.get_default_filename()) diff --git a/lib/matplotlib/backends/backend_qt5.py b/lib/matplotlib/backends/backend_qt5.py index 0523d4d8ca7d..47e8c121537c 100644 --- a/lib/matplotlib/backends/backend_qt5.py +++ b/lib/matplotlib/backends/backend_qt5.py @@ -241,6 +241,10 @@ def showEvent(self, event): window.screenChanged.connect(self._update_screen) self._update_screen(window.screen()) + def set_cursor(self, cursor): + # docstring inherited + self.setCursor(_api.check_getitem(cursord, cursor=cursor)) + def enterEvent(self, event): try: x, y = self.mouseEventCoords(event.pos()) @@ -702,9 +706,6 @@ def set_message(self, s): if self.coordinates: self.locLabel.setText(s) - def set_cursor(self, cursor): - self.canvas.setCursor(cursord[cursor]) - def draw_rubberband(self, event, x0, y0, x1, y1): height = self.canvas.figure.bbox.height y1 = height - y1 diff --git a/lib/matplotlib/backends/backend_webagg_core.py b/lib/matplotlib/backends/backend_webagg_core.py index ceb5c4be8e16..a66a9ec76cdc 100644 --- a/lib/matplotlib/backends/backend_webagg_core.py +++ b/lib/matplotlib/backends/backend_webagg_core.py @@ -157,6 +157,19 @@ def blit(self, bbox=None): def draw_idle(self): self.send_event("draw") + def set_cursor(self, cursor): + # docstring inherited + cursor = _api.check_getitem({ + backend_tools.Cursors.HAND: 'pointer', + backend_tools.Cursors.POINTER: 'default', + backend_tools.Cursors.SELECT_REGION: 'crosshair', + backend_tools.Cursors.MOVE: 'move', + backend_tools.Cursors.WAIT: 'wait', + backend_tools.Cursors.RESIZE_HORIZONTAL: 'ew-resize', + backend_tools.Cursors.RESIZE_VERTICAL: 'ns-resize', + }, cursor=cursor) + self.send_event('cursor', cursor=cursor) + def set_image_mode(self, mode): """ Set the image mode for any subsequent images which will be sent @@ -362,9 +375,11 @@ class NavigationToolbar2WebAgg(backend_bases.NavigationToolbar2): if name_of_method in _ALLOWED_TOOL_ITEMS ] + cursor = _api.deprecate_privatize_attribute("3.5") + def __init__(self, canvas): self.message = '' - self.cursor = None + self._cursor = None # Remove with deprecation. super().__init__(canvas) def set_message(self, message): @@ -372,20 +387,6 @@ def set_message(self, message): self.canvas.send_event("message", message=message) self.message = message - def set_cursor(self, cursor): - if cursor != self.cursor: - cursor = { - backend_tools.Cursors.HAND: 'pointer', - backend_tools.Cursors.POINTER: 'default', - backend_tools.Cursors.SELECT_REGION: 'crosshair', - backend_tools.Cursors.MOVE: 'move', - backend_tools.Cursors.WAIT: 'wait', - backend_tools.Cursors.RESIZE_HORIZONTAL: 'ew-resize', - backend_tools.Cursors.RESIZE_VERTICAL: 'ns-resize', - }[cursor] - self.canvas.send_event("cursor", cursor=cursor) - self.cursor = cursor - def draw_rubberband(self, event, x0, y0, x1, y1): self.canvas.send_event( "rubberband", x0=x0, y0=y0, x1=x1, y1=y1) diff --git a/lib/matplotlib/backends/backend_wx.py b/lib/matplotlib/backends/backend_wx.py index ece3db76955a..0a9796b3dce3 100644 --- a/lib/matplotlib/backends/backend_wx.py +++ b/lib/matplotlib/backends/backend_wx.py @@ -719,6 +719,12 @@ def _onKeyUp(self, event): if self: event.Skip() + def set_cursor(self, cursor): + # docstring inherited + cursor = wx.Cursor(_api.check_getitem(cursord, cursor=cursor)) + self.SetCursor(cursor) + self.Update() + def _set_capture(self, capture=True): """Control wx mouse capture.""" if self.HasCapture(): @@ -1155,11 +1161,6 @@ def save_figure(self, *args): except Exception as e: error_msg_wx(str(e)) - def set_cursor(self, cursor): - cursor = wx.Cursor(cursord[cursor]) - self.canvas.SetCursor(cursor) - self.canvas.Update() - def draw_rubberband(self, event, x0, y0, x1, y1): height = self.canvas.figure.bbox.height self.canvas._rubberband_rect = (x0, height - y0, x1, height - y1) From fa78c9726df5db754c173a373252b0e078c75b21 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 9 Jul 2021 20:25:21 -0400 Subject: [PATCH 2/3] Deprecate now unused NavigationToolbar2.set_cursor. --- doc/api/next_api_changes/deprecations/20620-ES.rst | 4 ++++ lib/matplotlib/backend_bases.py | 5 ++--- 2 files changed, 6 insertions(+), 3 deletions(-) create mode 100644 doc/api/next_api_changes/deprecations/20620-ES.rst diff --git a/doc/api/next_api_changes/deprecations/20620-ES.rst b/doc/api/next_api_changes/deprecations/20620-ES.rst new file mode 100644 index 000000000000..346ada08f486 --- /dev/null +++ b/doc/api/next_api_changes/deprecations/20620-ES.rst @@ -0,0 +1,4 @@ +``NavigationToolbar2.set_cursor`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Instead, use the `.FigureCanvasBase.set_cursor` method on the canvas (available +as the ``canvas`` attribute on the toolbar or the Figure.) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index cbd807c0bcfb..5be359b9c426 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -2882,9 +2882,6 @@ class NavigationToolbar2: :meth:`save_figure` save the current figure - :meth:`set_cursor` - if you want the pointer icon to change - :meth:`draw_rubberband` (optional) draw the zoom to rect "rubberband" rectangle @@ -3248,6 +3245,7 @@ def save_figure(self, *args): """Save the current figure.""" raise NotImplementedError + @_api.deprecated("3.5", alternative="canvas.set_cursor") def set_cursor(self, cursor): """ Set the current cursor to one of the :class:`Cursors` enums values. @@ -3257,6 +3255,7 @@ def set_cursor(self, cursor): called e.g. before a long-running task during which the GUI is not updated. """ + self.canvas.set_cursor(cursor) def update(self): """Reset the axes stack.""" From d380f538c59dd42824f9a7c2a4c7887fbadb124f Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 9 Jul 2021 22:24:46 -0400 Subject: [PATCH 3/3] Deprecate backend_tools.SetCursorBase.set_cursor. Which is now replaced by `FigureCanvasBase.set_cursor`. --- .../next_api_changes/deprecations/20620-ES.rst | 18 ++++++++++++++++-- lib/matplotlib/backend_managers.py | 10 ++++++++++ lib/matplotlib/backend_tools.py | 12 ++++++++---- lib/matplotlib/backends/_backend_tk.py | 2 +- lib/matplotlib/backends/backend_gtk3.py | 2 +- lib/matplotlib/backends/backend_qt5.py | 2 +- lib/matplotlib/backends/backend_wx.py | 2 +- 7 files changed, 38 insertions(+), 10 deletions(-) diff --git a/doc/api/next_api_changes/deprecations/20620-ES.rst b/doc/api/next_api_changes/deprecations/20620-ES.rst index 346ada08f486..02ac7e574603 100644 --- a/doc/api/next_api_changes/deprecations/20620-ES.rst +++ b/doc/api/next_api_changes/deprecations/20620-ES.rst @@ -1,4 +1,18 @@ -``NavigationToolbar2.set_cursor`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +``NavigationToolbar2.set_cursor`` and ``backend_tools.SetCursorBase.set_cursor`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Instead, use the `.FigureCanvasBase.set_cursor` method on the canvas (available as the ``canvas`` attribute on the toolbar or the Figure.) + +``backend_tools.SetCursorBase`` and subclasses +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +``backend_tools.SetCursorBase`` was subclassed to provide backend-specific +implementations of ``set_cursor``. As that is now deprecated, the subclassing +is no longer necessary. Consequently, the following subclasses are also +deprecated: + +- ``matplotlib.backends.backend_gtk3.SetCursorGTK3`` +- ``matplotlib.backends.backend_qt5.SetCursorQt`` +- ``matplotlib.backends._backend_tk.SetCursorTk`` +- ``matplotlib.backends.backend_wx.SetCursorWx`` + +Instead, use the `.backend_tools.ToolSetCursor` class. diff --git a/lib/matplotlib/backend_managers.py b/lib/matplotlib/backend_managers.py index a8d493aea177..6d2d9595545d 100644 --- a/lib/matplotlib/backend_managers.py +++ b/lib/matplotlib/backend_managers.py @@ -254,6 +254,16 @@ def add_tool(self, name, tool, *args, **kwargs): 'exists, not added') return self._tools[name] + if name == 'cursor' and tool_cls != tools.SetCursorBase: + _api.warn_deprecated("3.5", + message="Overriding ToolSetCursor with " + f"{tool_cls.__qualname__} was only " + "necessary to provide the .set_cursor() " + "method, which is deprecated since " + "%(since)s and will be removed " + "%(removal)s. Please report this to the " + f"{tool_cls.__module__} author.") + tool_obj = tool_cls(self, name, *args, **kwargs) self._tools[name] = tool_obj diff --git a/lib/matplotlib/backend_tools.py b/lib/matplotlib/backend_tools.py index 6bdde8fd4519..cc81b1f9269b 100644 --- a/lib/matplotlib/backend_tools.py +++ b/lib/matplotlib/backend_tools.py @@ -22,7 +22,7 @@ import matplotlib as mpl from matplotlib._pylab_helpers import Gcf -from matplotlib import cbook +from matplotlib import _api, cbook class Cursors(enum.IntEnum): # Must subclass int for the macOS backend. @@ -277,13 +277,17 @@ def _set_cursor_cbk(self, event): self.canvas.set_cursor(self._default_cursor) self._last_cursor = self._default_cursor + @_api.deprecated("3.5", alternative="figure.canvas.set_cursor") def set_cursor(self, cursor): """ Set the cursor. - - This method has to be implemented per backend. """ - raise NotImplementedError + self.canvas.set_cursor(cursor) + + +# This exists solely for deprecation warnings; remove with +# SetCursorBase.set_cursor. +ToolSetCursor = SetCursorBase class ToolCursorPosition(ToolBase): diff --git a/lib/matplotlib/backends/_backend_tk.py b/lib/matplotlib/backends/_backend_tk.py index a7d123594656..27cfc69e2bd9 100644 --- a/lib/matplotlib/backends/_backend_tk.py +++ b/lib/matplotlib/backends/_backend_tk.py @@ -794,6 +794,7 @@ def remove_rubberband(self): del self.lastrect +@_api.deprecated("3.5", alternative="ToolSetCursor") class SetCursorTk(backend_tools.SetCursorBase): def set_cursor(self, cursor): NavigationToolbar2Tk.set_cursor( @@ -907,7 +908,6 @@ def trigger(self, *args): backend_tools.ToolSaveFigure = SaveFigureTk backend_tools.ToolConfigureSubplots = ConfigureSubplotsTk -backend_tools.ToolSetCursor = SetCursorTk backend_tools.ToolRubberband = RubberbandTk backend_tools.ToolHelp = HelpTk backend_tools.ToolCopyToClipboard = backend_tools.ToolCopyToClipboardBase diff --git a/lib/matplotlib/backends/backend_gtk3.py b/lib/matplotlib/backends/backend_gtk3.py index 442e53b2bd43..fc24f3d614bc 100644 --- a/lib/matplotlib/backends/backend_gtk3.py +++ b/lib/matplotlib/backends/backend_gtk3.py @@ -718,6 +718,7 @@ class PseudoToolbar: return NavigationToolbar2GTK3.save_figure(PseudoToolbar()) +@_api.deprecated("3.5", alternative="ToolSetCursor") class SetCursorGTK3(backend_tools.SetCursorBase): def set_cursor(self, cursor): NavigationToolbar2GTK3.set_cursor( @@ -851,7 +852,6 @@ def error_msg_gtk(msg, parent=None): backend_tools.ToolSaveFigure = SaveFigureGTK3 backend_tools.ToolConfigureSubplots = ConfigureSubplotsGTK3 -backend_tools.ToolSetCursor = SetCursorGTK3 backend_tools.ToolRubberband = RubberbandGTK3 backend_tools.ToolHelp = HelpGTK3 backend_tools.ToolCopyToClipboard = ToolCopyToClipboardGTK3 diff --git a/lib/matplotlib/backends/backend_qt5.py b/lib/matplotlib/backends/backend_qt5.py index 47e8c121537c..754604a21820 100644 --- a/lib/matplotlib/backends/backend_qt5.py +++ b/lib/matplotlib/backends/backend_qt5.py @@ -932,6 +932,7 @@ def trigger(self, *args): self._make_classic_style_pseudo_toolbar()) +@_api.deprecated("3.5", alternative="ToolSetCursor") class SetCursorQt(backend_tools.SetCursorBase): def set_cursor(self, cursor): NavigationToolbar2QT.set_cursor( @@ -961,7 +962,6 @@ def trigger(self, *args, **kwargs): backend_tools.ToolSaveFigure = SaveFigureQt backend_tools.ToolConfigureSubplots = ConfigureSubplotsQt -backend_tools.ToolSetCursor = SetCursorQt backend_tools.ToolRubberband = RubberbandQt backend_tools.ToolHelp = HelpQt backend_tools.ToolCopyToClipboard = ToolCopyToClipboardQT diff --git a/lib/matplotlib/backends/backend_wx.py b/lib/matplotlib/backends/backend_wx.py index 0a9796b3dce3..61176e434c4c 100644 --- a/lib/matplotlib/backends/backend_wx.py +++ b/lib/matplotlib/backends/backend_wx.py @@ -1282,6 +1282,7 @@ def trigger(self, *args): self._make_classic_style_pseudo_toolbar()) +@_api.deprecated("3.5", alternative="ToolSetCursor") class SetCursorWx(backend_tools.SetCursorBase): def set_cursor(self, cursor): NavigationToolbar2Wx.set_cursor( @@ -1363,7 +1364,6 @@ def trigger(self, *args, **kwargs): backend_tools.ToolSaveFigure = SaveFigureWx backend_tools.ToolConfigureSubplots = ConfigureSubplotsWx -backend_tools.ToolSetCursor = SetCursorWx backend_tools.ToolRubberband = RubberbandWx backend_tools.ToolHelp = HelpWx backend_tools.ToolCopyToClipboard = ToolCopyToClipboardWx