Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.9/dist-packages/matplotlib/backend_tools.py: 36%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

500 statements  

1""" 

2Abstract base classes define the primitives for Tools. 

3These tools are used by `matplotlib.backend_managers.ToolManager` 

4 

5:class:`ToolBase` 

6 Simple stateless tool 

7 

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""" 

13 

14import enum 

15import functools 

16import re 

17import time 

18from types import SimpleNamespace 

19import uuid 

20from weakref import WeakKeyDictionary 

21 

22import numpy as np 

23 

24import matplotlib as mpl 

25from matplotlib._pylab_helpers import Gcf 

26from matplotlib import _api, cbook 

27 

28 

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. 

39 

40 

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. 

47 

48 

49_tool_registry = set() 

50 

51 

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 

58 

59 

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 

67 

68 

69# Views positions tool 

70_views_positions = 'viewpos' 

71 

72 

73class ToolBase: 

74 """ 

75 Base tool class. 

76 

77 A base tool, only implements `trigger` method or no method at all. 

78 The tool is instantiated by `matplotlib.backend_managers.ToolManager`. 

79 """ 

80 

81 default_keymap = None 

82 """ 

83 Keymap to associate with this tool. 

84 

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 """ 

90 

91 description = None 

92 """ 

93 Description of the Tool. 

94 

95 `str`: Tooltip used if the Tool is included in a Toolbar. 

96 """ 

97 

98 image = None 

99 """ 

100 Icon filename. 

101 

102 ``str | None``: Filename of the Toolbar icon; either absolute, or relative to the 

103 directory containing the Python source file where the ``Tool.image`` class attribute 

104 is defined (in the latter case, this cannot be defined as an instance attribute). 

105 In either case, the extension is optional; leaving it off lets individual backends 

106 select the icon format they prefer. If None, the *name* is used as a label in the 

107 toolbar button. 

108 """ 

109 

110 def __init__(self, toolmanager, name): 

111 self._name = name 

112 self._toolmanager = toolmanager 

113 self._figure = None 

114 

115 name = property( 

116 lambda self: self._name, 

117 doc="The tool id (str, must be unique among tools of a tool manager).") 

118 toolmanager = property( 

119 lambda self: self._toolmanager, 

120 doc="The `.ToolManager` that controls this tool.") 

121 canvas = property( 

122 lambda self: self._figure.canvas if self._figure is not None else None, 

123 doc="The canvas of the figure affected by this tool, or None.") 

124 

125 def set_figure(self, figure): 

126 self._figure = figure 

127 

128 figure = property( 

129 lambda self: self._figure, 

130 # The setter must explicitly call self.set_figure so that subclasses can 

131 # meaningfully override it. 

132 lambda self, figure: self.set_figure(figure), 

133 doc="The Figure affected by this tool, or None.") 

134 

135 def _make_classic_style_pseudo_toolbar(self): 

136 """ 

137 Return a placeholder object with a single `canvas` attribute. 

138 

139 This is useful to reuse the implementations of tools already provided 

140 by the classic Toolbars. 

141 """ 

142 return SimpleNamespace(canvas=self.canvas) 

143 

144 def trigger(self, sender, event, data=None): 

145 """ 

146 Called when this tool gets used. 

147 

148 This method is called by `.ToolManager.trigger_tool`. 

149 

150 Parameters 

151 ---------- 

152 event : `.Event` 

153 The canvas event that caused this tool to be called. 

154 sender : object 

155 Object that requested the tool to be triggered. 

156 data : object 

157 Extra data. 

158 """ 

159 pass 

160 

161 

162class ToolToggleBase(ToolBase): 

163 """ 

164 Toggleable tool. 

165 

166 Every time it is triggered, it switches between enable and disable. 

167 

168 Parameters 

169 ---------- 

170 ``*args`` 

171 Variable length argument to be used by the Tool. 

172 ``**kwargs`` 

173 `toggled` if present and True, sets the initial state of the Tool 

174 Arbitrary keyword arguments to be consumed by the Tool 

175 """ 

176 

177 radio_group = None 

178 """ 

179 Attribute to group 'radio' like tools (mutually exclusive). 

180 

181 `str` that identifies the group or **None** if not belonging to a group. 

182 """ 

183 

184 cursor = None 

185 """Cursor to use when the tool is active.""" 

186 

187 default_toggled = False 

188 """Default of toggled state.""" 

