Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/prompt_toolkit/widgets/base.py: 36%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

328 statements  

1""" 

2Collection of reusable components for building full screen applications. 

3 

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. 

7 

8.. warning:: 

9 

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

15 

16from __future__ import annotations 

17 

18from functools import partial 

19from typing import Callable, Generic, Sequence, TypeVar 

20 

21from prompt_toolkit.application.current import get_app 

22from prompt_toolkit.auto_suggest import AutoSuggest, DynamicAutoSuggest 

23from prompt_toolkit.buffer import Buffer, BufferAcceptHandler 

24from prompt_toolkit.completion import Completer, DynamicCompleter 

25from prompt_toolkit.document import Document 

26from prompt_toolkit.filters import ( 

27 Condition, 

28 FilterOrBool, 

29 has_focus, 

30 is_done, 

31 is_true, 

32 to_filter, 

33) 

34from prompt_toolkit.formatted_text import ( 

35 AnyFormattedText, 

36 StyleAndTextTuples, 

37 Template, 

38 to_formatted_text, 

39) 

40from prompt_toolkit.formatted_text.utils import fragment_list_to_text 

41from prompt_toolkit.history import History 

42from prompt_toolkit.key_binding.key_bindings import KeyBindings 

43from prompt_toolkit.key_binding.key_processor import KeyPressEvent 

44from prompt_toolkit.keys import Keys 

45from prompt_toolkit.layout.containers import ( 

46 AnyContainer, 

47 ConditionalContainer, 

48 Container, 

49 DynamicContainer, 

50 Float, 

51 FloatContainer, 

52 HSplit, 

53 VSplit, 

54 Window, 

55 WindowAlign, 

56) 

57from prompt_toolkit.layout.controls import ( 

58 BufferControl, 

59 FormattedTextControl, 

60 GetLinePrefixCallable, 

61) 

62from prompt_toolkit.layout.dimension import AnyDimension 

63from prompt_toolkit.layout.dimension import Dimension as D 

64from prompt_toolkit.layout.margins import ( 

65 ConditionalMargin, 

66 NumberedMargin, 

67 ScrollbarMargin, 

68) 

69from prompt_toolkit.layout.processors import ( 

70 AppendAutoSuggestion, 

71 BeforeInput, 

72 ConditionalProcessor, 

73 PasswordProcessor, 

74 Processor, 

75) 

76from prompt_toolkit.lexers import DynamicLexer, Lexer 

77from prompt_toolkit.mouse_events import MouseEvent, MouseEventType 

78from prompt_toolkit.utils import get_cwidth 

79from prompt_toolkit.validation import DynamicValidator, Validator 

80 

81from .toolbars import SearchToolbar 

82 

83__all__ = [ 

84 "TextArea", 

85 "Label", 

86 "Button", 

87 "Frame", 

88 "Shadow", 

89 "Box", 

90 "VerticalLine", 

91 "HorizontalLine", 

92 "RadioList", 

93 "CheckboxList", 

94 "Checkbox", # backward compatibility 

95 "ProgressBar", 

96] 

97 

98E = KeyPressEvent 

99 

100 

101class Border: 

102 "Box drawing characters. (Thin)" 

103 

104 HORIZONTAL = "\u2500" 

105 VERTICAL = "\u2502" 

106 TOP_LEFT = "\u250c" 

107 TOP_RIGHT = "\u2510" 

108 BOTTOM_LEFT = "\u2514" 

109 BOTTOM_RIGHT = "\u2518" 

110 

111 

112class TextArea: 

113 """ 

114 A simple input field. 

115 

116 This is a higher level abstraction on top of several other classes with 

117 sane defaults. 

118 

119 This widget does have the most common options, but it does not intend to 

120 cover every single use case. For more configurations options, you can 

121 always build a text area manually, using a 

122 :class:`~prompt_toolkit.buffer.Buffer`, 

123 :class:`~prompt_toolkit.layout.BufferControl` and 

124 :class:`~prompt_toolkit.layout.Window`. 

125 

126 Buffer attributes: 

127 

