Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/prompt_toolkit/widgets/base.py: 37%
316 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"""
2Collection of reusable components for building full screen applications.
4All of these widgets implement the ``__pt_container__`` method, which makes
5them usable in any situation where we are expecting a `prompt_toolkit`
6container object.
8.. warning::
10 At this point, the API for these widgets is considered unstable, and can
11 potentially change between minor releases (we try not too, but no
12 guarantees are made yet). The public API in
13 `prompt_toolkit.shortcuts.dialogs` on the other hand is considered stable.
14"""
15from __future__ import annotations
17from functools import partial
18from typing import Callable, Generic, Sequence, TypeVar
20from prompt_toolkit.application.current import get_app
21from prompt_toolkit.auto_suggest import AutoSuggest, DynamicAutoSuggest
22from prompt_toolkit.buffer import Buffer, BufferAcceptHandler
23from prompt_toolkit.completion import Completer, DynamicCompleter
24from prompt_toolkit.document import Document
25from prompt_toolkit.filters import (
26 Condition,
27 FilterOrBool,
28 has_focus,
29 is_done,
30 is_true,
31 to_filter,
32)
33from prompt_toolkit.formatted_text import (
34 AnyFormattedText,
35 StyleAndTextTuples,
36 Template,
37 to_formatted_text,
38)
39from prompt_toolkit.formatted_text.utils import fragment_list_to_text
40from prompt_toolkit.history import History
41from prompt_toolkit.key_binding.key_bindings import KeyBindings
42from prompt_toolkit.key_binding.key_processor import KeyPressEvent
43from prompt_toolkit.keys import Keys
44from prompt_toolkit.layout.containers import (
45 AnyContainer,
46 ConditionalContainer,
47 Container,
48 DynamicContainer,
49 Float,
50 FloatContainer,
51 HSplit,
52 VSplit,
53 Window,
54 WindowAlign,
55)
56from prompt_toolkit.layout.controls import (
57 BufferControl,
58 FormattedTextControl,
59 GetLinePrefixCallable,
60)
61from prompt_toolkit.layout.dimension import AnyDimension, to_dimension
62from prompt_toolkit.layout.dimension import Dimension as D
63from prompt_toolkit.layout.margins import (
64 ConditionalMargin,
65 NumberedMargin,
66 ScrollbarMargin,
67)
68from prompt_toolkit.layout.processors import (
69 AppendAutoSuggestion,
70 BeforeInput,
71 ConditionalProcessor,
72 PasswordProcessor,
73 Processor,
74)
75from prompt_toolkit.lexers import DynamicLexer, Lexer
76from prompt_toolkit.mouse_events import MouseEvent, MouseEventType
77from prompt_toolkit.utils import get_cwidth
78from prompt_toolkit.validation import DynamicValidator, Validator
80from .toolbars import SearchToolbar
82__all__ = [
83 "TextArea",
84 "Label",
85 "Button",
86 "Frame",
87 "Shadow",
88 "Box",
89 "VerticalLine",
90 "HorizontalLine",
91 "RadioList",
92 "CheckboxList",
93 "Checkbox", # backward compatibility
94 "ProgressBar",
95]
97E = KeyPressEvent
100class Border:
101 "Box drawing characters. (Thin)"
103 HORIZONTAL = "\u2500"
104 VERTICAL = "\u2502"
105 TOP_LEFT = "\u250c"
106 TOP_RIGHT = "\u2510"
107 BOTTOM_LEFT = "\u2514"
108 BOTTOM_RIGHT = "\u2518"
111class TextArea:
112 """
113 A simple input field.
115 This is a higher level abstraction on top of several other classes with
116 sane defaults.
118 This widget does have the most common options, but it does not intend to
119 cover every single use case. For more configurations options, you can
120 always build a text area manually, using a
121 :class:`~prompt_toolkit.buffer.Buffer`,
122 :class:`~prompt_toolkit.layout.BufferControl` and
123 :class:`~prompt_toolkit.layout.Window`.
125 Buffer attributes:
127 :param text: The initial text.
128 :param multiline: If True, allow multiline input.
129 :param completer: :class:`~prompt_toolkit.completion.Completer` instance
130 for auto completion.
131 :param complete_while_typing: Boolean.
132 :param accept_handler: Called when `Enter` is pressed (This should be a
133 callable that takes a buffer as input).
134 :param history: :class:`~prompt_toolkit.history.History` instance.
135 :param auto_suggest: :class:`~prompt_toolkit.auto_suggest.AutoSuggest`
136 instance for input suggestions.
138 BufferControl attributes:
140 :param password: When `True`, display using asterisks.
141 :param focusable: When `True`, allow this widget to receive the focus.
142 :param focus_on_click: When `True`, focus after mouse click.
143 :param input_processors: `None` or a list of
144 :class:`~prompt_toolkit.layout.Processor` objects.
145 :param validator: `None` or a :class:`~prompt_toolkit.validation.Validator`
146 object.
148 Window attributes:
150 :param lexer: :class:`~prompt_toolkit.lexers.Lexer` instance for syntax
151 highlighting.
152 :param wrap_lines: When `True`, don't scroll horizontally, but wrap lines.
153 :param width: Window width. (:class:`~prompt_toolkit.layout.Dimension` object.)
154 :param height: Window height. (:class:`~prompt_toolkit.layout.Dimension` object.)
155 :param scrollbar: When `True`, display a scroll bar.
156 :param style: A style string.
157 :param dont_extend_width: When `True`, don't take up more width then the
158 preferred width reported by the control.
159 :param dont_extend_height: When `True`, don't take up more width then the
160 preferred height reported by the control.
161 :param get_line_prefix: None or a callable that returns formatted text to
162 be inserted before a line. It takes a line number (int) and a
163 wrap_count and returns formatted text. This can be used for
164 implementation of line continuations, things like Vim "breakindent" and
165 so on.
167 Other attributes:
169 :param search_field: An optional `SearchToolbar` object.
170 """
172 def __init__(
173 self,
174 text: str = "",
175 multiline: FilterOrBool = True,
176 password: FilterOrBool = False,
177 lexer: Lexer | None = None,
178 auto_suggest: AutoSuggest | None = None,
179 completer: Completer | None = None,
180 complete_while_typing: FilterOrBool = True,
181 validator: Validator | None = None,
182 accept_handler: BufferAcceptHandler | None = None,
183 history: History | None = None,
184 focusable: FilterOrBool = True,
185 focus_on_click: FilterOrBool = False,
186 wrap_lines: FilterOrBool = True,
187 read_only: FilterOrBool = False,
188 width: AnyDimension = None,
189 height: AnyDimension = None,
190 dont_extend_height: FilterOrBool = False,
191 dont_extend_width: FilterOrBool = False,
192 line_numbers: bool = False,
193 get_line_prefix: GetLinePrefixCallable | None = None,
194 scrollbar: bool = False,
195 style: str = "",
196 search_field: SearchToolbar | None = None,
197 preview_search: FilterOrBool = True,
198 prompt: AnyFormattedText = "",
199 input_processors: list[Processor] | None = None,
200 name: str = "",
201 ) -> None:
202 if search_field is None:
203 search_control = None
204 elif isinstance(search_field, SearchToolbar):
205 search_control = search_field.control
207 if input_processors is None:
208 input_processors = []
210 # Writeable attributes.
211 self.completer = completer
212 self.complete_while_typing = complete_while_typing
213 self.lexer = lexer
214 self.auto_suggest = auto_suggest
215 self.read_only = read_only
216 self.wrap_lines = wrap_lines
217 self.validator = validator
219 self.buffer = Buffer(
220 document=Document(text, 0),
221 multiline=multiline,
222 read_only=Condition(lambda: is_true(self.read_only)),
223 completer=DynamicCompleter(lambda: self.completer),
224 complete_while_typing=Condition(
225 lambda: is_true(self.complete_while_typing)
226 ),
227 validator=DynamicValidator(lambda: self.validator),
228 auto_suggest=DynamicAutoSuggest(lambda: self.auto_suggest),
229 accept_handler=accept_handler,
230 history=history,
231 name=name,
232 )
234 self.control = BufferControl(
235 buffer=self.buffer,
236 lexer=DynamicLexer(lambda: self.lexer),
237 input_processors=[
238 ConditionalProcessor(
239 AppendAutoSuggestion(), has_focus(self.buffer) & ~is_done
240 ),
241 ConditionalProcessor(
242 processor=PasswordProcessor(), filter=to_filter(password)
243 ),
244 BeforeInput(prompt, style="class:text-area.prompt"),
245 ]
246 + input_processors,
247 search_buffer_control=search_control,
248 preview_search=preview_search,
249 focusable=focusable,
250 focus_on_click=focus_on_click,
251 )
253 if multiline:
254 if scrollbar:
255 right_margins = [ScrollbarMargin(display_arrows=True)]
256 else:
257 right_margins = []
258 if line_numbers:
259 left_margins = [NumberedMargin()]
260 else:
261 left_margins = []
262 else:
263 height = D.exact(1)
264 left_margins = []
265 right_margins = []
267 style = "class:text-area " + style
269 # If no height was given, guarantee height of at least 1.
270 if height is None:
271 height = D(min=1)
273 self.window = Window(
274 height=height,
275 width=width,
276 dont_extend_height=dont_extend_height,
277 dont_extend_width=dont_extend_width,
278 content=self.control,
279 style=style,
280 wrap_lines=Condition(lambda: is_true(self.wrap_lines)),
281 left_margins=left_margins,
282 right_margins=right_margins,
283 get_line_prefix=get_line_prefix,
284 )
286 @property
287 def text(self) -> str:
288 """
289 The `Buffer` text.
290 """
291 return self.buffer.text
293 @text.setter
294 def text(self, value: str) -> None:
295 self.document = Document(value, 0)
297 @property
298 def document(self) -> Document:
299 """
300 The `Buffer` document (text + cursor position).
301 """
302 return self.buffer.document
304 @document.setter
305 def document(self, value: Document) -> None:
306 self.buffer.set_document(value, bypass_readonly=True)
308 @property
309 def accept_handler(self) -> BufferAcceptHandler | None:
310 """
311 The accept handler. Called when the user accepts the input.
312 """
313 return self.buffer.accept_handler
315 @accept_handler.setter
316 def accept_handler(self, value: BufferAcceptHandler) -> None:
317 self.buffer.accept_handler = value
319 def __pt_container__(self) -> Container:
320 return self.window
323class Label:
324 """
325 Widget that displays the given text. It is not editable or focusable.
327 :param text: Text to display. Can be multiline. All value types accepted by
328 :class:`prompt_toolkit.layout.FormattedTextControl` are allowed,
329 including a callable.
330 :param style: A style string.
331 :param width: When given, use this width, rather than calculating it from
332 the text size.
333 :param dont_extend_width: When `True`, don't take up more width than
334 preferred, i.e. the length of the longest line of
335 the text, or value of `width` parameter, if
336 given. `True` by default
337 :param dont_extend_height: When `True`, don't take up more width than the
338 preferred height, i.e. the number of lines of
339 the text. `False` by default.
340 """
342 def __init__(
343 self,
344 text: AnyFormattedText,
345 style: str = "",
346 width: AnyDimension = None,
347 dont_extend_height: bool = True,
348 dont_extend_width: bool = False,
349 align: WindowAlign | Callable[[], WindowAlign] = WindowAlign.LEFT,
350 # There is no cursor navigation in a label, so it makes sense to always
351 # wrap lines by default.
352 wrap_lines: FilterOrBool = True,
353 ) -> None:
354 self.text = text
356 def get_width() -> AnyDimension:
357 if width is None:
358 text_fragments = to_formatted_text(self.text)
359 text = fragment_list_to_text(text_fragments)
360 if text:
361 longest_line = max(get_cwidth(line) for line in text.splitlines())
362 else:
363 return D(preferred=0)
364 return D(preferred=longest_line)
365 else:
366 return width
368 self.formatted_text_control = FormattedTextControl(text=lambda: self.text)
370 self.window = Window(
371 content=self.formatted_text_control,
372 width=get_width,
373 height=D(min=1),
374 style="class:label " + style,
375 dont_extend_height=dont_extend_height,
376 dont_extend_width=dont_extend_width,
377 align=align,
378 wrap_lines=wrap_lines,
379 )
381 def __pt_container__(self) -> Container:
382 return self.window
385class Button:
386 """
387 Clickable button.
389 :param text: The caption for the button.
390 :param handler: `None` or callable. Called when the button is clicked. No
391 parameters are passed to this callable. Use for instance Python's
392 `functools.partial` to pass parameters to this callable if needed.
393 :param width: Width of the button.
394 """
396 def __init__(
397 self,
398 text: str,
399 handler: Callable[[], None] | None = None,
400 width: int = 12,
401 left_symbol: str = "<",
402 right_symbol: str = ">",
403 ) -> None:
404 self.text = text
405 self.left_symbol = left_symbol
406 self.right_symbol = right_symbol
407 self.handler = handler
408 self.width = width
409 self.control = FormattedTextControl(
410 self._get_text_fragments,
411 key_bindings=self._get_key_bindings(),
412 focusable=True,
413 )
415 def get_style() -> str:
416 if get_app().layout.has_focus(self):
417 return "class:button.focused"
418 else:
419 return "class:button"
421 # Note: `dont_extend_width` is False, because we want to allow buttons
422 # to take more space if the parent container provides more space.
423 # Otherwise, we will also truncate the text.
424 # Probably we need a better way here to adjust to width of the
425 # button to the text.
427 self.window = Window(
428 self.control,
429 align=WindowAlign.CENTER,
430 height=1,
431 width=width,
432 style=get_style,
433 dont_extend_width=False,
434 dont_extend_height=True,
435 )
437 def _get_text_fragments(self) -> StyleAndTextTuples:
438 width = self.width - (
439 get_cwidth(self.left_symbol) + get_cwidth(self.right_symbol)
440 )
441 text = (f"{{:^{width}}}").format(self.text)
443 def handler(mouse_event: MouseEvent) -> None:
444 if (
445 self.handler is not None
446 and mouse_event.event_type == MouseEventType.MOUSE_UP
447 ):
448 self.handler()
450 return [
451 ("class:button.arrow", self.left_symbol, handler),
452 ("[SetCursorPosition]", ""),
453 ("class:button.text", text, handler),
454 ("class:button.arrow", self.right_symbol, handler),
455 ]
457 def _get_key_bindings(self) -> KeyBindings:
458 "Key bindings for the Button."
459 kb = KeyBindings()
461 @kb.add(" ")
462 @kb.add("enter")
463 def _(event: E) -> None:
464 if self.handler is not None:
465 self.handler()
467 return kb
469 def __pt_container__(self) -> Container:
470 return self.window
473class Frame:
474 """
475 Draw a border around any container, optionally with a title text.
477 Changing the title and body of the frame is possible at runtime by
478 assigning to the `body` and `title` attributes of this class.
480 :param body: Another container object.
481 :param title: Text to be displayed in the top of the frame (can be formatted text).
482 :param style: Style string to be applied to this widget.
483 """
485 def __init__(
486 self,
487 body: AnyContainer,
488 title: AnyFormattedText = "",
489 style: str = "",
490 width: AnyDimension = None,
491 height: AnyDimension = None,
492 key_bindings: KeyBindings | None = None,
493 modal: bool = False,
494 ) -> None:
495 self.title = title
496 self.body = body
498 fill = partial(Window, style="class:frame.border")
499 style = "class:frame " + style
501 top_row_with_title = VSplit(
502 [
503 fill(width=1, height=1, char=Border.TOP_LEFT),
504 fill(char=Border.HORIZONTAL),
505 fill(width=1, height=1, char="|"),
506 # Notice: we use `Template` here, because `self.title` can be an
507 # `HTML` object for instance.
508 Label(
509 lambda: Template(" {} ").format(self.title),
510 style="class:frame.label",
511 dont_extend_width=True,
512 ),
513 fill(width=1, height=1, char="|"),
514 fill(char=Border.HORIZONTAL),
515 fill(width=1, height=1, char=Border.TOP_RIGHT),
516 ],
517 height=1,
518 )
520 top_row_without_title = VSplit(
521 [
522 fill(width=1, height=1, char=Border.TOP_LEFT),
523 fill(char=Border.HORIZONTAL),
524 fill(width=1, height=1, char=Border.TOP_RIGHT),
525 ],
526 height=1,
527 )
529 @Condition
530 def has_title() -> bool:
531 return bool(self.title)
533 self.container = HSplit(
534 [
535 ConditionalContainer(content=top_row_with_title, filter=has_title),
536 ConditionalContainer(content=top_row_without_title, filter=~has_title),
537 VSplit(
538 [
539 fill(width=1, char=Border.VERTICAL),
540 DynamicContainer(lambda: self.body),
541 fill(width=1, char=Border.VERTICAL),
542 # Padding is required to make sure that if the content is
543 # too small, the right frame border is still aligned.
544 ],
545 padding=0,
546 ),
547 VSplit(
548 [
549 fill(width=1, height=1, char=Border.BOTTOM_LEFT),
550 fill(char=Border.HORIZONTAL),
551 fill(width=1, height=1, char=Border.BOTTOM_RIGHT),
552 ],
553 # specifying height here will increase the rendering speed.
554 height=1,
555 ),
556 ],
557 width=width,
558 height=height,
559 style=style,
560 key_bindings=key_bindings,
561 modal=modal,
562 )
564 def __pt_container__(self) -> Container:
565 return self.container
568class Shadow:
569 """
570 Draw a shadow underneath/behind this container.
571 (This applies `class:shadow` the the cells under the shadow. The Style
572 should define the colors for the shadow.)
574 :param body: Another container object.
575 """
577 def __init__(self, body: AnyContainer) -> None:
578 self.container = FloatContainer(
579 content=body,
580 floats=[
581 Float(
582 bottom=-1,
583 height=1,
584 left=1,
585 right=-1,
586 transparent=True,
587 content=Window(style="class:shadow"),
588 ),
589 Float(
590 bottom=-1,
591 top=1,
592 width=1,
593 right=-1,
594 transparent=True,
595 content=Window(style="class:shadow"),
596 ),
597 ],
598 )
600 def __pt_container__(self) -> Container:
601 return self.container
604class Box:
605 """
606 Add padding around a container.
608 This also makes sure that the parent can provide more space than required by
609 the child. This is very useful when wrapping a small element with a fixed
610 size into a ``VSplit`` or ``HSplit`` object. The ``HSplit`` and ``VSplit``
611 try to make sure to adapt respectively the width and height, possibly
612 shrinking other elements. Wrapping something in a ``Box`` makes it flexible.
614 :param body: Another container object.
615 :param padding: The margin to be used around the body. This can be
616 overridden by `padding_left`, padding_right`, `padding_top` and
617 `padding_bottom`.
618 :param style: A style string.
619 :param char: Character to be used for filling the space around the body.
620 (This is supposed to be a character with a terminal width of 1.)
621 """
623 def __init__(
624 self,
625 body: AnyContainer,
626 padding: AnyDimension = None,
627 padding_left: AnyDimension = None,
628 padding_right: AnyDimension = None,
629 padding_top: AnyDimension = None,
630 padding_bottom: AnyDimension = None,
631 width: AnyDimension = None,
632 height: AnyDimension = None,
633 style: str = "",
634 char: None | str | Callable[[], str] = None,
635 modal: bool = False,
636 key_bindings: KeyBindings | None = None,
637 ) -> None:
638 if padding is None:
639 padding = D(preferred=0)
641 def get(value: AnyDimension) -> D:
642 if value is None:
643 value = padding
644 return to_dimension(value)
646 self.padding_left = get(padding_left)
647 self.padding_right = get(padding_right)
648 self.padding_top = get(padding_top)
649 self.padding_bottom = get(padding_bottom)
650 self.body = body
652 self.container = HSplit(
653 [
654 Window(height=self.padding_top, char=char),
655 VSplit(
656 [
657 Window(width=self.padding_left, char=char),
658 body,
659 Window(width=self.padding_right, char=char),
660 ]
661 ),
662 Window(height=self.padding_bottom, char=char),
663 ],
664 width=width,
665 height=height,
666 style=style,
667 modal=modal,
668 key_bindings=None,
669 )
671 def __pt_container__(self) -> Container:
672 return self.container
675_T = TypeVar("_T")
678class _DialogList(Generic[_T]):
679 """
680 Common code for `RadioList` and `CheckboxList`.
681 """
683 open_character: str = ""
684 close_character: str = ""
685 container_style: str = ""
686 default_style: str = ""
687 selected_style: str = ""
688 checked_style: str = ""
689 multiple_selection: bool = False
690 show_scrollbar: bool = True
692 def __init__(
693 self,
694 values: Sequence[tuple[_T, AnyFormattedText]],
695 default_values: Sequence[_T] | None = None,
696 ) -> None:
697 assert len(values) > 0
698 default_values = default_values or []
700 self.values = values
701 # current_values will be used in multiple_selection,
702 # current_value will be used otherwise.
703 keys: list[_T] = [value for (value, _) in values]
704 self.current_values: list[_T] = [
705 value for value in default_values if value in keys
706 ]
707 self.current_value: _T = (
708 default_values[0]
709 if len(default_values) and default_values[0] in keys
710 else values[0][0]
711 )
713 # Cursor index: take first selected item or first item otherwise.
714 if len(self.current_values) > 0:
715 self._selected_index = keys.index(self.current_values[0])
716 else:
717 self._selected_index = 0
719 # Key bindings.
720 kb = KeyBindings()
722 @kb.add("up")
723 def _up(event: E) -> None:
724 self._selected_index = max(0, self._selected_index - 1)
726 @kb.add("down")
727 def _down(event: E) -> None:
728 self._selected_index = min(len(self.values) - 1, self._selected_index + 1)
730 @kb.add("pageup")
731 def _pageup(event: E) -> None:
732 w = event.app.layout.current_window
733 if w.render_info:
734 self._selected_index = max(
735 0, self._selected_index - len(w.render_info.displayed_lines)
736 )
738 @kb.add("pagedown")
739 def _pagedown(event: E) -> None:
740 w = event.app.layout.current_window
741 if w.render_info:
742 self._selected_index = min(
743 len(self.values) - 1,
744 self._selected_index + len(w.render_info.displayed_lines),
745 )
747 @kb.add("enter")
748 @kb.add(" ")
749 def _click(event: E) -> None:
750 self._handle_enter()
752 @kb.add(Keys.Any)
753 def _find(event: E) -> None:
754 # We first check values after the selected value, then all values.
755 values = list(self.values)
756 for value in values[self._selected_index + 1 :] + values:
757 text = fragment_list_to_text(to_formatted_text(value[1])).lower()
759 if text.startswith(event.data.lower()):
760 self._selected_index = self.values.index(value)
761 return
763 # Control and window.
764 self.control = FormattedTextControl(
765 self._get_text_fragments, key_bindings=kb, focusable=True
766 )
768 self.window = Window(
769 content=self.control,
770 style=self.container_style,
771 right_margins=[
772 ConditionalMargin(
773 margin=ScrollbarMargin(display_arrows=True),
774 filter=Condition(lambda: self.show_scrollbar),
775 ),
776 ],
777 dont_extend_height=True,
778 )
780 def _handle_enter(self) -> None:
781 if self.multiple_selection:
782 val = self.values[self._selected_index][0]
783 if val in self.current_values:
784 self.current_values.remove(val)
785 else:
786 self.current_values.append(val)
787 else:
788 self.current_value = self.values[self._selected_index][0]
790 def _get_text_fragments(self) -> StyleAndTextTuples:
791 def mouse_handler(mouse_event: MouseEvent) -> None:
792 """
793 Set `_selected_index` and `current_value` according to the y
794 position of the mouse click event.
795 """
796 if mouse_event.event_type == MouseEventType.MOUSE_UP:
797 self._selected_index = mouse_event.position.y
798 self._handle_enter()
800 result: StyleAndTextTuples = []
801 for i, value in enumerate(self.values):
802 if self.multiple_selection:
803 checked = value[0] in self.current_values
804 else:
805 checked = value[0] == self.current_value
806 selected = i == self._selected_index
808 style = ""
809 if checked:
810 style += " " + self.checked_style
811 if selected:
812 style += " " + self.selected_style
814 result.append((style, self.open_character))
816 if selected:
817 result.append(("[SetCursorPosition]", ""))
819 if checked:
820 result.append((style, "*"))
821 else:
822 result.append((style, " "))
824 result.append((style, self.close_character))
825 result.append((self.default_style, " "))
826 result.extend(to_formatted_text(value[1], style=self.default_style))
827 result.append(("", "\n"))
829 # Add mouse handler to all fragments.
830 for i in range(len(result)):
831 result[i] = (result[i][0], result[i][1], mouse_handler)
833 result.pop() # Remove last newline.
834 return result
836 def __pt_container__(self) -> Container:
837 return self.window
840class RadioList(_DialogList[_T]):
841 """
842 List of radio buttons. Only one can be checked at the same time.
844 :param values: List of (value, label) tuples.
845 """
847 open_character = "("
848 close_character = ")"
849 container_style = "class:radio-list"
850 default_style = "class:radio"
851 selected_style = "class:radio-selected"
852 checked_style = "class:radio-checked"
853 multiple_selection = False
855 def __init__(
856 self,
857 values: Sequence[tuple[_T, AnyFormattedText]],
858 default: _T | None = None,
859 ) -> None:
860 if default is None:
861 default_values = None
862 else:
863 default_values = [default]
865 super().__init__(values, default_values=default_values)
868class CheckboxList(_DialogList[_T]):
869 """
870 List of checkbox buttons. Several can be checked at the same time.
872 :param values: List of (value, label) tuples.
873 """
875 open_character = "["
876 close_character = "]"
877 container_style = "class:checkbox-list"
878 default_style = "class:checkbox"
879 selected_style = "class:checkbox-selected"
880 checked_style = "class:checkbox-checked"
881 multiple_selection = True
884class Checkbox(CheckboxList[str]):
885 """Backward compatibility util: creates a 1-sized CheckboxList
887 :param text: the text
888 """
890 show_scrollbar = False
892 def __init__(self, text: AnyFormattedText = "", checked: bool = False) -> None:
893 values = [("value", text)]
894 super().__init__(values=values)
895 self.checked = checked
897 @property
898 def checked(self) -> bool:
899 return "value" in self.current_values
901 @checked.setter
902 def checked(self, value: bool) -> None:
903 if value:
904 self.current_values = ["value"]
905 else:
906 self.current_values = []
909class VerticalLine:
910 """
911 A simple vertical line with a width of 1.
912 """
914 def __init__(self) -> None:
915 self.window = Window(
916 char=Border.VERTICAL, style="class:line,vertical-line", width=1
917 )
919 def __pt_container__(self) -> Container:
920 return self.window
923class HorizontalLine:
924 """
925 A simple horizontal line with a height of 1.
926 """
928 def __init__(self) -> None:
929 self.window = Window(
930 char=Border.HORIZONTAL, style="class:line,horizontal-line", height=1
931 )
933 def __pt_container__(self) -> Container:
934 return self.window
937class ProgressBar:
938 def __init__(self) -> None:
939 self._percentage = 60
941 self.label = Label("60%")
942 self.container = FloatContainer(
943 content=Window(height=1),
944 floats=[
945 # We first draw the label, then the actual progress bar. Right
946 # now, this is the only way to have the colors of the progress
947 # bar appear on top of the label. The problem is that our label
948 # can't be part of any `Window` below.
949 Float(content=self.label, top=0, bottom=0),
950 Float(
951 left=0,
952 top=0,
953 right=0,
954 bottom=0,
955 content=VSplit(
956 [
957 Window(
958 style="class:progress-bar.used",
959 width=lambda: D(weight=int(self._percentage)),
960 ),
961 Window(
962 style="class:progress-bar",
963 width=lambda: D(weight=int(100 - self._percentage)),
964 ),
965 ]
966 ),
967 ),
968 ],
969 )
971 @property
972 def percentage(self) -> int:
973 return self._percentage
975 @percentage.setter
976 def percentage(self, value: int) -> None:
977 self._percentage = value
978 self.label.text = f"{value}%"
980 def __pt_container__(self) -> Container:
981 return self.container