Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Explicit registration of canvas-specific tool subclasses. #20990

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Sep 14, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions doc/api/next_api_changes/removals/20990-AL.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Support for passing tool names to ``ToolManager.add_tool``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
... has been removed. The second parameter to add_tool must now always be a
tool class.
7 changes: 7 additions & 0 deletions lib/matplotlib/_api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,13 @@ def my_func(*args, **kwargs):
raise


def recursive_subclasses(cls):
"""Yield *cls* and direct and indirect subclasses of *cls*."""
yield cls
for subcls in cls.__subclasses__():
yield from recursive_subclasses(subcls)


def warn_external(message, category=None):
"""
`warnings.warn` wrapper that sets *stacklevel* to "outside Matplotlib".
Expand Down
36 changes: 10 additions & 26 deletions lib/matplotlib/backend_managers.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from matplotlib import _api, cbook, widgets
import matplotlib.backend_tools as tools
from matplotlib import _api, backend_tools, cbook, widgets


class ToolEvent:
Expand Down Expand Up @@ -233,8 +232,9 @@ def add_tool(self, name, tool, *args, **kwargs):
----------
name : str
Name of the tool, treated as the ID, has to be unique.
tool : class_like, i.e. str or type
Reference to find the class of the Tool to added.
tool : type
Class of the tool to be added. A subclass will be used
instead if one was registered for the current canvas class.

Notes
-----
Expand All @@ -245,7 +245,7 @@ def add_tool(self, name, tool, *args, **kwargs):
matplotlib.backend_tools.ToolBase : The base class for tools.
"""

tool_cls = self._get_cls_to_instantiate(tool)
tool_cls = backend_tools._find_tool_class(type(self.canvas), tool)
if not tool_cls:
raise ValueError('Impossible to find class for %s' % str(tool))

Expand All @@ -254,7 +254,7 @@ def add_tool(self, name, tool, *args, **kwargs):
'exists, not added')
return self._tools[name]

