Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/prompt_toolkit/layout/controls.py: 24%
327 statements
« prev ^ index » next coverage.py v7.4.4, created at 2024-04-20 06:09 +0000
« prev ^ index » next coverage.py v7.4.4, created at 2024-04-20 06:09 +0000
1"""
2User interface Controls for the layout.
3"""
4from __future__ import annotations
6import time
7from abc import ABCMeta, abstractmethod
8from typing import TYPE_CHECKING, Callable, Hashable, Iterable, NamedTuple
10from prompt_toolkit.application.current import get_app
11from prompt_toolkit.buffer import Buffer
12from prompt_toolkit.cache import SimpleCache
13from prompt_toolkit.data_structures import Point
14from prompt_toolkit.document import Document
15from prompt_toolkit.filters import FilterOrBool, to_filter
16from prompt_toolkit.formatted_text import (
17 AnyFormattedText,
18 StyleAndTextTuples,
19 to_formatted_text,
20)
21from prompt_toolkit.formatted_text.utils import (
22 fragment_list_to_text,
23 fragment_list_width,
24 split_lines,
25)
26from prompt_toolkit.lexers import Lexer, SimpleLexer
27from prompt_toolkit.mouse_events import MouseButton, MouseEvent, MouseEventType
28from prompt_toolkit.search import SearchState
29from prompt_toolkit.selection import SelectionType
30from prompt_toolkit.utils import get_cwidth
32from .processors import (
33 DisplayMultipleCursors,
34 HighlightIncrementalSearchProcessor,
35 HighlightSearchProcessor,
36 HighlightSelectionProcessor,
37 Processor,
38 TransformationInput,
39 merge_processors,
40)
42if TYPE_CHECKING:
43 from prompt_toolkit.key_binding.key_bindings import (
44 KeyBindingsBase,
45 NotImplementedOrNone,
46 )
47 from prompt_toolkit.utils import Event
50__all__ = [
51 "BufferControl",
52 "SearchBufferControl",
53 "DummyControl",
54 "FormattedTextControl",
55 "UIControl",
56 "UIContent",
57]
59GetLinePrefixCallable = Callable[[int, int], AnyFormattedText]
62class UIControl(metaclass=ABCMeta):
63 """
64 Base class for all user interface controls.
65 """
67 def reset(self) -> None:
68 # Default reset. (Doesn't have to be implemented.)
69 pass
71 def preferred_width(self, max_available_width: int) -> int | None:
72 return None
74 def preferred_height(
75 self,
76 width: int,
77 max_available_height: int,
78 wrap_lines: bool,
79 get_line_prefix: GetLinePrefixCallable | None,
80 ) -> int | None:
81 return None
83 def is_focusable(self) -> bool:
84 """
85 Tell whether this user control is focusable.
86 """
87 return False
89 @abstractmethod
90 def create_content(self, width: int, height: int) -> UIContent:
91 """
92 Generate the content for this user control.
94 Returns a :class:`.UIContent` instance.
95 """
97 def mouse_handler(self, mouse_event: MouseEvent) -> NotImplementedOrNone:
98 """
99 Handle mouse events.
101 When `NotImplemented` is returned, it means that the given event is not
102 handled by the `UIControl` itself. The `Window` or key bindings can
103 decide to handle this event as scrolling or changing focus.
105 :param mouse_event: `MouseEvent` instance.
106 """
107 return NotImplemented
109 def move_cursor_down(self) -> None:
110 """
111 Request to move the cursor down.
112 This happens when scrolling down and the cursor is completely at the
113 top.
114 """
116 def move_cursor_up(self) -> None:
117 """
118 Request to move the cursor up.
119 """
121 def get_key_bindings(self) -> KeyBindingsBase | None:
122 """
123 The key bindings that are specific for this user control.
125 Return a :class:`.KeyBindings` object if some key bindings are
126 specified, or `None` otherwise.
127 """
129 def get_invalidate_events(self) -> Iterable[Event[object]]:
130 """
131 Return a list of `Event` objects. This can be a generator.
132 (The application collects all these events, in order to bind redraw
133 handlers to these events.)
134 """
135 return []
138class UIContent:
139 """
140 Content generated by a user control. This content consists of a list of
141 lines.
143 :param get_line: Callable that takes a line number and returns the current
144 line. This is a list of (style_str, text) tuples.
145 :param line_count: The number of lines.
146 :param cursor_position: a :class:`.Point` for the cursor position.
147 :param menu_position: a :class:`.Point` for the menu position.
148 :param show_cursor: Make the cursor visible.
149 """
151 def __init__(
152 self,
153 get_line: Callable[[int], StyleAndTextTuples] = (lambda i: []),
154 line_count: int = 0,
155 cursor_position: Point | None = None,
156 menu_position: Point | None = None,
157 show_cursor: bool = True,
158 ):
159 self.get_line = get_line
160 self.line_count = line_count
161 self.cursor_position = cursor_position or Point(x=0, y=0)
162 self.menu_position = menu_position
163 self.show_cursor = show_cursor
165 # Cache for line heights. Maps cache key -> height
166 self._line_heights_cache: dict[Hashable, int] = {}
168 def __getitem__(self, lineno: int) -> StyleAndTextTuples:
169 "Make it iterable (iterate line by line)."
170 if lineno < self.line_count:
171 return self.get_line(lineno)
172 else:
173 raise IndexError
175 def get_height_for_line(
176 self,
177 lineno: int,
178 width: int,
179 get_line_prefix: GetLinePrefixCallable | None,
180 slice_stop: int | None = None,
181 ) -> int:
182 """
183 Return the height that a given line would need if it is rendered in a
184 space with the given width (using line wrapping).
186 :param get_line_prefix: None or a `Window.get_line_prefix` callable
187 that returns the prefix to be inserted before this line.
188 :param slice_stop: Wrap only "line[:slice_stop]" and return that
189 partial result. This is needed for scrolling the window correctly
190 when line wrapping.
191 :returns: The computed height.
192 """
193 # Instead of using `get_line_prefix` as key, we use render_counter
194 # instead. This is more reliable, because this function could still be
195 # the same, while the content would change over time.
196 key = get_app().render_counter, lineno, width, slice_stop
198 try:
199 return self._line_heights_cache[key]
200 except KeyError:
201 if width == 0:
202 height = 10**8
203 else:
204 # Calculate line width first.
205 line = fragment_list_to_text(self.get_line(lineno))[:slice_stop]
206 text_width = get_cwidth(line)
208 if get_line_prefix:
209 # Add prefix width.
210 text_width += fragment_list_width(
211 to_formatted_text(get_line_prefix(lineno, 0))
212 )
214 # Slower path: compute path when there's a line prefix.
215 height = 1
217 # Keep wrapping as long as the line doesn't fit.
218 # Keep adding new prefixes for every wrapped line.
219 while text_width > width:
220 height += 1
221 text_width -= width
223 fragments2 = to_formatted_text(
224 get_line_prefix(lineno, height - 1)
225 )
226 prefix_width = get_cwidth(fragment_list_to_text(fragments2))
228 if prefix_width >= width: # Prefix doesn't fit.
229 height = 10**8
230 break
232 text_width += prefix_width
233 else:
234 # Fast path: compute height when there's no line prefix.
235 try:
236 quotient, remainder = divmod(text_width, width)
237 except ZeroDivisionError:
238 height = 10**8
239 else:
240 if remainder:
241 quotient += 1 # Like math.ceil.
242 height = max(1, quotient)
244 # Cache and return
245 self._line_heights_cache[key] = height
246 return height
249class FormattedTextControl(UIControl):
250 """
251 Control that displays formatted text. This can be either plain text, an
252 :class:`~prompt_toolkit.formatted_text.HTML` object an
253 :class:`~prompt_toolkit.formatted_text.ANSI` object, a list of ``(style_str,
254 text)`` tuples or a callable that takes no argument and returns one of
255 those, depending on how you prefer to do the formatting. See
256 ``prompt_toolkit.layout.formatted_text`` for more information.
258 (It's mostly optimized for rather small widgets, like toolbars, menus, etc...)
260 When this UI control has the focus, the cursor will be shown in the upper
261 left corner of this control by default. There are two ways for specifying
262 the cursor position:
264 - Pass a `get_cursor_position` function which returns a `Point` instance
265 with the current cursor position.
267 - If the (formatted) text is passed as a list of ``(style, text)`` tuples
268 and there is one that looks like ``('[SetCursorPosition]', '')``, then
269 this will specify the cursor position.
271 Mouse support:
273 The list of fragments can also contain tuples of three items, looking like:
274 (style_str, text, handler). When mouse support is enabled and the user
275 clicks on this fragment, then the given handler is called. That handler
276 should accept two inputs: (Application, MouseEvent) and it should
277 either handle the event or return `NotImplemented` in case we want the
278 containing Window to handle this event.
280 :param focusable: `bool` or :class:`.Filter`: Tell whether this control is
281 focusable.
283 :param text: Text or formatted text to be displayed.
284 :param style: Style string applied to the content. (If you want to style
285 the whole :class:`~prompt_toolkit.layout.Window`, pass the style to the
286 :class:`~prompt_toolkit.layout.Window` instead.)
287 :param key_bindings: a :class:`.KeyBindings` object.
288 :param get_cursor_position: A callable that returns the cursor position as
289 a `Point` instance.
290 """
292 def __init__(
293 self,
294 text: AnyFormattedText = "",
295 style: str = "",
296 focusable: FilterOrBool = False,
297 key_bindings: KeyBindingsBase | None = None,
298 show_cursor: bool = True,
299 modal: bool = False,
300 get_cursor_position: Callable[[], Point | None] | None = None,
301 ) -> None:
302 self.text = text # No type check on 'text'. This is done dynamically.
303 self.style = style
304 self.focusable = to_filter(focusable)
306 # Key bindings.
307 self.key_bindings = key_bindings
308 self.show_cursor = show_cursor
309 self.modal = modal
310 self.get_cursor_position = get_cursor_position
312 #: Cache for the content.
313 self._content_cache: SimpleCache[Hashable, UIContent] = SimpleCache(maxsize=18)
314 self._fragment_cache: SimpleCache[int, StyleAndTextTuples] = SimpleCache(
315 maxsize=1
316 )
317 # Only cache one fragment list. We don't need the previous item.
319 # Render info for the mouse support.
320 self._fragments: StyleAndTextTuples | None = None
322 def reset(self) -> None:
323 self._fragments = None
325 def is_focusable(self) -> bool:
326 return self.focusable()
328 def __repr__(self) -> str:
329 return f"{self.__class__.__name__}({self.text!r})"
331 def _get_formatted_text_cached(self) -> StyleAndTextTuples:
332 """
333 Get fragments, but only retrieve fragments once during one render run.
334 (This function is called several times during one rendering, because
335 we also need those for calculating the dimensions.)
336 """
337 return self._fragment_cache.get(
338 get_app().render_counter, lambda: to_formatted_text(self.text, self.style)
339 )
341 def preferred_width(self, max_available_width: int) -> int:
342 """
343 Return the preferred width for this control.
344 That is the width of the longest line.
345 """
346 text = fragment_list_to_text(self._get_formatted_text_cached())
347 line_lengths = [get_cwidth(l) for l in text.split("\n")]
348 return max(line_lengths)
350 def preferred_height(
351 self,
352 width: int,
353 max_available_height: int,
354 wrap_lines: bool,
355 get_line_prefix: GetLinePrefixCallable | None,
356 ) -> int | None:
357 """
358 Return the preferred height for this control.
359 """
360 content = self.create_content(width, None)
361 if wrap_lines:
362 height = 0
363 for i in range(content.line_count):
364 height += content.get_height_for_line(i, width, get_line_prefix)
365 if height >= max_available_height:
366 return max_available_height
367 return height
368 else:
369 return content.line_count
371 def create_content(self, width: int, height: int | None) -> UIContent:
372 # Get fragments
373 fragments_with_mouse_handlers = self._get_formatted_text_cached()
374 fragment_lines_with_mouse_handlers = list(
375 split_lines(fragments_with_mouse_handlers)
376 )
378 # Strip mouse handlers from fragments.
379 fragment_lines: list[StyleAndTextTuples] = [
380 [(item[0], item[1]) for item in line]
381 for line in fragment_lines_with_mouse_handlers
382 ]
384 # Keep track of the fragments with mouse handler, for later use in
385 # `mouse_handler`.
386 self._fragments = fragments_with_mouse_handlers
388 # If there is a `[SetCursorPosition]` in the fragment list, set the
389 # cursor position here.
390 def get_cursor_position(
391 fragment: str = "[SetCursorPosition]",
392 ) -> Point | None:
393 for y, line in enumerate(fragment_lines):
394 x = 0
395 for style_str, text, *_ in line:
396 if fragment in style_str:
397 return Point(x=x, y=y)
398 x += len(text)
399 return None
401 # If there is a `[SetMenuPosition]`, set the menu over here.
402 def get_menu_position() -> Point | None:
403 return get_cursor_position("[SetMenuPosition]")
405 cursor_position = (self.get_cursor_position or get_cursor_position)()
407 # Create content, or take it from the cache.
408 key = (tuple(fragments_with_mouse_handlers), width, cursor_position)
410 def get_content() -> UIContent:
411 return UIContent(
412 get_line=lambda i: fragment_lines[i],
413 line_count=len(fragment_lines),
414 show_cursor=self.show_cursor,
415 cursor_position=cursor_position,
416 menu_position=get_menu_position(),
417 )
419 return self._content_cache.get(key, get_content)
421 def mouse_handler(self, mouse_event: MouseEvent) -> NotImplementedOrNone:
422 """
423 Handle mouse events.
425 (When the fragment list contained mouse handlers and the user clicked on
426 on any of these, the matching handler is called. This handler can still
427 return `NotImplemented` in case we want the
428 :class:`~prompt_toolkit.layout.Window` to handle this particular
429 event.)
430 """
431 if self._fragments:
432 # Read the generator.
433 fragments_for_line = list(split_lines(self._fragments))
435 try:
436 fragments = fragments_for_line[mouse_event.position.y]
437 except IndexError:
438 return NotImplemented
439 else:
440 # Find position in the fragment list.
441 xpos = mouse_event.position.x
443 # Find mouse handler for this character.
444 count = 0
445 for item in fragments:
446 count += len(item[1])
447 if count > xpos:
448 if len(item) >= 3:
449 # Handler found. Call it.
450 # (Handler can return NotImplemented, so return
451 # that result.)
452 handler = item[2]
453 return handler(mouse_event)
454 else:
455 break
457 # Otherwise, don't handle here.
458 return NotImplemented
460 def is_modal(self) -> bool:
461 return self.modal
463 def get_key_bindings(self) -> KeyBindingsBase | None:
464 return self.key_bindings
467class DummyControl(UIControl):
468 """
469 A dummy control object that doesn't paint any content.
471 Useful for filling a :class:`~prompt_toolkit.layout.Window`. (The
472 `fragment` and `char` attributes of the `Window` class can be used to
473 define the filling.)
474 """
476 def create_content(self, width: int, height: int) -> UIContent:
477 def get_line(i: int) -> StyleAndTextTuples:
478 return []
480 return UIContent(get_line=get_line, line_count=100**100) # Something very big.
482 def is_focusable(self) -> bool:
483 return False
486class _ProcessedLine(NamedTuple):
487 fragments: StyleAndTextTuples
488 source_to_display: Callable[[int], int]
489 display_to_source: Callable[[int], int]
492class BufferControl(UIControl):
493 """
494 Control for visualizing the content of a :class:`.Buffer`.
496 :param buffer: The :class:`.Buffer` object to be displayed.
497 :param input_processors: A list of
498 :class:`~prompt_toolkit.layout.processors.Processor` objects.
499 :param include_default_input_processors: When True, include the default
500 processors for highlighting of selection, search and displaying of
501 multiple cursors.
502 :param lexer: :class:`.Lexer` instance for syntax highlighting.
503 :param preview_search: `bool` or :class:`.Filter`: Show search while
504 typing. When this is `True`, probably you want to add a
505 ``HighlightIncrementalSearchProcessor`` as well. Otherwise only the
506 cursor position will move, but the text won't be highlighted.
507 :param focusable: `bool` or :class:`.Filter`: Tell whether this control is focusable.
508 :param focus_on_click: Focus this buffer when it's click, but not yet focused.
509 :param key_bindings: a :class:`.KeyBindings` object.
510 """
512 def __init__(
513 self,
514 buffer: Buffer | None = None,
515 input_processors: list[Processor] | None = None,
516 include_default_input_processors: bool = True,
517 lexer: Lexer | None = None,
518 preview_search: FilterOrBool = False,
519 focusable: FilterOrBool = True,
520 search_buffer_control: (
521 None | SearchBufferControl | Callable[[], SearchBufferControl]
522 ) = None,
523 menu_position: Callable[[], int | None] | None = None,
524 focus_on_click: FilterOrBool = False,
525 key_bindings: KeyBindingsBase | None = None,
526 ):
527 self.input_processors = input_processors
528 self.include_default_input_processors = include_default_input_processors
530 self.default_input_processors = [
531 HighlightSearchProcessor(),
532 HighlightIncrementalSearchProcessor(),
533 HighlightSelectionProcessor(),
534 DisplayMultipleCursors(),
535 ]
537 self.preview_search = to_filter(preview_search)
538 self.focusable = to_filter(focusable)
539 self.focus_on_click = to_filter(focus_on_click)
541 self.buffer = buffer or Buffer()
542 self.menu_position = menu_position
543 self.lexer = lexer or SimpleLexer()
544 self.key_bindings = key_bindings
545 self._search_buffer_control = search_buffer_control
547 #: Cache for the lexer.
548 #: Often, due to cursor movement, undo/redo and window resizing
549 #: operations, it happens that a short time, the same document has to be
550 #: lexed. This is a fairly easy way to cache such an expensive operation.
551 self._fragment_cache: SimpleCache[
552 Hashable, Callable[[int], StyleAndTextTuples]
553 ] = SimpleCache(maxsize=8)
555 self._last_click_timestamp: float | None = None
556 self._last_get_processed_line: Callable[[int], _ProcessedLine] | None = None
558 def __repr__(self) -> str:
559 return f"<{self.__class__.__name__} buffer={self.buffer!r} at {id(self)!r}>"
561 @property
562 def search_buffer_control(self) -> SearchBufferControl | None:
563 result: SearchBufferControl | None
565 if callable(self._search_buffer_control):
566 result = self._search_buffer_control()
567 else:
568 result = self._search_buffer_control
570 assert result is None or isinstance(result, SearchBufferControl)
571 return result
573 @property
574 def search_buffer(self) -> Buffer | None:
575 control = self.search_buffer_control
576 if control is not None:
577 return control.buffer
578 return None
580 @property
581 def search_state(self) -> SearchState:
582 """
583 Return the `SearchState` for searching this `BufferControl`. This is
584 always associated with the search control. If one search bar is used
585 for searching multiple `BufferControls`, then they share the same
586 `SearchState`.
587 """
588 search_buffer_control = self.search_buffer_control
589 if search_buffer_control:
590 return search_buffer_control.searcher_search_state
591 else:
592 return SearchState()
594 def is_focusable(self) -> bool:
595 return self.focusable()
597 def preferred_width(self, max_available_width: int) -> int | None:
598 """
599 This should return the preferred width.
601 Note: We don't specify a preferred width according to the content,
602 because it would be too expensive. Calculating the preferred
603 width can be done by calculating the longest line, but this would
604 require applying all the processors to each line. This is
605 unfeasible for a larger document, and doing it for small
606 documents only would result in inconsistent behavior.
607 """
608 return None
610 def preferred_height(
611 self,
612 width: int,
613 max_available_height: int,
614 wrap_lines: bool,
615 get_line_prefix: GetLinePrefixCallable | None,
616 ) -> int | None:
617 # Calculate the content height, if it was drawn on a screen with the
618 # given width.
619 height = 0
620 content = self.create_content(width, height=1) # Pass a dummy '1' as height.
622 # When line wrapping is off, the height should be equal to the amount
623 # of lines.
624 if not wrap_lines:
625 return content.line_count
627 # When the number of lines exceeds the max_available_height, just
628 # return max_available_height. No need to calculate anything.
629 if content.line_count >= max_available_height:
630 return max_available_height
632 for i in range(content.line_count):
633 height += content.get_height_for_line(i, width, get_line_prefix)
635 if height >= max_available_height:
636 return max_available_height
638 return height
640 def _get_formatted_text_for_line_func(
641 self, document: Document
642 ) -> Callable[[int], StyleAndTextTuples]:
643 """
644 Create a function that returns the fragments for a given line.
645 """
647 # Cache using `document.text`.
648 def get_formatted_text_for_line() -> Callable[[int], StyleAndTextTuples]:
649 return self.lexer.lex_document(document)
651 key = (document.text, self.lexer.invalidation_hash())
652 return self._fragment_cache.get(key, get_formatted_text_for_line)
654 def _create_get_processed_line_func(
655 self, document: Document, width: int, height: int
656 ) -> Callable[[int], _ProcessedLine]:
657 """
658 Create a function that takes a line number of the current document and
659 returns a _ProcessedLine(processed_fragments, source_to_display, display_to_source)
660 tuple.
661 """
662 # Merge all input processors together.
663 input_processors = self.input_processors or []
664 if self.include_default_input_processors:
665 input_processors = self.default_input_processors + input_processors
667 merged_processor = merge_processors(input_processors)
669 def transform(lineno: int, fragments: StyleAndTextTuples) -> _ProcessedLine:
670 "Transform the fragments for a given line number."
672 # Get cursor position at this line.
673 def source_to_display(i: int) -> int:
674 """X position from the buffer to the x position in the
675 processed fragment list. By default, we start from the 'identity'
676 operation."""
677 return i
679 transformation = merged_processor.apply_transformation(
680 TransformationInput(
681 self, document, lineno, source_to_display, fragments, width, height
682 )
683 )
685 return _ProcessedLine(
686 transformation.fragments,
687 transformation.source_to_display,
688 transformation.display_to_source,
689 )
691 def create_func() -> Callable[[int], _ProcessedLine]:
692 get_line = self._get_formatted_text_for_line_func(document)
693 cache: dict[int, _ProcessedLine] = {}
695 def get_processed_line(i: int) -> _ProcessedLine:
696 try:
697 return cache[i]
698 except KeyError:
699 processed_line = transform(i, get_line(i))
700 cache[i] = processed_line
701 return processed_line
703 return get_processed_line
705 return create_func()
707 def create_content(
708 self, width: int, height: int, preview_search: bool = False
709 ) -> UIContent:
710 """
711 Create a UIContent.
712 """
713 buffer = self.buffer
715 # Trigger history loading of the buffer. We do this during the
716 # rendering of the UI here, because it needs to happen when an
717 # `Application` with its event loop is running. During the rendering of
718 # the buffer control is the earliest place we can achieve this, where
719 # we're sure the right event loop is active, and don't require user
720 # interaction (like in a key binding).
721 buffer.load_history_if_not_yet_loaded()
723 # Get the document to be shown. If we are currently searching (the
724 # search buffer has focus, and the preview_search filter is enabled),
725 # then use the search document, which has possibly a different
726 # text/cursor position.)
727 search_control = self.search_buffer_control
728 preview_now = preview_search or bool(
729 # Only if this feature is enabled.
730 self.preview_search()
731 and
732 # And something was typed in the associated search field.
733 search_control
734 and search_control.buffer.text
735 and
736 # And we are searching in this control. (Many controls can point to
737 # the same search field, like in Pyvim.)
738 get_app().layout.search_target_buffer_control == self
739 )
741 if preview_now and search_control is not None:
742 ss = self.search_state
744 document = buffer.document_for_search(
745 SearchState(
746 text=search_control.buffer.text,
747 direction=ss.direction,
748 ignore_case=ss.ignore_case,
749 )
750 )
751 else:
752 document = buffer.document
754 get_processed_line = self._create_get_processed_line_func(
755 document, width, height
756 )
757 self._last_get_processed_line = get_processed_line
759 def translate_rowcol(row: int, col: int) -> Point:
760 "Return the content column for this coordinate."
761 return Point(x=get_processed_line(row).source_to_display(col), y=row)
763 def get_line(i: int) -> StyleAndTextTuples:
764 "Return the fragments for a given line number."
765 fragments = get_processed_line(i).fragments
767 # Add a space at the end, because that is a possible cursor
768 # position. (When inserting after the input.) We should do this on
769 # all the lines, not just the line containing the cursor. (Because
770 # otherwise, line wrapping/scrolling could change when moving the
771 # cursor around.)
772 fragments = fragments + [("", " ")]
773 return fragments
775 content = UIContent(
776 get_line=get_line,
777 line_count=document.line_count,
778 cursor_position=translate_rowcol(
779 document.cursor_position_row, document.cursor_position_col
780 ),
781 )
783 # If there is an auto completion going on, use that start point for a
784 # pop-up menu position. (But only when this buffer has the focus --
785 # there is only one place for a menu, determined by the focused buffer.)
786 if get_app().layout.current_control == self:
787 menu_position = self.menu_position() if self.menu_position else None
788 if menu_position is not None:
789 assert isinstance(menu_position, int)
790 menu_row, menu_col = buffer.document.translate_index_to_position(
791 menu_position
792 )
793 content.menu_position = translate_rowcol(menu_row, menu_col)
794 elif buffer.complete_state:
795 # Position for completion menu.
796 # Note: We use 'min', because the original cursor position could be
797 # behind the input string when the actual completion is for
798 # some reason shorter than the text we had before. (A completion
799 # can change and shorten the input.)
800 menu_row, menu_col = buffer.document.translate_index_to_position(
801 min(
802 buffer.cursor_position,
803 buffer.complete_state.original_document.cursor_position,
804 )
805 )
806 content.menu_position = translate_rowcol(menu_row, menu_col)
807 else:
808 content.menu_position = None
810 return content
812 def mouse_handler(self, mouse_event: MouseEvent) -> NotImplementedOrNone:
813 """
814 Mouse handler for this control.
815 """
816 buffer = self.buffer
817 position = mouse_event.position
819 # Focus buffer when clicked.
820 if get_app().layout.current_control == self:
821 if self._last_get_processed_line:
822 processed_line = self._last_get_processed_line(position.y)
824 # Translate coordinates back to the cursor position of the
825 # original input.
826 xpos = processed_line.display_to_source(position.x)
827 index = buffer.document.translate_row_col_to_index(position.y, xpos)
829 # Set the cursor position.
830 if mouse_event.event_type == MouseEventType.MOUSE_DOWN:
831 buffer.exit_selection()
832 buffer.cursor_position = index
834 elif (
835 mouse_event.event_type == MouseEventType.MOUSE_MOVE
836 and mouse_event.button != MouseButton.NONE
837 ):
838 # Click and drag to highlight a selection
839 if (
840 buffer.selection_state is None
841 and abs(buffer.cursor_position - index) > 0
842 ):
843 buffer.start_selection(selection_type=SelectionType.CHARACTERS)
844 buffer.cursor_position = index
846 elif mouse_event.event_type == MouseEventType.MOUSE_UP:
847 # When the cursor was moved to another place, select the text.
848 # (The >1 is actually a small but acceptable workaround for
849 # selecting text in Vi navigation mode. In navigation mode,
850 # the cursor can never be after the text, so the cursor
851 # will be repositioned automatically.)
852 if abs(buffer.cursor_position - index) > 1:
853 if buffer.selection_state is None:
854 buffer.start_selection(
855 selection_type=SelectionType.CHARACTERS
856 )
857 buffer.cursor_position = index
859 # Select word around cursor on double click.
860 # Two MOUSE_UP events in a short timespan are considered a double click.
861 double_click = (
862 self._last_click_timestamp
863 and time.time() - self._last_click_timestamp < 0.3
864 )
865 self._last_click_timestamp = time.time()
867 if double_click:
868 start, end = buffer.document.find_boundaries_of_current_word()
869 buffer.cursor_position += start
870 buffer.start_selection(selection_type=SelectionType.CHARACTERS)
871 buffer.cursor_position += end - start
872 else:
873 # Don't handle scroll events here.
874 return NotImplemented
876 # Not focused, but focusing on click events.
877 else:
878 if (
879 self.focus_on_click()
880 and mouse_event.event_type == MouseEventType.MOUSE_UP
881 ):
882 # Focus happens on mouseup. (If we did this on mousedown, the
883 # up event will be received at the point where this widget is
884 # focused and be handled anyway.)
885 get_app().layout.current_control = self
886 else:
887 return NotImplemented
889 return None
891 def move_cursor_down(self) -> None:
892 b = self.buffer
893 b.cursor_position += b.document.get_cursor_down_position()
895 def move_cursor_up(self) -> None:
896 b = self.buffer
897 b.cursor_position += b.document.get_cursor_up_position()
899 def get_key_bindings(self) -> KeyBindingsBase | None:
900 """
901 When additional key bindings are given. Return these.
902 """
903 return self.key_bindings
905 def get_invalidate_events(self) -> Iterable[Event[object]]:
906 """
907 Return the Window invalidate events.
908 """
909 # Whenever the buffer changes, the UI has to be updated.
910 yield self.buffer.on_text_changed
911 yield self.buffer.on_cursor_position_changed
913 yield self.buffer.on_completions_changed
914 yield self.buffer.on_suggestion_set
917class SearchBufferControl(BufferControl):
918 """
919 :class:`.BufferControl` which is used for searching another
920 :class:`.BufferControl`.
922 :param ignore_case: Search case insensitive.
923 """
925 def __init__(
926 self,
927 buffer: Buffer | None = None,
928 input_processors: list[Processor] | None = None,
929 lexer: Lexer | None = None,
930 focus_on_click: FilterOrBool = False,
931 key_bindings: KeyBindingsBase | None = None,
932 ignore_case: FilterOrBool = False,
933 ):
934 super().__init__(
935 buffer=buffer,
936 input_processors=input_processors,
937 lexer=lexer,
938 focus_on_click=focus_on_click,
939 key_bindings=key_bindings,
940 )
942 # If this BufferControl is used as a search field for one or more other
943 # BufferControls, then represents the search state.
944 self.searcher_search_state = SearchState(ignore_case=ignore_case)