189 

190 def __init__(self, *args, **kwargs): 

191 self._toggled = kwargs.pop('toggled', self.default_toggled) 

192 super().__init__(*args, **kwargs) 

193 

194 def trigger(self, sender, event, data=None): 

195 """Calls `enable` or `disable` based on `toggled` value.""" 

196 if self._toggled: 

197 self.disable(event) 

198 else: 

199 self.enable(event) 

200 self._toggled = not self._toggled 

201 

202 def enable(self, event=None): 

203 """ 

204 Enable the toggle tool. 

205 

206 `trigger` calls this method when `toggled` is False. 

207 """ 

208 pass 

209 

210 def disable(self, event=None): 

211 """ 

212 Disable the toggle tool. 

213 

214 `trigger` call this method when `toggled` is True. 

215 

216 This can happen in different circumstances. 

217 

218 * Click on the toolbar tool button. 

219 * Call to `matplotlib.backend_managers.ToolManager.trigger_tool`. 

220 * Another `ToolToggleBase` derived tool is triggered 

221 (from the same `.ToolManager`). 

222 """ 

223 pass 

224 

225 @property 

226 def toggled(self): 

227 """State of the toggled tool.""" 

228 return self._toggled 

229 

230 def set_figure(self, figure): 

231 toggled = self.toggled 

232 if toggled: 

233 if self.figure: 

234 self.trigger(self, None) 

235 else: 

236 # if no figure the internal state is not changed 

237 # we change it here so next call to trigger will change it back 

238 self._toggled = False 

239 super().set_figure(figure) 

240 if toggled: 

241 if figure: 

242 self.trigger(self, None) 

243 else: 

244 # if there is no figure, trigger won't change the internal 

245 # state we change it back 

246 self._toggled = True 

247 

248 

249class ToolSetCursor(ToolBase): 

250 """ 

251 Change to the current cursor while inaxes. 

252 

253 This tool, keeps track of all `ToolToggleBase` derived tools, and updates 

254 the cursor when a tool gets triggered. 

255 """ 

256 def __init__(self, *args, **kwargs): 

257 super().__init__(*args, **kwargs) 

258 self._id_drag = None 

259 self._current_tool = None 

260 self._default_cursor = cursors.POINTER 

261 self._last_cursor = self._default_cursor 

262 self.toolmanager.toolmanager_connect('tool_added_event', 

263 self._add_tool_cbk) 

264 # process current tools 

265 for tool in self.toolmanager.tools.values(): 

266 self._add_tool(tool) 

267 

268 def set_figure(self, figure): 

269 if self._id_drag: 

270 self.canvas.mpl_disconnect(self._id_drag) 

271 super().set_figure(figure) 

272 if figure: 

273 self._id_drag = self.canvas.mpl_connect( 

274 'motion_notify_event', self._set_cursor_cbk) 

275 

276 def _tool_trigger_cbk(self, event): 

277 if event.tool.toggled: 

278 self._current_tool = event.tool 

279 else: 

280 self._current_tool = None 

281 self._set_cursor_cbk(event.canvasevent) 

282 

283 def _add_tool(self, tool): 

284 """Set the cursor when the tool is triggered.""" 

285 if getattr(tool, 'cursor', None) is not None: 

286 self.toolmanager.toolmanager_connect('tool_trigger_%s' % tool.name, 

287 self._tool_trigger_cbk) 

288 

289 def _add_tool_cbk(self, event): 

290 """Process every newly added tool.""" 

291 if event.tool is self: 

292 return 

293 self._add_tool(event.tool) 

294 

295 def _set_cursor_cbk(self, event): 

296 if not event or not self.canvas: 

297 return 

298 if (self._current_tool and getattr(event, "inaxes", None) 

299 and event.inaxes.get_navigate()): 

300 if self._last_cursor != self._current_tool.cursor: 

301 self.canvas.set_cursor(self._current_tool.cursor) 

302 self._last_cursor = self._current_tool.cursor 

303 elif self._last_cursor != self._default_cursor: 

304 self.canvas.set_cursor(self._default_cursor) 

305 self._last_cursor = self._default_cursor 

306 

307 

308class ToolCursorPosition(ToolBase): 

309 """ 

310 Send message with the current pointer position. 

311 

312 This tool runs in the background reporting the position of the cursor. 

313 """ 

314 def __init__(self, *args, **kwargs): 