if name == 'cursor' and tool_cls != tools.SetCursorBase:
if name == 'cursor' and tool_cls != backend_tools.SetCursorBase:
_api.warn_deprecated("3.5",
message="Overriding ToolSetCursor with "
f"{tool_cls.__qualname__} was only "
Expand All @@ -271,7 +271,7 @@ def add_tool(self, name, tool, *args, **kwargs):
self.update_keymap(name, tool_cls.default_keymap)

# For toggle tools init the radio_group in self._toggled
if isinstance(tool_obj, tools.ToolToggleBase):
if isinstance(tool_obj, backend_tools.ToolToggleBase):
# None group is not mutually exclusive, a set is used to keep track
# of all toggled tools in this group
if tool_obj.radio_group is None:
Expand Down Expand Up @@ -337,23 +337,6 @@ def _handle_toggle(self, tool, sender, canvasevent, data):
# Keep track of the toggled tool in the radio_group
self._toggled[radio_group] = toggled

def _get_cls_to_instantiate(self, callback_class):
# Find the class that corresponds to the tool
if isinstance(callback_class, str):
# FIXME: make more complete searching structure
if callback_class in globals():
callback_class = globals()[callback_class]
else:
mod = 'backend_tools'
current_module = __import__(mod,
globals(), locals(), [mod], 1)

callback_class = getattr(current_module, callback_class, False)
if callable(callback_class):
return callback_class
else:
return None

def trigger_tool(self, name, sender=None, canvasevent=None, data=None):
"""
Trigger a tool and emit the ``tool_trigger_{name}`` event.
Expand All @@ -376,7 +359,7 @@ def trigger_tool(self, name, sender=None, canvasevent=None, data=None):
if sender is None:
sender = self

if isinstance(tool, tools.ToolToggleBase):
if isinstance(tool, backend_tools.ToolToggleBase):
self._handle_toggle(tool, sender, canvasevent, data)

tool.trigger(sender, canvasevent, data) # Actually trigger Tool.
Expand Down Expand Up @@ -418,7 +401,8 @@ def get_tool(self, name, warn=True):
`.ToolBase` or None
The tool or None if no tool with the given name exists.
"""
if isinstance(name, tools.ToolBase) and name.name in self._tools:
if (isinstance(name, backend_tools.ToolBase)
and name.name in self._tools):
return name
if name not in self._tools:
if warn:
Expand Down
42 changes: 36 additions & 6 deletions lib/matplotlib/backend_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"""

import enum
import functools
import re
import time
from types import SimpleNamespace
Expand All @@ -36,6 +37,35 @@ class Cursors(enum.IntEnum): # Must subclass int for the macOS backend.
RESIZE_VERTICAL = enum.auto()
cursors = Cursors # Backcompat.


# _tool_registry, _register_tool_class, and _find_tool_class implement a
# mechanism through which ToolManager.add_tool can determine whether a subclass
# of the requested tool class has been registered (either for the current
# canvas class or for a parent class), in which case that tool subclass will be
# instantiated instead. This is the mechanism used e.g. to allow different
# GUI backends to implement different specializations for ConfigureSubplots.


_tool_registry = set()


def _register_tool_class(canvas_cls, tool_cls=None):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see how to do it easily, but I still need to ask, is it possible to add test to these two functions?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"""Decorator registering *tool_cls* as a tool class for *canvas_cls*."""
if tool_cls is None:
return functools.partial(_register_tool_class, canvas_cls)
_tool_registry.add((canvas_cls, tool_cls))
return tool_cls


def _find_tool_class(canvas_cls, tool_cls):
"""Find a subclass of *tool_cls* registered for *canvas_cls*."""
for canvas_parent in canvas_cls.__mro__:
for tool_child in _api.recursive_subclasses(tool_cls):
if (canvas_parent, tool_child) in _tool_registry:
return tool_child
return tool_cls


# Views positions tool
_views_positions = 'viewpos'

Expand Down Expand Up @@ -943,8 +973,8 @@ def trigger(self, *args, **kwargs):

default_tools = {'home': ToolHome, 'back': ToolBack, 'forward': ToolForward,
'zoom': ToolZoom, 'pan': ToolPan,
'subplots': 'ToolConfigureSubplots',
'save': 'ToolSaveFigure',
'subplots': ConfigureSubplotsBase,
'save': SaveFigureBase,
'grid': ToolGrid,
'grid_minor': ToolMinorGrid,
'fullscreen': ToolFullScreen,
Expand All @@ -954,10 +984,10 @@ def trigger(self, *args, **kwargs):
'yscale': ToolYScale,
'position': ToolCursorPosition,
_views_positions: ToolViewsPositions,
'cursor': 'ToolSetCursor',
'rubberband': 'ToolRubberband',
'help': 'ToolHelp',
'copy': 'ToolCopyToClipboard',
'cursor': SetCursorBase,
'rubberband': RubberbandBase,
'help': ToolHelpBase,
'copy': ToolCopyToClipboardBase,
}
"""Default tools"""

Expand Down
9 changes: 4 additions & 5 deletions lib/matplotlib/backends/_backend_tk.py
Original file line number Diff line number Diff line change
Expand Up @@ -774,6 +774,7 @@ def hidetip(self):
tw.destroy()


@backend_tools._register_tool_class(FigureCanvasTk)
class RubberbandTk(backend_tools.RubberbandBase):
def draw_rubberband(self, x0, y0, x1, y1):
self.remove_rubberband()
Expand Down Expand Up @@ -859,12 +860,14 @@ def set_message(self, s):
self._message.set(s)


@backend_tools._register_tool_class(FigureCanvasTk)
class SaveFigureTk(backend_tools.SaveFigureBase):
def trigger(self, *args):
NavigationToolbar2Tk.save_figure(
self._make_classic_style_pseudo_toolbar())


@backend_tools._register_tool_class(FigureCanvasTk)
class ConfigureSubplotsTk(backend_tools.ConfigureSubplotsBase):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
Expand Down Expand Up @@ -894,18 +897,14 @@ def destroy(self, *args, **kwargs):
self.window = None


@backend_tools._register_tool_class(FigureCanvasTk)
class HelpTk(backend_tools.ToolHelpBase):
def trigger(self, *args):
dialog = SimpleDialog(
self.figure.canvas._tkcanvas, self._get_help_text(), ["OK"])
dialog.done = lambda num: dialog.frame.master.withdraw()


backend_tools.ToolSaveFigure = SaveFigureTk
backend_tools.ToolConfigureSubplots = ConfigureSubplotsTk
backend_tools.ToolRubberband = RubberbandTk
backend_tools.ToolHelp = HelpTk
backend_tools.ToolCopyToClipboard = backend_tools.ToolCopyToClipboardBase
Toolbar = ToolbarTk


Expand Down
16 changes: 8 additions & 8 deletions lib/matplotlib/backends/backend_gtk3.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,11 @@
raise ImportError from e

from gi.repository import Gio, GLib, GObject, Gtk, Gdk
from . import _backend_gtk
from ._backend_gtk import (
_create_application, _shutdown_application,
backend_version, _BackendGTK, _NavigationToolbar2GTK,
TimerGTK as TimerGTK3,
ConfigureSubplotsGTK as ConfigureSubplotsGTK3,
RubberbandGTK as RubberbandGTK3,
)


Expand Down Expand Up @@ -597,6 +596,7 @@ def set_message(self, s):
self._message.set_label(s)


@backend_tools._register_tool_class(FigureCanvasGTK3)
class SaveFigureGTK3(backend_tools.SaveFigureBase):
def trigger(self, *args, **kwargs):

Expand All @@ -613,6 +613,7 @@ def set_cursor(self, cursor):
self._make_classic_style_pseudo_toolbar(), cursor)


@backend_tools._register_tool_class(FigureCanvasGTK3)
class HelpGTK3(backend_tools.ToolHelpBase):
def _normalize_shortcut(self, key):
"""
Expand Down Expand Up @@ -698,6 +699,7 @@ def trigger(self, *args):
self._show_shortcuts_dialog()


@backend_tools._register_tool_class(FigureCanvasGTK3)
class ToolCopyToClipboardGTK3(backend_tools.ToolCopyToClipboardBase):
def trigger(self, *args, **kwargs):
clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
Expand All @@ -721,13 +723,11 @@ def error_msg_gtk(msg, parent=None):
dialog.destroy()


backend_tools.ToolSaveFigure = SaveFigureGTK3
backend_tools.ToolConfigureSubplots = ConfigureSubplotsGTK3
backend_tools.ToolRubberband = RubberbandGTK3
backend_tools.ToolHelp = HelpGTK3
backend_tools.ToolCopyToClipboard = ToolCopyToClipboardGTK3

Toolbar = ToolbarGTK3
backend_tools._register_tool_class(
FigureCanvasGTK3, _backend_gtk.ConfigureSubplotsGTK)
backend_tools._register_tool_class(
FigureCanvasGTK3, _backend_gtk.RubberbandGTK)


@_Backend.export
Expand Down
16 changes: 8 additions & 8 deletions lib/matplotlib/backends/backend_gtk4.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,11 @@
raise ImportError from e

from gi.repository import Gio, GLib, GObject, Gtk, Gdk, GdkPixbuf
from . import _backend_gtk
from ._backend_gtk import (
_create_application, _shutdown_application,
backend_version, _BackendGTK, _NavigationToolbar2GTK,
TimerGTK as TimerGTK4,
ConfigureSubplotsGTK as ConfigureSubplotsGTK4,
RubberbandGTK as RubberbandGTK4,
)


Expand Down Expand Up @@ -564,6 +563,7 @@ def set_message(self, s):
self._message.set_label(s)


@backend_tools._register_tool_class(FigureCanvasGTK4)
class SaveFigureGTK4(backend_tools.SaveFigureBase):
def trigger(self, *args, **kwargs):

Expand All @@ -573,6 +573,7 @@ class PseudoToolbar:
return NavigationToolbar2GTK4.save_figure(PseudoToolbar())


@backend_tools._register_tool_class(FigureCanvasGTK4)
class HelpGTK4(backend_tools.ToolHelpBase):
def _normalize_shortcut(self, key):
"""
Expand Down Expand Up @@ -646,6 +647,7 @@ def trigger(self, *args):
window.show()


@backend_tools._register_tool_class(FigureCanvasGTK4)
class ToolCopyToClipboardGTK4(backend_tools.ToolCopyToClipboardBase):
def trigger(self, *args, **kwargs):
with io.BytesIO() as f:
Expand All @@ -658,12 +660,10 @@ def trigger(self, *args, **kwargs):
clipboard.set(pb)


backend_tools.ToolSaveFigure = SaveFigureGTK4
backend_tools.ToolConfigureSubplots = ConfigureSubplotsGTK4
backend_tools.ToolRubberband = RubberbandGTK4
backend_tools.ToolHelp = HelpGTK4
backend_tools.ToolCopyToClipboard = ToolCopyToClipboardGTK4

backend_tools._register_tool_class(
FigureCanvasGTK4, _backend_gtk.ConfigureSubplotsGTK)
backend_tools._register_tool_class(
FigureCanvasGTK4, _backend_gtk.RubberbandGTK)
Toolbar = ToolbarGTK4


Expand Down
12 changes: 5 additions & 7 deletions lib/matplotlib/backends/backend_qt.py
Original file line number Diff line number Diff line change
Expand Up @@ -952,12 +952,14 @@ def set_message(self, s):
self.widgetForAction(self._message_action).setText(s)


@backend_tools._register_tool_class(FigureCanvasQT)
class ConfigureSubplotsQt(backend_tools.ConfigureSubplotsBase):
def trigger(self, *args):
NavigationToolbar2QT.configure_subplots(
self._make_classic_style_pseudo_toolbar())


@backend_tools._register_tool_class(FigureCanvasQT)
class SaveFigureQt(backend_tools.SaveFigureBase):
def trigger(self, *args):
NavigationToolbar2QT.save_figure(
Expand All @@ -971,6 +973,7 @@ def set_cursor(self, cursor):
self._make_classic_style_pseudo_toolbar(), cursor)


@backend_tools._register_tool_class(FigureCanvasQT)
class RubberbandQt(backend_tools.RubberbandBase):
def draw_rubberband(self, x0, y0, x1, y1):
NavigationToolbar2QT.draw_rubberband(
Expand All @@ -981,24 +984,19 @@ def remove_rubberband(self):
self._make_classic_style_pseudo_toolbar())


@backend_tools._register_tool_class(FigureCanvasQT)
class HelpQt(backend_tools.ToolHelpBase):
def trigger(self, *args):
QtWidgets.QMessageBox.information(None, "Help", self._get_help_html())


@backend_tools._register_tool_class(FigureCanvasQT)
class ToolCopyToClipboardQT(backend_tools.ToolCopyToClipboardBase):
def trigger(self, *args, **kwargs):
pixmap = self.canvas.grab()
qApp.clipboard().setPixmap(pixmap)


backend_tools.ToolSaveFigure = SaveFigureQt
backend_tools.ToolConfigureSubplots = ConfigureSubplotsQt
backend_tools.ToolRubberband = RubberbandQt
backend_tools.ToolHelp = HelpQt
backend_tools.ToolCopyToClipboard = ToolCopyToClipboardQT


@_Backend.export
class _BackendQT(_Backend):
FigureCanvas = FigureCanvasQT
Expand Down
Loading