backend_managers.py 12 KB

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