123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387 |
- from matplotlib import _api, backend_tools, cbook, widgets
- 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
- class ToolTriggerEvent(ToolEvent):
- """Event to inform that a tool has been triggered."""
- def __init__(self, name, sender, tool, canvasevent=None, data=None):
- super().__init__(name, sender, tool, data)
- self.canvasevent = canvasevent
- 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
- 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):
- 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)
- 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
- 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)
- 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)
- 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
- 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]
- def update_keymap(self, name, key):
- """
- Set the keymap to associate with the specified tool.
- Parameters
- ----------
- name : str
- Name of the Tool.
- key : str or list of str
- Keys to associate with the tool.
- """
- if name not in self._tools:
- raise KeyError(f'{name!r} not in Tools')
- self._remove_keys(name)
- if isinstance(key, str):
- key = [key]
- for k in key:
- if k in self._keys:
- _api.warn_external(
- f'Key {k} changed from {self._keys[k]} to {name}')
- self._keys[k] = name
- def remove_tool(self, name):
- """
- Remove tool named *name*.
- Parameters
- ----------
- name : str
- Name of the tool.
- """
- tool = self.get_tool(name)
- if getattr(tool, 'toggled', False): # If it's a toggled toggle tool, untoggle
- self.trigger_tool(tool, 'toolmanager')
- self._remove_keys(name)
- event = ToolEvent('tool_removed_event', self, tool)
- self._callbacks.process(event.name, event)
- del self._tools[name]
- 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 : type
- Class of the tool to be added. A subclass will be used
- instead if one was registered for the current canvas class.
- *args, **kwargs
- Passed to the *tool*'s constructor.
- See Also
- --------
- matplotlib.backend_tools.ToolBase : The base class for tools.
- """
- 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))
- if name in self._tools:
- _api.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_obj.default_keymap is not None:
- self.update_keymap(name, tool_obj.default_keymap)
- # For toggle tools init the radio_group in self._toggled
- 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:
- 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)
- tool_obj.set_figure(self.figure)
- event = ToolEvent('tool_added_event', self, tool_obj)
- self._callbacks.process(event.name, event)
- return tool_obj
- def _handle_toggle(self, tool, canvasevent, data):
- """
- Toggle tools, need to untoggle prior to using other Toggle tool.
- Called from trigger_tool.
- Parameters
- ----------
- tool : `.ToolBase`
- 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 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
- if isinstance(tool, backend_tools.ToolToggleBase):
- self._handle_toggle(tool, canvasevent, data)
- tool.trigger(sender, canvasevent, data) # Actually trigger Tool.
- s = 'tool_trigger_%s' % name
- event = ToolTriggerEvent(s, sender, tool, canvasevent, data)
- self._callbacks.process(s, event)
- 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
- 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, backend_tools.ToolBase)
- and name.name in self._tools):
- return name
- if name not in self._tools:
- if warn:
- _api.warn_external(
- f"ToolManager does not control tool {name!r}")
- return None
- return self._tools[name]
|