128 :param text: The initial text. 

129 :param multiline: If True, allow multiline input. 

130 :param completer: :class:`~prompt_toolkit.completion.Completer` instance 

131 for auto completion. 

132 :param complete_while_typing: Boolean. 

133 :param accept_handler: Called when `Enter` is pressed (This should be a 

134 callable that takes a buffer as input). 

135 :param history: :class:`~prompt_toolkit.history.History` instance. 

136 :param auto_suggest: :class:`~prompt_toolkit.auto_suggest.AutoSuggest` 

137 instance for input suggestions. 

138 

139 BufferControl attributes: 

140 

141 :param password: When `True`, display using asterisks. 

142 :param focusable: When `True`, allow this widget to receive the focus. 

143 :param focus_on_click: When `True`, focus after mouse click. 

144 :param input_processors: `None` or a list of 

145 :class:`~prompt_toolkit.layout.Processor` objects. 

146 :param validator: `None` or a :class:`~prompt_toolkit.validation.Validator` 

147 object. 

148 

149 Window attributes: 

150 

151 :param lexer: :class:`~prompt_toolkit.lexers.Lexer` instance for syntax 

152 highlighting. 

153 :param wrap_lines: When `True`, don't scroll horizontally, but wrap lines. 

154 :param width: Window width. (:class:`~prompt_toolkit.layout.Dimension` object.) 

155 :param height: Window height. (:class:`~prompt_toolkit.layout.Dimension` object.) 

156 :param scrollbar: When `True`, display a scroll bar. 

157 :param style: A style string. 

158 :param dont_extend_width: When `True`, don't take up more width then the 

159 preferred width reported by the control. 

160 :param dont_extend_height: When `True`, don't take up more width then the 

161 preferred height reported by the control. 

162 :param get_line_prefix: None or a callable that returns formatted text to 

163 be inserted before a line. It takes a line number (int) and a 

164 wrap_count and returns formatted text. This can be used for 

165 implementation of line continuations, things like Vim "breakindent" and 

166 so on. 

167 

168 Other attributes: 

169 

170 :param search_field: An optional `SearchToolbar` object. 