315 self._id_drag = None 

316 super().__init__(*args, **kwargs) 

317 

318 def set_figure(self, figure): 

319 if self._id_drag: 

320 self.canvas.mpl_disconnect(self._id_drag) 

321 super().set_figure(figure) 

322 if figure: 

323 self._id_drag = self.canvas.mpl_connect( 

324 'motion_notify_event', self.send_message) 

325 

326 def send_message(self, event): 

327 """Call `matplotlib.backend_managers.ToolManager.message_event`.""" 

328 if self.toolmanager.messagelock.locked(): 

329 return 

330 

331 from matplotlib.backend_bases import NavigationToolbar2 

332 message = NavigationToolbar2._mouse_event_to_message(event) 

333 self.toolmanager.message_event(message, self) 

334 

335 

336class RubberbandBase(ToolBase): 

337 """Draw and remove a rubberband.""" 

338 def trigger(self, sender, event, data=None): 

339 """Call `draw_rubberband` or `remove_rubberband` based on data.""" 

340 if not self.figure.canvas.widgetlock.available(sender): 

341 return 

342 if data is not None: 

343 self.draw_rubberband(*data) 

344 else: 

345 self.remove_rubberband() 

346 

347 def draw_rubberband(self, *data): 

348 """ 

349 Draw rubberband. 

350 

351 This method must get implemented per backend. 

352 """ 

353 raise NotImplementedError 

354 

355 def remove_rubberband(self): 

356 """ 

357 Remove rubberband. 

358 

359 This method should get implemented per backend. 

360 """ 

361 pass 

362 

363 

364class ToolQuit(ToolBase): 

365 """Tool to call the figure manager destroy method.""" 

366 

367 description = 'Quit the figure' 

368 default_keymap = property(lambda self: mpl.rcParams['keymap.quit']) 

369 

370 def trigger(self, sender, event, data=None): 

371 Gcf.destroy_fig(self.figure) 

372 

373 

374class ToolQuitAll(ToolBase): 

375 """Tool to call the figure manager destroy method.""" 

376 

377 description = 'Quit all figures' 

378 default_keymap = property(lambda self: mpl.rcParams['keymap.quit_all']) 

379 

380 def trigger(self, sender, event, data=None): 

381 Gcf.destroy_all() 

382 

383 

384class ToolGrid(ToolBase): 

385 """Tool to toggle the major grids of the figure.""" 

386 

387 description = 'Toggle major grids' 

388 default_keymap = property(lambda self: mpl.rcParams['keymap.grid']) 

389 

390 def trigger(self, sender, event, data=None): 

391 sentinel = str(uuid.uuid4()) 

392 # Trigger grid switching by temporarily setting :rc:`keymap.grid` 

393 # to a unique key and sending an appropriate event. 

394 with cbook._setattr_cm(event, key=sentinel), \ 

395 mpl.rc_context({'keymap.grid': sentinel}): 

396 mpl.backend_bases.key_press_handler(event, self.figure.canvas) 

397 

398 

399class ToolMinorGrid(ToolBase): 

400 """Tool to toggle the major and minor grids of the figure.""" 

401 

402 description = 'Toggle major and minor grids' 

403 default_keymap = property(lambda self: mpl.rcParams['keymap.grid_minor']) 

404 

405 def trigger(self, sender, event, data=None): 

406 sentinel = str(uuid.uuid4()) 

407 # Trigger grid switching by temporarily setting :rc:`keymap.grid_minor` 

408 # to a unique key and sending an appropriate event. 

409 with cbook._setattr_cm(event, key=sentinel), \ 

410 mpl.rc_context({'keymap.grid_minor': sentinel}): 

411 mpl.backend_bases.key_press_handler(event, self.figure.canvas) 

412 

413 

414class ToolFullScreen(ToolBase): 

415 """Tool to toggle full screen.""" 

416 

417 description = 'Toggle fullscreen mode' 

418 default_keymap = property(lambda self: mpl.rcParams['keymap.fullscreen']) 

419 

420 def trigger(self, sender, event, data=None): 

421 self.figure.canvas.manager.full_screen_toggle() 

422 

423 

424class AxisScaleBase(ToolToggleBase): 

425 """Base Tool to toggle between linear and logarithmic.""" 

426 

427 def trigger(self, sender, event, data=None): 

428 if event.inaxes is None: 

429 return 

