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

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 Filename of the image. 

101 

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

105 

106 def __init__(self, toolmanager, name): 

107 self._name = name 

108 self._toolmanager = toolmanager 

109 self._figure = None 

110 

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

120 

121 @property 

122 def figure(self): 

123 """The Figure affected by this tool, or None.""" 

124 return self._figure 

125 

126 @figure.setter 

127 def figure(self, figure): 

128 self._figure = figure 

129 

130 set_figure = figure.fset 

131 

132 def _make_classic_style_pseudo_toolbar(self): 

133 """ 

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

135 

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

137 by the classic Toolbars. 

138 """ 

139 return SimpleNamespace(canvas=self.canvas) 

140 

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

142 """ 

143 Called when this tool gets used. 

144 

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

146 

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 

157 

158 @_api.deprecated("3.6", alternative="tool_removed_event") 

159 def destroy(self): 

160 """ 

161 Destroy the tool. 

162 

163 This method is called by `.ToolManager.remove_tool`. 

164 """ 

165 pass 

166 

167 

168class ToolToggleBase(ToolBase): 

169 """ 

170 Toggleable tool. 

171 

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

173 

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

182 

183 radio_group = None 

184 """ 

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

186 

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

188 """ 

189 

190 cursor = None 

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

192 

193 default_toggled = False 

194 """Default of toggled state.""" 

195 

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

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

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

199 

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 

207 

208 def enable(self, event=None): 

209 """ 

210 Enable the toggle tool. 

211 

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

213 """ 

214 pass 

215 

216 def disable(self, event=None): 

217 """ 

218 Disable the toggle tool. 

219 

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

221 

222 This can happen in different circumstances. 

223 

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 

230 

231 @property 

232 def toggled(self): 

233 """State of the toggled tool.""" 

234 return self._toggled 

235 

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 

253 

254 

255class SetCursorBase(ToolBase): 

256 """ 

257 Change to the current cursor while inaxes. 

258 

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) 

273 

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) 

281 

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) 

288 

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) 

294 

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) 

300 

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 

312 

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) 

319 

320 

321# This exists solely for deprecation warnings; remove with 

322# SetCursorBase.set_cursor. 

323ToolSetCursor = SetCursorBase 

324 

325 

326class ToolCursorPosition(ToolBase): 

327 """ 

328 Send message with the current pointer position. 

329 

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) 

335 

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) 

343 

344 def send_message(self, event): 

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

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

347 return 

348 

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) 

354 

355 

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() 

366 

367 def draw_rubberband(self, *data): 

368 """ 

369 Draw rubberband. 

370 

371 This method must get implemented per backend. 

372 """ 

373 raise NotImplementedError 

374 

375 def remove_rubberband(self): 

376 """ 

377 Remove rubberband. 

378 

379 This method should get implemented per backend. 

380 """ 

381 pass 

382 

383 

384class ToolQuit(ToolBase): 

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

386 

387 description = 'Quit the figure' 

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

389 

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

391 Gcf.destroy_fig(self.figure) 

392 

393 

394class ToolQuitAll(ToolBase): 

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

396 

397 description = 'Quit all figures' 

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

399 

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

401 Gcf.destroy_all() 

402 

403 

404class ToolGrid(ToolBase): 

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

406 

407 description = 'Toggle major grids' 

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

409 

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) 

417 

418 

419class ToolMinorGrid(ToolBase): 

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

421 

422 description = 'Toggle major and minor grids' 

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

424 

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) 

432 

433 

434class ToolFullScreen(ToolBase): 

435 """Tool to toggle full screen.""" 

436 

437 description = 'Toggle fullscreen mode' 

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

439 

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

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

442 

443 

444class AxisScaleBase(ToolToggleBase): 

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

446 

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

448 if event.inaxes is None: 

449 return 

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

451 

452 def enable(self, event=None): 

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

454 self.figure.canvas.draw_idle() 

455 

456 def disable(self, event=None): 

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

458 self.figure.canvas.draw_idle() 

459 

460 

461class ToolYScale(AxisScaleBase): 

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

463 

464 description = 'Toggle scale Y axis' 

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

466 

467 def set_scale(self, ax, scale): 

468 ax.set_yscale(scale) 

469 

470 

471class ToolXScale(AxisScaleBase): 

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

473 

474 description = 'Toggle scale X axis' 

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

476 

477 def set_scale(self, ax, scale): 

478 ax.set_xscale(scale) 

479 

480 

481class ToolViewsPositions(ToolBase): 

482 """ 

483 Auxiliary Tool to handle changes in views and positions. 

484 

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. 

487 

488 * `ToolZoom` 

489 * `ToolPan` 

490 * `ToolHome` 

491 * `ToolBack` 

492 * `ToolForward` 

493 """ 

494 

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

496 self.views = WeakKeyDictionary() 

497 self.positions = WeakKeyDictionary() 

498 self.home_views = WeakKeyDictionary() 

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

500 

501 def add_figure(self, figure): 

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

503 

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

512 

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() 

520 

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

528 

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) 

543 

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') 

549 

550 self.figure.canvas.draw_idle() 

551 

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) 

565 

566 def _axes_pos(self, ax): 

567 """ 