171 """ 

172 

173 def __init__( 

174 self, 

175 text: str = "", 

176 multiline: FilterOrBool = True, 

177 password: FilterOrBool = False, 

178 lexer: Lexer | None = None, 

179 auto_suggest: AutoSuggest | None = None, 

180 completer: Completer | None = None, 

181 complete_while_typing: FilterOrBool = True, 

182 validator: Validator | None = None, 

183 accept_handler: BufferAcceptHandler | None = None, 

184 history: History | None = None, 

185 focusable: FilterOrBool = True, 

186 focus_on_click: FilterOrBool = False, 

187 wrap_lines: FilterOrBool = True, 

188 read_only: FilterOrBool = False, 

189 width: AnyDimension = None, 

190 height: AnyDimension = None, 

191 dont_extend_height: FilterOrBool = False, 

192 dont_extend_width: FilterOrBool = False, 

193 line_numbers: bool = False, 

194 get_line_prefix: GetLinePrefixCallable | None = None, 

195 scrollbar: bool = False, 

196 style: str = "", 

197 search_field: SearchToolbar | None = None, 

198 preview_search: FilterOrBool = True, 

199 prompt: AnyFormattedText = "", 

200 input_processors: list[Processor] | None = None, 

201 name: str = "", 

202 ) -> None: 

203 if search_field is None: 

204 search_control = None 

205 elif isinstance(search_field, SearchToolbar): 

206 search_control = search_field.control 

207 

208 if input_processors is None: 

209 input_processors = [] 

210 

211 # Writeable attributes. 

212 self.completer = completer 

213 self.complete_while_typing = complete_while_typing 

214 self.lexer = lexer 

215 self.auto_suggest = auto_suggest 

216 self.read_only = read_only 

217 self.wrap_lines = wrap_lines 

218 self.validator = validator 

219 

220 self.buffer = Buffer( 

221 document=Document(text, 0), 

222 multiline=multiline, 

223 read_only=Condition(lambda: is_true(self.read_only)), 

224 completer=DynamicCompleter(lambda: self.completer), 

225 complete_while_typing=Condition( 

226 lambda: is_true(self.complete_while_typing) 

227 ), 

228 validator=DynamicValidator(lambda: self.validator), 

229 auto_suggest=DynamicAutoSuggest(lambda: self.auto_suggest), 

230 accept_handler=accept_handler, 

231 history=history, 

232 name=name, 

233 ) 

234 

235 self.control = BufferControl( 

236 buffer=self.buffer, 

237 lexer=DynamicLexer(lambda: self.lexer), 

238 input_processors=[ 

239 ConditionalProcessor( 

240 AppendAutoSuggestion(), has_focus(self.buffer) & ~is_done 

241 ), 

242 ConditionalProcessor( 

243 processor=PasswordProcessor(), filter=to_filter(password) 

244 ), 

245 BeforeInput(prompt, style="class:text-area.prompt"), 

246 ] 

247 + input_processors, 

248 search_buffer_control=search_control, 

249 preview_search=preview_search, 

250 focusable=focusable, 

251 focus_on_click=focus_on_click, 

252 ) 

253 

254 if multiline: 

255 if scrollbar: 

256 right_margins = [ScrollbarMargin(display_arrows=True)] 

257 else: 

258 right_margins = [] 

259 if line_numbers: 

260 left_margins = [NumberedMargin()] 

261 else: 

262 left_margins = [] 

263 else: 

264 height = D.exact(1) 

265 left_margins = [] 

266 right_margins = [] 

267 

268 style = "class:text-area " + style 

269 

270 # If no height was given, guarantee height of at least 1. 

271 if height is None: 

272 height = D(min=1) 

273 

274 self.window = Window( 

275 height=height, 

276 width=width, 

277 dont_extend_height=dont_extend_height, 

278 dont_extend_width=dont_extend_width, 

279 content=self.control, 

280 style=style, 

281 wrap_lines=Condition(lambda: is_true(self.wrap_lines)), 

282 left_margins=left_margins, 

283 right_margins=right_margins, 

284 get_line_prefix=get_line_prefix, 

285 ) 

286 

287 @property 

288 def text(self) -> str: 

289 """ 

290 The `Buffer` text. 

291 """ 

292 return self.buffer.text 

293 

294 @text.setter 

295 def text(self, value: str) -> None: 

296 self.document = Document(value, 0) 

297 

298 @property 

299 def document(self) -> Document: 

300 """ 

301 The `Buffer` document (text + cursor position). 

302 """ 

303 return self.buffer.document 

304 

305 @document.setter 

306 def document(self, value: Document) -> None: 

307 self.buffer.set_document(value, bypass_readonly=True) 

308 

309 @property 

310 def accept_handler(self) -> BufferAcceptHandler | None: 

311 """ 

312 The accept handler. Called when the user accepts the input. 

313 """ 

314 return self.buffer.accept_handler 

315 

316 @accept_handler.setter 

317 def accept_handler(self, value: BufferAcceptHandler) -> None: 

318 self.buffer.accept_handler = value 

319 

320 def __pt_container__(self) -> Container: 

321 return self.window 

322 

323 

324class Label: 

325 """ 

326 Widget that displays the given text. It is not editable or focusable. 

327 

328 :param text: Text to display. Can be multiline. All value types accepted by 

329 :class:`prompt_toolkit.layout.FormattedTextControl` are allowed, 

330 including a callable. 

331 :param style: A style string. 

332 :param width: When given, use this width, rather than calculating it from 

333 the text size. 

334 :param dont_extend_width: When `True`, don't take up more width than 

335 preferred, i.e. the length of the longest line of 

336 the text, or value of `width` parameter, if 

337 given. `True` by default 

338 :param dont_extend_height: When `True`, don't take up more width than the 

339 preferred height, i.e. the number of lines of 

340 the text. `False` by default. 