430 super().trigger(sender, event, data) 

431 

432 def enable(self, event=None): 

433 self.set_scale(event.inaxes, 'log') 

434 self.figure.canvas.draw_idle() 

435 

436 def disable(self, event=None): 

437 self.set_scale(event.inaxes, 'linear') 

438 self.figure.canvas.draw_idle() 

439 

440 

441class ToolYScale(AxisScaleBase): 

442 """Tool to toggle between linear and logarithmic scales on the Y axis.""" 

443 

444 description = 'Toggle scale Y axis' 

445 default_keymap = property(lambda self: mpl.rcParams['keymap.yscale']) 

446 

447 def set_scale(self, ax, scale): 

448 ax.set_yscale(scale) 

449 

450 

451class ToolXScale(AxisScaleBase): 

452 """Tool to toggle between linear and logarithmic scales on the X axis.""" 

453 

454 description = 'Toggle scale X axis' 

455 default_keymap = property(lambda self: mpl.rcParams['keymap.xscale']) 

456 

457 def set_scale(self, ax, scale): 

458 ax.set_xscale(scale) 

459 

460 

461class ToolViewsPositions(ToolBase): 

462 """ 

463 Auxiliary Tool to handle changes in views and positions. 

464 

465 Runs in the background and should get used by all the tools that 

466 need to access the figure's history of views and positions, e.g. 

467 

468 * `ToolZoom` 

469 * `ToolPan` 

470 * `ToolHome` 

471 * `ToolBack` 

472 * `ToolForward` 

473 """ 

474 

475 def __init__(self, *args, **kwargs): 

476 self.views = WeakKeyDictionary() 

477 self.positions = WeakKeyDictionary() 

478 self.home_views = WeakKeyDictionary() 

479 super().__init__(*args, **kwargs) 

480 

481 def add_figure(self, figure): 

482 """Add the current figure to the stack of views and positions.""" 

483 

484 if figure not in self.views: 

485 self.views[figure] = cbook._Stack() 

486 self.positions[figure] = cbook._Stack() 

487 self.home_views[figure] = WeakKeyDictionary() 

488 # Define Home 

489 self.push_current(figure) 

490 # Make sure we add a home view for new Axes as they're added 

491 figure.add_axobserver(lambda fig: self.update_home_views(fig)) 

492 

493 def clear(self, figure): 

494 """Reset the Axes stack.""" 

495 if figure in self.views: 

496 self.views[figure].clear() 

497 self.positions[figure].clear() 

498 self.home_views[figure].clear() 

499 self.update_home_views() 

500 

501 def update_view(self): 

502 """ 

503 Update the view limits and position for each Axes from the current 

504 stack position. If any Axes are present in the figure that aren't in 

505 the current stack position, use the home view limits for those Axes and 

506 don't update *any* positions. 

507 """ 

508 

509 views = self.views[self.figure]() 

510 if views is None: 

511 return 

512 pos = self.positions[self.figure]() 

513 if pos is None: 

514 return 

515 home_views = self.home_views[self.figure] 

516 all_axes = self.figure.get_axes() 

517 for a in all_axes: 

518 if a in views: 

519 cur_view = views[a] 

520 else: 

521 cur_view = home_views[a] 

522 a._set_view(cur_view) 

523 

524 if set(all_axes).issubset(pos): 

525 for a in all_axes: 

526 # Restore both the original and modified positions 

527 a._set_position(pos[a][0], 'original') 

528 a._set_position(pos[a][1], 'active') 

529 

530 self.figure.canvas.draw_idle() 

531 

532 def push_current(self, figure=None): 

533 """ 

534 Push the current view limits and position onto their respective stacks. 

535 """ 

536 if not figure: 

537 figure = self.figure 

538 views = WeakKeyDictionary() 

539 pos = WeakKeyDictionary() 

540 for a in figure.get_axes(): 

541 views[a] = a._get_view() 

542 pos[a] = self._axes_pos(a) 

543 self.views[figure].push(views) 

544 self.positions[figure].push(pos) 

545 

546 def _axes_pos(self, ax): 

547 """ 

548 Return the original and modified positions for the specified Axes. 

549 

550 Parameters 

551 ---------- 

552 ax : matplotlib.axes.Axes 

553 The `.Axes` to get the positions for. 

554 

555 Returns 

556 ------- 

557 original_position, modified_position 

558 A tuple of the original and modified positions. 

559 """ 

