backend_managers.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444
  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 : `~matplotlib.widgets.LockDraw`
  36. `.LockDraw` object to know if the `canvas` key_press_event is locked.
  37. messagelock : `~matplotlib.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, default: True
  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 : str
  91. The name of the event. The following events are recognized:
  92. - 'tool_message_event'
  93. - 'tool_removed_event'
  94. - 'tool_added_event'
  95. For every tool added a new event is created
  96. - 'tool_trigger_TOOLNAME', where TOOLNAME is the id of the tool.
  97. func : callable
  98. Callback function for the toolmanager event with signature::
  99. def func(event: ToolEvent) -> Any
  100. Returns
  101. -------
  102. cid
  103. The callback id for the connection. This can be used in
  104. `.toolmanager_disconnect`.
  105. """
  106. return self._callbacks.connect(s, func)
  107. def toolmanager_disconnect(self, cid):
  108. """
  109. Disconnect callback id *cid*.
  110. Example usage::
  111. cid = toolmanager.toolmanager_connect('tool_trigger_zoom', onpress)
  112. #...later
  113. toolmanager.toolmanager_disconnect(cid)
  114. """
  115. return self._callbacks.disconnect(cid)
  116. def message_event(self, message, sender=None):
  117. """Emit a `ToolManagerMessageEvent`."""
  118. if sender is None:
  119. sender = self
  120. s = 'tool_message_event'
  121. event = ToolManagerMessageEvent(s, sender, message)
  122. self._callbacks.process(s, event)
  123. @property
  124. def active_toggle(self):
  125. """Currently toggled tools."""
  126. return self._toggled
  127. def get_tool_keymap(self, name):
  128. """
  129. Return the keymap associated with the specified tool.
  130. Parameters
  131. ----------
  132. name : str
  133. Name of the Tool.
  134. Returns
  135. -------
  136. list of str
  137. List of keys associated with the tool.
  138. """
  139. keys = [k for k, i in self._keys.items() if i == name]
  140. return keys
  141. def _remove_keys(self, name):
  142. for k in self.get_tool_keymap(name):
  143. del self._keys[k]
  144. @cbook._delete_parameter("3.3", "args")
  145. def update_keymap(self, name, key, *args):
  146. """
  147. Set the keymap to associate with the specified tool.
  148. Parameters
  149. ----------
  150. name : str
  151. Name of the Tool.
  152. keys : str or list of str
  153. Keys to associate with the tool.
  154. """
  155. if name not in self._tools:
  156. raise KeyError('%s not in Tools' % name)
  157. self._remove_keys(name)
  158. for key in [key, *args]:
  159. if isinstance(key, str) and validate_stringlist(key) != [key]:
  160. cbook.warn_deprecated(
  161. "3.3", message="Passing a list of keys as a single "
  162. "comma-separated string is deprecated since %(since)s and "
  163. "support will be removed %(removal)s; pass keys as a list "
  164. "of strings instead.")
  165. key = validate_stringlist(key)
  166. if isinstance(key, str):
  167. key = [key]
  168. for k in key:
  169. if k in self._keys:
  170. cbook._warn_external('Key %s changed from %s to %s' %
  171. (k, self._keys[k], name))
  172. self._keys[k] = name
  173. def remove_tool(self, name):
  174. """
  175. Remove tool named *name*.
  176. Parameters
  177. ----------
  178. name : str
  179. Name of the tool.
  180. """
  181. tool = self.get_tool(name)
  182. tool.destroy()
  183. # If is a toggle tool and toggled, untoggle
  184. if getattr(tool, 'toggled', False):
  185. self.trigger_tool(tool, 'toolmanager')
  186. self._remove_keys(name)
  187. s = 'tool_removed_event'
  188. event = ToolEvent(s, self, tool)
  189. self._callbacks.process(s, event)
  190. del self._tools[name]
  191. def add_tool(self, name, tool, *args, **kwargs):
  192. """
  193. Add *tool* to `ToolManager`.
  194. If successful, adds a new event ``tool_trigger_{name}`` where
  195. ``{name}`` is the *name* of the tool; the event is fired every time the
  196. tool is triggered.
  197. Parameters
  198. ----------
  199. name : str
  200. Name of the tool, treated as the ID, has to be unique.
  201. tool : class_like, i.e. str or type
  202. Reference to find the class of the Tool to added.
  203. Notes
  204. -----
  205. args and kwargs get passed directly to the tools constructor.
  206. See Also
  207. --------
  208. matplotlib.backend_tools.ToolBase : The base class for tools.
  209. """
  210. tool_cls = self._get_cls_to_instantiate(tool)
  211. if not tool_cls:
  212. raise ValueError('Impossible to find class for %s' % str(tool))
  213. if name in self._tools:
  214. cbook._warn_external('A "Tool class" with the same name already '
  215. 'exists, not added')
  216. return self._tools[name]
  217. tool_obj = tool_cls(self, name, *args, **kwargs)
  218. self._tools[name] = tool_obj
  219. if tool_cls.default_keymap is not None:
  220. self.update_keymap(name, tool_cls.default_keymap)
  221. # For toggle tools init the radio_group in self._toggled
  222. if isinstance(tool_obj, tools.ToolToggleBase):
  223. # None group is not mutually exclusive, a set is used to keep track
  224. # of all toggled tools in this group
  225. if tool_obj.radio_group is None:
  226. self._toggled.setdefault(None, set())
  227. else:
  228. self._toggled.setdefault(tool_obj.radio_group, None)
  229. # If initially toggled
  230. if tool_obj.toggled:
  231. self._handle_toggle(tool_obj, None, None, None)
  232. tool_obj.set_figure(self.figure)
  233. self._tool_added_event(tool_obj)
  234. return tool_obj
  235. def _tool_added_event(self, tool):
  236. s = 'tool_added_event'
  237. event = ToolEvent(s, self, tool)
  238. self._callbacks.process(s, event)
  239. def _handle_toggle(self, tool, sender, canvasevent, data):
  240. """
  241. Toggle tools, need to untoggle prior to using other Toggle tool.
  242. Called from trigger_tool.
  243. Parameters
  244. ----------
  245. tool : `.ToolBase`
  246. sender : object
  247. Object that wishes to trigger the tool.
  248. canvasevent : Event
  249. Original Canvas event or None.
  250. data : object
  251. Extra data to pass to the tool when triggering.
  252. """
  253. radio_group = tool.radio_group
  254. # radio_group None is not mutually exclusive
  255. # just keep track of toggled tools in this group
  256. if radio_group is None:
  257. if tool.name in self._toggled[None]:
  258. self._toggled[None].remove(tool.name)
  259. else:
  260. self._toggled[None].add(tool.name)
  261. return
  262. # If the tool already has a toggled state, untoggle it
  263. if self._toggled[radio_group] == tool.name:
  264. toggled = None
  265. # If no tool was toggled in the radio_group
  266. # toggle it
  267. elif self._toggled[radio_group] is None:
  268. toggled = tool.name
  269. # Other tool in the radio_group is toggled
  270. else:
  271. # Untoggle previously toggled tool
  272. self.trigger_tool(self._toggled[radio_group],
  273. self,
  274. canvasevent,
  275. data)
  276. toggled = tool.name
  277. # Keep track of the toggled tool in the radio_group
  278. self._toggled[radio_group] = toggled
  279. def _get_cls_to_instantiate(self, callback_class):
  280. # Find the class that corresponds to the tool
  281. if isinstance(callback_class, str):
  282. # FIXME: make more complete searching structure
  283. if callback_class in globals():
  284. callback_class = globals()[callback_class]
  285. else:
  286. mod = 'backend_tools'
  287. current_module = __import__(mod,
  288. globals(), locals(), [mod], 1)
  289. callback_class = getattr(current_module, callback_class, False)
  290. if callable(callback_class):
  291. return callback_class
  292. else:
  293. return None
  294. def trigger_tool(self, name, sender=None, canvasevent=None, data=None):
  295. """
  296. Trigger a tool and emit the ``tool_trigger_{name}`` event.
  297. Parameters
  298. ----------
  299. name : str
  300. Name of the tool.
  301. sender : object
  302. Object that wishes to trigger the tool.
  303. canvasevent : Event
  304. Original Canvas event or None.
  305. data : object
  306. Extra data to pass to the tool when triggering.
  307. """
  308. tool = self.get_tool(name)
  309. if tool is None:
  310. return
  311. if sender is None:
  312. sender = self
  313. self._trigger_tool(name, sender, canvasevent, data)
  314. s = 'tool_trigger_%s' % name
  315. event = ToolTriggerEvent(s, sender, tool, canvasevent, data)
  316. self._callbacks.process(s, event)
  317. def _trigger_tool(self, name, sender=None, canvasevent=None, data=None):
  318. """Actually trigger a tool."""
  319. tool = self.get_tool(name)
  320. if isinstance(tool, tools.ToolToggleBase):
  321. self._handle_toggle(tool, sender, canvasevent, data)
  322. # Important!!!
  323. # This is where the Tool object gets triggered
  324. tool.trigger(sender, canvasevent, data)
  325. def _key_press(self, event):
  326. if event.key is None or self.keypresslock.locked():
  327. return
  328. name = self._keys.get(event.key, None)
  329. if name is None:
  330. return
  331. self.trigger_tool(name, canvasevent=event)
  332. @property
  333. def tools(self):
  334. """A dict mapping tool name -> controlled tool."""
  335. return self._tools
  336. def get_tool(self, name, warn=True):
  337. """
  338. Return the tool object with the given name.
  339. For convenience, this passes tool objects through.
  340. Parameters
  341. ----------
  342. name : str or `.ToolBase`
  343. Name of the tool, or the tool itself.
  344. warn : bool, default: True
  345. Whether a warning should be emitted it no tool with the given name
  346. exists.
  347. Returns
  348. -------
  349. `.ToolBase` or None
  350. The tool or None if no tool with the given name exists.
  351. """
  352. if isinstance(name, tools.ToolBase) and name.name in self._tools:
  353. return name
  354. if name not in self._tools:
  355. if warn:
  356. cbook._warn_external("ToolManager does not control tool "
  357. "%s" % name)
  358. return None
  359. return self._tools[name]