backend_managers.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423
  1. import logging
  2. import matplotlib.cbook as cbook
  3. import matplotlib.widgets as widgets
  4. from matplotlib.rcsetup import validate_stringlist
  5. import matplotlib.backend_tools as tools
  6. _log = logging.getLogger(__name__)
  7. class ToolEvent:
  8. """Event for tool manipulation (add/remove)."""
  9. def __init__(self, name, sender, tool, data=None):
  10. self.name = name
  11. self.sender = sender
  12. self.tool = tool
  13. self.data = data
  14. class ToolTriggerEvent(ToolEvent):
  15. """Event to inform that a tool has been triggered."""
  16. def __init__(self, name, sender, tool, canvasevent=None, data=None):
  17. ToolEvent.__init__(self, name, sender, tool, data)
  18. self.canvasevent = canvasevent
  19. class ToolManagerMessageEvent:
  20. """
  21. Event carrying messages from toolmanager.
  22. Messages usually get displayed to the user by the toolbar.
  23. """
  24. def __init__(self, name, sender, message):
  25. self.name = name
  26. self.sender = sender
  27. self.message = message
  28. class ToolManager:
  29. """
  30. Manager for actions triggered by user interactions (key press, toolbar
  31. clicks, ...) on a Figure.
  32. Attributes
  33. ----------
  34. figure : `Figure`
  35. keypresslock : `widgets.LockDraw`
  36. `LockDraw` object to know if the `canvas` key_press_event is locked
  37. messagelock : `widgets.LockDraw`
  38. `LockDraw` object to know if the message is available to write
  39. """
  40. def __init__(self, figure=None):
  41. _log.warning('Treat the new Tool classes introduced in v1.5 as '
  42. 'experimental for now, the API will likely change in '
  43. 'version 2.1 and perhaps the rcParam as well')
  44. self._key_press_handler_id = None
  45. self._tools = {}
  46. self._keys = {}
  47. self._toggled = {}
  48. self._callbacks = cbook.CallbackRegistry()
  49. # to process keypress event
  50. self.keypresslock = widgets.LockDraw()
  51. self.messagelock = widgets.LockDraw()
  52. self._figure = None
  53. self.set_figure(figure)
  54. @property
  55. def canvas(self):
  56. """Canvas managed by FigureManager."""
  57. if not self._figure:
  58. return None
  59. return self._figure.canvas
  60. @property
  61. def figure(self):
  62. """Figure that holds the canvas."""
  63. return self._figure
  64. @figure.setter
  65. def figure(self, figure):
  66. self.set_figure(figure)
  67. def set_figure(self, figure, update_tools=True):
  68. """
  69. Bind the given figure to the tools.
  70. Parameters
  71. ----------
  72. figure : `.Figure`
  73. update_tools : bool
  74. Force tools to update figure
  75. """
  76. if self._key_press_handler_id:
  77. self.canvas.mpl_disconnect(self._key_press_handler_id)
  78. self._figure = figure
  79. if figure:
  80. self._key_press_handler_id = self.canvas.mpl_connect(
  81. 'key_press_event', self._key_press)
  82. if update_tools:
  83. for tool in self._tools.values():
  84. tool.figure = figure
  85. def toolmanager_connect(self, s, func):
  86. """
  87. Connect event with string *s* to *func*.
  88. Parameters
  89. ----------
  90. s : String
  91. Name of the event
  92. The following events are recognized
  93. - 'tool_message_event'
  94. - 'tool_removed_event'
  95. - 'tool_added_event'
  96. For every tool added a new event is created
  97. - 'tool_trigger_TOOLNAME`
  98. Where TOOLNAME is the id of the tool.
  99. func : function
  100. Function to be called with signature
  101. def func(event)
  102. """
  103. return self._callbacks.connect(s, func)
  104. def toolmanager_disconnect(self, cid):
  105. """
  106. Disconnect callback id *cid*.
  107. Example usage::
  108. cid = toolmanager.toolmanager_connect('tool_trigger_zoom', onpress)
  109. #...later
  110. toolmanager.toolmanager_disconnect(cid)
  111. """
  112. return self._callbacks.disconnect(cid)
  113. def message_event(self, message, sender=None):
  114. """Emit a `ToolManagerMessageEvent`."""
  115. if sender is None:
  116. sender = self
  117. s = 'tool_message_event'
  118. event = ToolManagerMessageEvent(s, sender, message)
  119. self._callbacks.process(s, event)
  120. @property
  121. def active_toggle(self):
  122. """Currently toggled tools."""
  123. return self._toggled
  124. def get_tool_keymap(self, name):
  125. """
  126. Get the keymap associated with the specified tool.
  127. Parameters
  128. ----------
  129. name : str
  130. Name of the Tool.
  131. Returns
  132. -------
  133. list : list of keys associated with the Tool
  134. """
  135. keys = [k for k, i in self._keys.items() if i == name]
  136. return keys
  137. def _remove_keys(self, name):
  138. for k in self.get_tool_keymap(name):
  139. del self._keys[k]
  140. def update_keymap(self, name, *keys):
  141. """
  142. Set the keymap to associate with the specified tool.
  143. Parameters
  144. ----------
  145. name : str
  146. Name of the Tool.
  147. keys : keys to associate with the Tool
  148. """
  149. if name not in self._tools:
  150. raise KeyError('%s not in Tools' % name)
  151. self._remove_keys(name)
  152. for key in keys:
  153. for k in validate_stringlist(key):
  154. if k in self._keys:
  155. cbook._warn_external('Key %s changed from %s to %s' %
  156. (k, self._keys[k], name))
  157. self._keys[k] = name
  158. def remove_tool(self, name):
  159. """
  160. Remove tool named *name*.
  161. Parameters
  162. ----------
  163. name : str
  164. Name of the Tool.
  165. """
  166. tool = self.get_tool(name)
  167. tool.destroy()
  168. # If is a toggle tool and toggled, untoggle
  169. if getattr(tool, 'toggled', False):
  170. self.trigger_tool(tool, 'toolmanager')
  171. self._remove_keys(name)
  172. s = 'tool_removed_event'
  173. event = ToolEvent(s, self, tool)
  174. self._callbacks.process(s, event)
  175. del self._tools[name]
  176. def add_tool(self, name, tool, *args, **kwargs):
  177. """
  178. Add *tool* to `ToolManager`.
  179. If successful, adds a new event ``tool_trigger_{name}`` where
  180. ``{name}`` is the *name* of the tool; the event is fired everytime the
  181. tool is triggered.
  182. Parameters
  183. ----------
  184. name : str
  185. Name of the tool, treated as the ID, has to be unique.
  186. tool : class_like, i.e. str or type
  187. Reference to find the class of the Tool to added.
  188. Notes
  189. -----
  190. args and kwargs get passed directly to the tools constructor.
  191. See Also
  192. --------
  193. matplotlib.backend_tools.ToolBase : The base class for tools.
  194. """
  195. tool_cls = self._get_cls_to_instantiate(tool)
  196. if not tool_cls:
  197. raise ValueError('Impossible to find class for %s' % str(tool))
  198. if name in self._tools:
  199. cbook._warn_external('A "Tool class" with the same name already '
  200. 'exists, not added')
  201. return self._tools[name]
  202. tool_obj = tool_cls(self, name, *args, **kwargs)
  203. self._tools[name] = tool_obj
  204. if tool_cls.default_keymap is not None:
  205. self.update_keymap(name, tool_cls.default_keymap)
  206. # For toggle tools init the radio_group in self._toggled
  207. if isinstance(tool_obj, tools.ToolToggleBase):
  208. # None group is not mutually exclusive, a set is used to keep track
  209. # of all toggled tools in this group
  210. if tool_obj.radio_group is None:
  211. self._toggled.setdefault(None, set())
  212. else:
  213. self._toggled.setdefault(tool_obj.radio_group, None)
  214. # If initially toggled
  215. if tool_obj.toggled:
  216. self._handle_toggle(tool_obj, None, None, None)
  217. tool_obj.set_figure(self.figure)
  218. self._tool_added_event(tool_obj)
  219. return tool_obj
  220. def _tool_added_event(self, tool):
  221. s = 'tool_added_event'
  222. event = ToolEvent(s, self, tool)
  223. self._callbacks.process(s, event)
  224. def _handle_toggle(self, tool, sender, canvasevent, data):
  225. """
  226. Toggle tools, need to untoggle prior to using other Toggle tool.
  227. Called from trigger_tool.
  228. Parameters
  229. ----------
  230. tool : Tool object
  231. sender : object
  232. Object that wishes to trigger the tool
  233. canvasevent : Event
  234. Original Canvas event or None
  235. data : Object
  236. Extra data to pass to the tool when triggering
  237. """
  238. radio_group = tool.radio_group
  239. # radio_group None is not mutually exclusive
  240. # just keep track of toggled tools in this group
  241. if radio_group is None:
  242. if tool.name in self._toggled[None]:
  243. self._toggled[None].remove(tool.name)
  244. else:
  245. self._toggled[None].add(tool.name)
  246. return
  247. # If the tool already has a toggled state, untoggle it
  248. if self._toggled[radio_group] == tool.name:
  249. toggled = None
  250. # If no tool was toggled in the radio_group
  251. # toggle it
  252. elif self._toggled[radio_group] is None:
  253. toggled = tool.name
  254. # Other tool in the radio_group is toggled
  255. else:
  256. # Untoggle previously toggled tool
  257. self.trigger_tool(self._toggled[radio_group],
  258. self,
  259. canvasevent,
  260. data)
  261. toggled = tool.name
  262. # Keep track of the toggled tool in the radio_group
  263. self._toggled[radio_group] = toggled
  264. def _get_cls_to_instantiate(self, callback_class):
  265. # Find the class that corresponds to the tool
  266. if isinstance(callback_class, str):
  267. # FIXME: make more complete searching structure
  268. if callback_class in globals():
  269. callback_class = globals()[callback_class]
  270. else:
  271. mod = 'backend_tools'
  272. current_module = __import__(mod,
  273. globals(), locals(), [mod], 1)
  274. callback_class = getattr(current_module, callback_class, False)
  275. if callable(callback_class):
  276. return callback_class
  277. else:
  278. return None
  279. def trigger_tool(self, name, sender=None, canvasevent=None, data=None):
  280. """
  281. Trigger a tool and emit the ``tool_trigger_{name}`` event.
  282. Parameters
  283. ----------
  284. name : str
  285. Name of the tool.
  286. sender : object
  287. Object that wishes to trigger the tool
  288. canvasevent : Event
  289. Original Canvas event or None
  290. data : Object
  291. Extra data to pass to the tool when triggering
  292. """
  293. tool = self.get_tool(name)
  294. if tool is None:
  295. return
  296. if sender is None:
  297. sender = self
  298. self._trigger_tool(name, sender, canvasevent, data)
  299. s = 'tool_trigger_%s' % name
  300. event = ToolTriggerEvent(s, sender, tool, canvasevent, data)
  301. self._callbacks.process(s, event)
  302. def _trigger_tool(self, name, sender=None, canvasevent=None, data=None):
  303. """Actually trigger a tool."""
  304. tool = self.get_tool(name)
  305. if isinstance(tool, tools.ToolToggleBase):
  306. self._handle_toggle(tool, sender, canvasevent, data)
  307. # Important!!!
  308. # This is where the Tool object gets triggered
  309. tool.trigger(sender, canvasevent, data)
  310. def _key_press(self, event):
  311. if event.key is None or self.keypresslock.locked():
  312. return
  313. name = self._keys.get(event.key, None)
  314. if name is None:
  315. return
  316. self.trigger_tool(name, canvasevent=event)
  317. @property
  318. def tools(self):
  319. """A dict mapping tool name -> controlled tool."""
  320. return self._tools
  321. def get_tool(self, name, warn=True):
  322. """
  323. Return the tool object, also accepts the actual tool for convenience.
  324. Parameters
  325. ----------
  326. name : str, ToolBase
  327. Name of the tool, or the tool itself
  328. warn : bool, optional
  329. If this method should give warnings.
  330. """
  331. if isinstance(name, tools.ToolBase) and name.name in self._tools:
  332. return name
  333. if name not in self._tools:
  334. if warn:
  335. cbook._warn_external("ToolManager does not control tool "
  336. "%s" % name)
  337. return None
  338. return self._tools[name]