layout.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412
  1. """
  2. Wrapper for the layout.
  3. """
  4. from __future__ import annotations
  5. from typing import Generator, Iterable, Union
  6. from prompt_toolkit.buffer import Buffer
  7. from .containers import (
  8. AnyContainer,
  9. ConditionalContainer,
  10. Container,
  11. Window,
  12. to_container,
  13. )
  14. from .controls import BufferControl, SearchBufferControl, UIControl
  15. __all__ = [
  16. "Layout",
  17. "InvalidLayoutError",
  18. "walk",
  19. ]
  20. FocusableElement = Union[str, Buffer, UIControl, AnyContainer]
  21. class Layout:
  22. """
  23. The layout for a prompt_toolkit
  24. :class:`~prompt_toolkit.application.Application`.
  25. This also keeps track of which user control is focused.
  26. :param container: The "root" container for the layout.
  27. :param focused_element: element to be focused initially. (Can be anything
  28. the `focus` function accepts.)
  29. """
  30. def __init__(
  31. self,
  32. container: AnyContainer,
  33. focused_element: FocusableElement | None = None,
  34. ) -> None:
  35. self.container = to_container(container)
  36. self._stack: list[Window] = []
  37. # Map search BufferControl back to the original BufferControl.
  38. # This is used to keep track of when exactly we are searching, and for
  39. # applying the search.
  40. # When a link exists in this dictionary, that means the search is
  41. # currently active.
  42. # Map: search_buffer_control -> original buffer control.
  43. self.search_links: dict[SearchBufferControl, BufferControl] = {}
  44. # Mapping that maps the children in the layout to their parent.
  45. # This relationship is calculated dynamically, each time when the UI
  46. # is rendered. (UI elements have only references to their children.)
  47. self._child_to_parent: dict[Container, Container] = {}
  48. if focused_element is None:
  49. try:
  50. self._stack.append(next(self.find_all_windows()))
  51. except StopIteration as e:
  52. raise InvalidLayoutError(
  53. "Invalid layout. The layout does not contain any Window object."
  54. ) from e
  55. else:
  56. self.focus(focused_element)
  57. # List of visible windows.
  58. self.visible_windows: list[Window] = [] # List of `Window` objects.
  59. def __repr__(self) -> str:
  60. return f"Layout({self.container!r}, current_window={self.current_window!r})"
  61. def find_all_windows(self) -> Generator[Window, None, None]:
  62. """
  63. Find all the :class:`.UIControl` objects in this layout.
  64. """
  65. for item in self.walk():
  66. if isinstance(item, Window):
  67. yield item
  68. def find_all_controls(self) -> Iterable[UIControl]:
  69. for container in self.find_all_windows():
  70. yield container.content
  71. def focus(self, value: FocusableElement) -> None:
  72. """
  73. Focus the given UI element.
  74. `value` can be either:
  75. - a :class:`.UIControl`
  76. - a :class:`.Buffer` instance or the name of a :class:`.Buffer`
  77. - a :class:`.Window`
  78. - Any container object. In this case we will focus the :class:`.Window`
  79. from this container that was focused most recent, or the very first
  80. focusable :class:`.Window` of the container.
  81. """
  82. # BufferControl by buffer name.
  83. if isinstance(value, str):
  84. for control in self.find_all_controls():
  85. if isinstance(control, BufferControl) and control.buffer.name == value:
  86. self.focus(control)
  87. return
  88. raise ValueError(f"Couldn't find Buffer in the current layout: {value!r}.")
  89. # BufferControl by buffer object.
  90. elif isinstance(value, Buffer):
  91. for control in self.find_all_controls():
  92. if isinstance(control, BufferControl) and control.buffer == value:
  93. self.focus(control)
  94. return
  95. raise ValueError(f"Couldn't find Buffer in the current layout: {value!r}.")
  96. # Focus UIControl.
  97. elif isinstance(value, UIControl):
  98. if value not in self.find_all_controls():
  99. raise ValueError(
  100. "Invalid value. Container does not appear in the layout."
  101. )
  102. if not value.is_focusable():
  103. raise ValueError("Invalid value. UIControl is not focusable.")
  104. self.current_control = value
  105. # Otherwise, expecting any Container object.
  106. else:
  107. value = to_container(value)
  108. if isinstance(value, Window):
  109. # This is a `Window`: focus that.
  110. if value not in self.find_all_windows():
  111. raise ValueError(
  112. f"Invalid value. Window does not appear in the layout: {value!r}"
  113. )
  114. self.current_window = value
  115. else:
  116. # Focus a window in this container.
  117. # If we have many windows as part of this container, and some
  118. # of them have been focused before, take the last focused
  119. # item. (This is very useful when the UI is composed of more
  120. # complex sub components.)
  121. windows = []
  122. for c in walk(value, skip_hidden=True):
  123. if isinstance(c, Window) and c.content.is_focusable():
  124. windows.append(c)
  125. # Take the first one that was focused before.
  126. for w in reversed(self._stack):
  127. if w in windows:
  128. self.current_window = w
  129. return
  130. # None was focused before: take the very first focusable window.
  131. if windows:
  132. self.current_window = windows[0]
  133. return
  134. raise ValueError(
  135. f"Invalid value. Container cannot be focused: {value!r}"
  136. )
  137. def has_focus(self, value: FocusableElement) -> bool:
  138. """
  139. Check whether the given control has the focus.
  140. :param value: :class:`.UIControl` or :class:`.Window` instance.
  141. """
  142. if isinstance(value, str):
  143. if self.current_buffer is None:
  144. return False
  145. return self.current_buffer.name == value
  146. if isinstance(value, Buffer):
  147. return self.current_buffer == value
  148. if isinstance(value, UIControl):
  149. return self.current_control == value
  150. else:
  151. value = to_container(value)
  152. if isinstance(value, Window):
  153. return self.current_window == value
  154. else:
  155. # Check whether this "container" is focused. This is true if
  156. # one of the elements inside is focused.
  157. for element in walk(value):
  158. if element == self.current_window:
  159. return True
  160. return False
  161. @property
  162. def current_control(self) -> UIControl:
  163. """
  164. Get the :class:`.UIControl` to currently has the focus.
  165. """
  166. return self._stack[-1].content
  167. @current_control.setter
  168. def current_control(self, control: UIControl) -> None:
  169. """
  170. Set the :class:`.UIControl` to receive the focus.
  171. """
  172. for window in self.find_all_windows():
  173. if window.content == control:
  174. self.current_window = window
  175. return
  176. raise ValueError("Control not found in the user interface.")
  177. @property
  178. def current_window(self) -> Window:
  179. "Return the :class:`.Window` object that is currently focused."
  180. return self._stack[-1]
  181. @current_window.setter
  182. def current_window(self, value: Window) -> None:
  183. "Set the :class:`.Window` object to be currently focused."
  184. self._stack.append(value)
  185. @property
  186. def is_searching(self) -> bool:
  187. "True if we are searching right now."
  188. return self.current_control in self.search_links
  189. @property
  190. def search_target_buffer_control(self) -> BufferControl | None:
  191. """
  192. Return the :class:`.BufferControl` in which we are searching or `None`.
  193. """
  194. # Not every `UIControl` is a `BufferControl`. This only applies to
  195. # `BufferControl`.
  196. control = self.current_control
  197. if isinstance(control, SearchBufferControl):
  198. return self.search_links.get(control)
  199. else:
  200. return None
  201. def get_focusable_windows(self) -> Iterable[Window]:
  202. """
  203. Return all the :class:`.Window` objects which are focusable (in the
  204. 'modal' area).
  205. """
  206. for w in self.walk_through_modal_area():
  207. if isinstance(w, Window) and w.content.is_focusable():
  208. yield w
  209. def get_visible_focusable_windows(self) -> list[Window]:
  210. """
  211. Return a list of :class:`.Window` objects that are focusable.
  212. """
  213. # focusable windows are windows that are visible, but also part of the
  214. # modal container. Make sure to keep the ordering.
  215. visible_windows = self.visible_windows
  216. return [w for w in self.get_focusable_windows() if w in visible_windows]
  217. @property
  218. def current_buffer(self) -> Buffer | None:
  219. """
  220. The currently focused :class:`~.Buffer` or `None`.
  221. """
  222. ui_control = self.current_control
  223. if isinstance(ui_control, BufferControl):
  224. return ui_control.buffer
  225. return None
  226. def get_buffer_by_name(self, buffer_name: str) -> Buffer | None:
  227. """
  228. Look in the layout for a buffer with the given name.
  229. Return `None` when nothing was found.
  230. """
  231. for w in self.walk():
  232. if isinstance(w, Window) and isinstance(w.content, BufferControl):
  233. if w.content.buffer.name == buffer_name:
  234. return w.content.buffer
  235. return None
  236. @property
  237. def buffer_has_focus(self) -> bool:
  238. """
  239. Return `True` if the currently focused control is a
  240. :class:`.BufferControl`. (For instance, used to determine whether the
  241. default key bindings should be active or not.)
  242. """
  243. ui_control = self.current_control
  244. return isinstance(ui_control, BufferControl)
  245. @property
  246. def previous_control(self) -> UIControl:
  247. """
  248. Get the :class:`.UIControl` to previously had the focus.
  249. """
  250. try:
  251. return self._stack[-2].content
  252. except IndexError:
  253. return self._stack[-1].content
  254. def focus_last(self) -> None:
  255. """
  256. Give the focus to the last focused control.
  257. """
  258. if len(self._stack) > 1:
  259. self._stack = self._stack[:-1]
  260. def focus_next(self) -> None:
  261. """
  262. Focus the next visible/focusable Window.
  263. """
  264. windows = self.get_visible_focusable_windows()
  265. if len(windows) > 0:
  266. try:
  267. index = windows.index(self.current_window)
  268. except ValueError:
  269. index = 0
  270. else:
  271. index = (index + 1) % len(windows)
  272. self.focus(windows[index])
  273. def focus_previous(self) -> None:
  274. """
  275. Focus the previous visible/focusable Window.
  276. """
  277. windows = self.get_visible_focusable_windows()
  278. if len(windows) > 0:
  279. try:
  280. index = windows.index(self.current_window)
  281. except ValueError:
  282. index = 0
  283. else:
  284. index = (index - 1) % len(windows)
  285. self.focus(windows[index])
  286. def walk(self) -> Iterable[Container]:
  287. """
  288. Walk through all the layout nodes (and their children) and yield them.
  289. """
  290. yield from walk(self.container)
  291. def walk_through_modal_area(self) -> Iterable[Container]:
  292. """
  293. Walk through all the containers which are in the current 'modal' part
  294. of the layout.
  295. """
  296. # Go up in the tree, and find the root. (it will be a part of the
  297. # layout, if the focus is in a modal part.)
  298. root: Container = self.current_window
  299. while not root.is_modal() and root in self._child_to_parent:
  300. root = self._child_to_parent[root]
  301. yield from walk(root)
  302. def update_parents_relations(self) -> None:
  303. """
  304. Update child->parent relationships mapping.
  305. """
  306. parents = {}
  307. def walk(e: Container) -> None:
  308. for c in e.get_children():
  309. parents[c] = e
  310. walk(c)
  311. walk(self.container)
  312. self._child_to_parent = parents
  313. def reset(self) -> None:
  314. # Remove all search links when the UI starts.
  315. # (Important, for instance when control-c is been pressed while
  316. # searching. The prompt cancels, but next `run()` call the search
  317. # links are still there.)
  318. self.search_links.clear()
  319. self.container.reset()
  320. def get_parent(self, container: Container) -> Container | None:
  321. """
  322. Return the parent container for the given container, or ``None``, if it
  323. wasn't found.
  324. """
  325. try:
  326. return self._child_to_parent[container]
  327. except KeyError:
  328. return None
  329. class InvalidLayoutError(Exception):
  330. pass
  331. def walk(container: Container, skip_hidden: bool = False) -> Iterable[Container]:
  332. """
  333. Walk through layout, starting at this container.
  334. """
  335. # When `skip_hidden` is set, don't go into disabled ConditionalContainer containers.
  336. if (
  337. skip_hidden
  338. and isinstance(container, ConditionalContainer)
  339. and not container.filter()
  340. ):
  341. return
  342. yield container
  343. for c in container.get_children():
  344. # yield from walk(c)
  345. yield from walk(c, skip_hidden=skip_hidden)