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..02ac7e574603 --- /dev/null +++ b/doc/api/next_api_changes/deprecations/20620-ES.rst @@ -0,0 +1,18 @@ +``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_bases.py b/lib/matplotlib/backend_bases.py index 8a5ba25a704f..5be359b9c426 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`. @@ -2864,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 @@ -2914,7 +2929,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 +2998,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 +3024,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 @@ -3230,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. @@ -3239,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.""" 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 15654cce2073..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. @@ -266,24 +266,28 @@ 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 + @_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 5747e6ced397..fc24f3d614bc 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 @@ -717,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( @@ -850,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_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..754604a21820 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 @@ -931,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( @@ -960,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_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..61176e434c4c 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) @@ -1281,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( @@ -1362,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