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

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

15from __future__ import annotations 

16 

17from functools import partial 

18from typing import Callable, Generic, Sequence, TypeVar 

19 

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 

79 

80from .toolbars import SearchToolbar 

81 

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] 

96 

97E = KeyPressEvent 

98 

99 

100class Border: 

101 "Box drawing characters. (Thin)" 

102 

103 HORIZONTAL = "\u2500" 

104 VERTICAL = "\u2502" 

105 TOP_LEFT = "\u250c" 

106 TOP_RIGHT = "\u2510" 

107 BOTTOM_LEFT = "\u2514" 

108 BOTTOM_RIGHT = "\u2518" 

109 

110 

111class TextArea: 

112 """ 

113 A simple input field. 

114 

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

116 sane defaults. 

117 

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`. 

124 

125 Buffer attributes: 

126 

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. 

137 

138 BufferControl attributes: 

139 

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. 

147 

148 Window attributes: 

149 

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. 

166 

167 Other attributes: 

168 

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

170 """ 

171 

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 

206 

207 if input_processors is None: 

208 input_processors = [] 

209 

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 

218 

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 ) 

233 

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 ) 

252 

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 = [] 

266 

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

268 

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

270 if height is None: 

271 height = D(min=1) 

272 

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 ) 

285 

286 @property 

287 def text(self) -> str: 

288 """ 

289 The `Buffer` text. 

290 """ 

291 return self.buffer.text 

292 

293 @text.setter 

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

295 self.document = Document(value, 0) 

296 

297 @property 

298 def document(self) -> Document: 

299 """ 

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

301 """ 

302 return self.buffer.document 

303 

304 @document.setter 

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

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

307 

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 

314 

315 @accept_handler.setter 

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

317 self.buffer.accept_handler = value 

318 

319 def __pt_container__(self) -> Container: 

320 return self.window 

321 

322 

323class Label: 

324 """ 

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

326 

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

341 

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 

355 

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 

367 

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

369 

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 ) 

380 

381 def __pt_container__(self) -> Container: 

382 return self.window 

383 

384 

385class Button: 

386 """ 

387 Clickable button. 

388 

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

395 

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 ) 

414 

415 def get_style() -> str: 

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

417 return "class:button.focused" 

418 else: 

419 return "class:button" 

420 

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. 

426 

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 ) 

436 

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) 

442 

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() 

449 

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 ] 

456 

457 def _get_key_bindings(self) -> KeyBindings: 

458 "Key bindings for the Button." 

459 kb = KeyBindings() 

460 

461 @kb.add(" ") 

462 @kb.add("enter") 

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

464 if self.handler is not None: 

465 self.handler() 

466 

467 return kb 

468 

469 def __pt_container__(self) -> Container: 

470 return self.window 

471 

472 

473class Frame: 

474 """ 

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

476 

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. 

479 

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

484 

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 

497 

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

499 style = "class:frame " + style 

500 

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 ) 

519 

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 ) 

528 

529 @Condition 

530 def has_title() -> bool: 

531 return bool(self.title) 

532 

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 ) 

563 

564 def __pt_container__(self) -> Container: 

565 return self.container 

566 

567 

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.) 

573 

574 :param body: Another container object. 

575 """ 

576 

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 ) 

599 

600 def __pt_container__(self) -> Container: 

601 return self.container 

602 

603 

604class Box: 

605 """ 

606 Add padding around a container. 

607 

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. 

613 

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

622 

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) 

640 

641 def get(value: AnyDimension) -> D: 

642 if value is None: 

643 value = padding 

644 return to_dimension(value) 

645 

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 

651 

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 ) 

670 

671 def __pt_container__(self) -> Container: 

672 return self.container 

673 

674 

675_T = TypeVar("_T") 

676 

677 

678class _DialogList(Generic[_T]): 

679 """ 

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

681 """ 

682 

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 

691 

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 [] 

699 

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 ) 

712 

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 

718 

719 # Key bindings. 

720 kb = KeyBindings() 

721 

722 @kb.add("up") 

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

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

725 

726 @kb.add("down") 

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

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

729 

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 ) 

737 

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 ) 

746 

747 @kb.add("enter") 

748 @kb.add(" ") 

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

750 self._handle_enter() 

751 

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() 

758 

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

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

761 return 

762 

763 # Control and window. 

764 self.control = FormattedTextControl( 

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

766 ) 

767 

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 ) 

779 

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] 

789 

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() 

799 

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 

807 

808 style = "" 

809 if checked: 

810 style += " " + self.checked_style 

811 if selected: 

812 style += " " + self.selected_style 

813 

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

815 

816 if selected: 

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

818 

819 if checked: 

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

821 else: 

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

823 

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

828 

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) 

832 

833 result.pop() # Remove last newline. 

834 return result 

835 

836 def __pt_container__(self) -> Container: 

837 return self.window 

838 

839 

840class RadioList(_DialogList[_T]): 

841 """ 

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

843 

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

845 """ 

846 

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 

854 

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] 

864 

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

866 

867 

868class CheckboxList(_DialogList[_T]): 

869 """ 

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

871 

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

873 """ 

874 

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 

882 

883 

884class Checkbox(CheckboxList[str]): 

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

886 

887 :param text: the text 

888 """ 

889 

890 show_scrollbar = False 

891 

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

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

894 super().__init__(values=values) 

895 self.checked = checked 

896 

897 @property 

898 def checked(self) -> bool: 

899 return "value" in self.current_values 

900 

901 @checked.setter 

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

903 if value: 

904 self.current_values = ["value"] 

905 else: 

906 self.current_values = [] 

907 

908 

909class VerticalLine: 

910 """ 

911 A simple vertical line with a width of 1. 

912 """ 

913 

914 def __init__(self) -> None: 

915 self.window = Window( 

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

917 ) 

918 

919 def __pt_container__(self) -> Container: 

920 return self.window 

921 

922 

923class HorizontalLine: 

924 """ 

925 A simple horizontal line with a height of 1. 

926 """ 

927 

928 def __init__(self) -> None: 

929 self.window = Window( 

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

931 ) 

932 

933 def __pt_container__(self) -> Container: 

934 return self.window 

935 

936 

937class ProgressBar: 

938 def __init__(self) -> None: 

939 self._percentage = 60 

940 

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 ) 

970 

971 @property 

972 def percentage(self) -> int: 

973 return self._percentage 

974 

975 @percentage.setter 

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

977 self._percentage = value 

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

979 

980 def __pt_container__(self) -> Container: 

981 return self.container