Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/prompt_toolkit/layout/layout.py: 25%

187 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-04-20 06:09 +0000

1""" 

2Wrapper for the layout. 

3""" 

4from __future__ import annotations 

5 

6from typing import Generator, Iterable, Union 

7 

8from prompt_toolkit.buffer import Buffer 

9 

10from .containers import ( 

11 AnyContainer, 

12 ConditionalContainer, 

13 Container, 

14 Window, 

15 to_container, 

16) 

17from .controls import BufferControl, SearchBufferControl, UIControl 

18 

19__all__ = [ 

20 "Layout", 

21 "InvalidLayoutError", 

22 "walk", 

23] 

24 

25FocusableElement = Union[str, Buffer, UIControl, AnyContainer] 

26 

27 

28class Layout: 

29 """ 

30 The layout for a prompt_toolkit 

31 :class:`~prompt_toolkit.application.Application`. 

32 This also keeps track of which user control is focused. 

33 

34 :param container: The "root" container for the layout. 

35 :param focused_element: element to be focused initially. (Can be anything 

36 the `focus` function accepts.) 

37 """ 

38 

39 def __init__( 

40 self, 

41 container: AnyContainer, 

42 focused_element: FocusableElement | None = None, 

43 ) -> None: 

44 self.container = to_container(container) 

45 self._stack: list[Window] = [] 

46 

47 # Map search BufferControl back to the original BufferControl. 

48 # This is used to keep track of when exactly we are searching, and for 

49 # applying the search. 

50 # When a link exists in this dictionary, that means the search is 

51 # currently active. 

52 # Map: search_buffer_control -> original buffer control. 

53 self.search_links: dict[SearchBufferControl, BufferControl] = {} 

54 

55 # Mapping that maps the children in the layout to their parent. 

56 # This relationship is calculated dynamically, each time when the UI 

57 # is rendered. (UI elements have only references to their children.) 

58 self._child_to_parent: dict[Container, Container] = {} 

59 

60 if focused_element is None: 

61 try: 

62 self._stack.append(next(self.find_all_windows())) 

63 except StopIteration as e: 

64 raise InvalidLayoutError( 

65 "Invalid layout. The layout does not contain any Window object." 

66 ) from e 

67 else: 

68 self.focus(focused_element) 

69 

70 # List of visible windows. 

71 self.visible_windows: list[Window] = [] # List of `Window` objects. 

72 

73 def __repr__(self) -> str: 

74 return f"Layout({self.container!r}, current_window={self.current_window!r})" 

75 

76 def find_all_windows(self) -> Generator[Window, None, None]: 

77 """ 

78 Find all the :class:`.UIControl` objects in this layout. 

79 """ 

80 for item in self.walk(): 

81 if isinstance(item, Window): 

82 yield item 

83 

84 def find_all_controls(self) -> Iterable[UIControl]: 

85 for container in self.find_all_windows(): 

86 yield container.content 

87 

88 def focus(self, value: FocusableElement) -> None: 

89 """ 

90 Focus the given UI element. 

91 

92 `value` can be either: 

93 

94 - a :class:`.UIControl` 

95 - a :class:`.Buffer` instance or the name of a :class:`.Buffer` 

96 - a :class:`.Window` 

97 - Any container object. In this case we will focus the :class:`.Window` 

98 from this container that was focused most recent, or the very first 

99 focusable :class:`.Window` of the container. 

100 """ 

101 # BufferControl by buffer name. 

102 if isinstance(value, str): 

103 for control in self.find_all_controls(): 

104 if isinstance(control, BufferControl) and control.buffer.name == value: 

105 self.focus(control) 

106 return 

107 raise ValueError(f"Couldn't find Buffer in the current layout: {value!r}.") 

108 

109 # BufferControl by buffer object. 

110 elif isinstance(value, Buffer): 

111 for control in self.find_all_controls(): 

112 if isinstance(control, BufferControl) and control.buffer == value: 

113 self.focus(control) 

114 return 

115 raise ValueError(f"Couldn't find Buffer in the current layout: {value!r}.") 

116 

117 # Focus UIControl. 

118 elif isinstance(value, UIControl): 

119 if value not in self.find_all_controls(): 

120 raise ValueError( 

121 "Invalid value. Container does not appear in the layout." 

122 ) 

123 if not value.is_focusable(): 

124 raise ValueError("Invalid value. UIControl is not focusable.") 

125 

126 self.current_control = value 

127 

