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
« 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
6from typing import Generator, Iterable, Union
8from prompt_toolkit.buffer import Buffer
10from .containers import (
11 AnyContainer,
12 ConditionalContainer,
13 Container,
14 Window,
15 to_container,
16)
17from .controls import BufferControl, SearchBufferControl, UIControl
19__all__ = [
20 "Layout",
21 "InvalidLayoutError",
22 "walk",
23]
25FocusableElement = Union[str, Buffer, UIControl, AnyContainer]
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.
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 """
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] = []
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] = {}
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] = {}
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)
70 # List of visible windows.
71 self.visible_windows: list[Window] = [] # List of `Window` objects.
73 def __repr__(self) -> str:
74 return f"Layout({self.container!r}, current_window={self.current_window!r})"
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
84 def find_all_controls(self) -> Iterable[UIControl]:
85 for container in self.find_all_windows():
86 yield container.content
88 def focus(self, value: FocusableElement) -> None:
89 """
90 Focus the given UI element.
92 `value` can be either:
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}.")
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}.")
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.")
126 self.current_control = value
128 # Otherwise, expecting any Container object.
129 else:
130 value = to_container(value)
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 )
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)
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
157 # None was focused before: take the very first focusable window.
158 if windows:
159 self.current_window = windows[0]
160 return
162 raise ValueError(
163 f"Invalid value. Container cannot be focused: {value!r}"
164 )
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
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
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
208 raise ValueError("Control not found in the user interface.")
210 @property
211 def current_window(self) -> Window:
212 "Return the :class:`.Window` object that is currently focused."
213 return self._stack[-1]
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)
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
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
234 if isinstance(control, SearchBufferControl):
235 return self.search_links.get(control)
236 else:
237 return None
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
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]
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
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
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)
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
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]
305 def focus_next(self) -> None:
306 """
307 Focus the next visible/focusable Window.
308 """
309 windows = self.get_visible_focusable_windows()
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)
319 self.focus(windows[index])
321 def focus_previous(self) -> None:
322 """
323 Focus the previous visible/focusable Window.
324 """
325 windows = self.get_visible_focusable_windows()
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)
335 self.focus(windows[index])
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)
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]
354 yield from walk(root)
356 def update_parents_relations(self) -> None:
357 """
358 Update child->parent relationships mapping.
359 """
360 parents = {}
362 def walk(e: Container) -> None:
363 for c in e.get_children():
364 parents[c] = e
365 walk(c)
367 walk(self.container)
369 self._child_to_parent = parents
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()
378 self.container.reset()
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
391class InvalidLayoutError(Exception):
392 pass
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
407 yield container
409 for c in container.get_children():
410 # yield from walk(c)
411 yield from walk(c, skip_hidden=skip_hidden)