341 """ 

342 

343 def __init__( 

344 self, 

345 text: AnyFormattedText, 

346 style: str = "", 

347 width: AnyDimension = None, 

348 dont_extend_height: bool = True, 

349 dont_extend_width: bool = False, 

350 align: WindowAlign | Callable[[], WindowAlign] = WindowAlign.LEFT, 

351 # There is no cursor navigation in a label, so it makes sense to always 

352 # wrap lines by default. 

353 wrap_lines: FilterOrBool = True, 

354 ) -> None: 

355 self.text = text 

356 

357 def get_width() -> AnyDimension: 

358 if width is None: 

359 text_fragments = to_formatted_text(self.text) 

360 text = fragment_list_to_text(text_fragments) 

361 if text: 

362 longest_line = max(get_cwidth(line) for line in text.splitlines()) 

363 else: 

364 return D(preferred=0) 

365 return D(preferred=longest_line) 

366 else: 

367 return width 

368 

369 self.formatted_text_control = FormattedTextControl(text=lambda: self.text) 

370 

371 self.window = Window( 

372 content=self.formatted_text_control, 

373 width=get_width, 

374 height=D(min=1), 

375 style="class:label " + style, 

376 dont_extend_height=dont_extend_height, 

377 dont_extend_width=dont_extend_width, 

378 align=align, 

379 wrap_lines=wrap_lines, 

380 ) 

381 

382 def __pt_container__(self) -> Container: 

383 return self.window 

384 

385 

386class Button: 

387 """ 

388 Clickable button. 

389 

390 :param text: The caption for the button. 

391 :param handler: `None` or callable. Called when the button is clicked. No 

392 parameters are passed to this callable. Use for instance Python's 

393 `functools.partial` to pass parameters to this callable if needed. 

394 :param width: Width of the button. 

395 """ 

396 

397 def __init__( 

398 self, 

399 text: str, 

400 handler: Callable[[], None] | None = None, 

401 width: int = 12, 

402 left_symbol: str = "<", 

403 right_symbol: str = ">", 

404 ) -> None: 

405 self.text = text 

406 self.left_symbol = left_symbol 

407 self.right_symbol = right_symbol 

408 self.handler = handler 

409 self.width = width 

410 self.control = FormattedTextControl( 

411 self._get_text_fragments, 

412 key_bindings=self._get_key_bindings(), 

413 focusable=True, 

414 ) 

415 

416 def get_style() -> str: 

417 if get_app().layout.has_focus(self): 

418 return "class:button.focused" 

419 else: 

420 return "class:button" 

421 

422 # Note: `dont_extend_width` is False, because we want to allow buttons 

423 # to take more space if the parent container provides more space. 

424 # Otherwise, we will also truncate the text. 

425 # Probably we need a better way here to adjust to width of the 

426 # button to the text. 

427 

428 self.window = Window( 

429 self.control, 

430 align=WindowAlign.CENTER, 

431 height=1, 

432 width=width, 

433 style=get_style, 

434 dont_extend_width=False, 

435 dont_extend_height=True, 

436 ) 

437 

438 def _get_text_fragments(self) -> StyleAndTextTuples: 

439 width = self.width - ( 

440 get_cwidth(self.left_symbol) + get_cwidth(self.right_symbol) 

441 ) 

442 text = (f"{{:^{width}}}").format(self.text) 

443 

444 def handler(mouse_event: MouseEvent) -> None: 

445 if ( 

446 self.handler is not None 

447 and mouse_event.event_type == MouseEventType.MOUSE_UP 

448 ): 

449 self.handler() 

450 

451 return [ 

452 ("class:button.arrow", self.left_symbol, handler), 

453 ("[SetCursorPosition]", ""), 

454 ("class:button.text", text, handler), 

455 ("class:button.arrow", self.right_symbol, handler), 

456 ] 

457 

458 def _get_key_bindings(self) -> KeyBindings: 

459 "Key bindings for the Button." 

460 kb = KeyBindings() 

461 

462 @kb.add(" ") 

463 @kb.add("enter") 

464 def _(event: E) -> None: 

465 if self.handler is not None: 

466 self.handler() 

467 

468 return kb 

469 

470 def __pt_container__(self) -> Container: 

471 return self.window 

472 

473 

474class Frame: 

475 """ 

