Spaces:
Sleeping
Sleeping
""" | |
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: | |
# Default reset. (Doesn't have to be implemented.) | |
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 | |
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 | |
# Cache for line heights. Maps cache key -> height | |
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. | |
""" | |
# Instead of using `get_line_prefix` as key, we use render_counter | |
# instead. This is more reliable, because this function could still be | |
# the same, while the content would change over time. | |
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: | |
# Calculate line width first. | |
line = fragment_list_to_text(self.get_line(lineno))[:slice_stop] | |
text_width = get_cwidth(line) | |
if get_line_prefix: | |
# Add prefix width. | |
text_width += fragment_list_width( | |
to_formatted_text(get_line_prefix(lineno, 0)) | |
) | |
# Slower path: compute path when there's a line prefix. | |
height = 1 | |
# Keep wrapping as long as the line doesn't fit. | |
# Keep adding new prefixes for every wrapped line. | |
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: # Prefix doesn't fit. | |
height = 10**8 | |
break | |
text_width += prefix_width | |
else: | |
# Fast path: compute height when there's no line prefix. | |
try: | |
quotient, remainder = divmod(text_width, width) | |
except ZeroDivisionError: | |
height = 10**8 | |
else: | |
if remainder: | |
quotient += 1 # Like math.ceil. | |
height = max(1, quotient) | |
# Cache and return | |
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 # No type check on 'text'. This is done dynamically. | |
self.style = style | |
self.focusable = to_filter(focusable) | |
# Key bindings. | |
self.key_bindings = key_bindings | |
self.show_cursor = show_cursor | |
self.modal = modal | |
self.get_cursor_position = get_cursor_position | |
#: Cache for the content. | |
self._content_cache: SimpleCache[Hashable, UIContent] = SimpleCache(maxsize=18) | |
self._fragment_cache: SimpleCache[int, StyleAndTextTuples] = SimpleCache( | |
maxsize=1 | |
) | |
# Only cache one fragment list. We don't need the previous item. | |
# Render info for the mouse support. | |
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: | |
# Get fragments | |
fragments_with_mouse_handlers = self._get_formatted_text_cached() | |
fragment_lines_with_mouse_handlers = list( | |
split_lines(fragments_with_mouse_handlers) | |
) | |
# Strip mouse handlers from fragments. | |
fragment_lines: list[StyleAndTextTuples] = [ | |
[(item[0], item[1]) for item in line] | |
for line in fragment_lines_with_mouse_handlers | |
] | |
# Keep track of the fragments with mouse handler, for later use in | |
# `mouse_handler`. | |
self._fragments = fragments_with_mouse_handlers | |
# If there is a `[SetCursorPosition]` in the fragment list, set the | |
# cursor position here. | |
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 | |
# If there is a `[SetMenuPosition]`, set the menu over here. | |
def get_menu_position() -> Point | None: | |
return get_cursor_position("[SetMenuPosition]") | |
cursor_position = (self.get_cursor_position or get_cursor_position)() | |
# Create content, or take it from the cache. | |
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: | |
# Read the generator. | |
fragments_for_line = list(split_lines(self._fragments)) | |
try: | |
fragments = fragments_for_line[mouse_event.position.y] | |
except IndexError: | |
return NotImplemented | |
else: | |
# Find position in the fragment list. | |
xpos = mouse_event.position.x | |
# Find mouse handler for this character. | |
count = 0 | |
for item in fragments: | |
count += len(item[1]) | |
if count > xpos: | |
if len(item) >= 3: | |
# Handler found. Call it. | |
# (Handler can return NotImplemented, so return | |
# that result.) | |
handler = item[2] | |
return handler(mouse_event) | |
else: | |
break | |
# Otherwise, don't handle here. | |
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) # Something very big. | |
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 | |
#: Cache for the lexer. | |
#: Often, due to cursor movement, undo/redo and window resizing | |
#: operations, it happens that a short time, the same document has to be | |
#: lexed. This is a fairly easy way to cache such an expensive operation. | |
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}>" | |
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 | |
def search_buffer(self) -> Buffer | None: | |
control = self.search_buffer_control | |
if control is not None: | |
return control.buffer | |
return None | |
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: | |
# Calculate the content height, if it was drawn on a screen with the | |
# given width. | |
height = 0 | |
content = self.create_content(width, height=1) # Pass a dummy '1' as height. | |
# When line wrapping is off, the height should be equal to the amount | |
# of lines. | |
if not wrap_lines: | |
return content.line_count | |
# When the number of lines exceeds the max_available_height, just | |
# return max_available_height. No need to calculate anything. | |
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. | |
""" | |
# Cache using `document.text`. | |
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. | |
""" | |
# Merge all input processors together. | |
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." | |
# Get cursor position at this line. | |
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 | |
# Trigger history loading of the buffer. We do this during the | |
# rendering of the UI here, because it needs to happen when an | |
# `Application` with its event loop is running. During the rendering of | |
# the buffer control is the earliest place we can achieve this, where | |
# we're sure the right event loop is active, and don't require user | |
# interaction (like in a key binding). | |
buffer.load_history_if_not_yet_loaded() | |
# Get the document to be shown. If we are currently searching (the | |
# search buffer has focus, and the preview_search filter is enabled), | |
# then use the search document, which has possibly a different | |
# text/cursor position.) | |
search_control = self.search_buffer_control | |
preview_now = preview_search or bool( | |
# Only if this feature is enabled. | |
self.preview_search() | |
and | |
# And something was typed in the associated search field. | |
search_control | |
and search_control.buffer.text | |
and | |
# And we are searching in this control. (Many controls can point to | |
# the same search field, like in Pyvim.) | |
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 | |
# Add a space at the end, because that is a possible cursor | |
# position. (When inserting after the input.) We should do this on | |
# all the lines, not just the line containing the cursor. (Because | |
# otherwise, line wrapping/scrolling could change when moving the | |
# cursor around.) | |
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 there is an auto completion going on, use that start point for a | |
# pop-up menu position. (But only when this buffer has the focus -- | |
# there is only one place for a menu, determined by the focused buffer.) | |
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: | |
# Position for completion menu. | |
# Note: We use 'min', because the original cursor position could be | |
# behind the input string when the actual completion is for | |
# some reason shorter than the text we had before. (A completion | |
# can change and shorten the input.) | |
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 | |
# Focus buffer when clicked. | |
if get_app().layout.current_control == self: | |
if self._last_get_processed_line: | |
processed_line = self._last_get_processed_line(position.y) | |
# Translate coordinates back to the cursor position of the | |
# original input. | |
xpos = processed_line.display_to_source(position.x) | |
index = buffer.document.translate_row_col_to_index(position.y, xpos) | |
# Set the cursor position. | |
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 | |
): | |
# Click and drag to highlight a selection | |
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: | |
# When the cursor was moved to another place, select the text. | |
# (The >1 is actually a small but acceptable workaround for | |
# selecting text in Vi navigation mode. In navigation mode, | |
# the cursor can never be after the text, so the cursor | |
# will be repositioned automatically.) | |
if abs(buffer.cursor_position - index) > 1: | |
if buffer.selection_state is None: | |
buffer.start_selection( | |
selection_type=SelectionType.CHARACTERS | |
) | |
buffer.cursor_position = index | |
# Select word around cursor on double click. | |
# Two MOUSE_UP events in a short timespan are considered a double click. | |
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: | |
# Don't handle scroll events here. | |
return NotImplemented | |
# Not focused, but focusing on click events. | |
else: | |
if ( | |
self.focus_on_click() | |
and mouse_event.event_type == MouseEventType.MOUSE_UP | |
): | |
# Focus happens on mouseup. (If we did this on mousedown, the | |
# up event will be received at the point where this widget is | |
# focused and be handled anyway.) | |
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. | |
""" | |
# Whenever the buffer changes, the UI has to be updated. | |
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, | |
) | |
# If this BufferControl is used as a search field for one or more other | |
# BufferControls, then represents the search state. | |
self.searcher_search_state = SearchState(ignore_case=ignore_case) | |