Coverage for /usr/lib/python3/dist-packages/matplotlib/backend_managers.py: 21%

154 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2025-06-14 15:25 +0200

1from matplotlib import _api, backend_tools, cbook, widgets 

2 

3 

4class ToolEvent: 

5 """Event for tool manipulation (add/remove).""" 

6 def __init__(self, name, sender, tool, data=None): 

7 self.name = name 

8 self.sender = sender 

9 self.tool = tool 

10 self.data = data 

11 

12 

13class ToolTriggerEvent(ToolEvent): 

14 """Event to inform that a tool has been triggered.""" 

15 def __init__(self, name, sender, tool, canvasevent=None, data=None): 

16 super().__init__(name, sender, tool, data) 

17 self.canvasevent = canvasevent 

18 

19 

20class ToolManagerMessageEvent: 

21 """ 

22 Event carrying messages from toolmanager. 

23 

24 Messages usually get displayed to the user by the toolbar. 

25 """ 

26 def __init__(self, name, sender, message): 

27 self.name = name 

28 self.sender = sender 

29 self.message = message 

30 

31 

32class ToolManager: 

33 """ 

34 Manager for actions triggered by user interactions (key press, toolbar 

35 clicks, ...) on a Figure. 

36 

37 Attributes 

38 ---------- 

39 figure : `.Figure` 

40 keypresslock : `~matplotlib.widgets.LockDraw` 

41 `.LockDraw` object to know if the `canvas` key_press_event is locked. 

42 messagelock : `~matplotlib.widgets.LockDraw` 

43 `.LockDraw` object to know if the message is available to write. 

44 """ 

45 

46 def __init__(self, figure=None): 

47 

48 self._key_press_handler_id = None 

49 

50 self._tools = {} 

51 self._keys = {} 

52 self._toggled = {} 

53 self._callbacks = cbook.CallbackRegistry() 

54 

55 # to process keypress event 

56 self.keypresslock = widgets.LockDraw() 

57 self.messagelock = widgets.LockDraw() 

58 

59 self._figure = None 

60 self.set_figure(figure) 

61 

62 @property 

63 def canvas(self): 

64 """Canvas managed by FigureManager.""" 

65 if not self._figure: 

66 return None 

67 return self._figure.canvas 

68 

69 @property 

70 def figure(self): 

71 """Figure that holds the canvas.""" 

72 return self._figure 

73 

74 @figure.setter 

75 def figure(self, figure): 

76 self.set_figure(figure) 

77 

78 def set_figure(self, figure, update_tools=True): 

79 """ 

80 Bind the given figure to the tools. 

81 

82 Parameters 

83 ---------- 

84 figure : `.Figure` 

85 update_tools : bool, default: True 

86 Force tools to update figure. 

87 """ 

88 if self._key_press_handler_id: 

89 self.canvas.mpl_disconnect(self._key_press_handler_id) 

90 self._figure = figure 

91 if figure: 

92 self._key_press_handler_id = self.canvas.mpl_connect( 

93 'key_press_event', self._key_press) 

94 if update_tools: 

95 for tool in self._tools.values(): 

96 tool.figure = figure 

97 

98 def toolmanager_connect(self, s, func): 

99 """ 

100 Connect event with string *s* to *func*. 

101 

102 Parameters 

103 ---------- 

104 s : str 

105 The name of the event. The following events are recognized: 

106 

107 - 'tool_message_event' 

108 - 'tool_removed_event' 

109 - 'tool_added_event' 

110 

111 For every tool added a new event is created 

112 

113 - 'tool_trigger_TOOLNAME', where TOOLNAME is the id of the tool. 

114 

115 func : callable 

116 Callback function for the toolmanager event with signature:: 

117 

118 def func(event: ToolEvent) -> Any 

119 

120 Returns 

121 ------- 

122 cid 

123 The callback id for the connection. This can be used in 

124 `.toolmanager_disconnect`. 

125 """ 

126 return self._callbacks.connect(s, func) 

127 

128 def toolmanager_disconnect(self, cid): 

129 """ 

130 Disconnect callback id *cid*. 

131 

132 Example usage:: 

133 

134 cid = toolmanager.toolmanager_connect('tool_trigger_zoom', onpress) 

135 #...later 

136 toolmanager.toolmanager_disconnect(cid) 

137 """ 

138 return self._callbacks.disconnect(cid) 

139 

140 def message_event(self, message, sender=None): 

