| | """ |
| | User interface Controls for the layout. |
| | """ |
| |
|
| | from __future__ import annotations |
| |
|
| | import time |
| | from abc import ABCMeta, abstractmethod |
| | from typing import TYPE_CHECKING, Callable, Hashable, Iterable, NamedTuple |
| |
|
| | from prompt_toolkit.application.current import get_app |
| | from prompt_toolkit.buffer import Buffer |
| | from prompt_toolkit.cache import SimpleCache |
| | from prompt_toolkit.data_structures import Point |
| | from prompt_toolkit.document import Document |
| | from prompt_toolkit.filters import FilterOrBool, to_filter |
| | from prompt_toolkit.formatted_text import ( |
| | AnyFormattedText, |
| | StyleAndTextTuples, |
| | to_formatted_text, |
| | ) |
| | from prompt_toolkit.formatted_text.utils import ( |
| | fragment_list_to_text, |
| | fragment_list_width, |
| | split_lines, |
| | ) |
| | from prompt_toolkit.lexers import Lexer, SimpleLexer |
| | from prompt_toolkit.mouse_events import MouseButton, MouseEvent, MouseEventType |
| | from prompt_toolkit.search import SearchState |
| | from prompt_toolkit.selection import SelectionType |
| | from prompt_toolkit.utils import get_cwidth |
| |
|
| | from .processors import ( |
| | DisplayMultipleCursors, |
| | HighlightIncrementalSearchProcessor, |
| | HighlightSearchProcessor, |
| | HighlightSelectionProcessor, |
| | Processor, |
| | TransformationInput, |
| | merge_processors, |
| | ) |
| |
|
| | if TYPE_CHECKING: |
| | from prompt_toolkit.key_binding.key_bindings import ( |
| | KeyBindingsBase, |
| | NotImplementedOrNone, |
| | ) |
| | from prompt_toolkit.utils import Event |
| |
|
| |
|
| | __all__ = [ |
| | "BufferControl", |
| | "SearchBufferControl", |
| | "DummyControl", |
| | "FormattedTextControl", |
| | "UIControl", |
| | "UIContent", |
| | ] |
| |
|
| | GetLinePrefixCallable = Callable[[int, int], AnyFormattedText] |
| |
|
| |
|
| | class UIControl(metaclass=ABCMeta): |
| | """ |
| | Base class for all user interface controls. |
| | """ |
| |
|
| | def reset(self) -> None: |
| | |
| | pass |
| |
|
| | def preferred_width(self, max_available_width: int) -> int | None: |
| | return None |
| |
|
| | def preferred_height( |
| | self, |
| | width: int, |
| | max_available_height: int, |
| | wrap_lines: bool, |
| | get_line_prefix: GetLinePrefixCallable | None, |
| | ) -> int | None: |
| | return None |
| |
|
| | def is_focusable(self) -> bool: |
| | """ |
| | Tell whether this user control is focusable. |
| | """ |
| | return False |
| |
|
| | @abstractmethod |
| | def create_content(self, width: int, height: int) -> UIContent: |
| | """ |
| | Generate the content for this user control. |
| | |
| | Returns a :class:`.UIContent` instance. |
| | """ |
| |
|
| | def mouse_handler(self, mouse_event: MouseEvent) -> NotImplementedOrNone: |
| | """ |
| | Handle mouse events. |
| | |
| | When `NotImplemented` is returned, it means that the given event is not |
| | handled by the `UIControl` itself. The `Window` or key bindings can |
| | decide to handle this event as scrolling or changing focus. |
| | |
| | :param mouse_event: `MouseEvent` instance. |
| | """ |
| | return NotImplemented |
| |
|
| | def move_cursor_down(self) -> None: |
| | """ |
| | Request to move the cursor down. |
| | This happens when scrolling down and the cursor is completely at the |
| | top. |
| | """ |
| |
|
| | def move_cursor_up(self) -> None: |
| | """ |
| | Request to move the cursor up. |
| | """ |
| |
|
| | def get_key_bindings(self) -> KeyBindingsBase | None: |
| | """ |
| | The key bindings that are specific for this user control. |
| | |
| | Return a :class:`.KeyBindings` object if some key bindings are |
| | specified, or `None` otherwise. |
| | """ |
| |
|
| | def get_invalidate_events(self) -> Iterable[Event[object]]: |
| | """ |
| | Return a list of `Event` objects. This can be a generator. |
| | (The application collects all these events, in order to bind redraw |
| | handlers to these events.) |
| | """ |
| | return [] |
| |
|
| |
|
| | class UIContent: |
| | """ |
| | Content generated by a user control. This content consists of a list of |
| | lines. |
| | |
| | :param get_line: Callable that takes a line number and returns the current |
| | line. This is a list of (style_str, text) tuples. |
| | :param line_count: The number of lines. |
| | :param cursor_position: a :class:`.Point` for the cursor position. |
| | :param menu_position: a :class:`.Point` for the menu position. |
| | :param show_cursor: Make the cursor visible. |
| | """ |
| |
|
| | def __init__( |
| | self, |
| | get_line: Callable[[int], StyleAndTextTuples] = (lambda i: []), |
| | line_count: int = 0, |
| | cursor_position: Point | None = None, |
| | menu_position: Point | None = None, |
| | show_cursor: bool = True, |
| | ): |
| | self.get_line = get_line |
| | self.line_count = line_count |
| | self.cursor_position = cursor_position or Point(x=0, y=0) |
| | self.menu_position = menu_position |
| | self.show_cursor = show_cursor |
| |
|
| | |
| | self._line_heights_cache: dict[Hashable, int] = {} |
| |
|
| | def __getitem__(self, lineno: int) -> StyleAndTextTuples: |
| | "Make it iterable (iterate line by line)." |
| | if lineno < self.line_count: |
| | return self.get_line(lineno) |
| | else: |
| | raise IndexError |
| |
|
| | def get_height_for_line( |
| | self, |
| | lineno: int, |
| | width: int, |
| | get_line_prefix: GetLinePrefixCallable | None, |
| | slice_stop: int | None = None, |
| | ) -> int: |
| | """ |
| | Return the height that a given line would need if it is rendered in a |
| | space with the given width (using line wrapping). |
| | |
| | :param get_line_prefix: None or a `Window.get_line_prefix` callable |
| | that returns the prefix to be inserted before this line. |
| | :param slice_stop: Wrap only "line[:slice_stop]" and return that |
| | partial result. This is needed for scrolling the window correctly |
| | when line wrapping. |
| | :returns: The computed height. |
| | """ |
| | |
| | |
| | |
| | key = get_app().render_counter, lineno, width, slice_stop |
| |
|
| | try: |
| | return self._line_heights_cache[key] |
| | except KeyError: |
| | if width == 0: |
| | height = 10**8 |
| | else: |
| | |
| | line = fragment_list_to_text(self.get_line(lineno))[:slice_stop] |
| | text_width = get_cwidth(line) |
| |
|
| | if get_line_prefix: |
| | |
| | text_width += fragment_list_width( |
| | to_formatted_text(get_line_prefix(lineno, 0)) |
| | ) |
| |
|
| | |
| | height = 1 |
| |
|
| | |
| | |
| | while text_width > width: |
| | height += 1 |
| | text_width -= width |
| |
|
| | fragments2 = to_formatted_text( |
| | get_line_prefix(lineno, height - 1) |
| | ) |
| | prefix_width = get_cwidth(fragment_list_to_text(fragments2)) |
| |
|
| | if prefix_width >= width: |
| | height = 10**8 |
| | break |
| |
|
| | text_width += prefix_width |
| | else: |
| | |
| | try: |
| | quotient, remainder = divmod(text_width, width) |
| | except ZeroDivisionError: |
| | height = 10**8 |
| | else: |
| | if remainder: |
| | quotient += 1 |
| | height = max(1, quotient) |
| |
|
| | |
| | self._line_heights_cache[key] = height |
| | return height |
| |
|
| |
|
| | class FormattedTextControl(UIControl): |
| | """ |
| | Control that displays formatted text. This can be either plain text, an |
| | :class:`~prompt_toolkit.formatted_text.HTML` object an |
| | :class:`~prompt_toolkit.formatted_text.ANSI` object, a list of ``(style_str, |
| | text)`` tuples or a callable that takes no argument and returns one of |
| | those, depending on how you prefer to do the formatting. See |
| | ``prompt_toolkit.layout.formatted_text`` for more information. |
| | |
| | (It's mostly optimized for rather small widgets, like toolbars, menus, etc...) |
| | |
| | When this UI control has the focus, the cursor will be shown in the upper |
| | left corner of this control by default. There are two ways for specifying |
| | the cursor position: |
| | |
| | - Pass a `get_cursor_position` function which returns a `Point` instance |
| | with the current cursor position. |
| | |
| | - If the (formatted) text is passed as a list of ``(style, text)`` tuples |
| | and there is one that looks like ``('[SetCursorPosition]', '')``, then |
| | this will specify the cursor position. |
| | |
| | Mouse support: |
| | |
| | The list of fragments can also contain tuples of three items, looking like: |
| | (style_str, text, handler). When mouse support is enabled and the user |
| | clicks on this fragment, then the given handler is called. That handler |
| | should accept two inputs: (Application, MouseEvent) and it should |
| | either handle the event or return `NotImplemented` in case we want the |
| | containing Window to handle this event. |
| | |
| | :param focusable: `bool` or :class:`.Filter`: Tell whether this control is |
| | focusable. |
| | |
| | :param text: Text or formatted text to be displayed. |
| | :param style: Style string applied to the content. (If you want to style |
| | the whole :class:`~prompt_toolkit.layout.Window`, pass the style to the |
| | :class:`~prompt_toolkit.layout.Window` instead.) |
| | :param key_bindings: a :class:`.KeyBindings` object. |
| | :param get_cursor_position: A callable that returns the cursor position as |
| | a `Point` instance. |
| | """ |
| |
|
| | def __init__( |
| | self, |
| | text: AnyFormattedText = "", |
| | style: str = "", |
| | focusable: FilterOrBool = False, |
| | key_bindings: KeyBindingsBase | None = None, |
| | show_cursor: bool = True, |
| | modal: bool = False, |
| | get_cursor_position: Callable[[], Point | None] | None = None, |
| | ) -> None: |
| | self.text = text |
| | self.style = style |
| | self.focusable = to_filter(focusable) |
| |
|
| | |
| | self.key_bindings = key_bindings |
| | self.show_cursor = show_cursor |
| | self.modal = modal |
| | self.get_cursor_position = get_cursor_position |
| |
|
| | |
| | self._content_cache: SimpleCache[Hashable, UIContent] = SimpleCache(maxsize=18) |
| | self._fragment_cache: SimpleCache[int, StyleAndTextTuples] = SimpleCache( |
| | maxsize=1 |
| | ) |
| | |
| |
|
| | |
| | self._fragments: StyleAndTextTuples | None = None |
| |
|
| | def reset(self) -> None: |
| | self._fragments = None |
| |
|
| | def is_focusable(self) -> bool: |
| | return self.focusable() |
| |
|
| | def __repr__(self) -> str: |
| | return f"{self.__class__.__name__}({self.text!r})" |
| |
|
| | def _get_formatted_text_cached(self) -> StyleAndTextTuples: |
| | """ |
| | Get fragments, but only retrieve fragments once during one render run. |
| | (This function is called several times during one rendering, because |
| | we also need those for calculating the dimensions.) |
| | """ |
| | return self._fragment_cache.get( |
| | get_app().render_counter, lambda: to_formatted_text(self.text, self.style) |
| | ) |
| |
|
| | def preferred_width(self, max_available_width: int) -> int: |
| | """ |
| | Return the preferred width for this control. |
| | That is the width of the longest line. |
| | """ |
| | text = fragment_list_to_text(self._get_formatted_text_cached()) |
| | line_lengths = [get_cwidth(l) for l in text.split("\n")] |
| | return max(line_lengths) |
| |
|
| | def preferred_height( |
| | self, |
| | width: int, |
| | max_available_height: int, |
| | wrap_lines: bool, |
| | get_line_prefix: GetLinePrefixCallable | None, |
| | ) -> int | None: |
| | """ |
| | Return the preferred height for this control. |
| | """ |
| | content = self.create_content(width, None) |
| | if wrap_lines: |
| | height = 0 |
| | for i in range(content.line_count): |
| | height += content.get_height_for_line(i, width, get_line_prefix) |
| | if height >= max_available_height: |
| | return max_available_height |
| | return height |
| | else: |
| | return content.line_count |
| |
|
| | def create_content(self, width: int, height: int | None) -> UIContent: |
| | |
| | fragments_with_mouse_handlers = self._get_formatted_text_cached() |
| | fragment_lines_with_mouse_handlers = list( |
| | split_lines(fragments_with_mouse_handlers) |
| | ) |
| |
|
| | |
| | fragment_lines: list[StyleAndTextTuples] = [ |
| | [(item[0], item[1]) for item in line] |
| | for line in fragment_lines_with_mouse_handlers |
| | ] |
| |
|
| | |
| | |
| | self._fragments = fragments_with_mouse_handlers |
| |
|
| | |
| | |
| | def get_cursor_position( |
| | fragment: str = "[SetCursorPosition]", |
| | ) -> Point | None: |
| | for y, line in enumerate(fragment_lines): |
| | x = 0 |
| | for style_str, text, *_ in line: |
| | if fragment in style_str: |
| | return Point(x=x, y=y) |
| | x += len(text) |
| | return None |
| |
|
| | |
| | def get_menu_position() -> Point | None: |
| | return get_cursor_position("[SetMenuPosition]") |
| |
|
| | cursor_position = (self.get_cursor_position or get_cursor_position)() |
| |
|
| | |
| | key = (tuple(fragments_with_mouse_handlers), width, cursor_position) |
| |
|
| | def get_content() -> UIContent: |
| | return UIContent( |
| | get_line=lambda i: fragment_lines[i], |
| | line_count=len(fragment_lines), |
| | show_cursor=self.show_cursor, |
| | cursor_position=cursor_position, |
| | menu_position=get_menu_position(), |
| | ) |
| |
|
| | return self._content_cache.get(key, get_content) |
| |
|
| | def mouse_handler(self, mouse_event: MouseEvent) -> NotImplementedOrNone: |
| | """ |
| | Handle mouse events. |
| | |
| | (When the fragment list contained mouse handlers and the user clicked on |
| | on any of these, the matching handler is called. This handler can still |
| | return `NotImplemented` in case we want the |
| | :class:`~prompt_toolkit.layout.Window` to handle this particular |
| | event.) |
| | """ |
| | if self._fragments: |
| | |
| | fragments_for_line = list(split_lines(self._fragments)) |
| |
|
| | try: |
| | fragments = fragments_for_line[mouse_event.position.y] |
| | except IndexError: |
| | return NotImplemented |
| | else: |
| | |
| | xpos = mouse_event.position.x |
| |
|
| | |
| | count = 0 |
| | for item in fragments: |
| | count += len(item[1]) |
| | if count > xpos: |
| | if len(item) >= 3: |
| | |
| | |
| | |
| | handler = item[2] |
| | return handler(mouse_event) |
| | else: |
| | break |
| |
|
| | |
| | return NotImplemented |
| |
|
| | def is_modal(self) -> bool: |
| | return self.modal |
| |
|
| | def get_key_bindings(self) -> KeyBindingsBase | None: |
| | return self.key_bindings |
| |
|
| |
|
| | class DummyControl(UIControl): |
| | """ |
| | A dummy control object that doesn't paint any content. |
| | |
| | Useful for filling a :class:`~prompt_toolkit.layout.Window`. (The |
| | `fragment` and `char` attributes of the `Window` class can be used to |
| | define the filling.) |
| | """ |
| |
|
| | def create_content(self, width: int, height: int) -> UIContent: |
| | def get_line(i: int) -> StyleAndTextTuples: |
| | return [] |
| |
|
| | return UIContent(get_line=get_line, line_count=100**100) |
| |
|
| | def is_focusable(self) -> bool: |
| | return False |
| |
|
| |
|
| | class _ProcessedLine(NamedTuple): |
| | fragments: StyleAndTextTuples |
| | source_to_display: Callable[[int], int] |
| | display_to_source: Callable[[int], int] |
| |
|
| |
|
| | class BufferControl(UIControl): |
| | """ |
| | Control for visualizing the content of a :class:`.Buffer`. |
| | |
| | :param buffer: The :class:`.Buffer` object to be displayed. |
| | :param input_processors: A list of |
| | :class:`~prompt_toolkit.layout.processors.Processor` objects. |
| | :param include_default_input_processors: When True, include the default |
| | processors for highlighting of selection, search and displaying of |
| | multiple cursors. |
| | :param lexer: :class:`.Lexer` instance for syntax highlighting. |
| | :param preview_search: `bool` or :class:`.Filter`: Show search while |
| | typing. When this is `True`, probably you want to add a |
| | ``HighlightIncrementalSearchProcessor`` as well. Otherwise only the |
| | cursor position will move, but the text won't be highlighted. |
| | :param focusable: `bool` or :class:`.Filter`: Tell whether this control is focusable. |
| | :param focus_on_click: Focus this buffer when it's click, but not yet focused. |
| | :param key_bindings: a :class:`.KeyBindings` object. |
| | """ |
| |
|
| | def __init__( |
| | self, |
| | buffer: Buffer | None = None, |
| | input_processors: list[Processor] | None = None, |
| | include_default_input_processors: bool = True, |
| | lexer: Lexer | None = None, |
| | preview_search: FilterOrBool = False, |
| | focusable: FilterOrBool = True, |
| | search_buffer_control: ( |
| | None | SearchBufferControl | Callable[[], SearchBufferControl] |
| | ) = None, |
| | menu_position: Callable[[], int | None] | None = None, |
| | focus_on_click: FilterOrBool = False, |
| | key_bindings: KeyBindingsBase | None = None, |
| | ): |
| | self.input_processors = input_processors |
| | self.include_default_input_processors = include_default_input_processors |
| |
|
| | self.default_input_processors = [ |
| | HighlightSearchProcessor(), |
| | HighlightIncrementalSearchProcessor(), |
| | HighlightSelectionProcessor(), |
| | DisplayMultipleCursors(), |
| | ] |
| |
|
| | self.preview_search = to_filter(preview_search) |
| | self.focusable = to_filter(focusable) |
| | self.focus_on_click = to_filter(focus_on_click) |
| |
|
| | self.buffer = buffer or Buffer() |
| | self.menu_position = menu_position |
| | self.lexer = lexer or SimpleLexer() |
| | self.key_bindings = key_bindings |
| | self._search_buffer_control = search_buffer_control |
| |
|
| | |
| | |
| | |
| | |
| | self._fragment_cache: SimpleCache[ |
| | Hashable, Callable[[int], StyleAndTextTuples] |
| | ] = SimpleCache(maxsize=8) |
| |
|
| | self._last_click_timestamp: float | None = None |
| | self._last_get_processed_line: Callable[[int], _ProcessedLine] | None = None |
| |
|
| | def __repr__(self) -> str: |
| | return f"<{self.__class__.__name__} buffer={self.buffer!r} at {id(self)!r}>" |
| |
|
| | @property |
| | def search_buffer_control(self) -> SearchBufferControl | None: |
| | result: SearchBufferControl | None |
| |
|
| | if callable(self._search_buffer_control): |
| | result = self._search_buffer_control() |
| | else: |
| | result = self._search_buffer_control |
| |
|
| | assert result is None or isinstance(result, SearchBufferControl) |
| | return result |
| |
|
| | @property |
| | def search_buffer(self) -> Buffer | None: |
| | control = self.search_buffer_control |
| | if control is not None: |
| | return control.buffer |
| | return None |
| |
|
| | @property |
| | def search_state(self) -> SearchState: |
| | """ |
| | Return the `SearchState` for searching this `BufferControl`. This is |
| | always associated with the search control. If one search bar is used |
| | for searching multiple `BufferControls`, then they share the same |
| | `SearchState`. |
| | """ |
| | search_buffer_control = self.search_buffer_control |
| | if search_buffer_control: |
| | return search_buffer_control.searcher_search_state |
| | else: |
| | return SearchState() |
| |
|
| | def is_focusable(self) -> bool: |
| | return self.focusable() |
| |
|
| | def preferred_width(self, max_available_width: int) -> int | None: |
| | """ |
| | This should return the preferred width. |
| | |
| | Note: We don't specify a preferred width according to the content, |
| | because it would be too expensive. Calculating the preferred |
| | width can be done by calculating the longest line, but this would |
| | require applying all the processors to each line. This is |
| | unfeasible for a larger document, and doing it for small |
| | documents only would result in inconsistent behavior. |
| | """ |
| | return None |
| |
|
| | def preferred_height( |
| | self, |
| | width: int, |
| | max_available_height: int, |
| | wrap_lines: bool, |
| | get_line_prefix: GetLinePrefixCallable | None, |
| | ) -> int | None: |
| | |
| | |
| | height = 0 |
| | content = self.create_content(width, height=1) |
| |
|
| | |
| | |
| | if not wrap_lines: |
| | return content.line_count |
| |
|
| | |
| | |
| | if content.line_count >= max_available_height: |
| | return max_available_height |
| |
|
| | for i in range(content.line_count): |
| | height += content.get_height_for_line(i, width, get_line_prefix) |
| |
|
| | if height >= max_available_height: |
| | return max_available_height |
| |
|
| | return height |
| |
|
| | def _get_formatted_text_for_line_func( |
| | self, document: Document |
| | ) -> Callable[[int], StyleAndTextTuples]: |
| | """ |
| | Create a function that returns the fragments for a given line. |
| | """ |
| |
|
| | |
| | def get_formatted_text_for_line() -> Callable[[int], StyleAndTextTuples]: |
| | return self.lexer.lex_document(document) |
| |
|
| | key = (document.text, self.lexer.invalidation_hash()) |
| | return self._fragment_cache.get(key, get_formatted_text_for_line) |
| |
|
| | def _create_get_processed_line_func( |
| | self, document: Document, width: int, height: int |
| | ) -> Callable[[int], _ProcessedLine]: |
| | """ |
| | Create a function that takes a line number of the current document and |
| | returns a _ProcessedLine(processed_fragments, source_to_display, display_to_source) |
| | tuple. |
| | """ |
| | |
| | input_processors = self.input_processors or [] |
| | if self.include_default_input_processors: |
| | input_processors = self.default_input_processors + input_processors |
| |
|
| | merged_processor = merge_processors(input_processors) |
| |
|
| | def transform( |
| | lineno: int, |
| | fragments: StyleAndTextTuples, |
| | get_line: Callable[[int], StyleAndTextTuples], |
| | ) -> _ProcessedLine: |
| | "Transform the fragments for a given line number." |
| |
|
| | |
| | def source_to_display(i: int) -> int: |
| | """X position from the buffer to the x position in the |
| | processed fragment list. By default, we start from the 'identity' |
| | operation.""" |
| | return i |
| |
|
| | transformation = merged_processor.apply_transformation( |
| | TransformationInput( |
| | self, |
| | document, |
| | lineno, |
| | source_to_display, |
| | fragments, |
| | width, |
| | height, |
| | get_line, |
| | ) |
| | ) |
| |
|
| | return _ProcessedLine( |
| | transformation.fragments, |
| | transformation.source_to_display, |
| | transformation.display_to_source, |
| | ) |
| |
|
| | def create_func() -> Callable[[int], _ProcessedLine]: |
| | get_line = self._get_formatted_text_for_line_func(document) |
| | cache: dict[int, _ProcessedLine] = {} |
| |
|
| | def get_processed_line(i: int) -> _ProcessedLine: |
| | try: |
| | return cache[i] |
| | except KeyError: |
| | processed_line = transform(i, get_line(i), get_line) |
| | cache[i] = processed_line |
| | return processed_line |
| |
|
| | return get_processed_line |
| |
|
| | return create_func() |
| |
|
| | def create_content( |
| | self, width: int, height: int, preview_search: bool = False |
| | ) -> UIContent: |
| | """ |
| | Create a UIContent. |
| | """ |
| | buffer = self.buffer |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | buffer.load_history_if_not_yet_loaded() |
| |
|
| | |
| | |
| | |
| | |
| | search_control = self.search_buffer_control |
| | preview_now = preview_search or bool( |
| | |
| | self.preview_search() |
| | and |
| | |
| | search_control |
| | and search_control.buffer.text |
| | and |
| | |
| | |
| | get_app().layout.search_target_buffer_control == self |
| | ) |
| |
|
| | if preview_now and search_control is not None: |
| | ss = self.search_state |
| |
|
| | document = buffer.document_for_search( |
| | SearchState( |
| | text=search_control.buffer.text, |
| | direction=ss.direction, |
| | ignore_case=ss.ignore_case, |
| | ) |
| | ) |
| | else: |
| | document = buffer.document |
| |
|
| | get_processed_line = self._create_get_processed_line_func( |
| | document, width, height |
| | ) |
| | self._last_get_processed_line = get_processed_line |
| |
|
| | def translate_rowcol(row: int, col: int) -> Point: |
| | "Return the content column for this coordinate." |
| | return Point(x=get_processed_line(row).source_to_display(col), y=row) |
| |
|
| | def get_line(i: int) -> StyleAndTextTuples: |
| | "Return the fragments for a given line number." |
| | fragments = get_processed_line(i).fragments |
| |
|
| | |
| | |
| | |
| | |
| | |
| | fragments = fragments + [("", " ")] |
| | return fragments |
| |
|
| | content = UIContent( |
| | get_line=get_line, |
| | line_count=document.line_count, |
| | cursor_position=translate_rowcol( |
| | document.cursor_position_row, document.cursor_position_col |
| | ), |
| | ) |
| |
|
| | |
| | |
| | |
| | if get_app().layout.current_control == self: |
| | menu_position = self.menu_position() if self.menu_position else None |
| | if menu_position is not None: |
| | assert isinstance(menu_position, int) |
| | menu_row, menu_col = buffer.document.translate_index_to_position( |
| | menu_position |
| | ) |
| | content.menu_position = translate_rowcol(menu_row, menu_col) |
| | elif buffer.complete_state: |
| | |
| | |
| | |
| | |
| | |
| | menu_row, menu_col = buffer.document.translate_index_to_position( |
| | min( |
| | buffer.cursor_position, |
| | buffer.complete_state.original_document.cursor_position, |
| | ) |
| | ) |
| | content.menu_position = translate_rowcol(menu_row, menu_col) |
| | else: |
| | content.menu_position = None |
| |
|
| | return content |
| |
|
| | def mouse_handler(self, mouse_event: MouseEvent) -> NotImplementedOrNone: |
| | """ |
| | Mouse handler for this control. |
| | """ |
| | buffer = self.buffer |
| | position = mouse_event.position |
| |
|
| | |
| | if get_app().layout.current_control == self: |
| | if self._last_get_processed_line: |
| | processed_line = self._last_get_processed_line(position.y) |
| |
|
| | |
| | |
| | xpos = processed_line.display_to_source(position.x) |
| | index = buffer.document.translate_row_col_to_index(position.y, xpos) |
| |
|
| | |
| | if mouse_event.event_type == MouseEventType.MOUSE_DOWN: |
| | buffer.exit_selection() |
| | buffer.cursor_position = index |
| |
|
| | elif ( |
| | mouse_event.event_type == MouseEventType.MOUSE_MOVE |
| | and mouse_event.button != MouseButton.NONE |
| | ): |
| | |
| | if ( |
| | buffer.selection_state is None |
| | and abs(buffer.cursor_position - index) > 0 |
| | ): |
| | buffer.start_selection(selection_type=SelectionType.CHARACTERS) |
| | buffer.cursor_position = index |
| |
|
| | elif mouse_event.event_type == MouseEventType.MOUSE_UP: |
| | |
| | |
| | |
| | |
| | |
| | if abs(buffer.cursor_position - index) > 1: |
| | if buffer.selection_state is None: |
| | buffer.start_selection( |
| | selection_type=SelectionType.CHARACTERS |
| | ) |
| | buffer.cursor_position = index |
| |
|
| | |
| | |
| | double_click = ( |
| | self._last_click_timestamp |
| | and time.time() - self._last_click_timestamp < 0.3 |
| | ) |
| | self._last_click_timestamp = time.time() |
| |
|
| | if double_click: |
| | start, end = buffer.document.find_boundaries_of_current_word() |
| | buffer.cursor_position += start |
| | buffer.start_selection(selection_type=SelectionType.CHARACTERS) |
| | buffer.cursor_position += end - start |
| | else: |
| | |
| | return NotImplemented |
| |
|
| | |
| | else: |
| | if ( |
| | self.focus_on_click() |
| | and mouse_event.event_type == MouseEventType.MOUSE_UP |
| | ): |
| | |
| | |
| | |
| | get_app().layout.current_control = self |
| | else: |
| | return NotImplemented |
| |
|
| | return None |
| |
|
| | def move_cursor_down(self) -> None: |
| | b = self.buffer |
| | b.cursor_position += b.document.get_cursor_down_position() |
| |
|
| | def move_cursor_up(self) -> None: |
| | b = self.buffer |
| | b.cursor_position += b.document.get_cursor_up_position() |
| |
|
| | def get_key_bindings(self) -> KeyBindingsBase | None: |
| | """ |
| | When additional key bindings are given. Return these. |
| | """ |
| | return self.key_bindings |
| |
|
| | def get_invalidate_events(self) -> Iterable[Event[object]]: |
| | """ |
| | Return the Window invalidate events. |
| | """ |
| | |
| | yield self.buffer.on_text_changed |
| | yield self.buffer.on_cursor_position_changed |
| |
|
| | yield self.buffer.on_completions_changed |
| | yield self.buffer.on_suggestion_set |
| |
|
| |
|
| | class SearchBufferControl(BufferControl): |
| | """ |
| | :class:`.BufferControl` which is used for searching another |
| | :class:`.BufferControl`. |
| | |
| | :param ignore_case: Search case insensitive. |
| | """ |
| |
|
| | def __init__( |
| | self, |
| | buffer: Buffer | None = None, |
| | input_processors: list[Processor] | None = None, |
| | lexer: Lexer | None = None, |
| | focus_on_click: FilterOrBool = False, |
| | key_bindings: KeyBindingsBase | None = None, |
| | ignore_case: FilterOrBool = False, |
| | ): |
| | super().__init__( |
| | buffer=buffer, |
| | input_processors=input_processors, |
| | lexer=lexer, |
| | focus_on_click=focus_on_click, |
| | key_bindings=key_bindings, |
| | ) |
| |
|
| | |
| | |
| | self.searcher_search_state = SearchState(ignore_case=ignore_case) |
| |
|