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 unicode_literals 16 17from functools import partial 18 19import six 20 21from prompt_toolkit.application.current import get_app 22from prompt_toolkit.auto_suggest import DynamicAutoSuggest 23from prompt_toolkit.buffer import Buffer 24from prompt_toolkit.completion import DynamicCompleter 25from prompt_toolkit.document import Document 26from prompt_toolkit.filters import ( 27 Condition, 28 has_focus, 29 is_done, 30 is_true, 31 to_filter, 32) 33from prompt_toolkit.formatted_text import ( 34 Template, 35 is_formatted_text, 36 to_formatted_text, 37) 38from prompt_toolkit.formatted_text.utils import fragment_list_to_text 39from prompt_toolkit.key_binding.key_bindings import KeyBindings 40from prompt_toolkit.keys import Keys 41from prompt_toolkit.layout.containers import ( 42 ConditionalContainer, 43 DynamicContainer, 44 Float, 45 FloatContainer, 46 HSplit, 47 VSplit, 48 Window, 49 WindowAlign, 50 is_container, 51) 52from prompt_toolkit.layout.controls import BufferControl, FormattedTextControl 53from prompt_toolkit.layout.dimension import Dimension as D 54from prompt_toolkit.layout.dimension import is_dimension, to_dimension 55from prompt_toolkit.layout.margins import NumberedMargin, ScrollbarMargin 56from prompt_toolkit.layout.processors import ( 57 AppendAutoSuggestion, 58 BeforeInput, 59 ConditionalProcessor, 60 PasswordProcessor, 61) 62from prompt_toolkit.lexers import DynamicLexer 63from prompt_toolkit.mouse_events import MouseEventType 64from prompt_toolkit.utils import get_cwidth 65 66from .toolbars import SearchToolbar 67 68__all__ = [ 69 'TextArea', 70 'Label', 71 'Button', 72 'Frame', 73 'Shadow', 74 'Box', 75 'VerticalLine', 76 'HorizontalLine', 77 'RadioList', 78 79 'Checkbox', # XXX: refactor into CheckboxList. 80 'ProgressBar', 81] 82 83 84class Border: 85 " Box drawing characters. (Thin) " 86 HORIZONTAL = '\u2500' 87 VERTICAL = '\u2502' 88 TOP_LEFT = '\u250c' 89 TOP_RIGHT = '\u2510' 90 BOTTOM_LEFT = '\u2514' 91 BOTTOM_RIGHT = '\u2518' 92 93 94class TextArea(object): 95 """ 96 A simple input field. 97 98 This is a higher level abstraction on top of several other classes with 99 sane defaults. 100 101 This widget does have the most common options, but it does not intend to 102 cover every single use case. For more configurations options, you can 103 always build a text area manually, using a 104 :class:`~prompt_toolkit.buffer.Buffer`, 105 :class:`~prompt_toolkit.layout.BufferControl` and 106 :class:`~prompt_toolkit.layout.Window`. 107 108 Buffer attributes: 109 110 :param text: The initial text. 111 :param multiline: If True, allow multiline input. 112 :param completer: :class:`~prompt_toolkit.completion.Completer` instance 113 for auto completion. 114 :param complete_while_typing: Boolean. 115 :param accept_handler: Called when `Enter` is pressed (This should be a 116 callable that takes a buffer as input). 117 :param history: :class:`~prompt_toolkit.history.History` instance. 118 :param auto_suggest: :class:`~prompt_toolkit.auto_suggest.AutoSuggest` 119 instance for input suggestions. 120 121 BufferControl attributes: 122 123 :param password: When `True`, display using asterisks. 124 :param focusable: When `True`, allow this widget to receive the focus. 125 :param focus_on_click: When `True`, focus after mouse click. 126 :param input_processors: `None` or a list of 127 :class:`~prompt_toolkit.layout.Processor` objects. 128 129 Window attributes: 130 131 :param lexer: :class:`~prompt_toolkit.lexers.Lexer` instance for syntax 132 highlighting. 133 :param wrap_lines: When `True`, don't scroll horizontally, but wrap lines. 134 :param width: Window width. (:class:`~prompt_toolkit.layout.Dimension` object.) 135 :param height: Window height. (:class:`~prompt_toolkit.layout.Dimension` object.) 136 :param scrollbar: When `True`, display a scroll bar. 137 :param style: A style string. 138 :param dont_extend_width: When `True`, don't take up more width then the 139 preferred width reported by the control. 140 :param dont_extend_height: When `True`, don't take up more width then the 141 preferred height reported by the control. 142 :param get_line_prefix: None or a callable that returns formatted text to 143 be inserted before a line. It takes a line number (int) and a 144 wrap_count and returns formatted text. This can be used for 145 implementation of line continuations, things like Vim "breakindent" and 146 so on. 147 148 Other attributes: 149 150 :param search_field: An optional `SearchToolbar` object. 151 """ 152 def __init__(self, text='', multiline=True, password=False, 153 lexer=None, auto_suggest=None, completer=None, 154 complete_while_typing=True, accept_handler=None, history=None, 155 focusable=True, focus_on_click=False, wrap_lines=True, 156 read_only=False, width=None, height=None, 157 dont_extend_height=False, dont_extend_width=False, 158 line_numbers=False, get_line_prefix=None, scrollbar=False, 159 style='', search_field=None, preview_search=True, prompt='', 160 input_processors=None): 161 assert isinstance(text, six.text_type) 162 assert search_field is None or isinstance(search_field, SearchToolbar) 163 164 if search_field is None: 165 search_control = None 166 elif isinstance(search_field, SearchToolbar): 167 search_control = search_field.control 168 169 if input_processors is None: 170 input_processors = [] 171 172 # Writeable attributes. 173 self.completer = completer 174 self.complete_while_typing = complete_while_typing 175 self.lexer = lexer 176 self.auto_suggest = auto_suggest 177 self.read_only = read_only 178 self.wrap_lines = wrap_lines 179 180 self.buffer = Buffer( 181 document=Document(text, 0), 182 multiline=multiline, 183 read_only=Condition(lambda: is_true(self.read_only)), 184 completer=DynamicCompleter(lambda: self.completer), 185 complete_while_typing=Condition( 186 lambda: is_true(self.complete_while_typing)), 187 auto_suggest=DynamicAutoSuggest(lambda: self.auto_suggest), 188 accept_handler=accept_handler, 189 history=history) 190 191 self.control = BufferControl( 192 buffer=self.buffer, 193 lexer=DynamicLexer(lambda: self.lexer), 194 input_processors=[ 195 ConditionalProcessor( 196 AppendAutoSuggestion(), 197 has_focus(self.buffer) & ~is_done), 198 ConditionalProcessor( 199 processor=PasswordProcessor(), 200 filter=to_filter(password) 201 ), 202 BeforeInput(prompt, style='class:text-area.prompt'), 203 ] + input_processors, 204 search_buffer_control=search_control, 205 preview_search=preview_search, 206 focusable=focusable, 207 focus_on_click=focus_on_click) 208 209 if multiline: 210 if scrollbar: 211 right_margins = [ScrollbarMargin(display_arrows=True)] 212 else: 213 right_margins = [] 214 if line_numbers: 215 left_margins = [NumberedMargin()] 216 else: 217 left_margins = [] 218 else: 219 height = D.exact(1) 220 left_margins = [] 221 right_margins = [] 222 223 style = 'class:text-area ' + style 224 225 self.window = Window( 226 height=height, 227 width=width, 228 dont_extend_height=dont_extend_height, 229 dont_extend_width=dont_extend_width, 230 content=self.control, 231 style=style, 232 wrap_lines=Condition(lambda: is_true(self.wrap_lines)), 233 left_margins=left_margins, 234 right_margins=right_margins, 235 get_line_prefix=get_line_prefix) 236 237 @property 238 def text(self): 239 """ 240 The `Buffer` text. 241 """ 242 return self.buffer.text 243 244 @text.setter 245 def text(self, value): 246 self.buffer.set_document(Document(value, 0), bypass_readonly=True) 247 248 @property 249 def document(self): 250 """ 251 The `Buffer` document (text + cursor position). 252 """ 253 return self.buffer.document 254 255 @document.setter 256 def document(self, value): 257 self.buffer.document = value 258 259 @property 260 def accept_handler(self): 261 """ 262 The accept handler. Called when the user accepts the input. 263 """ 264 return self.buffer.accept_handler 265 266 @accept_handler.setter 267 def accept_handler(self, value): 268 self.buffer.accept_handler = value 269 270 def __pt_container__(self): 271 return self.window 272 273 274class Label(object): 275 """ 276 Widget that displays the given text. It is not editable or focusable. 277 278 :param text: The text to be displayed. (This can be multiline. This can be 279 formatted text as well.) 280 :param style: A style string. 281 :param width: When given, use this width, rather than calculating it from 282 the text size. 283 """ 284 def __init__(self, text, style='', width=None, 285 dont_extend_height=True, dont_extend_width=False): 286 assert is_formatted_text(text) 287 self.text = text 288 289 def get_width(): 290 if width is None: 291 text_fragments = to_formatted_text(self.text) 292 text = fragment_list_to_text(text_fragments) 293 if text: 294 longest_line = max(get_cwidth(line) for line in text.splitlines()) 295 else: 296 return D(preferred=0) 297 return D(preferred=longest_line) 298 else: 299 return width 300 301 self.formatted_text_control = FormattedTextControl( 302 text=lambda: self.text) 303 304 self.window = Window( 305 content=self.formatted_text_control, 306 width=get_width, 307 style='class:label ' + style, 308 dont_extend_height=dont_extend_height, 309 dont_extend_width=dont_extend_width) 310 311 def __pt_container__(self): 312 return self.window 313 314 315class Button(object): 316 """ 317 Clickable button. 318 319 :param text: The caption for the button. 320 :param handler: `None` or callable. Called when the button is clicked. 321 :param width: Width of the button. 322 """ 323 def __init__(self, text, handler=None, width=12): 324 assert isinstance(text, six.text_type) 325 assert handler is None or callable(handler) 326 assert isinstance(width, int) 327 328 self.text = text 329 self.handler = handler 330 self.width = width 331 self.control = FormattedTextControl( 332 self._get_text_fragments, 333 key_bindings=self._get_key_bindings(), 334 focusable=True) 335 336 def get_style(): 337 if get_app().layout.has_focus(self): 338 return 'class:button.focused' 339 else: 340 return 'class:button' 341 342 self.window = Window( 343 self.control, 344 align=WindowAlign.CENTER, 345 height=1, 346 width=width, 347 style=get_style, 348 dont_extend_width=True, 349 dont_extend_height=True) 350 351 def _get_text_fragments(self): 352 text = ('{:^%s}' % (self.width - 2)).format(self.text) 353 354 def handler(mouse_event): 355 if mouse_event.event_type == MouseEventType.MOUSE_UP: 356 self.handler() 357 358 return [ 359 ('class:button.arrow', '<', handler), 360 ('[SetCursorPosition]', ''), 361 ('class:button.text', text, handler), 362 ('class:button.arrow', '>', handler), 363 ] 364 365 def _get_key_bindings(self): 366 " Key bindings for the Button. " 367 kb = KeyBindings() 368 369 @kb.add(' ') 370 @kb.add('enter') 371 def _(event): 372 if self.handler is not None: 373 self.handler() 374 375 return kb 376 377 def __pt_container__(self): 378 return self.window 379 380 381class Frame(object): 382 """ 383 Draw a border around any container, optionally with a title text. 384 385 Changing the title and body of the frame is possible at runtime by 386 assigning to the `body` and `title` attributes of this class. 387 388 :param body: Another container object. 389 :param title: Text to be displayed in the top of the frame (can be formatted text). 390 :param style: Style string to be applied to this widget. 391 """ 392 def __init__(self, body, title='', style='', width=None, height=None, 393 key_bindings=None, modal=False): 394 assert is_container(body) 395 assert is_formatted_text(title) 396 assert isinstance(style, six.text_type) 397 assert is_dimension(width) 398 assert is_dimension(height) 399 assert key_bindings is None or isinstance(key_bindings, KeyBindings) 400 assert isinstance(modal, bool) 401 402 self.title = title 403 self.body = body 404 405 fill = partial(Window, style='class:frame.border') 406 style = 'class:frame ' + style 407 408 top_row_with_title = VSplit([ 409 fill(width=1, height=1, char=Border.TOP_LEFT), 410 fill(char=Border.HORIZONTAL), 411 fill(width=1, height=1, char='|'), 412 # Notice: we use `Template` here, because `self.title` can be an 413 # `HTML` object for instance. 414 Label(lambda: Template(' {} ').format(self.title), 415 style='class:frame.label', 416 dont_extend_width=True), 417 fill(width=1, height=1, char='|'), 418 fill(char=Border.HORIZONTAL), 419 fill(width=1, height=1, char=Border.TOP_RIGHT), 420 ], height=1) 421 422 top_row_without_title = VSplit([ 423 fill(width=1, height=1, char=Border.TOP_LEFT), 424 fill(char=Border.HORIZONTAL), 425 fill(width=1, height=1, char=Border.TOP_RIGHT), 426 ], height=1) 427 428 @Condition 429 def has_title(): 430 return bool(self.title) 431 432 self.container = HSplit([ 433 ConditionalContainer( 434 content=top_row_with_title, 435 filter=has_title), 436 ConditionalContainer( 437 content=top_row_without_title, 438 filter=~has_title), 439 VSplit([ 440 fill(width=1, char=Border.VERTICAL), 441 DynamicContainer(lambda: self.body), 442 fill(width=1, char=Border.VERTICAL), 443 # Padding is required to make sure that if the content is 444 # too small, the right frame border is still aligned. 445 ], padding=0), 446 VSplit([ 447 fill(width=1, height=1, char=Border.BOTTOM_LEFT), 448 fill(char=Border.HORIZONTAL), 449 fill(width=1, height=1, char=Border.BOTTOM_RIGHT), 450 ]), 451 ], width=width, height=height, style=style, key_bindings=key_bindings, 452 modal=modal) 453 454 def __pt_container__(self): 455 return self.container 456 457 458class Shadow(object): 459 """ 460 Draw a shadow underneath/behind this container. 461 (This applies `class:shadow` the the cells under the shadow. The Style 462 should define the colors for the shadow.) 463 464 :param body: Another container object. 465 """ 466 def __init__(self, body): 467 assert is_container(body) 468 469 self.container = FloatContainer( 470 content=body, 471 floats=[ 472 Float(bottom=-1, height=1, left=1, right=-1, 473 transparent=True, 474 content=Window(style='class:shadow')), 475 Float(bottom=-1, top=1, width=1, right=-1, 476 transparent=True, 477 content=Window(style='class:shadow')), 478 ] 479 ) 480 481 def __pt_container__(self): 482 return self.container 483 484 485class Box(object): 486 """ 487 Add padding around a container. 488 489 This also makes sure that the parent can provide more space than required by 490 the child. This is very useful when wrapping a small element with a fixed 491 size into a ``VSplit`` or ``HSplit`` object. The ``HSplit`` and ``VSplit`` 492 try to make sure to adapt respectively the width and height, possibly 493 shrinking other elements. Wrapping something in a ``Box`` makes it flexible. 494 495 :param body: Another container object. 496 :param padding: The margin to be used around the body. This can be 497 overridden by `padding_left`, padding_right`, `padding_top` and 498 `padding_bottom`. 499 :param style: A style string. 500 :param char: Character to be used for filling the space around the body. 501 (This is supposed to be a character with a terminal width of 1.) 502 """ 503 def __init__(self, body, padding=None, 504 padding_left=None, padding_right=None, 505 padding_top=None, padding_bottom=None, 506 width=None, height=None, 507 style='', char=None, modal=False, key_bindings=None): 508 assert is_container(body) 509 510 if padding is None: 511 padding = D(preferred=0) 512 513 def get(value): 514 if value is None: 515 value = padding 516 return to_dimension(value) 517 518 self.padding_left = get(padding_left) 519 self.padding_right = get(padding_right) 520 self.padding_top = get(padding_top) 521 self.padding_bottom = get(padding_bottom) 522 self.body = body 523 524 self.container = HSplit([ 525 Window(height=self.padding_top, char=char), 526 VSplit([ 527 Window(width=self.padding_left, char=char), 528 body, 529 Window(width=self.padding_right, char=char), 530 ]), 531 Window(height=self.padding_bottom, char=char), 532 ], 533 width=width, height=height, style=style, modal=modal, 534 key_bindings=None) 535 536 def __pt_container__(self): 537 return self.container 538 539 540class Checkbox(object): 541 def __init__(self, text=''): 542 assert is_formatted_text(text) 543 544 self.checked = True 545 546 kb = KeyBindings() 547 548 @kb.add(' ') 549 @kb.add('enter') 550 def _(event): 551 self.checked = not self.checked 552 553 self.control = FormattedTextControl( 554 self._get_text_fragments, 555 key_bindings=kb, 556 focusable=True) 557 558 self.window = Window( 559 width=3, content=self.control, height=1) 560 561 self.container = VSplit([ 562 self.window, 563 Label(text=Template(' {}').format(text)) 564 ], style='class:checkbox') 565 566 def _get_text_fragments(self): 567 result = [('', '[')] 568 result.append(('[SetCursorPosition]', '')) 569 570 if self.checked: 571 result.append(('', '*')) 572 else: 573 result.append(('', ' ')) 574 575 result.append(('', ']')) 576 577 return result 578 579 def __pt_container__(self): 580 return self.container 581 582 583class RadioList(object): 584 """ 585 List of radio buttons. Only one can be checked at the same time. 586 587 :param values: List of (value, label) tuples. 588 """ 589 def __init__(self, values): 590 assert isinstance(values, list) 591 assert len(values) > 0 592 assert all(isinstance(i, tuple) and len(i) == 2 593 for i in values) 594 595 self.values = values 596 self.current_value = values[0][0] 597 self._selected_index = 0 598 599 # Key bindings. 600 kb = KeyBindings() 601 602 @kb.add('up') 603 def _(event): 604 self._selected_index = max(0, self._selected_index - 1) 605 606 @kb.add('down') 607 def _(event): 608 self._selected_index = min( 609 len(self.values) - 1, self._selected_index + 1) 610 611 @kb.add('pageup') 612 def _(event): 613 w = event.app.layout.current_window 614 self._selected_index = max( 615 0, 616 self._selected_index - len(w.render_info.displayed_lines) 617 ) 618 619 @kb.add('pagedown') 620 def _(event): 621 w = event.app.layout.current_window 622 self._selected_index = min( 623 len(self.values) - 1, 624 self._selected_index + len(w.render_info.displayed_lines) 625 ) 626 627 @kb.add('enter') 628 @kb.add(' ') 629 def _(event): 630 self.current_value = self.values[self._selected_index][0] 631 632 @kb.add(Keys.Any) 633 def _(event): 634 # We first check values after the selected value, then all values. 635 for value in self.values[self._selected_index + 1:] + self.values: 636 if value[1].startswith(event.data): 637 self._selected_index = self.values.index(value) 638 return 639 640 # Control and window. 641 self.control = FormattedTextControl( 642 self._get_text_fragments, 643 key_bindings=kb, 644 focusable=True) 645 646 self.window = Window( 647 content=self.control, 648 style='class:radio-list', 649 right_margins=[ 650 ScrollbarMargin(display_arrows=True), 651 ], 652 dont_extend_height=True) 653 654 def _get_text_fragments(self): 655 def mouse_handler(mouse_event): 656 """ 657 Set `_selected_index` and `current_value` according to the y 658 position of the mouse click event. 659 """ 660 if mouse_event.event_type == MouseEventType.MOUSE_UP: 661 self._selected_index = mouse_event.position.y 662 self.current_value = self.values[self._selected_index][0] 663 664 result = [] 665 for i, value in enumerate(self.values): 666 checked = (value[0] == self.current_value) 667 selected = (i == self._selected_index) 668 669 style = '' 670 if checked: 671 style += ' class:radio-checked' 672 if selected: 673 style += ' class:radio-selected' 674 675 result.append((style, '(')) 676 677 if selected: 678 result.append(('[SetCursorPosition]', '')) 679 680 if checked: 681 result.append((style, '*')) 682 else: 683 result.append((style, ' ')) 684 685 result.append((style, ')')) 686 result.append(('class:radio', ' ')) 687 result.extend(to_formatted_text(value[1], style='class:radio')) 688 result.append(('', '\n')) 689 690 # Add mouse handler to all fragments. 691 for i in range(len(result)): 692 result[i] = (result[i][0], result[i][1], mouse_handler) 693 694 result.pop() # Remove last newline. 695 return result 696 697 def __pt_container__(self): 698 return self.window 699 700 701class VerticalLine(object): 702 """ 703 A simple vertical line with a width of 1. 704 """ 705 def __init__(self): 706 self.window = Window( 707 char=Border.VERTICAL, 708 style='class:line,vertical-line', 709 width=1) 710 711 def __pt_container__(self): 712 return self.window 713 714 715class HorizontalLine(object): 716 """ 717 A simple horizontal line with a height of 1. 718 """ 719 def __init__(self): 720 self.window = Window( 721 char=Border.HORIZONTAL, 722 style='class:line,horizontal-line', 723 height=1) 724 725 def __pt_container__(self): 726 return self.window 727 728 729class ProgressBar(object): 730 def __init__(self): 731 self._percentage = 60 732 733 self.label = Label('60%') 734 self.container = FloatContainer( 735 content=Window(height=1), 736 floats=[ 737 # We first draw the label, then the actual progress bar. Right 738 # now, this is the only way to have the colors of the progress 739 # bar appear on top of the label. The problem is that our label 740 # can't be part of any `Window` below. 741 Float(content=self.label, top=0, bottom=0), 742 743 Float(left=0, top=0, right=0, bottom=0, content=VSplit([ 744 Window(style='class:progress-bar.used', 745 width=lambda: D(weight=int(self._percentage))), 746 747 Window(style='class:progress-bar', 748 width=lambda: D(weight=int(100 - self._percentage))), 749 ])), 750 ]) 751 752 @property 753 def percentage(self): 754 return self._percentage 755 756 @percentage.setter 757 def percentage(self, value): 758 assert isinstance(value, int) 759 self._percentage = value 760 self.label.text = '{0}%'.format(value) 761 762 def __pt_container__(self): 763 return self.container 764