141 """Emit a `ToolManagerMessageEvent`.""" 

142 if sender is None: 

143 sender = self 

144 

145 s = 'tool_message_event' 

146 event = ToolManagerMessageEvent(s, sender, message) 

147 self._callbacks.process(s, event) 

148 

149 @property 

150 def active_toggle(self): 

151 """Currently toggled tools.""" 

152 return self._toggled 

153 

154 def get_tool_keymap(self, name): 

155 """ 

156 Return the keymap associated with the specified tool. 

157 

158 Parameters 

159 ---------- 

160 name : str 

161 Name of the Tool. 

162 

163 Returns 

164 ------- 

165 list of str 

166 List of keys associated with the tool. 

167 """ 

168 

169 keys = [k for k, i in self._keys.items() if i == name] 

170 return keys 

171 

172 def _remove_keys(self, name): 

173 for k in self.get_tool_keymap(name): 

174 del self._keys[k] 

175 

176 def update_keymap(self, name, key): 

177 """ 

178 Set the keymap to associate with the specified tool. 

179 

180 Parameters 

181 ---------- 

182 name : str 

183 Name of the Tool. 

184 key : str or list of str 

185 Keys to associate with the tool. 

186 """ 

187 if name not in self._tools: 

188 raise KeyError(f'{name!r} not in Tools') 

189 self._remove_keys(name) 

190 if isinstance(key, str): 

191 key = [key] 

192 for k in key: 

193 if k in self._keys: 

194 _api.warn_external( 

195 f'Key {k} changed from {self._keys[k]} to {name}') 

196 self._keys[k] = name 

197 

198 def remove_tool(self, name): 

199 """ 

200 Remove tool named *name*. 

201 

202 Parameters 

203 ---------- 

204 name : str 

205 Name of the tool. 

206 """ 

207 

208 tool = self.get_tool(name) 

209 destroy = _api.deprecate_method_override( 

210 backend_tools.ToolBase.destroy, tool, since="3.6", 

211 alternative="tool_removed_event") 

212 if destroy is not None: 

213 destroy() 

214 

215 # If it's a toggle tool and toggled, untoggle 

216 if getattr(tool, 'toggled', False): 

217 self.trigger_tool(tool, 'toolmanager') 

218 

219 self._remove_keys(name) 

220 

221 event = ToolEvent('tool_removed_event', self, tool) 

222 self._callbacks.process(event.name, event) 

223 

224 del self._tools[name] 

225 

226 def add_tool(self, name, tool, *args, **kwargs): 

227 """ 

228 Add *tool* to `ToolManager`. 

229 

230 If successful, adds a new event ``tool_trigger_{name}`` where 

231 ``{name}`` is the *name* of the tool; the event is fired every time the 

232 tool is triggered. 

233 

234 Parameters 

235 ---------- 

236 name : str 

237 Name of the tool, treated as the ID, has to be unique. 

238 tool : type 

239 Class of the tool to be added. A subclass will be used 

240 instead if one was registered for the current canvas class. 

241 

242 Notes 

243 ----- 

244 args and kwargs get passed directly to the tools constructor. 

245 

246 See Also 

247 -------- 

248 matplotlib.backend_tools.ToolBase : The base class for tools. 

249 """ 

250 

251 tool_cls = backend_tools._find_tool_class(type(self.canvas), tool) 

252 if not tool_cls: 

253 raise ValueError('Impossible to find class for %s' % str(tool)) 

254 

255 if name in self._tools: 

256 _api.warn_external('A "Tool class" with the same name already ' 

257 'exists, not added') 

258 return self._tools[name] 

259 

260 if name == 'cursor' and tool_cls != backend_tools.SetCursorBase: 

261 _api.warn_deprecated("3.5", 

262 message="Overriding ToolSetCursor with " 

263 f"{tool_cls.__qualname__} was only " 

264 "necessary to provide the .set_cursor() " 

265 "method, which is deprecated since " 

266 "%(since)s and will be removed " 

267 "%(removal)s. Please report this to the " 

268 f"{tool_cls.__module__} author.") 

269 

270 tool_obj = tool_cls(self, name, *args, **kwargs) 

271 self._tools[name] = tool_obj 

272 

273 if tool_obj.default_keymap is not None: 

274 self.update_keymap(name, tool_obj.default_keymap) 

275 

276 # For toggle tools init the radio_group in self._toggled 