128 # Otherwise, expecting any Container object. 

129 else: 

130 value = to_container(value) 

131 

132 if isinstance(value, Window): 

133 # This is a `Window`: focus that. 

134 if value not in self.find_all_windows(): 

135 raise ValueError( 

136 f"Invalid value. Window does not appear in the layout: {value!r}" 

137 ) 

138 

139 self.current_window = value 

140 else: 

141 # Focus a window in this container. 

142 # If we have many windows as part of this container, and some 

143 # of them have been focused before, take the last focused 

144 # item. (This is very useful when the UI is composed of more 

145 # complex sub components.) 

146 windows = [] 

147 for c in walk(value, skip_hidden=True): 

148 if isinstance(c, Window) and c.content.is_focusable(): 

149 windows.append(c) 

150 

151 # Take the first one that was focused before. 

152 for w in reversed(self._stack): 

153 if w in windows: 

154 self.current_window = w 

155 return 

156 

157 # None was focused before: take the very first focusable window. 

158 if windows: 

159 self.current_window = windows[0] 

160 return 

161 

162 raise ValueError( 

163 f"Invalid value. Container cannot be focused: {value!r}" 

164 ) 

165 

166 def has_focus(self, value: FocusableElement) -> bool: 

167 """ 

168 Check whether the given control has the focus. 

169 :param value: :class:`.UIControl` or :class:`.Window` instance. 

170 """ 

171 if isinstance(value, str): 

172 if self.current_buffer is None: 

173 return False 

174 return self.current_buffer.name == value 

175 if isinstance(value, Buffer): 

176 return self.current_buffer == value 

177 if isinstance(value, UIControl): 

178 return self.current_control == value 

179 else: 

180 value = to_container(value) 

181 if isinstance(value, Window): 

182 return self.current_window == value 

183 else: 

184 # Check whether this "container" is focused. This is true if 

185 # one of the elements inside is focused. 

186 for element in walk(value): 

187 if element == self.current_window: 

188 return True 

189 return False 

190 

191 @property 

192 def current_control(self) -> UIControl: 

193 """ 

194 Get the :class:`.UIControl` to currently has the focus. 

195 """ 

196 return self._stack[-1].content 

197 

198 @current_control.setter 

199 def current_control(self, control: UIControl) -> None: 

200 """ 

201 Set the :class:`.UIControl` to receive the focus. 

202 """ 

203 for window in self.find_all_windows(): 

204 if window.content == control: 

205 self.current_window = window 

206 return 

207 

208 raise ValueError("Control not found in the user interface.") 

209 

210 @property 

211 def current_window(self) -> Window: 

212 "Return the :class:`.Window` object that is currently focused." 

213 return self._stack[-1] 

214 

215 @current_window.setter 

216 def current_window(self, value: Window) -> None: 

217 "Set the :class:`.Window` object to be currently focused." 

218 self._stack.append(value) 

219 

220 @property 

221 def is_searching(self) -> bool: 

222 "True if we are searching right now." 

223 return self.current_control in self.search_links 

224 

225 @property 

226 def search_target_buffer_control(self) -> BufferControl | None: 

227 """ 

228 Return the :class:`.BufferControl` in which we are searching or `None`. 

229 """ 

230 # Not every `UIControl` is a `BufferControl`. This only applies to 

231 # `BufferControl`. 

232 control = self.current_control 

233 

234 if isinstance(control, SearchBufferControl): 

235 return self.search_links.get(control) 

236 else: 

237 return None 

238 

239 def get_focusable_windows(self) -> Iterable[Window]: 

240 """ 

241 Return all the :class:`.Window` objects which are focusable (in the 

242 'modal' area). 

243 """ 

244 for w in self.walk_through_modal_area(): 

245 if isinstance(w, Window) and w.content.is_focusable(): 

246 yield w 

247 

248 def get_visible_focusable_windows(self) -> list[Window]: 

249 """ 

250 Return a list of :class:`.Window` objects that are focusable. 

251 """ 

252 # focusable windows are windows that are visible, but also part of the 

253 # modal container. Make sure to keep the ordering. 

254 visible_windows = self.visible_windows 

255 return [w for w in self.get_focusable_windows() if w in visible_windows] 

256 

257 @property 

258 def current_buffer(self) -> Buffer | None: 

259 """ 

260 The currently focused :class:`~.Buffer` or `None`. 

261 """ 

262 ui_control = self.current_control 

263 if isinstance(ui_control, BufferControl): 

