|
""" |
|
Collection of reusable components for building full screen applications. |
|
|
|
All of these widgets implement the ``__pt_container__`` method, which makes |
|
them usable in any situation where we are expecting a `prompt_toolkit` |
|
container object. |
|
|
|
.. warning:: |
|
|
|
At this point, the API for these widgets is considered unstable, and can |
|
potentially change between minor releases (we try not too, but no |
|
guarantees are made yet). The public API in |
|
`prompt_toolkit.shortcuts.dialogs` on the other hand is considered stable. |
|
""" |
|
|
|
from __future__ import annotations |
|
|
|
from functools import partial |
|
from typing import Callable, Generic, Sequence, TypeVar |
|
|
|
from prompt_toolkit.application.current import get_app |
|
from prompt_toolkit.auto_suggest import AutoSuggest, DynamicAutoSuggest |
|
from prompt_toolkit.buffer import Buffer, BufferAcceptHandler |
|
from prompt_toolkit.completion import Completer, DynamicCompleter |
|
from prompt_toolkit.document import Document |
|
from prompt_toolkit.filters import ( |
|
Condition, |
|
FilterOrBool, |
|
has_focus, |
|
is_done, |
|
is_true, |
|
to_filter, |
|
) |
|
from prompt_toolkit.formatted_text import ( |
|
AnyFormattedText, |
|
StyleAndTextTuples, |
|
Template, |
|
to_formatted_text, |
|
) |
|
from prompt_toolkit.formatted_text.utils import fragment_list_to_text |
|
from prompt_toolkit.history import History |
|
from prompt_toolkit.key_binding.key_bindings import KeyBindings |
|
from prompt_toolkit.key_binding.key_processor import KeyPressEvent |
|
from prompt_toolkit.keys import Keys |
|
from prompt_toolkit.layout.containers import ( |
|
AnyContainer, |
|
ConditionalContainer, |
|
Container, |
|
DynamicContainer, |
|
Float, |
|
FloatContainer, |
|
HSplit, |
|
VSplit, |
|
Window, |
|
WindowAlign, |
|
) |
|
from prompt_toolkit.layout.controls import ( |
|
BufferControl, |
|
FormattedTextControl, |
|
GetLinePrefixCallable, |
|
) |
|
from prompt_toolkit.layout.dimension import AnyDimension |
|
from prompt_toolkit.layout.dimension import Dimension as D |
|
from prompt_toolkit.layout.margins import ( |
|
ConditionalMargin, |
|
NumberedMargin, |
|
ScrollbarMargin, |
|
) |
|
from prompt_toolkit.layout.processors import ( |
|
AppendAutoSuggestion, |
|
BeforeInput, |
|
ConditionalProcessor, |
|
PasswordProcessor, |
|
Processor, |
|
) |
|
from prompt_toolkit.lexers import DynamicLexer, Lexer |
|
from prompt_toolkit.mouse_events import MouseEvent, MouseEventType |
|
from prompt_toolkit.utils import get_cwidth |
|
from prompt_toolkit.validation import DynamicValidator, Validator |
|
|
|
from .toolbars import SearchToolbar |
|
|
|
__all__ = [ |
|
"TextArea", |
|
"Label", |
|
"Button", |
|
"Frame", |
|
"Shadow", |
|
"Box", |
|
"VerticalLine", |
|
"HorizontalLine", |
|
"RadioList", |
|
"CheckboxList", |
|
"Checkbox", |
|
"ProgressBar", |
|
] |
|
|
|
E = KeyPressEvent |
|
|
|
|
|
class Border: |
|
"Box drawing characters. (Thin)" |
|
|
|
HORIZONTAL = "\u2500" |
|
VERTICAL = "\u2502" |
|
TOP_LEFT = "\u250c" |
|
TOP_RIGHT = "\u2510" |
|
BOTTOM_LEFT = "\u2514" |
|
BOTTOM_RIGHT = "\u2518" |
|
|
|
|
|
class TextArea: |
|
""" |
|
A simple input field. |
|
|
|
This is a higher level abstraction on top of several other classes with |
|
sane defaults. |
|
|
|
This widget does have the most common options, but it does not intend to |
|
cover every single use case. For more configurations options, you can |
|
always build a text area manually, using a |
|
:class:`~prompt_toolkit.buffer.Buffer`, |
|
:class:`~prompt_toolkit.layout.BufferControl` and |
|
:class:`~prompt_toolkit.layout.Window`. |
|
|
|
Buffer attributes: |
|
|
|
:param text: The initial text. |
|
:param multiline: If True, allow multiline input. |
|
:param completer: :class:`~prompt_toolkit.completion.Completer` instance |
|
for auto completion. |
|
:param complete_while_typing: Boolean. |
|
:param accept_handler: Called when `Enter` is pressed (This should be a |
|
callable that takes a buffer as input). |
|
:param history: :class:`~prompt_toolkit.history.History` instance. |
|
:param auto_suggest: :class:`~prompt_toolkit.auto_suggest.AutoSuggest` |
|
instance for input suggestions. |
|
|
|
BufferControl attributes: |
|
|
|
:param password: When `True`, display using asterisks. |
|
:param focusable: When `True`, allow this widget to receive the focus. |
|
:param focus_on_click: When `True`, focus after mouse click. |
|
:param input_processors: `None` or a list of |
|
:class:`~prompt_toolkit.layout.Processor` objects. |
|
:param validator: `None` or a :class:`~prompt_toolkit.validation.Validator` |
|
object. |
|
|
|
Window attributes: |
|
|
|
:param lexer: :class:`~prompt_toolkit.lexers.Lexer` instance for syntax |
|
highlighting. |
|
:param wrap_lines: When `True`, don't scroll horizontally, but wrap lines. |
|
:param width: Window width. (:class:`~prompt_toolkit.layout.Dimension` object.) |
|
:param height: Window height. (:class:`~prompt_toolkit.layout.Dimension` object.) |
|
:param scrollbar: When `True`, display a scroll bar. |
|
:param style: A style string. |
|
:param dont_extend_width: When `True`, don't take up more width then the |
|
preferred width reported by the control. |
|
:param dont_extend_height: When `True`, don't take up more width then the |
|
preferred height reported by the control. |
|
:param get_line_prefix: None or a callable that returns formatted text to |
|
be inserted before a line. It takes a line number (int) and a |
|
wrap_count and returns formatted text. This can be used for |
|
implementation of line continuations, things like Vim "breakindent" and |
|
so on. |
|
|
|
Other attributes: |
|
|
|
:param search_field: An optional `SearchToolbar` object. |
|
""" |
|
|
|
def __init__( |
|
self, |
|
text: str = "", |
|
multiline: FilterOrBool = True, |
|
password: FilterOrBool = False, |
|
lexer: Lexer | None = None, |
|
auto_suggest: AutoSuggest | None = None, |
|
completer: Completer | None = None, |
|
complete_while_typing: FilterOrBool = True, |
|
validator: Validator | None = None, |
|
accept_handler: BufferAcceptHandler | None = None, |
|
history: History | None = None, |
|
focusable: FilterOrBool = True, |
|
focus_on_click: FilterOrBool = False, |
|
wrap_lines: FilterOrBool = True, |
|
read_only: FilterOrBool = False, |
|
width: AnyDimension = None, |
|
height: AnyDimension = None, |
|
dont_extend_height: FilterOrBool = False, |
|
dont_extend_width: FilterOrBool = False, |
|
line_numbers: bool = False, |
|
get_line_prefix: GetLinePrefixCallable | None = None, |
|
scrollbar: bool = False, |
|
style: str = "", |
|
search_field: SearchToolbar | None = None, |
|
preview_search: FilterOrBool = True, |
|
prompt: AnyFormattedText = "", |
|
input_processors: list[Processor] | None = None, |
|
name: str = "", |
|
) -> None: |
|
if search_field is None: |
|
search_control = None |
|
elif isinstance(search_field, SearchToolbar): |
|
search_control = search_field.control |
|
|
|
if input_processors is None: |
|
input_processors = [] |
|
|
|
|
|
self.completer = completer |
|
self.complete_while_typing = complete_while_typing |
|
self.lexer = lexer |
|
self.auto_suggest = auto_suggest |
|
self.read_only = read_only |
|
self.wrap_lines = wrap_lines |
|
self.validator = validator |
|
|
|
self.buffer = Buffer( |
|
document=Document(text, 0), |
|
multiline=multiline, |
|
read_only=Condition(lambda: is_true(self.read_only)), |
|
completer=DynamicCompleter(lambda: self.completer), |
|
complete_while_typing=Condition( |
|
lambda: is_true(self.complete_while_typing) |
|
), |
|
validator=DynamicValidator(lambda: self.validator), |
|
auto_suggest=DynamicAutoSuggest(lambda: self.auto_suggest), |
|
accept_handler=accept_handler, |
|
history=history, |
|
name=name, |
|
) |
|
|
|
self.control = BufferControl( |
|
buffer=self.buffer, |
|
lexer=DynamicLexer(lambda: self.lexer), |
|
input_processors=[ |
|
ConditionalProcessor( |
|
AppendAutoSuggestion(), has_focus(self.buffer) & ~is_done |
|
), |
|
ConditionalProcessor( |
|
processor=PasswordProcessor(), filter=to_filter(password) |
|
), |
|
BeforeInput(prompt, style="class:text-area.prompt"), |
|
] |
|
+ input_processors, |
|
search_buffer_control=search_control, |
|
preview_search=preview_search, |
|
focusable=focusable, |
|
focus_on_click=focus_on_click, |
|
) |
|
|
|
if multiline: |
|
if scrollbar: |
|
right_margins = [ScrollbarMargin(display_arrows=True)] |
|
else: |
|
right_margins = [] |
|
if line_numbers: |
|
left_margins = [NumberedMargin()] |
|
else: |
|
left_margins = [] |
|
else: |
|
height = D.exact(1) |
|
left_margins = [] |
|
right_margins = [] |
|
|
|
style = "class:text-area " + style |
|
|
|
|
|
if height is None: |
|
height = D(min=1) |
|
|
|
self.window = Window( |
|
height=height, |
|
width=width, |
|
dont_extend_height=dont_extend_height, |
|
dont_extend_width=dont_extend_width, |
|
content=self.control, |
|
style=style, |
|
wrap_lines=Condition(lambda: is_true(self.wrap_lines)), |
|
left_margins=left_margins, |
|
right_margins=right_margins, |
|
get_line_prefix=get_line_prefix, |
|
) |
|
|
|
@property |
|
def text(self) -> str: |
|
""" |
|
The `Buffer` text. |
|
""" |
|
return self.buffer.text |
|
|
|
@text.setter |
|
def text(self, value: str) -> None: |
|
self.document = Document(value, 0) |
|
|
|
@property |
|
def document(self) -> Document: |
|
""" |
|
The `Buffer` document (text + cursor position). |
|
""" |
|
return self.buffer.document |
|
|
|
@document.setter |
|
def document(self, value: Document) -> None: |
|
self.buffer.set_document(value, bypass_readonly=True) |
|
|
|
@property |
|
def accept_handler(self) -> BufferAcceptHandler | None: |
|
""" |
|
The accept handler. Called when the user accepts the input. |
|
""" |
|
return self.buffer.accept_handler |
|
|
|
@accept_handler.setter |
|
def accept_handler(self, value: BufferAcceptHandler) -> None: |
|
self.buffer.accept_handler = value |
|
|
|
def __pt_container__(self) -> Container: |
|
return self.window |
|
|
|
|
|
class Label: |
|
""" |
|
Widget that displays the given text. It is not editable or focusable. |
|
|
|
:param text: Text to display. Can be multiline. All value types accepted by |
|
:class:`prompt_toolkit.layout.FormattedTextControl` are allowed, |
|
including a callable. |
|
:param style: A style string. |
|
:param width: When given, use this width, rather than calculating it from |
|
the text size. |
|
:param dont_extend_width: When `True`, don't take up more width than |
|
preferred, i.e. the length of the longest line of |
|
the text, or value of `width` parameter, if |
|
given. `True` by default |
|
:param dont_extend_height: When `True`, don't take up more width than the |
|
preferred height, i.e. the number of lines of |
|
the text. `False` by default. |
|
""" |
|
|
|
def __init__( |
|
self, |
|
text: AnyFormattedText, |
|
style: str = "", |
|
width: AnyDimension = None, |
|
dont_extend_height: bool = True, |
|
dont_extend_width: bool = False, |
|
align: WindowAlign | Callable[[], WindowAlign] = WindowAlign.LEFT, |
|
|
|
|
|
wrap_lines: FilterOrBool = True, |
|
) -> None: |
|
self.text = text |
|
|
|
def get_width() -> AnyDimension: |
|
if width is None: |
|
text_fragments = to_formatted_text(self.text) |
|
text = fragment_list_to_text(text_fragments) |
|
if text: |
|
longest_line = max(get_cwidth(line) for line in text.splitlines()) |
|
else: |
|
return D(preferred=0) |
|
return D(preferred=longest_line) |
|
else: |
|
return width |
|
|
|
self.formatted_text_control = FormattedTextControl(text=lambda: self.text) |
|
|
|
self.window = Window( |
|
content=self.formatted_text_control, |
|
width=get_width, |
|
height=D(min=1), |
|
style="class:label " + style, |
|
dont_extend_height=dont_extend_height, |
|
dont_extend_width=dont_extend_width, |
|
align=align, |
|
wrap_lines=wrap_lines, |
|
) |
|
|
|
def __pt_container__(self) -> Container: |
|
return self.window |
|
|
|
|
|
class Button: |
|
""" |
|
Clickable button. |
|
|
|
:param text: The caption for the button. |
|
:param handler: `None` or callable. Called when the button is clicked. No |
|
parameters are passed to this callable. Use for instance Python's |
|
`functools.partial` to pass parameters to this callable if needed. |
|
:param width: Width of the button. |
|
""" |
|
|
|
def __init__( |
|
self, |
|
text: str, |
|
handler: Callable[[], None] | None = None, |
|
width: int = 12, |
|
left_symbol: str = "<", |
|
right_symbol: str = ">", |
|
) -> None: |
|
self.text = text |
|
self.left_symbol = left_symbol |
|
self.right_symbol = right_symbol |
|
self.handler = handler |
|
self.width = width |
|
self.control = FormattedTextControl( |
|
self._get_text_fragments, |
|
key_bindings=self._get_key_bindings(), |
|
focusable=True, |
|
) |
|
|
|
def get_style() -> str: |
|
if get_app().layout.has_focus(self): |
|
return "class:button.focused" |
|
else: |
|
return "class:button" |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
self.window = Window( |
|
self.control, |
|
align=WindowAlign.CENTER, |
|
height=1, |
|
width=width, |
|
style=get_style, |
|
dont_extend_width=False, |
|
dont_extend_height=True, |
|
) |
|
|
|
def _get_text_fragments(self) -> StyleAndTextTuples: |
|
width = self.width - ( |
|
get_cwidth(self.left_symbol) + get_cwidth(self.right_symbol) |
|
) |
|
text = (f"{{:^{width}}}").format(self.text) |
|
|
|
def handler(mouse_event: MouseEvent) -> None: |
|
if ( |
|
self.handler is not None |
|
and mouse_event.event_type == MouseEventType.MOUSE_UP |
|
): |
|
self.handler() |
|
|
|
return [ |
|
("class:button.arrow", self.left_symbol, handler), |
|
("[SetCursorPosition]", ""), |
|
("class:button.text", text, handler), |
|
("class:button.arrow", self.right_symbol, handler), |
|
] |
|
|
|
def _get_key_bindings(self) -> KeyBindings: |
|
"Key bindings for the Button." |
|
kb = KeyBindings() |
|
|
|
@kb.add(" ") |
|
@kb.add("enter") |
|
def _(event: E) -> None: |
|
if self.handler is not None: |
|
self.handler() |
|
|
|
return kb |
|
|
|
def __pt_container__(self) -> Container: |
|
return self.window |
|
|
|
|
|
class Frame: |
|
""" |
|
Draw a border around any container, optionally with a title text. |
|
|
|
Changing the title and body of the frame is possible at runtime by |
|
assigning to the `body` and `title` attributes of this class. |
|
|
|
:param body: Another container object. |
|
:param title: Text to be displayed in the top of the frame (can be formatted text). |
|
:param style: Style string to be applied to this widget. |
|
""" |
|
|
|
def __init__( |
|
self, |
|
body: AnyContainer, |
|
title: AnyFormattedText = "", |
|
style: str = "", |
|
width: AnyDimension = None, |
|
height: AnyDimension = None, |
|
key_bindings: KeyBindings | None = None, |
|
modal: bool = False, |
|
) -> None: |
|
self.title = title |
|
self.body = body |
|
|
|
fill = partial(Window, style="class:frame.border") |
|
style = "class:frame " + style |
|
|
|
top_row_with_title = VSplit( |
|
[ |
|
fill(width=1, height=1, char=Border.TOP_LEFT), |
|
fill(char=Border.HORIZONTAL), |
|
fill(width=1, height=1, char="|"), |
|
|
|
|
|
Label( |
|
lambda: Template(" {} ").format(self.title), |
|
style="class:frame.label", |
|
dont_extend_width=True, |
|
), |
|
fill(width=1, height=1, char="|"), |
|
fill(char=Border.HORIZONTAL), |
|
fill(width=1, height=1, char=Border.TOP_RIGHT), |
|
], |
|
height=1, |
|
) |
|
|
|
top_row_without_title = VSplit( |
|
[ |
|
fill(width=1, height=1, char=Border.TOP_LEFT), |
|
fill(char=Border.HORIZONTAL), |
|
fill(width=1, height=1, char=Border.TOP_RIGHT), |
|
], |
|
height=1, |
|
) |
|
|
|
@Condition |
|
def has_title() -> bool: |
|
return bool(self.title) |
|
|
|
self.container = HSplit( |
|
[ |
|
ConditionalContainer(content=top_row_with_title, filter=has_title), |
|
ConditionalContainer(content=top_row_without_title, filter=~has_title), |
|
VSplit( |
|
[ |
|
fill(width=1, char=Border.VERTICAL), |
|
DynamicContainer(lambda: self.body), |
|
fill(width=1, char=Border.VERTICAL), |
|
|
|
|
|
], |
|
padding=0, |
|
), |
|
VSplit( |
|
[ |
|
fill(width=1, height=1, char=Border.BOTTOM_LEFT), |
|
fill(char=Border.HORIZONTAL), |
|
fill(width=1, height=1, char=Border.BOTTOM_RIGHT), |
|
], |
|
|
|
height=1, |
|
), |
|
], |
|
width=width, |
|
height=height, |
|
style=style, |
|
key_bindings=key_bindings, |
|
modal=modal, |
|
) |
|
|
|
def __pt_container__(self) -> Container: |
|
return self.container |
|
|
|
|
|
class Shadow: |
|
""" |
|
Draw a shadow underneath/behind this container. |
|
(This applies `class:shadow` the the cells under the shadow. The Style |
|
should define the colors for the shadow.) |
|
|
|
:param body: Another container object. |
|
""" |
|
|
|
def __init__(self, body: AnyContainer) -> None: |
|
self.container = FloatContainer( |
|
content=body, |
|
floats=[ |
|
Float( |
|
bottom=-1, |
|
height=1, |
|
left=1, |
|
right=-1, |
|
transparent=True, |
|
content=Window(style="class:shadow"), |
|
), |
|
Float( |
|
bottom=-1, |
|
top=1, |
|
width=1, |
|
right=-1, |
|
transparent=True, |
|
content=Window(style="class:shadow"), |
|
), |
|
], |
|
) |
|
|
|
def __pt_container__(self) -> Container: |
|
return self.container |
|
|
|
|
|
class Box: |
|
""" |
|
Add padding around a container. |
|
|
|
This also makes sure that the parent can provide more space than required by |
|
the child. This is very useful when wrapping a small element with a fixed |
|
size into a ``VSplit`` or ``HSplit`` object. The ``HSplit`` and ``VSplit`` |
|
try to make sure to adapt respectively the width and height, possibly |
|
shrinking other elements. Wrapping something in a ``Box`` makes it flexible. |
|
|
|
:param body: Another container object. |
|
:param padding: The margin to be used around the body. This can be |
|
overridden by `padding_left`, padding_right`, `padding_top` and |
|
`padding_bottom`. |
|
:param style: A style string. |
|
:param char: Character to be used for filling the space around the body. |
|
(This is supposed to be a character with a terminal width of 1.) |
|
""" |
|
|
|
def __init__( |
|
self, |
|
body: AnyContainer, |
|
padding: AnyDimension = None, |
|
padding_left: AnyDimension = None, |
|
padding_right: AnyDimension = None, |
|
padding_top: AnyDimension = None, |
|
padding_bottom: AnyDimension = None, |
|
width: AnyDimension = None, |
|
height: AnyDimension = None, |
|
style: str = "", |
|
char: None | str | Callable[[], str] = None, |
|
modal: bool = False, |
|
key_bindings: KeyBindings | None = None, |
|
) -> None: |
|
self.padding = padding |
|
self.padding_left = padding_left |
|
self.padding_right = padding_right |
|
self.padding_top = padding_top |
|
self.padding_bottom = padding_bottom |
|
self.body = body |
|
|
|
def left() -> AnyDimension: |
|
if self.padding_left is None: |
|
return self.padding |
|
return self.padding_left |
|
|
|
def right() -> AnyDimension: |
|
if self.padding_right is None: |
|
return self.padding |
|
return self.padding_right |
|
|
|
def top() -> AnyDimension: |
|
if self.padding_top is None: |
|
return self.padding |
|
return self.padding_top |
|
|
|
def bottom() -> AnyDimension: |
|
if self.padding_bottom is None: |
|
return self.padding |
|
return self.padding_bottom |
|
|
|
self.container = HSplit( |
|
[ |
|
Window(height=top, char=char), |
|
VSplit( |
|
[ |
|
Window(width=left, char=char), |
|
body, |
|
Window(width=right, char=char), |
|
] |
|
), |
|
Window(height=bottom, char=char), |
|
], |
|
width=width, |
|
height=height, |
|
style=style, |
|
modal=modal, |
|
key_bindings=None, |
|
) |
|
|
|
def __pt_container__(self) -> Container: |
|
return self.container |
|
|
|
|
|
_T = TypeVar("_T") |
|
|
|
|
|
class _DialogList(Generic[_T]): |
|
""" |
|
Common code for `RadioList` and `CheckboxList`. |
|
""" |
|
|
|
open_character: str = "" |
|
close_character: str = "" |
|
container_style: str = "" |
|
default_style: str = "" |
|
selected_style: str = "" |
|
checked_style: str = "" |
|
multiple_selection: bool = False |
|
show_scrollbar: bool = True |
|
|
|
def __init__( |
|
self, |
|
values: Sequence[tuple[_T, AnyFormattedText]], |
|
default_values: Sequence[_T] | None = None, |
|
) -> None: |
|
assert len(values) > 0 |
|
default_values = default_values or [] |
|
|
|
self.values = values |
|
|
|
|
|
keys: list[_T] = [value for (value, _) in values] |
|
self.current_values: list[_T] = [ |
|
value for value in default_values if value in keys |
|
] |
|
self.current_value: _T = ( |
|
default_values[0] |
|
if len(default_values) and default_values[0] in keys |
|
else values[0][0] |
|
) |
|
|
|
|
|
if len(self.current_values) > 0: |
|
self._selected_index = keys.index(self.current_values[0]) |
|
else: |
|
self._selected_index = 0 |
|
|
|
|
|
kb = KeyBindings() |
|
|
|
@kb.add("up") |
|
def _up(event: E) -> None: |
|
self._selected_index = max(0, self._selected_index - 1) |
|
|
|
@kb.add("down") |
|
def _down(event: E) -> None: |
|
self._selected_index = min(len(self.values) - 1, self._selected_index + 1) |
|
|
|
@kb.add("pageup") |
|
def _pageup(event: E) -> None: |
|
w = event.app.layout.current_window |
|
if w.render_info: |
|
self._selected_index = max( |
|
0, self._selected_index - len(w.render_info.displayed_lines) |
|
) |
|
|
|
@kb.add("pagedown") |
|
def _pagedown(event: E) -> None: |
|
w = event.app.layout.current_window |
|
if w.render_info: |
|
self._selected_index = min( |
|
len(self.values) - 1, |
|
self._selected_index + len(w.render_info.displayed_lines), |
|
) |
|
|
|
@kb.add("enter") |
|
@kb.add(" ") |
|
def _click(event: E) -> None: |
|
self._handle_enter() |
|
|
|
@kb.add(Keys.Any) |
|
def _find(event: E) -> None: |
|
|
|
values = list(self.values) |
|
for value in values[self._selected_index + 1 :] + values: |
|
text = fragment_list_to_text(to_formatted_text(value[1])).lower() |
|
|
|
if text.startswith(event.data.lower()): |
|
self._selected_index = self.values.index(value) |
|
return |
|
|
|
|
|
self.control = FormattedTextControl( |
|
self._get_text_fragments, key_bindings=kb, focusable=True |
|
) |
|
|
|
self.window = Window( |
|
content=self.control, |
|
style=self.container_style, |
|
right_margins=[ |
|
ConditionalMargin( |
|
margin=ScrollbarMargin(display_arrows=True), |
|
filter=Condition(lambda: self.show_scrollbar), |
|
), |
|
], |
|
dont_extend_height=True, |
|
) |
|
|
|
def _handle_enter(self) -> None: |
|
if self.multiple_selection: |
|
val = self.values[self._selected_index][0] |
|
if val in self.current_values: |
|
self.current_values.remove(val) |
|
else: |
|
self.current_values.append(val) |
|
else: |
|
self.current_value = self.values[self._selected_index][0] |
|
|
|
def _get_text_fragments(self) -> StyleAndTextTuples: |
|
def mouse_handler(mouse_event: MouseEvent) -> None: |
|
""" |
|
Set `_selected_index` and `current_value` according to the y |
|
position of the mouse click event. |
|
""" |
|
if mouse_event.event_type == MouseEventType.MOUSE_UP: |
|
self._selected_index = mouse_event.position.y |
|
self._handle_enter() |
|
|
|
result: StyleAndTextTuples = [] |
|
for i, value in enumerate(self.values): |
|
if self.multiple_selection: |
|
checked = value[0] in self.current_values |
|
else: |
|
checked = value[0] == self.current_value |
|
selected = i == self._selected_index |
|
|
|
style = "" |
|
if checked: |
|
style += " " + self.checked_style |
|
if selected: |
|
style += " " + self.selected_style |
|
|
|
result.append((style, self.open_character)) |
|
|
|
if selected: |
|
result.append(("[SetCursorPosition]", "")) |
|
|
|
if checked: |
|
result.append((style, "*")) |
|
else: |
|
result.append((style, " ")) |
|
|
|
result.append((style, self.close_character)) |
|
result.append((self.default_style, " ")) |
|
result.extend(to_formatted_text(value[1], style=self.default_style)) |
|
result.append(("", "\n")) |
|
|
|
|
|
for i in range(len(result)): |
|
result[i] = (result[i][0], result[i][1], mouse_handler) |
|
|
|
result.pop() |
|
return result |
|
|
|
def __pt_container__(self) -> Container: |
|
return self.window |
|
|
|
|
|
class RadioList(_DialogList[_T]): |
|
""" |
|
List of radio buttons. Only one can be checked at the same time. |
|
|
|
:param values: List of (value, label) tuples. |
|
""" |
|
|
|
open_character = "(" |
|
close_character = ")" |
|
container_style = "class:radio-list" |
|
default_style = "class:radio" |
|
selected_style = "class:radio-selected" |
|
checked_style = "class:radio-checked" |
|
multiple_selection = False |
|
|
|
def __init__( |
|
self, |
|
values: Sequence[tuple[_T, AnyFormattedText]], |
|
default: _T | None = None, |
|
) -> None: |
|
if default is None: |
|
default_values = None |
|
else: |
|
default_values = [default] |
|
|
|
super().__init__(values, default_values=default_values) |
|
|
|
|
|
class CheckboxList(_DialogList[_T]): |
|
""" |
|
List of checkbox buttons. Several can be checked at the same time. |
|
|
|
:param values: List of (value, label) tuples. |
|
""" |
|
|
|
open_character = "[" |
|
close_character = "]" |
|
container_style = "class:checkbox-list" |
|
default_style = "class:checkbox" |
|
selected_style = "class:checkbox-selected" |
|
checked_style = "class:checkbox-checked" |
|
multiple_selection = True |
|
|
|
|
|
class Checkbox(CheckboxList[str]): |
|
"""Backward compatibility util: creates a 1-sized CheckboxList |
|
|
|
:param text: the text |
|
""" |
|
|
|
show_scrollbar = False |
|
|
|
def __init__(self, text: AnyFormattedText = "", checked: bool = False) -> None: |
|
values = [("value", text)] |
|
super().__init__(values=values) |
|
self.checked = checked |
|
|
|
@property |
|
def checked(self) -> bool: |
|
return "value" in self.current_values |
|
|
|
@checked.setter |
|
def checked(self, value: bool) -> None: |
|
if value: |
|
self.current_values = ["value"] |
|
else: |
|
self.current_values = [] |
|
|
|
|
|
class VerticalLine: |
|
""" |
|
A simple vertical line with a width of 1. |
|
""" |
|
|
|
def __init__(self) -> None: |
|
self.window = Window( |
|
char=Border.VERTICAL, style="class:line,vertical-line", width=1 |
|
) |
|
|
|
def __pt_container__(self) -> Container: |
|
return self.window |
|
|
|
|
|
class HorizontalLine: |
|
""" |
|
A simple horizontal line with a height of 1. |
|
""" |
|
|
|
def __init__(self) -> None: |
|
self.window = Window( |
|
char=Border.HORIZONTAL, style="class:line,horizontal-line", height=1 |
|
) |
|
|
|
def __pt_container__(self) -> Container: |
|
return self.window |
|
|
|
|
|
class ProgressBar: |
|
def __init__(self) -> None: |
|
self._percentage = 60 |
|
|
|
self.label = Label("60%") |
|
self.container = FloatContainer( |
|
content=Window(height=1), |
|
floats=[ |
|
|
|
|
|
|
|
|
|
Float(content=self.label, top=0, bottom=0), |
|
Float( |
|
left=0, |
|
top=0, |
|
right=0, |
|
bottom=0, |
|
content=VSplit( |
|
[ |
|
Window( |
|
style="class:progress-bar.used", |
|
width=lambda: D(weight=int(self._percentage)), |
|
), |
|
Window( |
|
style="class:progress-bar", |
|
width=lambda: D(weight=int(100 - self._percentage)), |
|
), |
|
] |
|
), |
|
), |
|
], |
|
) |
|
|
|
@property |
|
def percentage(self) -> int: |
|
return self._percentage |
|
|
|
@percentage.setter |
|
def percentage(self, value: int) -> None: |
|
self._percentage = value |
|
self.label.text = f"{value}%" |
|
|
|
def __pt_container__(self) -> Container: |
|
return self.container |
|
|