summaryrefslogtreecommitdiffstats
path: root/src/prompt_toolkit/layout/layout.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/prompt_toolkit/layout/layout.py')
-rw-r--r--src/prompt_toolkit/layout/layout.py411
1 files changed, 411 insertions, 0 deletions
diff --git a/src/prompt_toolkit/layout/layout.py b/src/prompt_toolkit/layout/layout.py
new file mode 100644
index 0000000..a5e7a80
--- /dev/null
+++ b/src/prompt_toolkit/layout/layout.py
@@ -0,0 +1,411 @@
+"""
+Wrapper for the layout.
+"""
+from __future__ import annotations
+
+from typing import Generator, Iterable, Union
+
+from prompt_toolkit.buffer import Buffer
+
+from .containers import (
+ AnyContainer,
+ ConditionalContainer,
+ Container,
+ Window,
+ to_container,
+)
+from .controls import BufferControl, SearchBufferControl, UIControl
+
+__all__ = [
+ "Layout",
+ "InvalidLayoutError",
+ "walk",
+]
+
+FocusableElement = Union[str, Buffer, UIControl, AnyContainer]
+
+
+class Layout:
+ """
+ The layout for a prompt_toolkit
+ :class:`~prompt_toolkit.application.Application`.
+ This also keeps track of which user control is focused.
+
+ :param container: The "root" container for the layout.
+ :param focused_element: element to be focused initially. (Can be anything
+ the `focus` function accepts.)
+ """
+
+ def __init__(
+ self,
+ container: AnyContainer,
+ focused_element: FocusableElement | None = None,
+ ) -> None:
+ self.container = to_container(container)
+ self._stack: list[Window] = []
+
+ # Map search BufferControl back to the original BufferControl.
+ # This is used to keep track of when exactly we are searching, and for
+ # applying the search.
+ # When a link exists in this dictionary, that means the search is
+ # currently active.
+ # Map: search_buffer_control -> original buffer control.
+ self.search_links: dict[SearchBufferControl, BufferControl] = {}
+
+ # Mapping that maps the children in the layout to their parent.
+ # This relationship is calculated dynamically, each time when the UI
+ # is rendered. (UI elements have only references to their children.)
+ self._child_to_parent: dict[Container, Container] = {}
+
+ if focused_element is None:
+ try:
+ self._stack.append(next(self.find_all_windows()))
+ except StopIteration as e:
+ raise InvalidLayoutError(
+ "Invalid layout. The layout does not contain any Window object."
+ ) from e
+ else:
+ self.focus(focused_element)
+
+ # List of visible windows.
+ self.visible_windows: list[Window] = [] # List of `Window` objects.
+
+ def __repr__(self) -> str:
+ return f"Layout({self.container!r}, current_window={self.current_window!r})"
+
+ def find_all_windows(self) -> Generator[Window, None, None]:
+ """
+ Find all the :class:`.UIControl` objects in this layout.
+ """
+ for item in self.walk():
+ if isinstance(item, Window):
+ yield item
+
+ def find_all_controls(self) -> Iterable[UIControl]:
+ for container in self.find_all_windows():
+ yield container.content
+
+ def focus(self, value: FocusableElement) -> None:
+ """
+ Focus the given UI element.
+
+ `value` can be either:
+
+ - a :class:`.UIControl`
+ - a :class:`.Buffer` instance or the name of a :class:`.Buffer`
+ - a :class:`.Window`
+ - Any container object. In this case we will focus the :class:`.Window`
+ from this container that was focused most recent, or the very first
+ focusable :class:`.Window` of the container.
+ """
+ # BufferControl by buffer name.
+ if isinstance(value, str):
+ for control in self.find_all_controls():
+ if isinstance(control, BufferControl) and control.buffer.name == value:
+ self.focus(control)
+ return
+ raise ValueError(f"Couldn't find Buffer in the current layout: {value!r}.")
+
+ # BufferControl by buffer object.
+ elif isinstance(value, Buffer):
+ for control in self.find_all_controls():
+ if isinstance(control, BufferControl) and control.buffer == value:
+ self.focus(control)
+ return
+ raise ValueError(f"Couldn't find Buffer in the current layout: {value!r}.")
+
+ # Focus UIControl.
+ elif isinstance(value, UIControl):
+ if value not in self.find_all_controls():
+ raise ValueError(
+ "Invalid value. Container does not appear in the layout."
+ )
+ if not value.is_focusable():
+ raise ValueError("Invalid value. UIControl is not focusable.")
+
+ self.current_control = value
+
+ # Otherwise, expecting any Container object.
+ else:
+ value = to_container(value)
+
+ if isinstance(value, Window):
+ # This is a `Window`: focus that.
+ if value not in self.find_all_windows():
+ raise ValueError(
+ f"Invalid value. Window does not appear in the layout: {value!r}"
+ )
+
+ self.current_window = value
+ else:
+ # Focus a window in this container.
+ # If we have many windows as part of this container, and some
+ # of them have been focused before, take the last focused
+ # item. (This is very useful when the UI is composed of more
+ # complex sub components.)
+ windows = []
+ for c in walk(value, skip_hidden=True):
+ if isinstance(c, Window) and c.content.is_focusable():
+ windows.append(c)
+
+ # Take the first one that was focused before.
+ for w in reversed(self._stack):
+ if w in windows:
+ self.current_window = w
+ return
+
+ # None was focused before: take the very first focusable window.
+ if windows:
+ self.current_window = windows[0]
+ return
+
+ raise ValueError(
+ f"Invalid value. Container cannot be focused: {value!r}"
+ )
+
+ def has_focus(self, value: FocusableElement) -> bool:
+ """
+ Check whether the given control has the focus.
+ :param value: :class:`.UIControl` or :class:`.Window` instance.
+ """
+ if isinstance(value, str):
+ if self.current_buffer is None:
+ return False
+ return self.current_buffer.name == value
+ if isinstance(value, Buffer):
+ return self.current_buffer == value
+ if isinstance(value, UIControl):
+ return self.current_control == value
+ else:
+ value = to_container(value)
+ if isinstance(value, Window):
+ return self.current_window == value
+ else:
+ # Check whether this "container" is focused. This is true if
+ # one of the elements inside is focused.
+ for element in walk(value):
+ if element == self.current_window:
+ return True
+ return False
+
+ @property
+ def current_control(self) -> UIControl:
+ """
+ Get the :class:`.UIControl` to currently has the focus.
+ """
+ return self._stack[-1].content
+
+ @current_control.setter
+ def current_control(self, control: UIControl) -> None:
+ """
+ Set the :class:`.UIControl` to receive the focus.
+ """
+ for window in self.find_all_windows():
+ if window.content == control:
+ self.current_window = window
+ return
+
+ raise ValueError("Control not found in the user interface.")
+
+ @property
+ def current_window(self) -> Window:
+ "Return the :class:`.Window` object that is currently focused."
+ return self._stack[-1]
+
+ @current_window.setter
+ def current_window(self, value: Window) -> None:
+ "Set the :class:`.Window` object to be currently focused."
+ self._stack.append(value)
+
+ @property
+ def is_searching(self) -> bool:
+ "True if we are searching right now."
+ return self.current_control in self.search_links
+
+ @property
+ def search_target_buffer_control(self) -> BufferControl | None:
+ """
+ Return the :class:`.BufferControl` in which we are searching or `None`.
+ """
+ # Not every `UIControl` is a `BufferControl`. This only applies to
+ # `BufferControl`.
+ control = self.current_control
+
+ if isinstance(control, SearchBufferControl):
+ return self.search_links.get(control)
+ else:
+ return None
+
+ def get_focusable_windows(self) -> Iterable[Window]:
+ """
+ Return all the :class:`.Window` objects which are focusable (in the
+ 'modal' area).
+ """
+ for w in self.walk_through_modal_area():
+ if isinstance(w, Window) and w.content.is_focusable():
+ yield w
+
+ def get_visible_focusable_windows(self) -> list[Window]:
+ """
+ Return a list of :class:`.Window` objects that are focusable.
+ """
+ # focusable windows are windows that are visible, but also part of the
+ # modal container. Make sure to keep the ordering.
+ visible_windows = self.visible_windows
+ return [w for w in self.get_focusable_windows() if w in visible_windows]
+
+ @property
+ def current_buffer(self) -> Buffer | None:
+ """
+ The currently focused :class:`~.Buffer` or `None`.
+ """
+ ui_control = self.current_control
+ if isinstance(ui_control, BufferControl):
+ return ui_control.buffer
+ return None
+
+ def get_buffer_by_name(self, buffer_name: str) -> Buffer | None:
+ """
+ Look in the layout for a buffer with the given name.
+ Return `None` when nothing was found.
+ """
+ for w in self.walk():
+ if isinstance(w, Window) and isinstance(w.content, BufferControl):
+ if w.content.buffer.name == buffer_name:
+ return w.content.buffer
+ return None
+
+ @property
+ def buffer_has_focus(self) -> bool:
+ """
+ Return `True` if the currently focused control is a
+ :class:`.BufferControl`. (For instance, used to determine whether the
+ default key bindings should be active or not.)
+ """
+ ui_control = self.current_control
+ return isinstance(ui_control, BufferControl)
+
+ @property
+ def previous_control(self) -> UIControl:
+ """
+ Get the :class:`.UIControl` to previously had the focus.
+ """
+ try:
+ return self._stack[-2].content
+ except IndexError:
+ return self._stack[-1].content
+
+ def focus_last(self) -> None:
+ """
+ Give the focus to the last focused control.
+ """
+ if len(self._stack) > 1:
+ self._stack = self._stack[:-1]
+
+ def focus_next(self) -> None:
+ """
+ Focus the next visible/focusable Window.
+ """
+ windows = self.get_visible_focusable_windows()
+
+ if len(windows) > 0:
+ try:
+ index = windows.index(self.current_window)
+ except ValueError:
+ index = 0
+ else:
+ index = (index + 1) % len(windows)
+
+ self.focus(windows[index])
+
+ def focus_previous(self) -> None:
+ """
+ Focus the previous visible/focusable Window.
+ """
+ windows = self.get_visible_focusable_windows()
+
+ if len(windows) > 0:
+ try:
+ index = windows.index(self.current_window)
+ except ValueError:
+ index = 0
+ else:
+ index = (index - 1) % len(windows)
+
+ self.focus(windows[index])
+
+ def walk(self) -> Iterable[Container]:
+ """
+ Walk through all the layout nodes (and their children) and yield them.
+ """
+ yield from walk(self.container)
+
+ def walk_through_modal_area(self) -> Iterable[Container]:
+ """
+ Walk through all the containers which are in the current 'modal' part
+ of the layout.
+ """
+ # Go up in the tree, and find the root. (it will be a part of the
+ # layout, if the focus is in a modal part.)
+ root: Container = self.current_window
+ while not root.is_modal() and root in self._child_to_parent:
+ root = self._child_to_parent[root]
+
+ yield from walk(root)
+
+ def update_parents_relations(self) -> None:
+ """
+ Update child->parent relationships mapping.
+ """
+ parents = {}
+
+ def walk(e: Container) -> None:
+ for c in e.get_children():
+ parents[c] = e
+ walk(c)
+
+ walk(self.container)
+
+ self._child_to_parent = parents
+
+ def reset(self) -> None:
+ # Remove all search links when the UI starts.
+ # (Important, for instance when control-c is been pressed while
+ # searching. The prompt cancels, but next `run()` call the search
+ # links are still there.)
+ self.search_links.clear()
+
+ self.container.reset()
+
+ def get_parent(self, container: Container) -> Container | None:
+ """
+ Return the parent container for the given container, or ``None``, if it
+ wasn't found.
+ """
+ try:
+ return self._child_to_parent[container]
+ except KeyError:
+ return None
+
+
+class InvalidLayoutError(Exception):
+ pass
+
+
+def walk(container: Container, skip_hidden: bool = False) -> Iterable[Container]:
+ """
+ Walk through layout, starting at this container.
+ """
+ # When `skip_hidden` is set, don't go into disabled ConditionalContainer containers.
+ if (
+ skip_hidden
+ and isinstance(container, ConditionalContainer)
+ and not container.filter()
+ ):
+ return
+
+ yield container
+
+ for c in container.get_children():
+ # yield from walk(c)
+ yield from walk(c, skip_hidden=skip_hidden)