560 

561 return (ax.get_position(True).frozen(), 

562 ax.get_position().frozen()) 

563 

564 def update_home_views(self, figure=None): 

565 """ 

566 Make sure that ``self.home_views`` has an entry for all Axes present 

567 in the figure. 

568 """ 

569 

570 if not figure: 

571 figure = self.figure 

572 for a in figure.get_axes(): 

573 if a not in self.home_views[figure]: 

574 self.home_views[figure][a] = a._get_view() 

575 

576 def home(self): 

577 """Recall the first view and position from the stack.""" 

578 self.views[self.figure].home() 

579 self.positions[self.figure].home() 

580 

581 def back(self): 

582 """Back one step in the stack of views and positions.""" 

583 self.views[self.figure].back() 

584 self.positions[self.figure].back() 

585 

586 def forward(self): 

587 """Forward one step in the stack of views and positions.""" 

588 self.views[self.figure].forward() 

589 self.positions[self.figure].forward() 

590 

591 

592class ViewsPositionsBase(ToolBase): 

593 """Base class for `ToolHome`, `ToolBack` and `ToolForward`.""" 

594 

595 _on_trigger = None 

596 

597 def trigger(self, sender, event, data=None): 

598 self.toolmanager.get_tool(_views_positions).add_figure(self.figure) 

599 getattr(self.toolmanager.get_tool(_views_positions), 

600 self._on_trigger)() 

601 self.toolmanager.get_tool(_views_positions).update_view() 

602 

603 

604class ToolHome(ViewsPositionsBase): 

605 """Restore the original view limits.""" 

606 

607 description = 'Reset original view' 

608 image = 'mpl-data/images/home' 

609 default_keymap = property(lambda self: mpl.rcParams['keymap.home']) 

610 _on_trigger = 'home' 

611 

612 

613class ToolBack(ViewsPositionsBase): 

614 """Move back up the view limits stack.""" 

615 

616 description = 'Back to previous view' 

617 image = 'mpl-data/images/back' 

618 default_keymap = property(lambda self: mpl.rcParams['keymap.back']) 

619 _on_trigger = 'back' 

620 

621 

622class ToolForward(ViewsPositionsBase): 

623 """Move forward in the view lim stack.""" 

624 

625 description = 'Forward to next view' 

626 image = 'mpl-data/images/forward' 

627 default_keymap = property(lambda self: mpl.rcParams['keymap.forward']) 

628 _on_trigger = 'forward' 

629 

630 

631class ConfigureSubplotsBase(ToolBase): 

632 """Base tool for the configuration of subplots.""" 

633 

634 description = 'Configure subplots' 

635 image = 'mpl-data/images/subplots' 

636 

637 

638class SaveFigureBase(ToolBase): 

639 """Base tool for figure saving.""" 

640 

641 description = 'Save the figure' 

642 image = 'mpl-data/images/filesave' 

643 default_keymap = property(lambda self: mpl.rcParams['keymap.save']) 

644 

645 

646class ZoomPanBase(ToolToggleBase): 

647 """Base class for `ToolZoom` and `ToolPan`.""" 

648 def __init__(self, *args): 

649 super().__init__(*args) 

650 self._button_pressed = None 

651 self._xypress = None 

652 self._idPress = None 

653 self._idRelease = None 

654 self._idScroll = None 

655 self.base_scale = 2. 

656 self.scrollthresh = .5 # .5 second scroll threshold 

657 self.lastscroll = time.time()-self.scrollthresh 

658 

659 def enable(self, event=None): 

660 """Connect press/release events and lock the canvas.""" 

661 self.figure.canvas.widgetlock(self) 

662 self._idPress = self.figure.canvas.mpl_connect( 

663 'button_press_event', self._press) 

664 self._idRelease = self.figure.canvas.mpl_connect( 

665 'button_release_event', self._release) 

666 self._idScroll = self.figure.canvas.mpl_connect( 

667 'scroll_event', self.scroll_zoom) 

668 

669 def disable(self, event=None): 

670 """Release the canvas and disconnect press/release events.""" 

671 self._cancel_action() 

672 self.figure.canvas.widgetlock.release(self) 

673 self.figure.canvas.mpl_disconnect(self._idPress) 

674 self.figure.canvas.mpl_disconnect(self._idRelease) 

675 self.figure.canvas.mpl_disconnect(self._idScroll) 