568 Return the original and modified positions for the specified axes. 

569 

570 Parameters 

571 ---------- 

572 ax : matplotlib.axes.Axes 

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

574 

575 Returns 

576 ------- 

577 original_position, modified_position 

578 A tuple of the original and modified positions. 

579 """ 

580 

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

582 ax.get_position().frozen()) 

583 

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

589 

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() 

595 

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() 

600 

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() 

605 

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() 

610 

611 

612class ViewsPositionsBase(ToolBase): 

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

614 

615 _on_trigger = None 

616 

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() 

622 

623 

624class ToolHome(ViewsPositionsBase): 

625 """Restore the original view limits.""" 

626 

627 description = 'Reset original view' 

628 image = 'home' 

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

630 _on_trigger = 'home' 

631 

632 

633class ToolBack(ViewsPositionsBase): 

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

635 

636 description = 'Back to previous view' 

637 image = 'back' 

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

639 _on_trigger = 'back' 

640 

641 

642class ToolForward(ViewsPositionsBase): 

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

644 

645 description = 'Forward to next view' 

646 image = 'forward' 

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

648 _on_trigger = 'forward' 

649 

650 

651class ConfigureSubplotsBase(ToolBase): 

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

653 

654 description = 'Configure subplots' 

655 image = 'subplots' 

656 

657 

658class SaveFigureBase(ToolBase): 

659 """Base tool for figure saving.""" 

660 

661 description = 'Save the figure' 

662 image = 'filesave' 

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

664 

665 

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 

678 

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) 

688 

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) 

696 

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) 

703 

704 def scroll_zoom(self, event): 

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

706 if event.inaxes is None: 

707 return 

708 

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 

718 

719 ax = event.inaxes 

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

721 

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() 

726 

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

728 

729 self.lastscroll = time.time() 

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

731 

732 

733class ToolZoom(ZoomPanBase): 

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

735 

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' 

741 

742 def __init__(self, *args): 

743 super().__init__(*args) 

744 self._ids_zoom = [] 

745 

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 

755 

756 def _press(self, event): 

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

758 

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() 

763 

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 

771 

772 x, y = event.x, event.y 

773 

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())) 

779 

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) 

786 

787 self._ids_zoom = id1, id2, id3 

788 self._zoom_mode = event.key 

789 

790 def _switch_on_zoom_mode(self, event): 

791 self._zoom_mode = event.key 

792 self._mouse_move(event) 

793 

794 def _switch_off_zoom_mode(self, event): 

795 self._zoom_mode = None 

796 self._mouse_move(event) 

797 

798 def _mouse_move(self, event): 

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

800 

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

812 

813 def _release(self, event): 

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

815 

816 for zoom_id in self._ids_zoom: 

817 self.figure.canvas.mpl_disconnect(zoom_id) 

818 self._ids_zoom = [] 

819 

820 if not self._xypress: 

821 self._cancel_action() 

822 return 

823 

824 done_ax = [] 

825 

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 

833 

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) 

838 

839 if self._button_pressed == 1: 

840 direction = 'in' 

841 elif self._button_pressed == 3: 

842 direction = 'out' 

843 else: 

844 continue 

845 

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

847 self._zoom_mode, twinx, twiny) 

848 

849 self._zoom_mode = None 

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

851 self._cancel_action() 

852 

853 

854class ToolPan(ZoomPanBase): 

855 """Pan axes with left mouse, zoom with right.""" 

856 

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' 

862 

863 def __init__(self, *args): 

864 super().__init__(*args) 

865 self._id_drag = None 

866 

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() 

873 

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 

882 

883 x, y = event.x, event.y 

884 

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) 

894 

895 def _release(self, event): 

896 if self._button_pressed is None: 

897 self._cancel_action() 

898 return 

899 

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

901 self.toolmanager.messagelock.release(self) 

902 

903 for a, _ind in self._xypress: 

904 a.end_pan() 

905 if not self._xypress: 

906 self._cancel_action() 

907 return 

908 

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

910 self._cancel_action() 

911 

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() 

918 

919 

920class ToolHelpBase(ToolBase): 

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

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

923 image = 'help' 

924 

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()) 

933 

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) 

937 

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] 

942 

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) 

947 

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

956 

957 

958class ToolCopyToClipboardBase(ToolBase): 

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

960 

961 description = 'Copy the canvas figure to clipboard' 

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

963 

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

965 message = "Copy tool is not available" 

966 self.toolmanager.message_event(message, self) 

967 

968 

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 } 

987 

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

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

990 ['io', ['save', 'help']]] 

991 

992 

993def add_tools_to_manager(toolmanager, tools=default_tools): 

994 """ 

995 Add multiple tools to a `.ToolManager`. 

996 

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

1005 

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

1007 toolmanager.add_tool(name, tool) 

1008 

1009 

1010def add_tools_to_container(container, tools=default_toolbar_tools): 

1011 """ 

1012 Add multiple tools to the container. 

1013 

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

1024 

1025 for group, grouptools in tools: 

1026 for position, tool in enumerate(grouptools): 

1027 container.add_tool(tool, group, position)