476 Draw a border around any container, optionally with a title text. 

477 

478 Changing the title and body of the frame is possible at runtime by 

479 assigning to the `body` and `title` attributes of this class. 

480 

481 :param body: Another container object. 

482 :param title: Text to be displayed in the top of the frame (can be formatted text). 

483 :param style: Style string to be applied to this widget. 

484 """ 

485 

486 def __init__( 

487 self, 

488 body: AnyContainer, 

489 title: AnyFormattedText = "", 

490 style: str = "", 

491 width: AnyDimension = None, 

492 height: AnyDimension = None, 

493 key_bindings: KeyBindings | None = None, 

494 modal: bool = False, 

495 ) -> None: 

496 self.title = title 

497 self.body = body 

498 

499 fill = partial(Window, style="class:frame.border") 

500 style = "class:frame " + style 

501 

502 top_row_with_title = VSplit( 

503 [ 

504 fill(width=1, height=1, char=Border.TOP_LEFT), 

505 fill(char=Border.HORIZONTAL), 

506 fill(width=1, height=1, char="|"), 

507 # Notice: we use `Template` here, because `self.title` can be an 

508 # `HTML` object for instance. 

509 Label( 

510 lambda: Template(" {} ").format(self.title), 

511 style="class:frame.label", 

512 dont_extend_width=True, 

513 ), 

514 fill(width=1, height=1, char="|"), 

515 fill(char=Border.HORIZONTAL), 

516 fill(width=1, height=1, char=Border.TOP_RIGHT), 

517 ], 

518 height=1, 

519 ) 

520 

521 top_row_without_title = VSplit( 

522 [ 

523 fill(width=1, height=1, char=Border.TOP_LEFT), 

524 fill(char=Border.HORIZONTAL), 

525 fill(width=1, height=1, char=Border.TOP_RIGHT), 

526 ], 

527 height=1, 

528 ) 

529 

530 @Condition 

531 def has_title() -> bool: 

532 return bool(self.title) 

533 

534 self.container = HSplit( 

535 [ 

536 ConditionalContainer(content=top_row_with_title, filter=has_title), 

537 ConditionalContainer(content=top_row_without_title, filter=~has_title), 

538 VSplit( 

539 [ 

540 fill(width=1, char=Border.VERTICAL), 

541 DynamicContainer(lambda: self.body), 

542 fill(width=1, char=Border.VERTICAL), 

543 # Padding is required to make sure that if the content is 

544 # too small, the right frame border is still aligned. 

545 ], 

546 padding=0, 

547 ), 

548 VSplit( 

549 [ 

550 fill(width=1, height=1, char=Border.BOTTOM_LEFT), 

551 fill(char=Border.HORIZONTAL), 

552 fill(width=1, height=1, char=Border.BOTTOM_RIGHT), 

553 ], 

554 # specifying height here will increase the rendering speed. 

555 height=1, 

556 ), 

557 ], 

558 width=width, 

559 height=height, 

560 style=style, 

561 key_bindings=key_bindings, 

562 modal=modal, 

563 ) 

564 

565 def __pt_container__(self) -> Container: 

566 return self.container 

567 

568 

569class Shadow: 

570 """ 

571 Draw a shadow underneath/behind this container. 

572 (This applies `class:shadow` the the cells under the shadow. The Style 

573 should define the colors for the shadow.) 

574 

575 :param body: Another container object. 

576 """ 

577 

578 def __init__(self, body: AnyContainer) -> None: 

579 self.container = FloatContainer( 

580 content=body, 

581 floats=[ 

582 Float( 

583 bottom=-1, 

584 height=1, 

585 left=1, 

586 right=-1, 

587 transparent=True, 

588 content=Window(style="class:shadow"), 

589 ), 

590 Float( 

591 bottom=-1, 

592 top=1, 

593 width=1, 

594 right=-1, 

595 transparent=True, 

596 content=Window(style="class:shadow"), 

597 ), 

598 ], 

599 ) 

600 

601 def __pt_container__(self) -> Container: 

602 return self.container 

603 

604 

605class Box: 

606 """ 