676 

677 def trigger(self, sender, event, data=None): 

678 self.toolmanager.get_tool(_views_positions).add_figure(self.figure) 

679 super().trigger(sender, event, data) 

680 new_navigate_mode = self.name.upper() if self.toggled else None 

681 for ax in self.figure.axes: 

682 ax.set_navigate_mode(new_navigate_mode) 

683 

684 def scroll_zoom(self, event): 

685 # https://gist.github.com/tacaswell/3144287 

686 if event.inaxes is None: 

687 return 

688 

689 if event.button == 'up': 

690 # deal with zoom in 

691 scl = self.base_scale 

692 elif event.button == 'down': 

693 # deal with zoom out 

694 scl = 1/self.base_scale 

695 else: 

696 # deal with something that should never happen 

697 scl = 1 

698 

699 ax = event.inaxes 

700 ax._set_view_from_bbox([event.x, event.y, scl]) 

701 

702 # If last scroll was done within the timing threshold, delete the 

703 # previous view 

704 if (time.time()-self.lastscroll) < self.scrollthresh: 

705 self.toolmanager.get_tool(_views_positions).back() 

706 

707 self.figure.canvas.draw_idle() # force re-draw 

708 

709 self.lastscroll = time.time() 

710 self.toolmanager.get_tool(_views_positions).push_current() 

711 

712 

713class ToolZoom(ZoomPanBase): 

714 """A Tool for zooming using a rectangle selector.""" 

715 

716 description = 'Zoom to rectangle' 

717 image = 'mpl-data/images/zoom_to_rect' 

718 default_keymap = property(lambda self: mpl.rcParams['keymap.zoom']) 

719 cursor = cursors.SELECT_REGION 

720 radio_group = 'default' 

721 

722 def __init__(self, *args): 

723 super().__init__(*args) 

724 self._ids_zoom = [] 

725 

726 def _cancel_action(self): 

727 for zoom_id in self._ids_zoom: 

728 self.figure.canvas.mpl_disconnect(zoom_id) 

729 self.toolmanager.trigger_tool('rubberband', self) 

730 self.figure.canvas.draw_idle() 

731 self._xypress = None 

732 self._button_pressed = None 

733 self._ids_zoom = [] 

734 return 

735 

736 def _press(self, event): 

737 """Callback for mouse button presses in zoom-to-rectangle mode.""" 

738 

739 # If we're already in the middle of a zoom, pressing another 

740 # button works to "cancel" 

741 if self._ids_zoom: 

742 self._cancel_action() 

743 

744 if event.button == 1: 

745 self._button_pressed = 1 

746 elif event.button == 3: 

747 self._button_pressed = 3 

748 else: 

749 self._cancel_action() 

750 return 

751 

752 x, y = event.x, event.y 

753 

754 self._xypress = [] 

755 for i, a in enumerate(self.figure.get_axes()): 

756 if (x is not None and y is not None and a.in_axes(event) and 

757 a.get_navigate() and a.can_zoom()): 

758 self._xypress.append((x, y, a, i, a._get_view())) 

759 

760 id1 = self.figure.canvas.mpl_connect( 

761 'motion_notify_event', self._mouse_move) 

762 id2 = self.figure.canvas.mpl_connect( 

763 'key_press_event', self._switch_on_zoom_mode) 

764 id3 = self.figure.canvas.mpl_connect( 

765 'key_release_event', self._switch_off_zoom_mode) 

766 

767 self._ids_zoom = id1, id2, id3 

768 self._zoom_mode = event.key 

769 

770 def _switch_on_zoom_mode(self, event): 

771 self._zoom_mode = event.key 

772 self._mouse_move(event) 

773 

774 def _switch_off_zoom_mode(self, event): 

775 self._zoom_mode = None 

776 self._mouse_move(event) 

777 

778 def _mouse_move(self, event): 

779 """Callback for mouse moves in zoom-to-rectangle mode.""" 

780 

781 if self._xypress: 

782 x, y = event.x, event.y 

783 lastx, lasty, a, ind, view = self._xypress[0] 

784 (x1, y1), (x2, y2) = np.clip( 

785 [[lastx, lasty], [x, y]], a.bbox.min, a.bbox.max) 

786 if self._zoom_mode == "x": 

787 y1, y2 = a.bbox.intervaly 

788 elif self._zoom_mode == "y": 

