Coverage for /usr/lib/python3/dist-packages/matplotlib/backend_managers.py: 21%
154 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-14 15:55 +0200
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-14 15:55 +0200
1from matplotlib import _api, backend_tools, cbook, widgets
4class ToolEvent:
5 """Event for tool manipulation (add/remove)."""
6 def __init__(self, name, sender, tool, data=None):
7 self.name = name
8 self.sender = sender
9 self.tool = tool
10 self.data = data
13class ToolTriggerEvent(ToolEvent):
14 """Event to inform that a tool has been triggered."""
15 def __init__(self, name, sender, tool, canvasevent=None, data=None):
16 super().__init__(name, sender, tool, data)
17 self.canvasevent = canvasevent
20class ToolManagerMessageEvent:
21 """
22 Event carrying messages from toolmanager.
24 Messages usually get displayed to the user by the toolbar.
25 """
26 def __init__(self, name, sender, message):
27 self.name = name
28 self.sender = sender
29 self.message = message
32class ToolManager:
33 """
34 Manager for actions triggered by user interactions (key press, toolbar
35 clicks, ...) on a Figure.
37 Attributes
38 ----------
39 figure : `.Figure`
40 keypresslock : `~matplotlib.widgets.LockDraw`
41 `.LockDraw` object to know if the `canvas` key_press_event is locked.
42 messagelock : `~matplotlib.widgets.LockDraw`
43 `.LockDraw` object to know if the message is available to write.
44 """
46 def __init__(self, figure=None):
48 self._key_press_handler_id = None
50 self._tools = {}
51 self._keys = {}
52 self._toggled = {}
53 self._callbacks = cbook.CallbackRegistry()
55 # to process keypress event
56 self.keypresslock = widgets.LockDraw()
57 self.messagelock = widgets.LockDraw()
59 self._figure = None
60 self.set_figure(figure)
62 @property
63 def canvas(self):
64 """Canvas managed by FigureManager."""
65 if not self._figure:
66 return None
67 return self._figure.canvas
69 @property
70 def figure(self):
71 """Figure that holds the canvas."""
72 return self._figure
74 @figure.setter
75 def figure(self, figure):
76 self.set_figure(figure)
78 def set_figure(self, figure, update_tools=True):
79 """
80 Bind the given figure to the tools.
82 Parameters
83 ----------
84 figure : `.Figure`
85 update_tools : bool, default: True
86 Force tools to update figure.
87 """
88 if self._key_press_handler_id:
89 self.canvas.mpl_disconnect(self._key_press_handler_id)
90 self._figure = figure
91 if figure:
92 self._key_press_handler_id = self.canvas.mpl_connect(
93 'key_press_event', self._key_press)
94 if update_tools:
95 for tool in self._tools.values():
96 tool.figure = figure
98 def toolmanager_connect(self, s, func):
99 """
100 Connect event with string *s* to *func*.
102 Parameters
103 ----------
104 s : str
105 The name of the event. The following events are recognized:
107 - 'tool_message_event'
108 - 'tool_removed_event'
109 - 'tool_added_event'
111 For every tool added a new event is created
113 - 'tool_trigger_TOOLNAME', where TOOLNAME is the id of the tool.
115 func : callable
116 Callback function for the toolmanager event with signature::
118 def func(event: ToolEvent) -> Any
120 Returns
121 -------
122 cid
123 The callback id for the connection. This can be used in
124 `.toolmanager_disconnect`.
125 """
126 return self._callbacks.connect(s, func)
128 def toolmanager_disconnect(self, cid):
129 """
130 Disconnect callback id *cid*.
132 Example usage::
134 cid = toolmanager.toolmanager_connect('tool_trigger_zoom', onpress)
135 #...later
136 toolmanager.toolmanager_disconnect(cid)
137 """
138 return self._callbacks.disconnect(cid)
140 def message_event(self, message, sender=None):
141 """Emit a `ToolManagerMessageEvent`."""
142 if sender is None:
143 sender = self
145 s = 'tool_message_event'
146 event = ToolManagerMessageEvent(s, sender, message)
147 self._callbacks.process(s, event)
149 @property
150 def active_toggle(self):
151 """Currently toggled tools."""
152 return self._toggled
154 def get_tool_keymap(self, name):
155 """
156 Return the keymap associated with the specified tool.
158 Parameters
159 ----------
160 name : str
161 Name of the Tool.
163 Returns
164 -------
165 list of str
166 List of keys associated with the tool.
167 """
169 keys = [k for k, i in self._keys.items() if i == name]
170 return keys
172 def _remove_keys(self, name):
173 for k in self.get_tool_keymap(name):
174 del self._keys[k]
176 def update_keymap(self, name, key):
177 """
178 Set the keymap to associate with the specified tool.
180 Parameters
181 ----------
182 name : str
183 Name of the Tool.
184 key : str or list of str
185 Keys to associate with the tool.
186 """
187 if name not in self._tools:
188 raise KeyError(f'{name!r} not in Tools')
189 self._remove_keys(name)
190 if isinstance(key, str):
191 key = [key]
192 for k in key:
193 if k in self._keys:
194 _api.warn_external(
195 f'Key {k} changed from {self._keys[k]} to {name}')
196 self._keys[k] = name
198 def remove_tool(self, name):
199 """
200 Remove tool named *name*.
202 Parameters
203 ----------
204 name : str
205 Name of the tool.
206 """
208 tool = self.get_tool(name)
209 destroy = _api.deprecate_method_override(
210 backend_tools.ToolBase.destroy, tool, since="3.6",
211 alternative="tool_removed_event")
212 if destroy is not None:
213 destroy()
215 # If it's a toggle tool and toggled, untoggle
216 if getattr(tool, 'toggled', False):
217 self.trigger_tool(tool, 'toolmanager')
219 self._remove_keys(name)
221 event = ToolEvent('tool_removed_event', self, tool)
222 self._callbacks.process(event.name, event)
224 del self._tools[name]
226 def add_tool(self, name, tool, *args, **kwargs):
227 """
228 Add *tool* to `ToolManager`.
230 If successful, adds a new event ``tool_trigger_{name}`` where
231 ``{name}`` is the *name* of the tool; the event is fired every time the
232 tool is triggered.
234 Parameters
235 ----------
236 name : str
237 Name of the tool, treated as the ID, has to be unique.
238 tool : type
239 Class of the tool to be added. A subclass will be used
240 instead if one was registered for the current canvas class.
242 Notes
243 -----
244 args and kwargs get passed directly to the tools constructor.
246 See Also
247 --------
248 matplotlib.backend_tools.ToolBase : The base class for tools.
249 """
251 tool_cls = backend_tools._find_tool_class(type(self.canvas), tool)
252 if not tool_cls:
253 raise ValueError('Impossible to find class for %s' % str(tool))
255 if name in self._tools:
256 _api.warn_external('A "Tool class" with the same name already '
257 'exists, not added')
258 return self._tools[name]
260 if name == 'cursor' and tool_cls != backend_tools.SetCursorBase:
261 _api.warn_deprecated("3.5",
262 message="Overriding ToolSetCursor with "
263 f"{tool_cls.__qualname__} was only "
264 "necessary to provide the .set_cursor() "
265 "method, which is deprecated since "
266 "%(since)s and will be removed "
267 "%(removal)s. Please report this to the "
268 f"{tool_cls.__module__} author.")
270 tool_obj = tool_cls(self, name, *args, **kwargs)
271 self._tools[name] = tool_obj
273 if tool_obj.default_keymap is not None:
274 self.update_keymap(name, tool_obj.default_keymap)
276 # For toggle tools init the radio_group in self._toggled
277 if isinstance(tool_obj, backend_tools.ToolToggleBase):
278 # None group is not mutually exclusive, a set is used to keep track
279 # of all toggled tools in this group
280 if tool_obj.radio_group is None:
281 self._toggled.setdefault(None, set())
282 else:
283 self._toggled.setdefault(tool_obj.radio_group, None)
285 # If initially toggled
286 if tool_obj.toggled:
287 self._handle_toggle(tool_obj, None, None)
288 tool_obj.set_figure(self.figure)
290 event = ToolEvent('tool_added_event', self, tool_obj)
291 self._callbacks.process(event.name, event)
293 return tool_obj
295 def _handle_toggle(self, tool, canvasevent, data):
296 """
297 Toggle tools, need to untoggle prior to using other Toggle tool.
298 Called from trigger_tool.
300 Parameters
301 ----------
302 tool : `.ToolBase`
303 canvasevent : Event
304 Original Canvas event or None.
305 data : object
306 Extra data to pass to the tool when triggering.
307 """
309 radio_group = tool.radio_group
310 # radio_group None is not mutually exclusive
311 # just keep track of toggled tools in this group
312 if radio_group is None:
313 if tool.name in self._toggled[None]:
314 self._toggled[None].remove(tool.name)
315 else:
316 self._toggled[None].add(tool.name)
317 return
319 # If the tool already has a toggled state, untoggle it
320 if self._toggled[radio_group] == tool.name:
321 toggled = None
322 # If no tool was toggled in the radio_group
323 # toggle it
324 elif self._toggled[radio_group] is None:
325 toggled = tool.name
326 # Other tool in the radio_group is toggled
327 else:
328 # Untoggle previously toggled tool
329 self.trigger_tool(self._toggled[radio_group],
330 self,
331 canvasevent,
332 data)
333 toggled = tool.name
335 # Keep track of the toggled tool in the radio_group
336 self._toggled[radio_group] = toggled
338 def trigger_tool(self, name, sender=None, canvasevent=None, data=None):
339 """
340 Trigger a tool and emit the ``tool_trigger_{name}`` event.
342 Parameters
343 ----------
344 name : str
345 Name of the tool.
346 sender : object
347 Object that wishes to trigger the tool.
348 canvasevent : Event
349 Original Canvas event or None.
350 data : object
351 Extra data to pass to the tool when triggering.
352 """
353 tool = self.get_tool(name)
354 if tool is None:
355 return
357 if sender is None:
358 sender = self
360 if isinstance(tool, backend_tools.ToolToggleBase):
361 self._handle_toggle(tool, canvasevent, data)
363 tool.trigger(sender, canvasevent, data) # Actually trigger Tool.
365 s = 'tool_trigger_%s' % name
366 event = ToolTriggerEvent(s, sender, tool, canvasevent, data)
367 self._callbacks.process(s, event)
369 def _key_press(self, event):
370 if event.key is None or self.keypresslock.locked():
371 return
373 name = self._keys.get(event.key, None)
374 if name is None:
375 return
376 self.trigger_tool(name, canvasevent=event)
378 @property
379 def tools(self):
380 """A dict mapping tool name -> controlled tool."""
381 return self._tools
383 def get_tool(self, name, warn=True):
384 """
385 Return the tool object with the given name.
387 For convenience, this passes tool objects through.
389 Parameters
390 ----------
391 name : str or `.ToolBase`
392 Name of the tool, or the tool itself.
393 warn : bool, default: True
394 Whether a warning should be emitted it no tool with the given name
395 exists.
397 Returns
398 -------
399 `.ToolBase` or None
400 The tool or None if no tool with the given name exists.
401 """
402 if (isinstance(name, backend_tools.ToolBase)
403 and name.name in self._tools):
404 return name
405 if name not in self._tools:
406 if warn:
407 _api.warn_external(
408 f"ToolManager does not control tool {name!r}")
409 return None
410 return self._tools[name]