123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423 |
- 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__)
- 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):
- ToolEvent.__init__(self, 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 : `widgets.LockDraw`
- `LockDraw` object to know if the `canvas` key_press_event is locked
- messagelock : `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)
- def set_figure(self, figure, update_tools=True):
- """
- Bind the given figure to the tools.
- Parameters
- ----------
- figure : `.Figure`
- update_tools : bool
- 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 : String
- 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 : function
- Function to be called with signature
- def func(event)
- """
- 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):
- """
- Get the keymap associated with the specified tool.
- Parameters
- ----------
- name : str
- Name of the Tool.
- Returns
- -------
- list : 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, *keys):
- """
- Set the keymap to associate with the specified tool.
- Parameters
- ----------
- name : str
- Name of the Tool.
- keys : 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 keys:
- for k in validate_stringlist(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
- 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]
- 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 everytime 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 : Tool object
- 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
- 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
- def get_tool(self, name, warn=True):
- """
- Return the tool object, also accepts the actual tool for convenience.
- Parameters
- ----------
- name : str, ToolBase
- Name of the tool, or the tool itself
- warn : bool, optional
- If this method should give warnings.
- """
- 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]
|