789 x1, x2 = a.bbox.intervalx 

790 self.toolmanager.trigger_tool( 

791 'rubberband', self, data=(x1, y1, x2, y2)) 

792 

793 def _release(self, event): 

794 """Callback for mouse button releases in zoom-to-rectangle mode.""" 

795 

796 for zoom_id in self._ids_zoom: 

797 self.figure.canvas.mpl_disconnect(zoom_id) 

798 self._ids_zoom = [] 

799 

800 if not self._xypress: 

801 self._cancel_action() 

802 return 

803 

804 done_ax = [] 

805 

806 for cur_xypress in self._xypress: 

807 x, y = event.x, event.y 

808 lastx, lasty, a, _ind, view = cur_xypress 

809 # ignore singular clicks - 5 pixels is a threshold 

810 if abs(x - lastx) < 5 or abs(y - lasty) < 5: 

811 self._cancel_action() 

812 return 

813 

814 # detect twinx, twiny Axes and avoid double zooming 

815 twinx = any(a.get_shared_x_axes().joined(a, a1) for a1 in done_ax) 

816 twiny = any(a.get_shared_y_axes().joined(a, a1) for a1 in done_ax) 

817 done_ax.append(a) 

818 

819 if self._button_pressed == 1: 

820 direction = 'in' 

821 elif self._button_pressed == 3: 

822 direction = 'out' 

823 else: 

824 continue 

825 

826 a._set_view_from_bbox((lastx, lasty, x, y), direction, 

827 self._zoom_mode, twinx, twiny) 

828 

829 self._zoom_mode = None 

830 self.toolmanager.get_tool(_views_positions).push_current() 

831 self._cancel_action() 

832 

833 

834class ToolPan(ZoomPanBase): 

835 """Pan Axes with left mouse, zoom with right.""" 

836 

837 default_keymap = property(lambda self: mpl.rcParams['keymap.pan']) 

838 description = 'Pan axes with left mouse, zoom with right' 

839 image = 'mpl-data/images/move' 

840 cursor = cursors.MOVE 

841 radio_group = 'default' 

842 

843 def __init__(self, *args): 

844 super().__init__(*args) 

845 self._id_drag = None 

846 

847 def _cancel_action(self): 

848 self._button_pressed = None 

849 self._xypress = [] 

850 self.figure.canvas.mpl_disconnect(self._id_drag) 

851 self.toolmanager.messagelock.release(self) 

852 self.figure.canvas.draw_idle() 

853 

854 def _press(self, event): 

855 if event.button == 1: 

856 self._button_pressed = 1 

857 elif event.button == 3: 

858 self._button_pressed = 3 

859 else: 

860 self._cancel_action() 

861 return 

862 

863 x, y = event.x, event.y 

864 

865 self._xypress = [] 

866 for i, a in enumerate(self.figure.get_axes()): 

867 if (x is not None and y is not None and a.in_axes(event) and 

868 a.get_navigate() and a.can_pan()): 

869 a.start_pan(x, y, event.button) 

870 self._xypress.append((a, i)) 

871 self.toolmanager.messagelock(self) 

872 self._id_drag = self.figure.canvas.mpl_connect( 

873 'motion_notify_event', self._mouse_move) 

874 

875 def _release(self, event): 

876 if self._button_pressed is None: 

877 self._cancel_action() 

878 return 

879 

880 self.figure.canvas.mpl_disconnect(self._id_drag) 

881 self.toolmanager.messagelock.release(self) 

882 

883 for a, _ind in self._xypress: 

884 a.end_pan() 

885 if not self._xypress: 

886 self._cancel_action() 

887 return 

888 

889 self.toolmanager.get_tool(_views_positions).push_current() 

890 self._cancel_action() 

891 

892 def _mouse_move(self, event): 

893 for a, _ind in self._xypress: 

894 # safer to use the recorded button at the _press than current 

895 # button: # multiple button can get pressed during motion... 

896 a.drag_pan(self._button_pressed, event.key, event.x, event.y) 

897 self.toolmanager.canvas.draw_idle() 

898 

899 

900class ToolHelpBase(ToolBase): 

901 description = 'Print tool list, shortcuts and description' 

902 default_keymap = property(lambda self: mpl.rcParams['keymap.help']) 

903 image = 'mpl-data/images/help' 

904 

905 @staticmethod 

906 def format_shortcut(key_sequence): 