264 return ui_control.buffer 

265 return None 

266 

267 def get_buffer_by_name(self, buffer_name: str) -> Buffer | None: 

268 """ 

269 Look in the layout for a buffer with the given name. 

270 Return `None` when nothing was found. 

271 """ 

272 for w in self.walk(): 

273 if isinstance(w, Window) and isinstance(w.content, BufferControl): 

274 if w.content.buffer.name == buffer_name: 

275 return w.content.buffer 

276 return None 

277 

278 @property 

279 def buffer_has_focus(self) -> bool: 

280 """ 

281 Return `True` if the currently focused control is a 

282 :class:`.BufferControl`. (For instance, used to determine whether the 

283 default key bindings should be active or not.) 

284 """ 

285 ui_control = self.current_control 

286 return isinstance(ui_control, BufferControl) 

287 

288 @property 

289 def previous_control(self) -> UIControl: 

290 """ 

291 Get the :class:`.UIControl` to previously had the focus. 

292 """ 

293 try: 

294 return self._stack[-2].content 

295 except IndexError: 

296 return self._stack[-1].content 

297 

298 def focus_last(self) -> None: 

299 """ 

300 Give the focus to the last focused control. 

301 """ 

302 if len(self._stack) > 1: 

303 self._stack = self._stack[:-1] 

304 

305 def focus_next(self) -> None: 

306 """ 

307 Focus the next visible/focusable Window. 

308 """ 

309 windows = self.get_visible_focusable_windows() 

310 

311 if len(windows) > 0: 

312 try: 

313 index = windows.index(self.current_window) 

314 except ValueError: 

315 index = 0 

316 else: 

317 index = (index + 1) % len(windows) 

318 

319 self.focus(windows[index]) 

320 

321 def focus_previous(self) -> None: 

322 """ 

323 Focus the previous visible/focusable Window. 

324 """ 

325 windows = self.get_visible_focusable_windows() 

326 

327 if len(windows) > 0: 

328 try: 

329 index = windows.index(self.current_window) 

330 except ValueError: 

331 index = 0 

332 else: 

333 index = (index - 1) % len(windows) 

334 

335 self.focus(windows[index]) 

336 

337 def walk(self) -> Iterable[Container]: 

338 """ 

339 Walk through all the layout nodes (and their children) and yield them. 

340 """ 

341 yield from walk(self.container) 

342 

343 def walk_through_modal_area(self) -> Iterable[Container]: 

344 """ 

345 Walk through all the containers which are in the current 'modal' part 

346 of the layout. 

347 """ 

348 # Go up in the tree, and find the root. (it will be a part of the 

349 # layout, if the focus is in a modal part.) 

350 root: Container = self.current_window 

351 while not root.is_modal() and root in self._child_to_parent: 

352 root = self._child_to_parent[root] 

353 

354 yield from walk(root) 

355 

356 def update_parents_relations(self) -> None: 

357 """ 

358 Update child->parent relationships mapping. 

359 """ 

360 parents = {} 

361 

362 def walk(e: Container) -> None: 

363 for c in e.get_children(): 

364 parents[c] = e 

365 walk(c) 

366 

367 walk(self.container) 

368 

369 self._child_to_parent = parents 

370 

371 def reset(self) -> None: 

372 # Remove all search links when the UI starts. 

373 # (Important, for instance when control-c is been pressed while 

374 # searching. The prompt cancels, but next `run()` call the search 

375 # links are still there.) 

376 self.search_links.clear() 

377 

378 self.container.reset() 

379 

380 def get_parent(self, container: Container) -> Container | None: 

381 """ 

382 Return the parent container for the given container, or ``None``, if it 

383 wasn't found. 

384 """ 

385 try: 

386 return self._child_to_parent[container] 

387 except KeyError: 

388 return None 

389 

390 

391class InvalidLayoutError(Exception): 

392 pass 

393 

394 

395def walk(container: Container, skip_hidden: bool = False) -> Iterable[Container]: 

396 """ 

397 Walk through layout, starting at this container. 

398 """ 

399 # When `skip_hidden` is set, don't go into disabled ConditionalContainer containers. 

400 if ( 

401 skip_hidden 

402 and isinstance(container, ConditionalContainer) 

403 and not container.filter() 

404 ): 

405 return 

406 

407 yield container 

408 

409 for c in container.get_children(): 

410 # yield from walk(c) 

411 yield from walk(c, skip_hidden=skip_hidden)