Source code for matplotlib.backend_managers
import logging
import matplotlib.cbook as cbook
import matplotlib.widgets as widgets
from matplotlib.rcsetup import validate_stringlist
import matplotlib.backend_tools as tools
_log = logging.getLogger(__name__)
[docs]class ToolEvent:
"""Event for tool manipulation (add/remove)."""
def __init__(self, name, sender, tool, data=None):
self.name = name
self.sender = sender
self.tool = tool
self.data = data
[docs]class ToolTriggerEvent(ToolEvent):
"""Event to inform that a tool has been triggered."""
def __init__(self, name, sender, tool, canvasevent=None, data=None):
ToolEvent.__init__(self, name, sender, tool, data)
self.canvasevent = canvasevent
[docs]class ToolManagerMessageEvent:
"""
Event carrying messages from toolmanager.
Messages usually get displayed to the user by the toolbar.
"""
def __init__(self, name, sender, message):
self.name = name
self.sender = sender
self.message = message
[docs]class ToolManager:
"""
Manager for actions triggered by user interactions (key press, toolbar
clicks, ...) on a Figure.
Attributes
----------
figure : `.Figure`
keypresslock : `~matplotlib.widgets.LockDraw`
`.LockDraw` object to know if the `canvas` key_press_event is locked.
messagelock : `~matplotlib.widgets.LockDraw`
`.LockDraw` object to know if the message is available to write.
"""
def __init__(self, figure=None):
_log.warning('Treat the new Tool classes introduced in v1.5 as '
'experimental for now, the API will likely change in '
'version 2.1 and perhaps the rcParam as well')
self._key_press_handler_id = None
self._tools = {}
self._keys = {}
self._toggled = {}
self._callbacks = cbook.CallbackRegistry()
# to process keypress event
self.keypresslock = widgets.LockDraw()
self.messagelock = widgets.LockDraw()
self._figure = None
self.set_figure(figure)
@property
def canvas(self):
"""Canvas managed by FigureManager."""
if not self._figure:
return None
return self._figure.canvas
@property
def figure(self):
"""Figure that holds the canvas."""
return self._figure
@figure.setter
def figure(self, figure):
self.set_figure(figure)
[docs] def set_figure(self, figure, update_tools=True):
"""
Bind the given figure to the tools.
Parameters
----------
figure : `.Figure`
update_tools : bool, default: True
Force tools to update figure.
"""
if self._key_press_handler_id:
self.canvas.mpl_disconnect(self._key_press_handler_id)
self._figure = figure
if figure:
self._key_press_handler_id = self.canvas.mpl_connect(
'key_press_event', self._key_press)
if update_tools:
for tool in self._tools.values():
tool.figure = figure
[docs] def toolmanager_connect(self, s, func):
"""
Connect event with string *s* to *func*.
Parameters
----------
s : str
The name of the event. The following events are recognized:
- 'tool_message_event'
- 'tool_removed_event'
- 'tool_added_event'
For every tool added a new event is created
- 'tool_trigger_TOOLNAME', where TOOLNAME is the id of the tool.
func : callable
Callback function for the toolmanager event with signature::
def func(event: ToolEvent) -> Any
Returns
-------
cid
The callback id for the connection. This can be used in
`.toolmanager_disconnect`.
"""
return self._callbacks.connect(s, func)
[docs] def toolmanager_disconnect(self, cid):
"""
Disconnect callback id *cid*.
Example usage::
cid = toolmanager.toolmanager_connect('tool_trigger_zoom', onpress)
#...later
toolmanager.toolmanager_disconnect(cid)
"""
return self._callbacks.disconnect(cid)
[docs] def message_event(self, message, sender=None):
"""Emit a `ToolManagerMessageEvent`."""
if sender is None:
sender = self
s = 'tool_message_event'
event = ToolManagerMessageEvent(s, sender, message)
self._callbacks.process(s, event)
@property
def active_toggle(self):
"""Currently toggled tools."""
return self._toggled
[docs] def get_tool_keymap(self, name):
"""
Return the keymap associated with the specified tool.
Parameters
----------
name : str
Name of the Tool.
Returns
-------
list of str
List of keys associated with the tool.
"""
keys = [k for k, i in self._keys.items() if i == name]
return keys
def _remove_keys(self, name):
for k in self.get_tool_keymap(name):
del self._keys[k]
[docs] @cbook._delete_parameter("3.3", "args")
def update_keymap(self, name, key, *args):
"""
Set the keymap to associate with the specified tool.
Parameters
----------
name : str
Name of the Tool.
keys : str or list of str
Keys to associate with the tool.
"""
if name not in self._tools:
raise KeyError('%s not in Tools' % name)
self._remove_keys(name)
for key in [key, *args]:
if isinstance(key, str) and validate_stringlist(key) != [key]:
cbook.warn_deprecated(
"3.3", message="Passing a list of keys as a single "
"comma-separated string is deprecated since %(since)s and "
"support will be removed %(removal)s; pass keys as a list "
"of strings instead.")
key = validate_stringlist(key)
if isinstance(key, str):
key = [key]
for k in key:
if k in self._keys:
cbook._warn_external('Key %s changed from %s to %s' %
(k, self._keys[k], name))
self._keys[k] = name
[docs] def remove_tool(self, name):
"""
Remove tool named *name*.
Parameters
----------
name : str
Name of the tool.
"""
tool = self.get_tool(name)
tool.destroy()
# If is a toggle tool and toggled, untoggle
if getattr(tool, 'toggled', False):
self.trigger_tool(tool, 'toolmanager')
self._remove_keys(name)
s = 'tool_removed_event'
event = ToolEvent(s, self, tool)
self._callbacks.process(s, event)
del self._tools[name]
[docs] def add_tool(self, name, tool, *args, **kwargs):
"""
Add *tool* to `ToolManager`.
If successful, adds a new event ``tool_trigger_{name}`` where
``{name}`` is the *name* of the tool; the event is fired every time the
tool is triggered.
Parameters
----------
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.
Notes
-----
args and kwargs get passed directly to the tools constructor.
See Also
--------
matplotlib.backend_tools.ToolBase : The base class for tools.
"""
tool_cls = self._get_cls_to_instantiate(tool)
if not tool_cls:
raise ValueError('Impossible to find class for %s' % str(tool))
if name in self._tools:
cbook._warn_external('A "Tool class" with the same name already '
'exists, not added')
return self._tools[name]
tool_obj = tool_cls(self, name, *args, **kwargs)
self._tools[name] = tool_obj
if tool_cls.default_keymap is not None:
self.update_keymap(name, tool_cls.default_keymap)
# For toggle tools init the radio_group in self._toggled
if isinstance(tool_obj, 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:
self._toggled.setdefault(None, set())
else:
self._toggled.setdefault(tool_obj.radio_group, None)
# If initially toggled
if tool_obj.toggled:
self._handle_toggle(tool_obj, None, None, None)
tool_obj.set_figure(self.figure)
self._tool_added_event(tool_obj)
return tool_obj
def _tool_added_event(self, tool):
s = 'tool_added_event'
event = ToolEvent(s, self, tool)
self._callbacks.process(s, event)
def _handle_toggle(self, tool, sender, canvasevent, data):
"""
Toggle tools, need to untoggle prior to using other Toggle tool.
Called from trigger_tool.
Parameters
----------
tool : `.ToolBase`
sender : object
Object that wishes to trigger the tool.
canvasevent : Event
Original Canvas event or None.
data : object
Extra data to pass to the tool when triggering.
"""
radio_group = tool.radio_group
# radio_group None is not mutually exclusive
# just keep track of toggled tools in this group
if radio_group is None:
if tool.name in self._toggled[None]:
self._toggled[None].remove(tool.name)
else:
self._toggled[None].add(tool.name)
return
# If the tool already has a toggled state, untoggle it
if self._toggled[radio_group] == tool.name:
toggled = None
# If no tool was toggled in the radio_group
# toggle it
elif self._toggled[radio_group] is None:
toggled = tool.name
# Other tool in the radio_group is toggled
else:
# Untoggle previously toggled tool
self.trigger_tool(self._toggled[radio_group],
self,
canvasevent,
data)
toggled = tool.name
# 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
[docs] def trigger_tool(self, name, sender=None, canvasevent=None, data=None):
"""
Trigger a tool and emit the ``tool_trigger_{name}`` event.
Parameters
----------
name : str
Name of the tool.
sender : object
Object that wishes to trigger the tool.
canvasevent : Event
Original Canvas event or None.
data : object
Extra data to pass to the tool when triggering.
"""
tool = self.get_tool(name)
if tool is None:
return
if sender is None:
sender = self
self._trigger_tool(name, sender, canvasevent, data)
s = 'tool_trigger_%s' % name
event = ToolTriggerEvent(s, sender, tool, canvasevent, data)
self._callbacks.process(s, event)
def _trigger_tool(self, name, sender=None, canvasevent=None, data=None):
"""Actually trigger a tool."""
tool = self.get_tool(name)
if isinstance(tool, tools.ToolToggleBase):
self._handle_toggle(tool, sender, canvasevent, data)
# Important!!!
# This is where the Tool object gets triggered
tool.trigger(sender, canvasevent, data)
def _key_press(self, event):
if event.key is None or self.keypresslock.locked():
return
name = self._keys.get(event.key, None)
if name is None:
return
self.trigger_tool(name, canvasevent=event)
@property
def tools(self):
"""A dict mapping tool name -> controlled tool."""
return self._tools
[docs] def get_tool(self, name, warn=True):
"""
Return the tool object with the given name.
For convenience, this passes tool objects through.
Parameters
----------
name : str or `.ToolBase`
Name of the tool, or the tool itself.
warn : bool, default: True
Whether a warning should be emitted it no tool with the given name
exists.
Returns
-------
`.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:
return name
if name not in self._tools:
if warn:
cbook._warn_external("ToolManager does not control tool "
"%s" % name)
return None
return self._tools[name]