607 Add padding around a container. 

608 

609 This also makes sure that the parent can provide more space than required by 

610 the child. This is very useful when wrapping a small element with a fixed 

611 size into a ``VSplit`` or ``HSplit`` object. The ``HSplit`` and ``VSplit`` 

612 try to make sure to adapt respectively the width and height, possibly 

613 shrinking other elements. Wrapping something in a ``Box`` makes it flexible. 

614 

615 :param body: Another container object. 

616 :param padding: The margin to be used around the body. This can be 

617 overridden by `padding_left`, padding_right`, `padding_top` and 

618 `padding_bottom`. 

619 :param style: A style string. 

620 :param char: Character to be used for filling the space around the body. 

621 (This is supposed to be a character with a terminal width of 1.) 

622 """ 

623 

624 def __init__( 

625 self, 

626 body: AnyContainer, 

627 padding: AnyDimension = None, 

628 padding_left: AnyDimension = None, 

629 padding_right: AnyDimension = None, 

630 padding_top: AnyDimension = None, 

631 padding_bottom: AnyDimension = None, 

632 width: AnyDimension = None, 

633 height: AnyDimension = None, 

634 style: str = "", 

635 char: None | str | Callable[[], str] = None, 

636 modal: bool = False, 

637 key_bindings: KeyBindings | None = None, 

638 ) -> None: 

639 self.padding = padding 

640 self.padding_left = padding_left 

641 self.padding_right = padding_right 

642 self.padding_top = padding_top 

643 self.padding_bottom = padding_bottom 

644 self.body = body 

645 

646 def left() -> AnyDimension: 

647 if self.padding_left is None: 

648 return self.padding 

649 return self.padding_left 

650 

651 def right() -> AnyDimension: 

652 if self.padding_right is None: 

653 return self.padding 

654 return self.padding_right 

655 

656 def top() -> AnyDimension: 

657 if self.padding_top is None: 

658 return self.padding 

659 return self.padding_top 

660 

661 def bottom() -> AnyDimension: 

662 if self.padding_bottom is None: 

663 return self.padding 

664 return self.padding_bottom 

665 

666 self.container = HSplit( 

667 [ 

668 Window(height=top, char=char), 

669 VSplit( 

670 [ 

671 Window(width=left, char=char), 

672 body, 

673 Window(width=right, char=char), 

674 ] 

675 ), 

676 Window(height=bottom, char=char), 

677 ], 

678 width=width, 

679 height=height, 

680 style=style, 

681 modal=modal, 

682 key_bindings=None, 

683 ) 

684 

685 def __pt_container__(self) -> Container: 

686 return self.container 

687 

688 

689_T = TypeVar("_T") 

690 

691 

692class _DialogList(Generic[_T]): 

693 """ 

694 Common code for `RadioList` and `CheckboxList`. 

695 """ 

696 

697 open_character: str = "" 

698 close_character: str = "" 

699 container_style: str = "" 

700 default_style: str = "" 

701 selected_style: str = "" 

702 checked_style: str = "" 

703 multiple_selection: bool = False 

704 show_scrollbar: bool = True 

705 

706 def __init__( 

707 self, 

708 values: Sequence[tuple[_T, AnyFormattedText]], 

709 default_values: Sequence[_T] | None = None, 

710 ) -> None: 

711 assert len(values) > 0 

712 default_values = default_values or [] 

713 

714 self.values = values 

715 # current_values will be used in multiple_selection, 

716 # current_value will be used otherwise. 

717 keys: list[_T] = [value for (value, _) in values] 

718 self.current_values: list[_T] = [ 

719 value for value in default_values if value in keys 

720 ] 

721 self.current_value: _T = ( 

722 default_values[0] 

723 if len(default_values) and default_values[0] in keys 

724 else values[0][0] 

725 ) 

726 

727 # Cursor index: take first selected item or first item otherwise. 

728 if len(self.current_values) > 0: 

729 self._selected_index = keys.index(self.current_values[0]) 

730 else: 

731 self._selected_index = 0 

732 

733 # Key bindings. 

734 kb = KeyBindings() 

735 

736 @kb.add("up") 

737 def _up(event: E) -> None: 

738 self._selected_index = max(0, self._selected_index - 1) 

739 

740 @kb.add("down") 

741 def _down(event: E) -> None: 

742 self._selected_index = min(len(self.values) - 1, self._selected_index + 1) 

743 

744 @kb.add("pageup") 

745 def _pageup(event: E) -> None: 

746 w = event.app.layout.current_window 

747 if w.render_info: 

748 self._selected_index = max( 

749 0, self._selected_index - len(w.render_info.displayed_lines) 

750 ) 

751 

752 @kb.add("pagedown") 

753 def _pagedown(event: E) -> None: 

754 w = event.app.layout.current_window 

755 if w.render_info: 

756 self._selected_index = min( 

757 len(self.values) - 1, 

758 self._selected_index + len(w.render_info.displayed_lines), 

759 ) 

760 

761 @kb.add("enter") 

762 @kb.add(" ") 

763 def _click(event: E) -> None: 

764 self._handle_enter() 

765 

766 @kb.add(Keys.Any) 

767 def _find(event: E) -> None: 

768 # We first check values after the selected value, then all values. 

769 values = list(self.values) 

770 for value in values[self._selected_index + 1 :] + values: 

771 text = fragment_list_to_text(to_formatted_text(value[1])).lower() 

772 

773 if text.startswith(event.data.lower()): 

774 self._selected_index = self.values.index(value) 

775 return 

776 

777 # Control and window. 

778 self.control = FormattedTextControl( 

779 self._get_text_fragments, key_bindings=kb, focusable=True 

780 ) 

781 

782 self.window = Window( 

783 content=self.control, 

784 style=self.container_style, 

785 right_margins=[ 

786 ConditionalMargin( 

787 margin=ScrollbarMargin(display_arrows=True), 

788 filter=Condition(lambda: self.show_scrollbar), 

789 ), 

790 ], 

791 dont_extend_height=True, 

792 ) 

793 

794 def _handle_enter(self) -> None: 

795 if self.multiple_selection: 

796 val = self.values[self._selected_index][0] 

797 if val in self.current_values: 

798 self.current_values.remove(val) 

799 else: 

800 self.current_values.append(val) 

801 else: 

802 self.current_value = self.values[self._selected_index][0] 

803 

804 def _get_text_fragments(self) -> StyleAndTextTuples: 

805 def mouse_handler(mouse_event: MouseEvent) -> None: 

806 """ 

807 Set `_selected_index` and `current_value` according to the y 

808 position of the mouse click event. 

809 """ 

810 if mouse_event.event_type == MouseEventType.MOUSE_UP: 

811 self._selected_index = mouse_event.position.y 

812 self._handle_enter() 

813 

814 result: StyleAndTextTuples = [] 

815 for i, value in enumerate(self.values): 

816 if self.multiple_selection: 

817 checked = value[0] in self.current_values 

818 else: 

819 checked = value[0] == self.current_value 

820 selected = i == self._selected_index 

821 

822 style = "" 

823 if checked: 

824 style += " " + self.checked_style 

825 if selected: 

826 style += " " + self.selected_style 

827 

828 result.append((style, self.open_character)) 

829 

830 if selected: 

831 result.append(("[SetCursorPosition]", "")) 

832 

833 if checked: 

834 result.append((style, "*")) 

835 else: 

836 result.append((style, " ")) 

837 

838 result.append((style, self.close_character)) 

839 result.append((self.default_style, " ")) 

840 result.extend(to_formatted_text(value[1], style=self.default_style)) 

841 result.append(("", "\n")) 

842 

843 # Add mouse handler to all fragments. 

844 for i in range(len(result)): 

845 result[i] = (result[i][0], result[i][1], mouse_handler) 

846 

847 result.pop() # Remove last newline. 

848 return result 

849 

850 def __pt_container__(self) -> Container: 

851 return self.window 

852 

853 

854class RadioList(_DialogList[_T]): 

855 """ 

856 List of radio buttons. Only one can be checked at the same time. 

857 

858 :param values: List of (value, label) tuples. 

859 """ 

860 

861 open_character = "(" 

862 close_character = ")" 

863 container_style = "class:radio-list" 

864 default_style = "class:radio" 

865 selected_style = "class:radio-selected" 

866 checked_style = "class:radio-checked" 

867 multiple_selection = False 

868 

869 def __init__( 

870 self, 

871 values: Sequence[tuple[_T, AnyFormattedText]], 

872 default: _T | None = None, 

873 ) -> None: 

874 if default is None: 

875 default_values = None 

876 else: 

877 default_values = [default] 

878 

879 super().__init__(values, default_values=default_values) 

880 

881 

882class CheckboxList(_DialogList[_T]): 

883 """ 

884 List of checkbox buttons. Several can be checked at the same time. 

885 

886 :param values: List of (value, label) tuples. 

887 """ 

888 

889 open_character = "[" 

890 close_character = "]" 

891 container_style = "class:checkbox-list" 

892 default_style = "class:checkbox" 

893 selected_style = "class:checkbox-selected" 

894 checked_style = "class:checkbox-checked" 

895 multiple_selection = True 

896 

897 

898class Checkbox(CheckboxList[str]): 

899 """Backward compatibility util: creates a 1-sized CheckboxList 

900 

901 :param text: the text 

902 """ 

903 

904 show_scrollbar = False 

905 

906 def __init__(self, text: AnyFormattedText = "", checked: bool = False) -> None: 

907 values = [("value", text)] 

908 super().__init__(values=values) 

909 self.checked = checked 

910 

911 @property 

912 def checked(self) -> bool: 

913 return "value" in self.current_values 

914 

915 @checked.setter 

916 def checked(self, value: bool) -> None: 

917 if value: 

918 self.current_values = ["value"] 

919 else: 

920 self.current_values = [] 

921 

922 

923class VerticalLine: 

924 """ 

925 A simple vertical line with a width of 1. 

926 """ 

927 

928 def __init__(self) -> None: 

929 self.window = Window( 

930 char=Border.VERTICAL, style="class:line,vertical-line", width=1 

931 ) 

932 

933 def __pt_container__(self) -> Container: 

934 return self.window 

935 

936 

937class HorizontalLine: 

938 """ 

939 A simple horizontal line with a height of 1. 

940 """ 

941 

942 def __init__(self) -> None: 

943 self.window = Window( 

944 char=Border.HORIZONTAL, style="class:line,horizontal-line", height=1 

945 ) 

946 

947 def __pt_container__(self) -> Container: 

948 return self.window 

949 

950 

951class ProgressBar: 

952 def __init__(self) -> None: 

953 self._percentage = 60 

954 

955 self.label = Label("60%") 

956 self.container = FloatContainer( 

957 content=Window(height=1), 

958 floats=[ 

959 # We first draw the label, then the actual progress bar. Right 

960 # now, this is the only way to have the colors of the progress 

961 # bar appear on top of the label. The problem is that our label 

962 # can't be part of any `Window` below. 

963 Float(content=self.label, top=0, bottom=0), 

964 Float( 

965 left=0, 

966 top=0, 

967 right=0, 

968 bottom=0, 

969 content=VSplit( 

970 [ 

971 Window( 

972 style="class:progress-bar.used", 

973 width=lambda: D(weight=int(self._percentage)), 

974 ), 

975 Window( 

976 style="class:progress-bar", 

977 width=lambda: D(weight=int(100 - self._percentage)), 

978 ), 

979 ] 

980 ), 

981 ), 

982 ], 

983 ) 

984 

985 @property 

986 def percentage(self) -> int: 

987 return self._percentage 

988 

989 @percentage.setter 

990 def percentage(self, value: int) -> None: 

991 self._percentage = value 

992 self.label.text = f"{value}%" 

993 

994 def __pt_container__(self) -> Container: 

995 return self.container