Coverage for /usr/lib/python3/dist-packages/matplotlib/backend_tools.py: 37%
513 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
1"""
2Abstract base classes define the primitives for Tools.
3These tools are used by `matplotlib.backend_managers.ToolManager`
5:class:`ToolBase`
6 Simple stateless tool
8:class:`ToolToggleBase`
9 Tool that has two states, only one Toggle tool can be
10 active at any given time for the same
11 `matplotlib.backend_managers.ToolManager`
12"""
14import enum
15import functools
16import re
17import time
18from types import SimpleNamespace
19import uuid
20from weakref import WeakKeyDictionary
22import numpy as np
24import matplotlib as mpl
25from matplotlib._pylab_helpers import Gcf
26from matplotlib import _api, cbook
29class Cursors(enum.IntEnum): # Must subclass int for the macOS backend.
30 """Backend-independent cursor types."""
31 POINTER = enum.auto()
32 HAND = enum.auto()
33 SELECT_REGION = enum.auto()
34 MOVE = enum.auto()
35 WAIT = enum.auto()
36 RESIZE_HORIZONTAL = enum.auto()
37 RESIZE_VERTICAL = enum.auto()
38cursors = Cursors # Backcompat.
41# _tool_registry, _register_tool_class, and _find_tool_class implement a
42# mechanism through which ToolManager.add_tool can determine whether a subclass
43# of the requested tool class has been registered (either for the current
44# canvas class or for a parent class), in which case that tool subclass will be
45# instantiated instead. This is the mechanism used e.g. to allow different
46# GUI backends to implement different specializations for ConfigureSubplots.
49_tool_registry = set()
52def _register_tool_class(canvas_cls, tool_cls=None):
53 """Decorator registering *tool_cls* as a tool class for *canvas_cls*."""
54 if tool_cls is None:
55 return functools.partial(_register_tool_class, canvas_cls)
56 _tool_registry.add((canvas_cls, tool_cls))
57 return tool_cls
60def _find_tool_class(canvas_cls, tool_cls):
61 """Find a subclass of *tool_cls* registered for *canvas_cls*."""
62 for canvas_parent in canvas_cls.__mro__:
63 for tool_child in _api.recursive_subclasses(tool_cls):
64 if (canvas_parent, tool_child) in _tool_registry:
65 return tool_child
66 return tool_cls
69# Views positions tool
70_views_positions = 'viewpos'
73class ToolBase:
74 """
75 Base tool class.
77 A base tool, only implements `trigger` method or no method at all.
78 The tool is instantiated by `matplotlib.backend_managers.ToolManager`.
79 """
81 default_keymap = None
82 """
83 Keymap to associate with this tool.
85 ``list[str]``: List of keys that will trigger this tool when a keypress
86 event is emitted on ``self.figure.canvas``. Note that this attribute is
87 looked up on the instance, and can therefore be a property (this is used
88 e.g. by the built-in tools to load the rcParams at instantiation time).
89 """
91 description = None
92 """
93 Description of the Tool.
95 `str`: Tooltip used if the Tool is included in a Toolbar.
96 """
98 image = None
99 """
100 Filename of the image.
102 `str`: Filename of the image to use in a Toolbar. If None, the *name* is
103 used as a label in the toolbar button.
104 """
106 def __init__(self, toolmanager, name):
107 self._name = name
108 self._toolmanager = toolmanager
109 self._figure = None
111 name = property(
112 lambda self: self._name,
113 doc="The tool id (str, must be unique among tools of a tool manager).")
114 toolmanager = property(
115 lambda self: self._toolmanager,
116 doc="The `.ToolManager` that controls this tool.")
117 canvas = property(
118 lambda self: self._figure.canvas if self._figure is not None else None,
119 doc="The canvas of the figure affected by this tool, or None.")
121 @property
122 def figure(self):
123 """The Figure affected by this tool, or None."""
124 return self._figure
126 @figure.setter
127 def figure(self, figure):
128 self._figure = figure
130 set_figure = figure.fset
132 def _make_classic_style_pseudo_toolbar(self):
133 """
134 Return a placeholder object with a single `canvas` attribute.
136 This is useful to reuse the implementations of tools already provided
137 by the classic Toolbars.
138 """
139 return SimpleNamespace(canvas=self.canvas)
141 def trigger(self, sender, event, data=None):
142 """
143 Called when this tool gets used.
145 This method is called by `.ToolManager.trigger_tool`.
147 Parameters
148 ----------
149 event : `.Event`
150 The canvas event that caused this tool to be called.
151 sender : object
152 Object that requested the tool to be triggered.
153 data : object
154 Extra data.
155 """
156 pass
158 @_api.deprecated("3.6", alternative="tool_removed_event")
159 def destroy(self):
160 """
161 Destroy the tool.
163 This method is called by `.ToolManager.remove_tool`.
164 """
165 pass
168class ToolToggleBase(ToolBase):
169 """
170 Toggleable tool.
172 Every time it is triggered, it switches between enable and disable.
174 Parameters
175 ----------
176 ``*args``
177 Variable length argument to be used by the Tool.
178 ``**kwargs``
179 `toggled` if present and True, sets the initial state of the Tool
180 Arbitrary keyword arguments to be consumed by the Tool
181 """
183 radio_group = None
184 """
185 Attribute to group 'radio' like tools (mutually exclusive).
187 `str` that identifies the group or **None** if not belonging to a group.
188 """
190 cursor = None
191 """Cursor to use when the tool is active."""
193 default_toggled = False
194 """Default of toggled state."""
196 def __init__(self, *args, **kwargs):
197 self._toggled = kwargs.pop('toggled', self.default_toggled)
198 super().__init__(*args, **kwargs)
200 def trigger(self, sender, event, data=None):
201 """Calls `enable` or `disable` based on `toggled` value."""
202 if self._toggled:
203 self.disable(event)
204 else:
205 self.enable(event)
206 self._toggled = not self._toggled
208 def enable(self, event=None):
209 """
210 Enable the toggle tool.
212 `trigger` calls this method when `toggled` is False.
213 """
214 pass
216 def disable(self, event=None):
217 """
218 Disable the toggle tool.
220 `trigger` call this method when `toggled` is True.
222 This can happen in different circumstances.
224 * Click on the toolbar tool button.
225 * Call to `matplotlib.backend_managers.ToolManager.trigger_tool`.
226 * Another `ToolToggleBase` derived tool is triggered
227 (from the same `.ToolManager`).
228 """
229 pass
231 @property
232 def toggled(self):
233 """State of the toggled tool."""
234 return self._toggled
236 def set_figure(self, figure):
237 toggled = self.toggled
238 if toggled:
239 if self.figure:
240 self.trigger(self, None)
241 else:
242 # if no figure the internal state is not changed
243 # we change it here so next call to trigger will change it back
244 self._toggled = False
245 super().set_figure(figure)
246 if toggled:
247 if figure:
248 self.trigger(self, None)
249 else:
250 # if there is no figure, trigger won't change the internal
251 # state we change it back
252 self._toggled = True
255class SetCursorBase(ToolBase):
256 """
257 Change to the current cursor while inaxes.
259 This tool, keeps track of all `ToolToggleBase` derived tools, and calls
260 `set_cursor` when a tool gets triggered.
261 """
262 def __init__(self, *args, **kwargs):
263 super().__init__(*args, **kwargs)
264 self._id_drag = None
265 self._current_tool = None
266 self._default_cursor = cursors.POINTER
267 self._last_cursor = self._default_cursor
268 self.toolmanager.toolmanager_connect('tool_added_event',
269 self._add_tool_cbk)
270 # process current tools
271 for tool in self.toolmanager.tools.values():
272 self._add_tool(tool)
274 def set_figure(self, figure):
275 if self._id_drag:
276 self.canvas.mpl_disconnect(self._id_drag)
277 super().set_figure(figure)
278 if figure:
279 self._id_drag = self.canvas.mpl_connect(
280 'motion_notify_event', self._set_cursor_cbk)
282 def _tool_trigger_cbk(self, event):
283 if event.tool.toggled:
284 self._current_tool = event.tool
285 else:
286 self._current_tool = None
287 self._set_cursor_cbk(event.canvasevent)
289 def _add_tool(self, tool):
290 """Set the cursor when the tool is triggered."""
291 if getattr(tool, 'cursor', None) is not None:
292 self.toolmanager.toolmanager_connect('tool_trigger_%s' % tool.name,
293 self._tool_trigger_cbk)
295 def _add_tool_cbk(self, event):
296 """Process every newly added tool."""
297 if event.tool is self:
298 return
299 self._add_tool(event.tool)
301 def _set_cursor_cbk(self, event):
302 if not event or not self.canvas:
303 return
304 if (self._current_tool and getattr(event, "inaxes", None)
305 and event.inaxes.get_navigate()):
306 if self._last_cursor != self._current_tool.cursor:
307 self.canvas.set_cursor(self._current_tool.cursor)
308 self._last_cursor = self._current_tool.cursor
309 elif self._last_cursor != self._default_cursor:
310 self.canvas.set_cursor(self._default_cursor)
311 self._last_cursor = self._default_cursor
313 @_api.deprecated("3.5", alternative="`.FigureCanvasBase.set_cursor`")
314 def set_cursor(self, cursor):
315 """
316 Set the cursor.
317 """
318 self.canvas.set_cursor(cursor)
321# This exists solely for deprecation warnings; remove with
322# SetCursorBase.set_cursor.
323ToolSetCursor = SetCursorBase
326class ToolCursorPosition(ToolBase):
327 """
328 Send message with the current pointer position.
330 This tool runs in the background reporting the position of the cursor.
331 """
332 def __init__(self, *args, **kwargs):
333 self._id_drag = None
334 super().__init__(*args, **kwargs)
336 def set_figure(self, figure):
337 if self._id_drag:
338 self.canvas.mpl_disconnect(self._id_drag)
339 super().set_figure(figure)
340 if figure:
341 self._id_drag = self.canvas.mpl_connect(
342 'motion_notify_event', self.send_message)
344 def send_message(self, event):
345 """Call `matplotlib.backend_managers.ToolManager.message_event`."""
346 if self.toolmanager.messagelock.locked():
347 return
349 from matplotlib.backend_bases import NavigationToolbar2
350 message = NavigationToolbar2._mouse_event_to_message(event)
351 if message is None:
352 message = ' '
353 self.toolmanager.message_event(message, self)
356class RubberbandBase(ToolBase):
357 """Draw and remove a rubberband."""
358 def trigger(self, sender, event, data=None):
359 """Call `draw_rubberband` or `remove_rubberband` based on data."""
360 if not self.figure.canvas.widgetlock.available(sender):
361 return
362 if data is not None:
363 self.draw_rubberband(*data)
364 else:
365 self.remove_rubberband()
367 def draw_rubberband(self, *data):
368 """
369 Draw rubberband.
371 This method must get implemented per backend.
372 """
373 raise NotImplementedError
375 def remove_rubberband(self):
376 """
377 Remove rubberband.
379 This method should get implemented per backend.
380 """
381 pass
384class ToolQuit(ToolBase):
385 """Tool to call the figure manager destroy method."""
387 description = 'Quit the figure'
388 default_keymap = property(lambda self: mpl.rcParams['keymap.quit'])
390 def trigger(self, sender, event, data=None):
391 Gcf.destroy_fig(self.figure)
394class ToolQuitAll(ToolBase):
395 """Tool to call the figure manager destroy method."""
397 description = 'Quit all figures'
398 default_keymap = property(lambda self: mpl.rcParams['keymap.quit_all'])
400 def trigger(self, sender, event, data=None):
401 Gcf.destroy_all()
404class ToolGrid(ToolBase):
405 """Tool to toggle the major grids of the figure."""
407 description = 'Toggle major grids'
408 default_keymap = property(lambda self: mpl.rcParams['keymap.grid'])
410 def trigger(self, sender, event, data=None):
411 sentinel = str(uuid.uuid4())
412 # Trigger grid switching by temporarily setting :rc:`keymap.grid`
413 # to a unique key and sending an appropriate event.
414 with cbook._setattr_cm(event, key=sentinel), \
415 mpl.rc_context({'keymap.grid': sentinel}):
416 mpl.backend_bases.key_press_handler(event, self.figure.canvas)
419class ToolMinorGrid(ToolBase):
420 """Tool to toggle the major and minor grids of the figure."""
422 description = 'Toggle major and minor grids'
423 default_keymap = property(lambda self: mpl.rcParams['keymap.grid_minor'])
425 def trigger(self, sender, event, data=None):
426 sentinel = str(uuid.uuid4())
427 # Trigger grid switching by temporarily setting :rc:`keymap.grid_minor`
428 # to a unique key and sending an appropriate event.
429 with cbook._setattr_cm(event, key=sentinel), \
430 mpl.rc_context({'keymap.grid_minor': sentinel}):
431 mpl.backend_bases.key_press_handler(event, self.figure.canvas)
434class ToolFullScreen(ToolBase):
435 """Tool to toggle full screen."""
437 description = 'Toggle fullscreen mode'
438 default_keymap = property(lambda self: mpl.rcParams['keymap.fullscreen'])
440 def trigger(self, sender, event, data=None):
441 self.figure.canvas.manager.full_screen_toggle()
444class AxisScaleBase(ToolToggleBase):
445 """Base Tool to toggle between linear and logarithmic."""
447 def trigger(self, sender, event, data=None):
448 if event.inaxes is None:
449 return
450 super().trigger(sender, event, data)
452 def enable(self, event=None):
453 self.set_scale(event.inaxes, 'log')
454 self.figure.canvas.draw_idle()
456 def disable(self, event=None):
457 self.set_scale(event.inaxes, 'linear')
458 self.figure.canvas.draw_idle()
461class ToolYScale(AxisScaleBase):
462 """Tool to toggle between linear and logarithmic scales on the Y axis."""
464 description = 'Toggle scale Y axis'
465 default_keymap = property(lambda self: mpl.rcParams['keymap.yscale'])
467 def set_scale(self, ax, scale):
468 ax.set_yscale(scale)
471class ToolXScale(AxisScaleBase):
472 """Tool to toggle between linear and logarithmic scales on the X axis."""
474 description = 'Toggle scale X axis'
475 default_keymap = property(lambda self: mpl.rcParams['keymap.xscale'])
477 def set_scale(self, ax, scale):
478 ax.set_xscale(scale)
481class ToolViewsPositions(ToolBase):
482 """
483 Auxiliary Tool to handle changes in views and positions.
485 Runs in the background and should get used by all the tools that
486 need to access the figure's history of views and positions, e.g.
488 * `ToolZoom`
489 * `ToolPan`
490 * `ToolHome`
491 * `ToolBack`
492 * `ToolForward`
493 """
495 def __init__(self, *args, **kwargs):
496 self.views = WeakKeyDictionary()
497 self.positions = WeakKeyDictionary()
498 self.home_views = WeakKeyDictionary()
499 super().__init__(*args, **kwargs)
501 def add_figure(self, figure):
502 """Add the current figure to the stack of views and positions."""
504 if figure not in self.views:
505 self.views[figure] = cbook.Stack()
506 self.positions[figure] = cbook.Stack()
507 self.home_views[figure] = WeakKeyDictionary()
508 # Define Home
509 self.push_current(figure)
510 # Make sure we add a home view for new axes as they're added
511 figure.add_axobserver(lambda fig: self.update_home_views(fig))
513 def clear(self, figure):
514 """Reset the axes stack."""
515 if figure in self.views:
516 self.views[figure].clear()
517 self.positions[figure].clear()
518 self.home_views[figure].clear()
519 self.update_home_views()
521 def update_view(self):
522 """
523 Update the view limits and position for each axes from the current
524 stack position. If any axes are present in the figure that aren't in
525 the current stack position, use the home view limits for those axes and
526 don't update *any* positions.
527 """
529 views = self.views[self.figure]()
530 if views is None:
531 return
532 pos = self.positions[self.figure]()
533 if pos is None:
534 return
535 home_views = self.home_views[self.figure]
536 all_axes = self.figure.get_axes()
537 for a in all_axes:
538 if a in views:
539 cur_view = views[a]
540 else:
541 cur_view = home_views[a]
542 a._set_view(cur_view)
544 if set(all_axes).issubset(pos):
545 for a in all_axes:
546 # Restore both the original and modified positions
547 a._set_position(pos[a][0], 'original')
548 a._set_position(pos[a][1], 'active')
550 self.figure.canvas.draw_idle()
552 def push_current(self, figure=None):
553 """
554 Push the current view limits and position onto their respective stacks.
555 """
556 if not figure:
557 figure = self.figure
558 views = WeakKeyDictionary()
559 pos = WeakKeyDictionary()
560 for a in figure.get_axes():
561 views[a] = a._get_view()
562 pos[a] = self._axes_pos(a)
563 self.views[figure].push(views)
564 self.positions[figure].push(pos)
566 def _axes_pos(self, ax):
567 """
568 Return the original and modified positions for the specified axes.
570 Parameters
571 ----------
572 ax : matplotlib.axes.Axes
573 The `.Axes` to get the positions for.
575 Returns
576 -------
577 original_position, modified_position
578 A tuple of the original and modified positions.
579 """
581 return (ax.get_position(True).frozen(),
582 ax.get_position().frozen())
584 def update_home_views(self, figure=None):
585 """
586 Make sure that ``self.home_views`` has an entry for all axes present
587 in the figure.
588 """
590 if not figure:
591 figure = self.figure
592 for a in figure.get_axes():
593 if a not in self.home_views[figure]:
594 self.home_views[figure][a] = a._get_view()
596 def home(self):
597 """Recall the first view and position from the stack."""
598 self.views[self.figure].home()
599 self.positions[self.figure].home()
601 def back(self):
602 """Back one step in the stack of views and positions."""
603 self.views[self.figure].back()
604 self.positions[self.figure].back()
606 def forward(self):
607 """Forward one step in the stack of views and positions."""
608 self.views[self.figure].forward()
609 self.positions[self.figure].forward()
612class ViewsPositionsBase(ToolBase):
613 """Base class for `ToolHome`, `ToolBack` and `ToolForward`."""
615 _on_trigger = None
617 def trigger(self, sender, event, data=None):
618 self.toolmanager.get_tool(_views_positions).add_figure(self.figure)
619 getattr(self.toolmanager.get_tool(_views_positions),
620 self._on_trigger)()
621 self.toolmanager.get_tool(_views_positions).update_view()
624class ToolHome(ViewsPositionsBase):
625 """Restore the original view limits."""
627 description = 'Reset original view'
628 image = 'home'
629 default_keymap = property(lambda self: mpl.rcParams['keymap.home'])
630 _on_trigger = 'home'
633class ToolBack(ViewsPositionsBase):
634 """Move back up the view limits stack."""
636 description = 'Back to previous view'
637 image = 'back'
638 default_keymap = property(lambda self: mpl.rcParams['keymap.back'])
639 _on_trigger = 'back'
642class ToolForward(ViewsPositionsBase):
643 """Move forward in the view lim stack."""
645 description = 'Forward to next view'
646 image = 'forward'
647 default_keymap = property(lambda self: mpl.rcParams['keymap.forward'])
648 _on_trigger = 'forward'
651class ConfigureSubplotsBase(ToolBase):
652 """Base tool for the configuration of subplots."""
654 description = 'Configure subplots'
655 image = 'subplots'
658class SaveFigureBase(ToolBase):
659 """Base tool for figure saving."""
661 description = 'Save the figure'
662 image = 'filesave'
663 default_keymap = property(lambda self: mpl.rcParams['keymap.save'])
666class ZoomPanBase(ToolToggleBase):
667 """Base class for `ToolZoom` and `ToolPan`."""
668 def __init__(self, *args):
669 super().__init__(*args)
670 self._button_pressed = None
671 self._xypress = None
672 self._idPress = None
673 self._idRelease = None
674 self._idScroll = None
675 self.base_scale = 2.
676 self.scrollthresh = .5 # .5 second scroll threshold
677 self.lastscroll = time.time()-self.scrollthresh
679 def enable(self, event=None):
680 """Connect press/release events and lock the canvas."""
681 self.figure.canvas.widgetlock(self)
682 self._idPress = self.figure.canvas.mpl_connect(
683 'button_press_event', self._press)
684 self._idRelease = self.figure.canvas.mpl_connect(
685 'button_release_event', self._release)
686 self._idScroll = self.figure.canvas.mpl_connect(
687 'scroll_event', self.scroll_zoom)
689 def disable(self, event=None):
690 """Release the canvas and disconnect press/release events."""
691 self._cancel_action()
692 self.figure.canvas.widgetlock.release(self)
693 self.figure.canvas.mpl_disconnect(self._idPress)
694 self.figure.canvas.mpl_disconnect(self._idRelease)
695 self.figure.canvas.mpl_disconnect(self._idScroll)
697 def trigger(self, sender, event, data=None):
698 self.toolmanager.get_tool(_views_positions).add_figure(self.figure)
699 super().trigger(sender, event, data)
700 new_navigate_mode = self.name.upper() if self.toggled else None
701 for ax in self.figure.axes:
702 ax.set_navigate_mode(new_navigate_mode)
704 def scroll_zoom(self, event):
705 # https://gist.github.com/tacaswell/3144287
706 if event.inaxes is None:
707 return
709 if event.button == 'up':
710 # deal with zoom in
711 scl = self.base_scale
712 elif event.button == 'down':
713 # deal with zoom out
714 scl = 1/self.base_scale
715 else:
716 # deal with something that should never happen
717 scl = 1
719 ax = event.inaxes
720 ax._set_view_from_bbox([event.x, event.y, scl])
722 # If last scroll was done within the timing threshold, delete the
723 # previous view
724 if (time.time()-self.lastscroll) < self.scrollthresh:
725 self.toolmanager.get_tool(_views_positions).back()
727 self.figure.canvas.draw_idle() # force re-draw
729 self.lastscroll = time.time()
730 self.toolmanager.get_tool(_views_positions).push_current()
733class ToolZoom(ZoomPanBase):
734 """A Tool for zooming using a rectangle selector."""
736 description = 'Zoom to rectangle'
737 image = 'zoom_to_rect'
738 default_keymap = property(lambda self: mpl.rcParams['keymap.zoom'])
739 cursor = cursors.SELECT_REGION
740 radio_group = 'default'
742 def __init__(self, *args):
743 super().__init__(*args)
744 self._ids_zoom = []
746 def _cancel_action(self):
747 for zoom_id in self._ids_zoom:
748 self.figure.canvas.mpl_disconnect(zoom_id)
749 self.toolmanager.trigger_tool('rubberband', self)
750 self.figure.canvas.draw_idle()
751 self._xypress = None
752 self._button_pressed = None
753 self._ids_zoom = []
754 return
756 def _press(self, event):
757 """Callback for mouse button presses in zoom-to-rectangle mode."""
759 # If we're already in the middle of a zoom, pressing another
760 # button works to "cancel"
761 if self._ids_zoom:
762 self._cancel_action()
764 if event.button == 1:
765 self._button_pressed = 1
766 elif event.button == 3:
767 self._button_pressed = 3
768 else:
769 self._cancel_action()
770 return
772 x, y = event.x, event.y
774 self._xypress = []
775 for i, a in enumerate(self.figure.get_axes()):
776 if (x is not None and y is not None and a.in_axes(event) and
777 a.get_navigate() and a.can_zoom()):
778 self._xypress.append((x, y, a, i, a._get_view()))
780 id1 = self.figure.canvas.mpl_connect(
781 'motion_notify_event', self._mouse_move)
782 id2 = self.figure.canvas.mpl_connect(
783 'key_press_event', self._switch_on_zoom_mode)
784 id3 = self.figure.canvas.mpl_connect(
785 'key_release_event', self._switch_off_zoom_mode)
787 self._ids_zoom = id1, id2, id3
788 self._zoom_mode = event.key
790 def _switch_on_zoom_mode(self, event):
791 self._zoom_mode = event.key
792 self._mouse_move(event)
794 def _switch_off_zoom_mode(self, event):
795 self._zoom_mode = None
796 self._mouse_move(event)
798 def _mouse_move(self, event):
799 """Callback for mouse moves in zoom-to-rectangle mode."""
801 if self._xypress:
802 x, y = event.x, event.y
803 lastx, lasty, a, ind, view = self._xypress[0]
804 (x1, y1), (x2, y2) = np.clip(
805 [[lastx, lasty], [x, y]], a.bbox.min, a.bbox.max)
806 if self._zoom_mode == "x":
807 y1, y2 = a.bbox.intervaly
808 elif self._zoom_mode == "y":
809 x1, x2 = a.bbox.intervalx
810 self.toolmanager.trigger_tool(
811 'rubberband', self, data=(x1, y1, x2, y2))
813 def _release(self, event):
814 """Callback for mouse button releases in zoom-to-rectangle mode."""
816 for zoom_id in self._ids_zoom:
817 self.figure.canvas.mpl_disconnect(zoom_id)
818 self._ids_zoom = []
820 if not self._xypress:
821 self._cancel_action()
822 return
824 done_ax = []
826 for cur_xypress in self._xypress:
827 x, y = event.x, event.y
828 lastx, lasty, a, _ind, view = cur_xypress
829 # ignore singular clicks - 5 pixels is a threshold
830 if abs(x - lastx) < 5 or abs(y - lasty) < 5:
831 self._cancel_action()
832 return
834 # detect twinx, twiny axes and avoid double zooming
835 twinx = any(a.get_shared_x_axes().joined(a, a1) for a1 in done_ax)
836 twiny = any(a.get_shared_y_axes().joined(a, a1) for a1 in done_ax)
837 done_ax.append(a)
839 if self._button_pressed == 1:
840 direction = 'in'
841 elif self._button_pressed == 3:
842 direction = 'out'
843 else:
844 continue
846 a._set_view_from_bbox((lastx, lasty, x, y), direction,
847 self._zoom_mode, twinx, twiny)
849 self._zoom_mode = None
850 self.toolmanager.get_tool(_views_positions).push_current()
851 self._cancel_action()
854class ToolPan(ZoomPanBase):
855 """Pan axes with left mouse, zoom with right."""
857 default_keymap = property(lambda self: mpl.rcParams['keymap.pan'])
858 description = 'Pan axes with left mouse, zoom with right'
859 image = 'move'
860 cursor = cursors.MOVE
861 radio_group = 'default'
863 def __init__(self, *args):
864 super().__init__(*args)
865 self._id_drag = None
867 def _cancel_action(self):
868 self._button_pressed = None
869 self._xypress = []
870 self.figure.canvas.mpl_disconnect(self._id_drag)
871 self.toolmanager.messagelock.release(self)
872 self.figure.canvas.draw_idle()
874 def _press(self, event):
875 if event.button == 1:
876 self._button_pressed = 1
877 elif event.button == 3:
878 self._button_pressed = 3
879 else:
880 self._cancel_action()
881 return
883 x, y = event.x, event.y
885 self._xypress = []
886 for i, a in enumerate(self.figure.get_axes()):
887 if (x is not None and y is not None and a.in_axes(event) and
888 a.get_navigate() and a.can_pan()):
889 a.start_pan(x, y, event.button)
890 self._xypress.append((a, i))
891 self.toolmanager.messagelock(self)
892 self._id_drag = self.figure.canvas.mpl_connect(
893 'motion_notify_event', self._mouse_move)
895 def _release(self, event):
896 if self._button_pressed is None:
897 self._cancel_action()
898 return
900 self.figure.canvas.mpl_disconnect(self._id_drag)
901 self.toolmanager.messagelock.release(self)
903 for a, _ind in self._xypress:
904 a.end_pan()
905 if not self._xypress:
906 self._cancel_action()
907 return
909 self.toolmanager.get_tool(_views_positions).push_current()
910 self._cancel_action()
912 def _mouse_move(self, event):
913 for a, _ind in self._xypress:
914 # safer to use the recorded button at the _press than current
915 # button: # multiple button can get pressed during motion...
916 a.drag_pan(self._button_pressed, event.key, event.x, event.y)
917 self.toolmanager.canvas.draw_idle()
920class ToolHelpBase(ToolBase):
921 description = 'Print tool list, shortcuts and description'
922 default_keymap = property(lambda self: mpl.rcParams['keymap.help'])
923 image = 'help'
925 @staticmethod
926 def format_shortcut(key_sequence):
927 """
928 Convert a shortcut string from the notation used in rc config to the
929 standard notation for displaying shortcuts, e.g. 'ctrl+a' -> 'Ctrl+A'.
930 """
931 return (key_sequence if len(key_sequence) == 1 else
932 re.sub(r"\+[A-Z]", r"+Shift\g<0>", key_sequence).title())
934 def _format_tool_keymap(self, name):
935 keymaps = self.toolmanager.get_tool_keymap(name)
936 return ", ".join(self.format_shortcut(keymap) for keymap in keymaps)
938 def _get_help_entries(self):
939 return [(name, self._format_tool_keymap(name), tool.description)
940 for name, tool in sorted(self.toolmanager.tools.items())
941 if tool.description]
943 def _get_help_text(self):
944 entries = self._get_help_entries()
945 entries = ["{}: {}\n\t{}".format(*entry) for entry in entries]
946 return "\n".join(entries)
948 def _get_help_html(self):
949 fmt = "<tr><td>{}</td><td>{}</td><td>{}</td></tr>"
950 rows = [fmt.format(
951 "<b>Action</b>", "<b>Shortcuts</b>", "<b>Description</b>")]
952 rows += [fmt.format(*row) for row in self._get_help_entries()]
953 return ("<style>td {padding: 0px 4px}</style>"
954 "<table><thead>" + rows[0] + "</thead>"
955 "<tbody>".join(rows[1:]) + "</tbody></table>")
958class ToolCopyToClipboardBase(ToolBase):
959 """Tool to copy the figure to the clipboard."""
961 description = 'Copy the canvas figure to clipboard'
962 default_keymap = property(lambda self: mpl.rcParams['keymap.copy'])
964 def trigger(self, *args, **kwargs):
965 message = "Copy tool is not available"
966 self.toolmanager.message_event(message, self)
969default_tools = {'home': ToolHome, 'back': ToolBack, 'forward': ToolForward,
970 'zoom': ToolZoom, 'pan': ToolPan,
971 'subplots': ConfigureSubplotsBase,
972 'save': SaveFigureBase,
973 'grid': ToolGrid,
974 'grid_minor': ToolMinorGrid,
975 'fullscreen': ToolFullScreen,
976 'quit': ToolQuit,
977 'quit_all': ToolQuitAll,
978 'xscale': ToolXScale,
979 'yscale': ToolYScale,
980 'position': ToolCursorPosition,
981 _views_positions: ToolViewsPositions,
982 'cursor': SetCursorBase,
983 'rubberband': RubberbandBase,
984 'help': ToolHelpBase,
985 'copy': ToolCopyToClipboardBase,
986 }
988default_toolbar_tools = [['navigation', ['home', 'back', 'forward']],
989 ['zoompan', ['pan', 'zoom', 'subplots']],
990 ['io', ['save', 'help']]]
993def add_tools_to_manager(toolmanager, tools=default_tools):
994 """
995 Add multiple tools to a `.ToolManager`.
997 Parameters
998 ----------
999 toolmanager : `.backend_managers.ToolManager`
1000 Manager to which the tools are added.
1001 tools : {str: class_like}, optional
1002 The tools to add in a {name: tool} dict, see
1003 `.backend_managers.ToolManager.add_tool` for more info.
1004 """
1006 for name, tool in tools.items():
1007 toolmanager.add_tool(name, tool)
1010def add_tools_to_container(container, tools=default_toolbar_tools):
1011 """
1012 Add multiple tools to the container.
1014 Parameters
1015 ----------
1016 container : Container
1017 `.backend_bases.ToolContainerBase` object that will get the tools
1018 added.
1019 tools : list, optional
1020 List in the form ``[[group1, [tool1, tool2 ...]], [group2, [...]]]``
1021 where the tools ``[tool1, tool2, ...]`` will display in group1.
1022 See `.backend_bases.ToolContainerBase.add_tool` for details.
1023 """
1025 for group, grouptools in tools:
1026 for position, tool in enumerate(grouptools):
1027 container.add_tool(tool, group, position)