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