Coverage for /home/martinb/.local/share/virtualenvs/camcops/lib/python3.6/site-packages/matplotlib/backend_tools.py : 32%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
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"""
14from enum import IntEnum
15import logging
16import re
17import time
18from types import SimpleNamespace
19from weakref import WeakKeyDictionary
21import numpy as np
23from matplotlib import rcParams
24from matplotlib._pylab_helpers import Gcf
25import matplotlib.cbook as cbook
27_log = logging.getLogger(__name__)
30class Cursors(IntEnum): # Must subclass int for the macOS backend.
31 """Backend-independent cursor types."""
32 HAND, POINTER, SELECT_REGION, MOVE, WAIT = range(5)
33cursors = Cursors # Backcompat.
35# Views positions tool
36_views_positions = 'viewpos'
39class ToolBase:
40 """
41 Base tool class
43 A base tool, only implements `trigger` method or not method at all.
44 The tool is instantiated by `matplotlib.backend_managers.ToolManager`
46 Attributes
47 ----------
48 toolmanager : `matplotlib.backend_managers.ToolManager`
49 ToolManager that controls this Tool
50 figure : `FigureCanvas`
51 Figure instance that is affected by this Tool
52 name : str
53 Used as **Id** of the tool, has to be unique among tools of the same
54 ToolManager
55 """
57 default_keymap = None
58 """
59 Keymap to associate with this tool
61 **String**: List of comma separated keys that will be used to call this
62 tool when the keypress event of *self.figure.canvas* is emitted
63 """
65 description = None
66 """
67 Description of the Tool
69 **String**: If the Tool is included in the Toolbar this text is used
70 as a Tooltip
71 """
73 image = None
74 """
75 Filename of the image
77 **String**: Filename of the image to use in the toolbar. If None, the
78 *name* is used as a label in the toolbar button
79 """
81 def __init__(self, toolmanager, name):
82 cbook._warn_external(
83 'The new Tool classes introduced in v1.5 are experimental; their '
84 'API (including names) will likely change in future versions.')
85 self._name = name
86 self._toolmanager = toolmanager
87 self._figure = None
89 @property
90 def figure(self):
91 return self._figure
93 @figure.setter
94 def figure(self, figure):
95 self.set_figure(figure)
97 @property
98 def canvas(self):
99 if not self._figure:
100 return None
101 return self._figure.canvas
103 @property
104 def toolmanager(self):
105 return self._toolmanager
107 def _make_classic_style_pseudo_toolbar(self):
108 """
109 Return a placeholder object with a single `canvas` attribute.
111 This is useful to reuse the implementations of tools already provided
112 by the classic Toolbars.
113 """
114 return SimpleNamespace(canvas=self.canvas)
116 def set_figure(self, figure):
117 """
118 Assign a figure to the tool
120 Parameters
121 ----------
122 figure : `Figure`
123 """
124 self._figure = figure
126 def trigger(self, sender, event, data=None):
127 """
128 Called when this tool gets used
130 This method is called by
131 `matplotlib.backend_managers.ToolManager.trigger_tool`
133 Parameters
134 ----------
135 event : `Event`
136 The Canvas event that caused this tool to be called
137 sender : object
138 Object that requested the tool to be triggered
139 data : object
140 Extra data
141 """
143 pass
145 @property
146 def name(self):
147 """Tool Id"""
148 return self._name
150 def destroy(self):
151 """
152 Destroy the tool
154 This method is called when the tool is removed by
155 `matplotlib.backend_managers.ToolManager.remove_tool`
156 """
157 pass
160class ToolToggleBase(ToolBase):
161 """
162 Toggleable tool
164 Every time it is triggered, it switches between enable and disable
166 Parameters
167 ----------
168 ``*args``
169 Variable length argument to be used by the Tool
170 ``**kwargs``
171 `toggled` if present and True, sets the initial state of the Tool
172 Arbitrary keyword arguments to be consumed by the Tool
173 """
175 radio_group = None
176 """Attribute to group 'radio' like tools (mutually exclusive)
178 **String** that identifies the group or **None** if not belonging to a
179 group
180 """
182 cursor = None
183 """Cursor to use when the tool is active"""
185 default_toggled = False
186 """Default of toggled state"""
188 def __init__(self, *args, **kwargs):
189 self._toggled = kwargs.pop('toggled', self.default_toggled)
190 ToolBase.__init__(self, *args, **kwargs)
192 def trigger(self, sender, event, data=None):
193 """Calls `enable` or `disable` based on `toggled` value"""
194 if self._toggled:
195 self.disable(event)
196 else:
197 self.enable(event)
198 self._toggled = not self._toggled
200 def enable(self, event=None):
201 """
202 Enable the toggle tool
204 `trigger` calls this method when `toggled` is False
205 """
206 pass
208 def disable(self, event=None):
209 """
210 Disable the toggle tool
212 `trigger` call this method when `toggled` is True.
214 This can happen in different circumstances
216 * Click on the toolbar tool button
217 * Call to `matplotlib.backend_managers.ToolManager.trigger_tool`
218 * Another `ToolToggleBase` derived tool is triggered
219 (from the same `ToolManager`)
220 """
221 pass
223 @property
224 def toggled(self):
225 """State of the toggled tool"""
227 return self._toggled
229 def set_figure(self, figure):
230 toggled = self.toggled
231 if toggled:
232 if self.figure:
233 self.trigger(self, None)
234 else:
235 # if no figure the internal state is not changed
236 # we change it here so next call to trigger will change it back
237 self._toggled = False
238 ToolBase.set_figure(self, figure)
239 if toggled:
240 if figure:
241 self.trigger(self, None)
242 else:
243 # if there is no figure, trigger won't change the internal
244 # state we change it back
245 self._toggled = True
248class SetCursorBase(ToolBase):
249 """
250 Change to the current cursor while inaxes
252 This tool, keeps track of all `ToolToggleBase` derived tools, and calls
253 set_cursor when a tool gets triggered
254 """
255 def __init__(self, *args, **kwargs):
256 ToolBase.__init__(self, *args, **kwargs)
257 self._idDrag = None
258 self._cursor = None
259 self._default_cursor = cursors.POINTER
260 self._last_cursor = self._default_cursor
261 self.toolmanager.toolmanager_connect('tool_added_event',
262 self._add_tool_cbk)
264 # process current tools
265 for tool in self.toolmanager.tools.values():
266 self._add_tool(tool)
268 def set_figure(self, figure):
269 if self._idDrag:
270 self.canvas.mpl_disconnect(self._idDrag)
271 ToolBase.set_figure(self, figure)
272 if figure:
273 self._idDrag = self.canvas.mpl_connect(
274 'motion_notify_event', self._set_cursor_cbk)
276 def _tool_trigger_cbk(self, event):
277 if event.tool.toggled:
278 self._cursor = event.tool.cursor
279 else:
280 self._cursor = None
282 self._set_cursor_cbk(event.canvasevent)
284 def _add_tool(self, tool):
285 """Set the cursor when the tool is triggered."""
286 if getattr(tool, 'cursor', None) is not None:
287 self.toolmanager.toolmanager_connect('tool_trigger_%s' % tool.name,
288 self._tool_trigger_cbk)
290 def _add_tool_cbk(self, event):
291 """Process every newly added tool."""
292 if event.tool is self:
293 return
294 self._add_tool(event.tool)
296 def _set_cursor_cbk(self, event):
297 if not event:
298 return
300 if not getattr(event, 'inaxes', False) or not self._cursor:
301 if self._last_cursor != self._default_cursor:
302 self.set_cursor(self._default_cursor)
303 self._last_cursor = self._default_cursor
304 elif self._cursor:
305 cursor = self._cursor
306 if cursor and self._last_cursor != cursor:
307 self.set_cursor(cursor)
308 self._last_cursor = cursor
310 def set_cursor(self, cursor):
311 """
312 Set the cursor
314 This method has to be implemented per backend
315 """
316 raise NotImplementedError
319class ToolCursorPosition(ToolBase):
320 """
321 Send message with the current pointer position
323 This tool runs in the background reporting the position of the cursor
324 """
325 def __init__(self, *args, **kwargs):
326 self._idDrag = None
327 ToolBase.__init__(self, *args, **kwargs)
329 def set_figure(self, figure):
330 if self._idDrag:
331 self.canvas.mpl_disconnect(self._idDrag)
332 ToolBase.set_figure(self, figure)
333 if figure:
334 self._idDrag = self.canvas.mpl_connect(
335 'motion_notify_event', self.send_message)
337 def send_message(self, event):
338 """Call `matplotlib.backend_managers.ToolManager.message_event`"""
339 if self.toolmanager.messagelock.locked():
340 return
342 message = ' '
344 if event.inaxes and event.inaxes.get_navigate():
345 try:
346 s = event.inaxes.format_coord(event.xdata, event.ydata)
347 except (ValueError, OverflowError):
348 pass
349 else:
350 artists = [a for a in event.inaxes._mouseover_set
351 if a.contains(event) and a.get_visible()]
353 if artists:
354 a = cbook._topmost_artist(artists)
355 if a is not event.inaxes.patch:
356 data = a.get_cursor_data(event)
357 if data is not None:
358 data_str = a.format_cursor_data(data)
359 if data_str is not None:
360 s = s + ' ' + data_str
362 message = s
363 self.toolmanager.message_event(message, self)
366class RubberbandBase(ToolBase):
367 """Draw and remove rubberband"""
368 def trigger(self, sender, event, data):
369 """Call `draw_rubberband` or `remove_rubberband` based on data"""
370 if not self.figure.canvas.widgetlock.available(sender):
371 return
372 if data is not None:
373 self.draw_rubberband(*data)
374 else:
375 self.remove_rubberband()
377 def draw_rubberband(self, *data):
378 """
379 Draw rubberband
381 This method must get implemented per backend
382 """
383 raise NotImplementedError
385 def remove_rubberband(self):
386 """
387 Remove rubberband
389 This method should get implemented per backend
390 """
391 pass
394class ToolQuit(ToolBase):
395 """Tool to call the figure manager destroy method"""
397 description = 'Quit the figure'
398 default_keymap = rcParams['keymap.quit']
400 def trigger(self, sender, event, data=None):
401 Gcf.destroy_fig(self.figure)
404class ToolQuitAll(ToolBase):
405 """Tool to call the figure manager destroy method"""
407 description = 'Quit all figures'
408 default_keymap = rcParams['keymap.quit_all']
410 def trigger(self, sender, event, data=None):
411 Gcf.destroy_all()
414class ToolEnableAllNavigation(ToolBase):
415 """Tool to enable all axes for toolmanager interaction"""
417 description = 'Enable all axes toolmanager'
418 default_keymap = rcParams['keymap.all_axes']
420 def trigger(self, sender, event, data=None):
421 if event.inaxes is None:
422 return
424 for a in self.figure.get_axes():
425 if (event.x is not None and event.y is not None
426 and a.in_axes(event)):
427 a.set_navigate(True)
430class ToolEnableNavigation(ToolBase):
431 """Tool to enable a specific axes for toolmanager interaction"""
433 description = 'Enable one axes toolmanager'
434 default_keymap = (1, 2, 3, 4, 5, 6, 7, 8, 9)
436 def trigger(self, sender, event, data=None):
437 if event.inaxes is None:
438 return
440 n = int(event.key) - 1
441 if n < len(self.figure.get_axes()):
442 for i, a in enumerate(self.figure.get_axes()):
443 if (event.x is not None and event.y is not None
444 and a.in_axes(event)):
445 a.set_navigate(i == n)
448class _ToolGridBase(ToolBase):
449 """Common functionality between ToolGrid and ToolMinorGrid."""
451 _cycle = [(False, False), (True, False), (True, True), (False, True)]
453 def trigger(self, sender, event, data=None):
454 ax = event.inaxes
455 if ax is None:
456 return
457 try:
458 x_state, x_which, y_state, y_which = self._get_next_grid_states(ax)
459 except ValueError:
460 pass
461 else:
462 ax.grid(x_state, which=x_which, axis="x")
463 ax.grid(y_state, which=y_which, axis="y")
464 ax.figure.canvas.draw_idle()
466 @staticmethod
467 def _get_uniform_grid_state(ticks):
468 """
469 Check whether all grid lines are in the same visibility state.
471 Returns True/False if all grid lines are on or off, None if they are
472 not all in the same state.
473 """
474 if all(tick.gridline.get_visible() for tick in ticks):
475 return True
476 elif not any(tick.gridline.get_visible() for tick in ticks):
477 return False
478 else:
479 return None
482class ToolGrid(_ToolGridBase):
483 """Tool to toggle the major grids of the figure"""
485 description = 'Toggle major grids'
486 default_keymap = rcParams['keymap.grid']
488 def _get_next_grid_states(self, ax):
489 if None in map(self._get_uniform_grid_state,
490 [ax.xaxis.minorTicks, ax.yaxis.minorTicks]):
491 # Bail out if minor grids are not in a uniform state.
492 raise ValueError
493 x_state, y_state = map(self._get_uniform_grid_state,
494 [ax.xaxis.majorTicks, ax.yaxis.majorTicks])
495 cycle = self._cycle
496 # Bail out (via ValueError) if major grids are not in a uniform state.
497 x_state, y_state = (
498 cycle[(cycle.index((x_state, y_state)) + 1) % len(cycle)])
499 return (x_state, "major" if x_state else "both",
500 y_state, "major" if y_state else "both")
503class ToolMinorGrid(_ToolGridBase):
504 """Tool to toggle the major and minor grids of the figure"""
506 description = 'Toggle major and minor grids'
507 default_keymap = rcParams['keymap.grid_minor']
509 def _get_next_grid_states(self, ax):
510 if None in map(self._get_uniform_grid_state,
511 [ax.xaxis.majorTicks, ax.yaxis.majorTicks]):
512 # Bail out if major grids are not in a uniform state.
513 raise ValueError
514 x_state, y_state = map(self._get_uniform_grid_state,
515 [ax.xaxis.minorTicks, ax.yaxis.minorTicks])
516 cycle = self._cycle
517 # Bail out (via ValueError) if minor grids are not in a uniform state.
518 x_state, y_state = (
519 cycle[(cycle.index((x_state, y_state)) + 1) % len(cycle)])
520 return x_state, "both", y_state, "both"
523class ToolFullScreen(ToolToggleBase):
524 """Tool to toggle full screen"""
526 description = 'Toggle fullscreen mode'
527 default_keymap = rcParams['keymap.fullscreen']
529 def enable(self, event):
530 self.figure.canvas.manager.full_screen_toggle()
532 def disable(self, event):
533 self.figure.canvas.manager.full_screen_toggle()
536class AxisScaleBase(ToolToggleBase):
537 """Base Tool to toggle between linear and logarithmic"""
539 def trigger(self, sender, event, data=None):
540 if event.inaxes is None:
541 return
542 ToolToggleBase.trigger(self, sender, event, data)
544 def enable(self, event):
545 self.set_scale(event.inaxes, 'log')
546 self.figure.canvas.draw_idle()
548 def disable(self, event):
549 self.set_scale(event.inaxes, 'linear')
550 self.figure.canvas.draw_idle()
553class ToolYScale(AxisScaleBase):
554 """Tool to toggle between linear and logarithmic scales on the Y axis"""
556 description = 'Toggle scale Y axis'
557 default_keymap = rcParams['keymap.yscale']
559 def set_scale(self, ax, scale):
560 ax.set_yscale(scale)
563class ToolXScale(AxisScaleBase):
564 """Tool to toggle between linear and logarithmic scales on the X axis"""
566 description = 'Toggle scale X axis'
567 default_keymap = rcParams['keymap.xscale']
569 def set_scale(self, ax, scale):
570 ax.set_xscale(scale)
573class ToolViewsPositions(ToolBase):
574 """
575 Auxiliary Tool to handle changes in views and positions
577 Runs in the background and should get used by all the tools that
578 need to access the figure's history of views and positions, e.g.
580 * `ToolZoom`
581 * `ToolPan`
582 * `ToolHome`
583 * `ToolBack`
584 * `ToolForward`
585 """
587 def __init__(self, *args, **kwargs):
588 self.views = WeakKeyDictionary()
589 self.positions = WeakKeyDictionary()
590 self.home_views = WeakKeyDictionary()
591 ToolBase.__init__(self, *args, **kwargs)
593 def add_figure(self, figure):
594 """Add the current figure to the stack of views and positions"""
596 if figure not in self.views:
597 self.views[figure] = cbook.Stack()
598 self.positions[figure] = cbook.Stack()
599 self.home_views[figure] = WeakKeyDictionary()
600 # Define Home
601 self.push_current(figure)
602 # Make sure we add a home view for new axes as they're added
603 figure.add_axobserver(lambda fig: self.update_home_views(fig))
605 def clear(self, figure):
606 """Reset the axes stack"""
607 if figure in self.views:
608 self.views[figure].clear()
609 self.positions[figure].clear()
610 self.home_views[figure].clear()
611 self.update_home_views()
613 def update_view(self):
614 """
615 Update the view limits and position for each axes from the current
616 stack position. If any axes are present in the figure that aren't in
617 the current stack position, use the home view limits for those axes and
618 don't update *any* positions.
619 """
621 views = self.views[self.figure]()
622 if views is None:
623 return
624 pos = self.positions[self.figure]()
625 if pos is None:
626 return
627 home_views = self.home_views[self.figure]
628 all_axes = self.figure.get_axes()
629 for a in all_axes:
630 if a in views:
631 cur_view = views[a]
632 else:
633 cur_view = home_views[a]
634 a._set_view(cur_view)
636 if set(all_axes).issubset(pos):
637 for a in all_axes:
638 # Restore both the original and modified positions
639 a._set_position(pos[a][0], 'original')
640 a._set_position(pos[a][1], 'active')
642 self.figure.canvas.draw_idle()
644 def push_current(self, figure=None):
645 """
646 Push the current view limits and position onto their respective stacks
647 """
648 if not figure:
649 figure = self.figure
650 views = WeakKeyDictionary()
651 pos = WeakKeyDictionary()
652 for a in figure.get_axes():
653 views[a] = a._get_view()
654 pos[a] = self._axes_pos(a)
655 self.views[figure].push(views)
656 self.positions[figure].push(pos)
658 def _axes_pos(self, ax):
659 """
660 Return the original and modified positions for the specified axes
662 Parameters
663 ----------
664 ax : (matplotlib.axes.AxesSubplot)
665 The axes to get the positions for
667 Returns
668 -------
669 limits : (tuple)
670 A tuple of the original and modified positions
671 """
673 return (ax.get_position(True).frozen(),
674 ax.get_position().frozen())
676 def update_home_views(self, figure=None):
677 """
678 Make sure that self.home_views has an entry for all axes present in the
679 figure
680 """
682 if not figure:
683 figure = self.figure
684 for a in figure.get_axes():
685 if a not in self.home_views[figure]:
686 self.home_views[figure][a] = a._get_view()
688 def refresh_locators(self):
689 """Redraw the canvases, update the locators"""
690 for a in self.figure.get_axes():
691 xaxis = getattr(a, 'xaxis', None)
692 yaxis = getattr(a, 'yaxis', None)
693 zaxis = getattr(a, 'zaxis', None)
694 locators = []
695 if xaxis is not None:
696 locators.append(xaxis.get_major_locator())
697 locators.append(xaxis.get_minor_locator())
698 if yaxis is not None:
699 locators.append(yaxis.get_major_locator())
700 locators.append(yaxis.get_minor_locator())
701 if zaxis is not None:
702 locators.append(zaxis.get_major_locator())
703 locators.append(zaxis.get_minor_locator())
705 for loc in locators:
706 loc.refresh()
707 self.figure.canvas.draw_idle()
709 def home(self):
710 """Recall the first view and position from the stack"""
711 self.views[self.figure].home()
712 self.positions[self.figure].home()
714 def back(self):
715 """Back one step in the stack of views and positions"""
716 self.views[self.figure].back()
717 self.positions[self.figure].back()
719 def forward(self):
720 """Forward one step in the stack of views and positions"""
721 self.views[self.figure].forward()
722 self.positions[self.figure].forward()
725class ViewsPositionsBase(ToolBase):
726 """Base class for `ToolHome`, `ToolBack` and `ToolForward`"""
728 _on_trigger = None
730 def trigger(self, sender, event, data=None):
731 self.toolmanager.get_tool(_views_positions).add_figure(self.figure)
732 getattr(self.toolmanager.get_tool(_views_positions),
733 self._on_trigger)()
734 self.toolmanager.get_tool(_views_positions).update_view()
737class ToolHome(ViewsPositionsBase):
738 """Restore the original view lim"""
740 description = 'Reset original view'
741 image = 'home'
742 default_keymap = rcParams['keymap.home']
743 _on_trigger = 'home'
746class ToolBack(ViewsPositionsBase):
747 """Move back up the view lim stack"""
749 description = 'Back to previous view'
750 image = 'back'
751 default_keymap = rcParams['keymap.back']
752 _on_trigger = 'back'
755class ToolForward(ViewsPositionsBase):
756 """Move forward in the view lim stack"""
758 description = 'Forward to next view'
759 image = 'forward'
760 default_keymap = rcParams['keymap.forward']
761 _on_trigger = 'forward'
764class ConfigureSubplotsBase(ToolBase):
765 """Base tool for the configuration of subplots"""
767 description = 'Configure subplots'
768 image = 'subplots'
771class SaveFigureBase(ToolBase):
772 """Base tool for figure saving"""
774 description = 'Save the figure'
775 image = 'filesave'
776 default_keymap = rcParams['keymap.save']
779class ZoomPanBase(ToolToggleBase):
780 """Base class for `ToolZoom` and `ToolPan`"""
781 def __init__(self, *args):
782 ToolToggleBase.__init__(self, *args)
783 self._button_pressed = None
784 self._xypress = None
785 self._idPress = None
786 self._idRelease = None
787 self._idScroll = None
788 self.base_scale = 2.
789 self.scrollthresh = .5 # .5 second scroll threshold
790 self.lastscroll = time.time()-self.scrollthresh
792 def enable(self, event):
793 """Connect press/release events and lock the canvas"""
794 self.figure.canvas.widgetlock(self)
795 self._idPress = self.figure.canvas.mpl_connect(
796 'button_press_event', self._press)
797 self._idRelease = self.figure.canvas.mpl_connect(
798 'button_release_event', self._release)
799 self._idScroll = self.figure.canvas.mpl_connect(
800 'scroll_event', self.scroll_zoom)
802 def disable(self, event):
803 """Release the canvas and disconnect press/release events"""
804 self._cancel_action()
805 self.figure.canvas.widgetlock.release(self)
806 self.figure.canvas.mpl_disconnect(self._idPress)
807 self.figure.canvas.mpl_disconnect(self._idRelease)
808 self.figure.canvas.mpl_disconnect(self._idScroll)
810 def trigger(self, sender, event, data=None):
811 self.toolmanager.get_tool(_views_positions).add_figure(self.figure)
812 ToolToggleBase.trigger(self, sender, event, data)
814 def scroll_zoom(self, event):
815 # https://gist.github.com/tacaswell/3144287
816 if event.inaxes is None:
817 return
819 if event.button == 'up':
820 # deal with zoom in
821 scl = self.base_scale
822 elif event.button == 'down':
823 # deal with zoom out
824 scl = 1/self.base_scale
825 else:
826 # deal with something that should never happen
827 scl = 1
829 ax = event.inaxes
830 ax._set_view_from_bbox([event.x, event.y, scl])
832 # If last scroll was done within the timing threshold, delete the
833 # previous view
834 if (time.time()-self.lastscroll) < self.scrollthresh:
835 self.toolmanager.get_tool(_views_positions).back()
837 self.figure.canvas.draw_idle() # force re-draw
839 self.lastscroll = time.time()
840 self.toolmanager.get_tool(_views_positions).push_current()
843class ToolZoom(ZoomPanBase):
844 """Zoom to rectangle"""
846 description = 'Zoom to rectangle'
847 image = 'zoom_to_rect'
848 default_keymap = rcParams['keymap.zoom']
849 cursor = cursors.SELECT_REGION
850 radio_group = 'default'
852 def __init__(self, *args):
853 ZoomPanBase.__init__(self, *args)
854 self._ids_zoom = []
856 def _cancel_action(self):
857 for zoom_id in self._ids_zoom:
858 self.figure.canvas.mpl_disconnect(zoom_id)
859 self.toolmanager.trigger_tool('rubberband', self)
860 self.toolmanager.get_tool(_views_positions).refresh_locators()
861 self._xypress = None
862 self._button_pressed = None
863 self._ids_zoom = []
864 return
866 def _press(self, event):
867 """Callback for mouse button presses in zoom-to-rectangle mode."""
869 # If we're already in the middle of a zoom, pressing another
870 # button works to "cancel"
871 if self._ids_zoom != []:
872 self._cancel_action()
874 if event.button == 1:
875 self._button_pressed = 1
876 elif event.button == 3:
877 self._button_pressed = 3
878 else:
879 self._cancel_action()
880 return
882 x, y = event.x, event.y
884 self._xypress = []
885 for i, a in enumerate(self.figure.get_axes()):
886 if (x is not None and y is not None and a.in_axes(event) and
887 a.get_navigate() and a.can_zoom()):
888 self._xypress.append((x, y, a, i, a._get_view()))
890 id1 = self.figure.canvas.mpl_connect(
891 'motion_notify_event', self._mouse_move)
892 id2 = self.figure.canvas.mpl_connect(
893 'key_press_event', self._switch_on_zoom_mode)
894 id3 = self.figure.canvas.mpl_connect(
895 'key_release_event', self._switch_off_zoom_mode)
897 self._ids_zoom = id1, id2, id3
898 self._zoom_mode = event.key
900 def _switch_on_zoom_mode(self, event):
901 self._zoom_mode = event.key
902 self._mouse_move(event)
904 def _switch_off_zoom_mode(self, event):
905 self._zoom_mode = None
906 self._mouse_move(event)
908 def _mouse_move(self, event):
909 """Callback for mouse moves in zoom-to-rectangle mode."""
911 if self._xypress:
912 x, y = event.x, event.y
913 lastx, lasty, a, ind, view = self._xypress[0]
914 (x1, y1), (x2, y2) = np.clip(
915 [[lastx, lasty], [x, y]], a.bbox.min, a.bbox.max)
916 if self._zoom_mode == "x":
917 y1, y2 = a.bbox.intervaly
918 elif self._zoom_mode == "y":
919 x1, x2 = a.bbox.intervalx
920 self.toolmanager.trigger_tool(
921 'rubberband', self, data=(x1, y1, x2, y2))
923 def _release(self, event):
924 """Callback for mouse button releases in zoom-to-rectangle mode."""
926 for zoom_id in self._ids_zoom:
927 self.figure.canvas.mpl_disconnect(zoom_id)
928 self._ids_zoom = []
930 if not self._xypress:
931 self._cancel_action()
932 return
934 last_a = []
936 for cur_xypress in self._xypress:
937 x, y = event.x, event.y
938 lastx, lasty, a, _ind, view = cur_xypress
939 # ignore singular clicks - 5 pixels is a threshold
940 if abs(x - lastx) < 5 or abs(y - lasty) < 5:
941 self._cancel_action()
942 return
944 # detect twinx, twiny axes and avoid double zooming
945 twinx, twiny = False, False
946 if last_a:
947 for la in last_a:
948 if a.get_shared_x_axes().joined(a, la):
949 twinx = True
950 if a.get_shared_y_axes().joined(a, la):
951 twiny = True
952 last_a.append(a)
954 if self._button_pressed == 1:
955 direction = 'in'
956 elif self._button_pressed == 3:
957 direction = 'out'
958 else:
959 continue
961 a._set_view_from_bbox((lastx, lasty, x, y), direction,
962 self._zoom_mode, twinx, twiny)
964 self._zoom_mode = None
965 self.toolmanager.get_tool(_views_positions).push_current()
966 self._cancel_action()
969class ToolPan(ZoomPanBase):
970 """Pan axes with left mouse, zoom with right"""
972 default_keymap = rcParams['keymap.pan']
973 description = 'Pan axes with left mouse, zoom with right'
974 image = 'move'
975 cursor = cursors.MOVE
976 radio_group = 'default'
978 def __init__(self, *args):
979 ZoomPanBase.__init__(self, *args)
980 self._idDrag = None
982 def _cancel_action(self):
983 self._button_pressed = None
984 self._xypress = []
985 self.figure.canvas.mpl_disconnect(self._idDrag)
986 self.toolmanager.messagelock.release(self)
987 self.toolmanager.get_tool(_views_positions).refresh_locators()
989 def _press(self, event):
990 if event.button == 1:
991 self._button_pressed = 1
992 elif event.button == 3:
993 self._button_pressed = 3
994 else:
995 self._cancel_action()
996 return
998 x, y = event.x, event.y
1000 self._xypress = []
1001 for i, a in enumerate(self.figure.get_axes()):
1002 if (x is not None and y is not None and a.in_axes(event) and
1003 a.get_navigate() and a.can_pan()):
1004 a.start_pan(x, y, event.button)
1005 self._xypress.append((a, i))
1006 self.toolmanager.messagelock(self)
1007 self._idDrag = self.figure.canvas.mpl_connect(
1008 'motion_notify_event', self._mouse_move)
1010 def _release(self, event):
1011 if self._button_pressed is None:
1012 self._cancel_action()
1013 return
1015 self.figure.canvas.mpl_disconnect(self._idDrag)
1016 self.toolmanager.messagelock.release(self)
1018 for a, _ind in self._xypress:
1019 a.end_pan()
1020 if not self._xypress:
1021 self._cancel_action()
1022 return
1024 self.toolmanager.get_tool(_views_positions).push_current()
1025 self._cancel_action()
1027 def _mouse_move(self, event):
1028 for a, _ind in self._xypress:
1029 # safer to use the recorded button at the _press than current
1030 # button: # multiple button can get pressed during motion...
1031 a.drag_pan(self._button_pressed, event.key, event.x, event.y)
1032 self.toolmanager.canvas.draw_idle()
1035class ToolHelpBase(ToolBase):
1036 description = 'Print tool list, shortcuts and description'
1037 default_keymap = rcParams['keymap.help']
1038 image = 'help.png'
1040 @staticmethod
1041 def format_shortcut(key_sequence):
1042 """
1043 Converts a shortcut string from the notation used in rc config to the
1044 standard notation for displaying shortcuts, e.g. 'ctrl+a' -> 'Ctrl+A'.
1045 """
1046 return (key_sequence if len(key_sequence) == 1 else
1047 re.sub(r"\+[A-Z]", r"+Shift\g<0>", key_sequence).title())
1049 def _format_tool_keymap(self, name):
1050 keymaps = self.toolmanager.get_tool_keymap(name)
1051 return ", ".join(self.format_shortcut(keymap) for keymap in keymaps)
1053 def _get_help_entries(self):
1054 return [(name, self._format_tool_keymap(name), tool.description)
1055 for name, tool in sorted(self.toolmanager.tools.items())
1056 if tool.description]
1058 def _get_help_text(self):
1059 entries = self._get_help_entries()
1060 entries = ["{}: {}\n\t{}".format(*entry) for entry in entries]
1061 return "\n".join(entries)
1063 def _get_help_html(self):
1064 fmt = "<tr><td>{}</td><td>{}</td><td>{}</td></tr>"
1065 rows = [fmt.format(
1066 "<b>Action</b>", "<b>Shortcuts</b>", "<b>Description</b>")]
1067 rows += [fmt.format(*row) for row in self._get_help_entries()]
1068 return ("<style>td {padding: 0px 4px}</style>"
1069 "<table><thead>" + rows[0] + "</thead>"
1070 "<tbody>".join(rows[1:]) + "</tbody></table>")
1073class ToolCopyToClipboardBase(ToolBase):
1074 """Tool to copy the figure to the clipboard"""
1076 description = 'Copy the canvas figure to clipboard'
1077 default_keymap = rcParams['keymap.copy']
1079 def trigger(self, *args, **kwargs):
1080 message = "Copy tool is not available"
1081 self.toolmanager.message_event(message, self)
1084default_tools = {'home': ToolHome, 'back': ToolBack, 'forward': ToolForward,
1085 'zoom': ToolZoom, 'pan': ToolPan,
1086 'subplots': 'ToolConfigureSubplots',
1087 'save': 'ToolSaveFigure',
1088 'grid': ToolGrid,
1089 'grid_minor': ToolMinorGrid,
1090 'fullscreen': ToolFullScreen,
1091 'quit': ToolQuit,
1092 'quit_all': ToolQuitAll,
1093 'allnav': ToolEnableAllNavigation,
1094 'nav': ToolEnableNavigation,
1095 'xscale': ToolXScale,
1096 'yscale': ToolYScale,
1097 'position': ToolCursorPosition,
1098 _views_positions: ToolViewsPositions,
1099 'cursor': 'ToolSetCursor',
1100 'rubberband': 'ToolRubberband',
1101 'help': 'ToolHelp',
1102 'copy': 'ToolCopyToClipboard',
1103 }
1104"""Default tools"""
1106default_toolbar_tools = [['navigation', ['home', 'back', 'forward']],
1107 ['zoompan', ['pan', 'zoom', 'subplots']],
1108 ['io', ['save', 'help']]]
1109"""Default tools in the toolbar"""
1112def add_tools_to_manager(toolmanager, tools=default_tools):
1113 """
1114 Add multiple tools to `ToolManager`
1116 Parameters
1117 ----------
1118 toolmanager : ToolManager
1119 `backend_managers.ToolManager` object that will get the tools added
1120 tools : {str: class_like}, optional
1121 The tools to add in a {name: tool} dict, see `add_tool` for more
1122 info.
1123 """
1125 for name, tool in tools.items():
1126 toolmanager.add_tool(name, tool)
1129def add_tools_to_container(container, tools=default_toolbar_tools):
1130 """
1131 Add multiple tools to the container.
1133 Parameters
1134 ----------
1135 container : Container
1136 `backend_bases.ToolContainerBase` object that will get the tools added
1137 tools : list, optional
1138 List in the form
1139 [[group1, [tool1, tool2 ...]], [group2, [...]]]
1140 Where the tools given by tool1, and tool2 will display in group1.
1141 See `add_tool` for details.
1142 """
1144 for group, grouptools in tools:
1145 for position, tool in enumerate(grouptools):
1146 container.add_tool(tool, group, position)