277 if isinstance(tool_obj, backend_tools.ToolToggleBase): 

278 # None group is not mutually exclusive, a set is used to keep track 

279 # of all toggled tools in this group 

280 if tool_obj.radio_group is None: 

281 self._toggled.setdefault(None, set()) 

282 else: 

283 self._toggled.setdefault(tool_obj.radio_group, None) 

284 

285 # If initially toggled 

286 if tool_obj.toggled: 

287 self._handle_toggle(tool_obj, None, None) 

288 tool_obj.set_figure(self.figure) 

289 

290 event = ToolEvent('tool_added_event', self, tool_obj) 

291 self._callbacks.process(event.name, event) 

292 

293 return tool_obj 

294 

295 def _handle_toggle(self, tool, canvasevent, data): 

296 """ 

297 Toggle tools, need to untoggle prior to using other Toggle tool. 

298 Called from trigger_tool. 

299 

300 Parameters 

301 ---------- 

302 tool : `.ToolBase` 

303 canvasevent : Event 

304 Original Canvas event or None. 

305 data : object 

306 Extra data to pass to the tool when triggering. 

307 """ 

308 

309 radio_group = tool.radio_group 

310 # radio_group None is not mutually exclusive 

311 # just keep track of toggled tools in this group 

312 if radio_group is None: 

313 if tool.name in self._toggled[None]: 

314 self._toggled[None].remove(tool.name) 

315 else: 

316 self._toggled[None].add(tool.name) 

317 return 

318 

319 # If the tool already has a toggled state, untoggle it 

320 if self._toggled[radio_group] == tool.name: 

321 toggled = None 

322 # If no tool was toggled in the radio_group 

323 # toggle it 

324 elif self._toggled[radio_group] is None: 

325 toggled = tool.name 

326 # Other tool in the radio_group is toggled 

327 else: 

328 # Untoggle previously toggled tool 

329 self.trigger_tool(self._toggled[radio_group], 

330 self, 

331 canvasevent, 

332 data) 

333 toggled = tool.name 

334 

335 # Keep track of the toggled tool in the radio_group 

336 self._toggled[radio_group] = toggled 

337 

338 def trigger_tool(self, name, sender=None, canvasevent=None, data=None): 

339 """ 

340 Trigger a tool and emit the ``tool_trigger_{name}`` event. 

341 

342 Parameters 

343 ---------- 

344 name : str 

345 Name of the tool. 

346 sender : object 

347 Object that wishes to trigger the tool. 

348 canvasevent : Event 

349 Original Canvas event or None. 

350 data : object 

351 Extra data to pass to the tool when triggering. 

352 """ 

353 tool = self.get_tool(name) 

354 if tool is None: 

355 return 

356 

357 if sender is None: 

358 sender = self 

359 

360 if isinstance(tool, backend_tools.ToolToggleBase): 

361 self._handle_toggle(tool, canvasevent, data) 

362 

363 tool.trigger(sender, canvasevent, data) # Actually trigger Tool. 

364 

365 s = 'tool_trigger_%s' % name 

366 event = ToolTriggerEvent(s, sender, tool, canvasevent, data) 

367 self._callbacks.process(s, event) 

368 

369 def _key_press(self, event): 

370 if event.key is None or self.keypresslock.locked(): 

371 return 

372 

373 name = self._keys.get(event.key, None) 

374 if name is None: 

375 return 

376 self.trigger_tool(name, canvasevent=event) 

377 

378 @property 

379 def tools(self): 

380 """A dict mapping tool name -> controlled tool.""" 

381 return self._tools 

382 

383 def get_tool(self, name, warn=True): 

384 """ 

385 Return the tool object with the given name. 

386 

387 For convenience, this passes tool objects through. 

388 

389 Parameters 

390 ---------- 

391 name : str or `.ToolBase` 

392 Name of the tool, or the tool itself. 

393 warn : bool, default: True 

394 Whether a warning should be emitted it no tool with the given name 

395 exists. 

396 

397 Returns 

398 ------- 

399 `.ToolBase` or None 

400 The tool or None if no tool with the given name exists. 

401 """ 

402 if (isinstance(name, backend_tools.ToolBase) 

403 and name.name in self._tools): 

404 return name 

405 if name not in self._tools: 

406 if warn: 

407 _api.warn_external( 

408 f"ToolManager does not control tool {name!r}") 

409 return None 

410 return self._tools[name]