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]