907 """ 

908 Convert a shortcut string from the notation used in rc config to the 

909 standard notation for displaying shortcuts, e.g. 'ctrl+a' -> 'Ctrl+A'. 

910 """ 

911 return (key_sequence if len(key_sequence) == 1 else 

912 re.sub(r"\+[A-Z]", r"+Shift\g<0>", key_sequence).title()) 

913 

914 def _format_tool_keymap(self, name): 

915 keymaps = self.toolmanager.get_tool_keymap(name) 

916 return ", ".join(self.format_shortcut(keymap) for keymap in keymaps) 

917 

918 def _get_help_entries(self): 

919 return [(name, self._format_tool_keymap(name), tool.description) 

920 for name, tool in sorted(self.toolmanager.tools.items()) 

921 if tool.description] 

922 

923 def _get_help_text(self): 

924 entries = self._get_help_entries() 

925 entries = ["{}: {}\n\t{}".format(*entry) for entry in entries] 

926 return "\n".join(entries) 

927 

928 def _get_help_html(self): 

929 fmt = "<tr><td>{}</td><td>{}</td><td>{}</td></tr>" 

930 rows = [fmt.format( 

931 "<b>Action</b>", "<b>Shortcuts</b>", "<b>Description</b>")] 

932 rows += [fmt.format(*row) for row in self._get_help_entries()] 

933 return ("<style>td {padding: 0px 4px}</style>" 

934 "<table><thead>" + rows[0] + "</thead>" 

935 "<tbody>".join(rows[1:]) + "</tbody></table>") 

936 

937 

938class ToolCopyToClipboardBase(ToolBase): 

939 """Tool to copy the figure to the clipboard.""" 

940 

941 description = 'Copy the canvas figure to clipboard' 

942 default_keymap = property(lambda self: mpl.rcParams['keymap.copy']) 

943 

944 def trigger(self, *args, **kwargs): 

945 message = "Copy tool is not available" 

946 self.toolmanager.message_event(message, self) 

947 

948 

949default_tools = {'home': ToolHome, 'back': ToolBack, 'forward': ToolForward, 

950 'zoom': ToolZoom, 'pan': ToolPan, 

951 'subplots': ConfigureSubplotsBase, 

952 'save': SaveFigureBase, 

953 'grid': ToolGrid, 

954 'grid_minor': ToolMinorGrid, 

955 'fullscreen': ToolFullScreen, 

956 'quit': ToolQuit, 

957 'quit_all': ToolQuitAll, 

958 'xscale': ToolXScale, 

959 'yscale': ToolYScale, 

960 'position': ToolCursorPosition, 

961 _views_positions: ToolViewsPositions, 

962 'cursor': ToolSetCursor, 

963 'rubberband': RubberbandBase, 

964 'help': ToolHelpBase, 

965 'copy': ToolCopyToClipboardBase, 

966 } 

967 

968default_toolbar_tools = [['navigation', ['home', 'back', 'forward']], 

969 ['zoompan', ['pan', 'zoom', 'subplots']], 

970 ['io', ['save', 'help']]] 

971 

972 

973def add_tools_to_manager(toolmanager, tools=default_tools): 

974 """ 

975 Add multiple tools to a `.ToolManager`. 

976 

977 Parameters 

978 ---------- 

979 toolmanager : `.backend_managers.ToolManager` 

980 Manager to which the tools are added. 

981 tools : {str: class_like}, optional 

982 The tools to add in a {name: tool} dict, see 

983 `.backend_managers.ToolManager.add_tool` for more info. 

984 """ 

985 

986 for name, tool in tools.items(): 

987 toolmanager.add_tool(name, tool) 

988 

989 

990def add_tools_to_container(container, tools=default_toolbar_tools): 

991 """ 

992 Add multiple tools to the container. 

993 

994 Parameters 

995 ---------- 

996 container : Container 

997 `.backend_bases.ToolContainerBase` object that will get the tools 

998 added. 

999 tools : list, optional 

1000 List in the form ``[[group1, [tool1, tool2 ...]], [group2, [...]]]`` 

1001 where the tools ``[tool1, tool2, ...]`` will display in group1. 

1002 See `.backend_bases.ToolContainerBase.add_tool` for details. 

1003 """ 

1004 

1005 for group, grouptools in tools: 

1006 for position, tool in enumerate(grouptools): 

1007 container.add_tool(tool, group, position)