| """ |
| 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) -> _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 |
| ) |
| ) |
|
|
| 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)) |
| 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) |
|
|