pytermgui.widgets.base
The basic building blocks making up the Widget system.
1""" 2The basic building blocks making up the Widget system. 3""" 4 5# The classes defined here need more than 7 instance attributes, 6# and there is no cyclic import during runtime. 7# pylint: disable=too-many-instance-attributes, cyclic-import 8 9from __future__ import annotations 10 11from copy import deepcopy 12from inspect import signature 13from typing import Any, Callable, Generator, Iterator, Optional, Type, Union 14 15from ..ansi_interface import MouseAction, MouseEvent, reset 16from ..enums import HorizontalAlignment, SizePolicy, WidgetChange 17from ..helpers import break_line 18from ..input import keys 19from ..parser import markup 20from ..regex import real_length 21from ..terminal import Terminal, get_terminal 22from . import styles as w_styles 23 24__all__ = ["Widget", "Label"] 25 26BoundCallback = Callable[..., Any] 27WidgetType = Union["Widget", Type["Widget"]] 28 29 30def _set_obj_or_cls_style( 31 obj_or_cls: Type[Widget] | Widget, key: str, value: w_styles.StyleType 32) -> Type[Widget] | Widget: 33 """Sets a style for an object or class 34 35 Args: 36 obj_or_cls: The Widget instance or type to update. 37 key: The style key. 38 value: The new style. 39 40 Returns: 41 Type[Widget] | Widget: The updated class. 42 43 Raises: 44 See `pytermgui.widgets.styles.StyleManager`. 45 """ 46 47 obj_or_cls.styles[key] = value 48 49 return obj_or_cls 50 51 52def _set_obj_or_cls_char( 53 obj_or_cls: Type[Widget] | Widget, key: str, value: w_styles.CharType 54) -> Type[Widget] | Widget: 55 """Sets a char for an object or class 56 57 Args: 58 obj_or_cls: The Widget instance or type to update. 59 key: The char key. 60 value: The new char. 61 62 Returns: 63 Type[Widget] | Widget: The updated class. 64 65 Raises: 66 KeyError: The char key provided is invalid. 67 """ 68 69 if not key in obj_or_cls.chars.keys(): 70 raise KeyError(f"Char {key} is not valid for {obj_or_cls}!") 71 72 obj_or_cls.chars[key] = value 73 74 return obj_or_cls 75 76 77class Widget: # pylint: disable=too-many-public-methods 78 """The base of the Widget system""" 79 80 set_style = classmethod(_set_obj_or_cls_style) 81 set_char = classmethod(_set_obj_or_cls_char) 82 83 styles = w_styles.StyleManager() 84 """Default styles for this class""" 85 86 chars: dict[str, w_styles.CharType] = {} 87 """Default characters for this class""" 88 89 keys: dict[str, set[str]] = {} 90 """Groups of keys that are used in `handle_key`""" 91 92 serialized: list[str] = [ 93 "id", 94 "pos", 95 "depth", 96 "width", 97 "height", 98 "selected_index", 99 "selectables_length", 100 ] 101 """Fields of widget that shall be serialized by `pytermgui.serializer.Serializer`""" 102 103 # This class is loaded after this module, 104 # and thus mypy doesn't see its existence. 105 _id_manager: Optional["_IDManager"] = None # type: ignore 106 107 is_bindable = False 108 """Allow binding support""" 109 110 size_policy = SizePolicy.get_default() 111 """`pytermgui.enums.SizePolicy` to set widget's width according to""" 112 113 parent_align = HorizontalAlignment.get_default() 114 """`pytermgui.enums.HorizontalAlignment` to align widget by""" 115 116 from_data: Callable[..., Widget | list[Widget] | None] 117 118 # We cannot import boxes here due to cyclic imports. 119 box: Any 120 121 def __init__(self, **attrs: Any) -> None: 122 """Initialize object""" 123 124 self.set_style = lambda key, value: _set_obj_or_cls_style(self, key, value) 125 self.set_char = lambda key, value: _set_obj_or_cls_char(self, key, value) 126 127 self.width = 1 128 self.height = 1 129 self.pos = self.terminal.origin 130 131 self.depth = 0 132 133 self.styles = type(self).styles.branch(self) 134 self.chars = type(self).chars.copy() 135 136 self.parent: Widget | None = None 137 self.selected_index: int | None = None 138 139 self._selectables_length = 0 140 self._id: Optional[str] = None 141 self._serialized_fields = type(self).serialized 142 self._bindings: dict[str | Type[MouseEvent], tuple[BoundCallback, str]] = {} 143 self._relative_width: float | None = None 144 self._previous_state: tuple[tuple[int, int], list[str]] | None = None 145 146 self.positioned_line_buffer: list[tuple[tuple[int, int], str]] = [] 147 148 for attr, value in attrs.items(): 149 setattr(self, attr, value) 150 151 def __repr__(self) -> str: 152 """Return repr string of this widget. 153 154 Returns: 155 Whatever this widget's `debug` method gives. 156 """ 157 158 return self.debug() 159 160 def __fancy_repr__(self) -> Generator[str, None, None]: 161 """Yields the repr of this object, then a preview of it.""" 162 163 yield self.debug() 164 yield "\n\n" 165 yield { 166 "text": "\n".join((line + reset() for line in self.get_lines())), 167 "highlight": False, 168 } 169 170 def __iter__(self) -> Iterator[Widget]: 171 """Return self for iteration""" 172 173 yield self 174 175 @property 176 def bindings(self) -> dict[str | Type[MouseEvent], tuple[BoundCallback, str]]: 177 """Gets a copy of the bindings internal dictionary. 178 179 Returns: 180 A copy of the internal bindings dictionary, such as: 181 182 ``` 183 { 184 "*": (star_callback, "This is a callback activated when '*' is pressed.") 185 } 186 ``` 187 """ 188 189 return self._bindings.copy() 190 191 @property 192 def id(self) -> Optional[str]: # pylint: disable=invalid-name 193 """Gets this widget's id property 194 195 Returns: 196 The id string if one is present, None otherwise. 197 """ 198 199 return self._id 200 201 @id.setter 202 def id(self, value: str) -> None: # pylint: disable=invalid-name 203 """Registers a widget to the Widget._id_manager. 204 205 If this widget already had an id, the old value is deregistered 206 before the new one is assigned. 207 208 Args: 209 value: The new id this widget will be registered as. 210 """ 211 212 if self._id == value: 213 return 214 215 manager = Widget._id_manager 216 assert manager is not None 217 218 old = manager.get_id(self) 219 if old is not None: 220 manager.deregister(old) 221 222 self._id = value 223 manager.register(self) 224 225 @property 226 def selectables_length(self) -> int: 227 """Gets how many selectables this widget contains. 228 229 Returns: 230 An integer describing the amount of selectables in this widget. 231 """ 232 233 return self._selectables_length 234 235 @property 236 def selectables(self) -> list[tuple[Widget, int]]: 237 """Gets a list of all selectables within this widget 238 239 Returns: 240 A list of tuples. In the default implementation this will be 241 a list of one tuple, containing a reference to `self`, as well 242 as the lowest index, 0. 243 """ 244 245 return [(self, 0)] 246 247 @property 248 def is_selectable(self) -> bool: 249 """Determines whether this widget has any selectables. 250 251 Returns: 252 A boolean, representing `self.selectables_length != 0`. 253 """ 254 255 return self.selectables_length != 0 256 257 @property 258 def static_width(self) -> int: 259 """Allows for a shorter way of setting a width, and SizePolicy.STATIC. 260 261 Args: 262 value: The new width integer. 263 264 Returns: 265 None, as this is setter only. 266 """ 267 268 return None # type: ignore 269 270 @static_width.setter 271 def static_width(self, value: int) -> None: 272 """See the static_width getter.""" 273 274 self.width = value 275 self.size_policy = SizePolicy.STATIC 276 277 @property 278 def relative_width(self) -> float | None: 279 """Sets this widget's relative width, and changes size_policy to RELATIVE. 280 281 The value is clamped to 1.0. 282 283 If a Container holds a width of 30, and it has a subwidget with a relative 284 width of 0.5, it will be resized to 15. 285 286 Args: 287 value: The multiplier to apply to the parent's width. 288 289 Returns: 290 The current relative_width. 291 """ 292 293 return self._relative_width 294 295 @relative_width.setter 296 def relative_width(self, value: float) -> None: 297 """See the relative_width getter.""" 298 299 self.size_policy = SizePolicy.RELATIVE 300 self._relative_width = min(1.0, value) 301 302 @property 303 def terminal(self) -> Terminal: 304 """Returns the current global terminal instance.""" 305 306 return get_terminal() 307 308 def get_change(self) -> WidgetChange | None: 309 """Determines whether widget lines changed since the last call to this function.""" 310 311 lines = self.get_lines() 312 313 if self._previous_state is None: 314 self._previous_state = (self.width, self.height), lines 315 return WidgetChange.LINES 316 317 lines = self.get_lines() 318 (old_width, old_height), old_lines = self._previous_state 319 320 self._previous_state = (self.width, self.height), lines 321 322 if old_width != self.width and old_height != self.height: 323 return WidgetChange.SIZE 324 325 if old_width != self.width: 326 return WidgetChange.WIDTH 327 328 if old_height != self.height: 329 return WidgetChange.HEIGHT 330 331 if old_lines != lines: 332 return WidgetChange.LINES 333 334 return None 335 336 def contains(self, pos: tuple[int, int]) -> bool: 337 """Determines whether widget contains `pos`. 338 339 Args: 340 pos: Position to compare. 341 342 Returns: 343 Boolean describing whether the position is inside 344 this widget. 345 """ 346 347 rect = self.pos, ( 348 self.pos[0] + self.width, 349 self.pos[1] + self.height, 350 ) 351 352 (left, top), (right, bottom) = rect 353 354 return left <= pos[0] < right and top <= pos[1] < bottom 355 356 def handle_mouse(self, event: MouseEvent) -> bool: 357 """Tries to call the most specific mouse handler function available. 358 359 This function looks for a set of mouse action handlers. Each handler follows 360 the format 361 362 on_{event_name} 363 364 For example, the handler triggered on MouseAction.LEFT_CLICK would be 365 `on_left_click`. If no handler is found nothing is done. 366 367 You can also define more general handlers, for example to group left & right 368 clicks you can use `on_click`, and to catch both up and down scroll you can use 369 `on_scroll`. General handlers are only used if they are the most specific ones, 370 i.e. there is no "specific" handler. 371 372 Args: 373 event: The event to handle. 374 375 Returns: 376 Whether the parent of this widget should treat it as one to "stick" events 377 to, e.g. to keep sending mouse events to it. One can "unstick" a widget by 378 returning False in the handler. 379 """ 380 381 def _get_names(action: MouseAction) -> tuple[str, ...]: 382 if action.value in ["hover", "release"]: 383 return (action.value,) 384 385 parts = action.value.split("_") 386 387 # left click & right click 388 if parts[0] in ["left", "right"]: 389 return (action.value, parts[1]) 390 391 # scroll up & down 392 return (action.value, parts[0]) 393 394 possible_names = _get_names(event.action) 395 for name in possible_names: 396 if hasattr(self, f"on_{name}"): 397 handle = getattr(self, f"on_{name}") 398 399 return handle(event) 400 401 return False 402 403 def handle_key(self, key: str) -> bool: 404 """Handles a mouse event, returning its success. 405 406 Args: 407 key: String representation of input string. 408 The `pytermgui.input.keys` object can be 409 used to retrieve special keys. 410 411 Returns: 412 A boolean describing whether the key was handled. 413 """ 414 415 return False and hasattr(self, key) 416 417 def serialize(self) -> dict[str, Any]: 418 """Serializes a widget. 419 420 The fields looked at are defined `Widget.serialized`. Note that 421 this method is not very commonly used at the moment, so it might 422 not have full functionality in non-nuclear widgets. 423 424 Returns: 425 Dictionary of widget attributes. The dictionary will always 426 have a `type` field. Any styles are converted into markup 427 strings during serialization, so they can be loaded again in 428 their original form. 429 430 Example return: 431 ``` 432 { 433 "type": "Label", 434 "value": "[210 bold]I am a title", 435 "parent_align": 0, 436 ... 437 } 438 ``` 439 """ 440 441 fields = self._serialized_fields 442 443 out: dict[str, Any] = {"type": type(self).__name__} 444 for key in fields: 445 # Detect styled values 446 if key.startswith("*"): 447 style = True 448 key = key[1:] 449 else: 450 style = False 451 452 value = getattr(self, key) 453 454 # Convert styled value into markup 455 if style: 456 style_call = self._get_style(key) 457 if isinstance(value, list): 458 out[key] = [markup.get_markup(style_call(char)) for char in value] 459 else: 460 out[key] = markup.get_markup(style_call(value)) 461 462 continue 463 464 out[key] = value 465 466 # The chars need to be handled separately 467 out["chars"] = {} 468 for key, value in self.chars.items(): 469 style_call = self._get_style(key) 470 471 if isinstance(value, list): 472 out["chars"][key] = [ 473 markup.get_markup(style_call(char)) for char in value 474 ] 475 else: 476 out["chars"][key] = markup.get_markup(style_call(value)) 477 478 return out 479 480 def copy(self) -> Widget: 481 """Creates a deep copy of this widget""" 482 483 return deepcopy(self) 484 485 def _get_style(self, key: str) -> w_styles.DepthlessStyleType: 486 """Gets style call from its key. 487 488 This is analogous to using `self.styles.{key}` 489 490 Args: 491 key: A key into the widget's style manager. 492 493 Returns: 494 A `pytermgui.styles.StyleCall` object containing the referenced 495 style. StyleCall objects should only be used internally inside a 496 widget. 497 498 Raises: 499 KeyError: Style key is invalid. 500 """ 501 502 return self.styles[key] 503 504 def _get_char(self, key: str) -> w_styles.CharType: 505 """Gets character from its key. 506 507 Args: 508 key: A key into the widget's chars dictionary. 509 510 Returns: 511 Either a `list[str]` or a simple `str`, depending on the character. 512 513 Raises: 514 KeyError: Style key is invalid. 515 """ 516 517 chars = self.chars[key] 518 if isinstance(chars, str): 519 return chars 520 521 return chars.copy() 522 523 def get_lines(self) -> list[str]: 524 """Gets lines representing this widget. 525 526 These lines have to be equal to the widget in length. All 527 widgets must provide this method. Make sure to keep it performant, 528 as it will be called very often, often multiple times per WindowManager frame. 529 530 Any longer actions should be done outside of this method, and only their 531 result should be looked up here. 532 533 Returns: 534 Nothing by default. 535 536 Raises: 537 NotImplementedError: As this method is required for **all** widgets, not 538 having it defined will raise NotImplementedError. 539 """ 540 541 raise NotImplementedError(f"get_lines() is not defined for type {type(self)}.") 542 543 def bind( 544 self, key: str, action: BoundCallback, description: Optional[str] = None 545 ) -> None: 546 """Binds an action to a keypress. 547 548 This function is only called by implementations above this layer. To use this 549 functionality use `pytermgui.window_manager.WindowManager`, or write your own 550 custom layer. 551 552 Special keys: 553 - keys.ANY_KEY: Any and all keypresses execute this binding. 554 - keys.MouseAction: Any and all mouse inputs execute this binding. 555 556 Args: 557 key: The key that the action will be bound to. 558 action: The action executed when the key is pressed. 559 description: An optional description for this binding. It is not really 560 used anywhere, but you can provide a helper menu and display them. 561 562 Raises: 563 TypeError: This widget is not bindable, i.e. widget.is_bindable == False. 564 """ 565 566 if not self.is_bindable: 567 raise TypeError(f"Widget of type {type(self)} does not accept bindings.") 568 569 if description is None: 570 description = f"Binding of {key} to {action}" 571 572 self._bindings[key] = (action, description) 573 574 def unbind(self, key: str) -> None: 575 """Unbinds the given key.""" 576 577 del self._bindings[key] 578 579 def execute_binding(self, key: Any) -> bool: 580 """Executes a binding belonging to key, when present. 581 582 Use this method inside custom widget `handle_keys` methods, or to run a callback 583 without its corresponding key having been pressed. 584 585 Args: 586 key: Usually a string, indexing into the `_bindings` dictionary. These are the 587 same strings as defined in `Widget.bind`. 588 589 Returns: 590 True if the binding was found, False otherwise. Bindings will always be 591 executed if they are found. 592 """ 593 594 # Execute special binding 595 if keys.ANY_KEY in self._bindings: 596 method, _ = self._bindings[keys.ANY_KEY] 597 method(self, key) 598 599 if key in self._bindings: 600 method, _ = self._bindings[key] 601 method(self, key) 602 603 return True 604 605 return False 606 607 def select(self, index: int | None = None) -> None: 608 """Selects a part of this Widget. 609 610 Args: 611 index: The index to select. 612 613 Raises: 614 TypeError: This widget has no selectables, i.e. widget.is_selectable == False. 615 """ 616 617 if not self.is_selectable: 618 raise TypeError(f"Object of type {type(self)} has no selectables.") 619 620 if index is not None: 621 index = min(max(0, index), self.selectables_length - 1) 622 self.selected_index = index 623 624 def print(self) -> None: 625 """Prints this widget""" 626 627 for line in self.get_lines(): 628 print(line) 629 630 def debug(self) -> str: 631 """Returns identifiable information about this widget. 632 633 This method is used to easily differentiate between widgets. By default, all widget's 634 __repr__ method is an alias to this. The signature of each widget is used to generate 635 the return value. 636 637 Returns: 638 A string almost exactly matching the line of code that could have defined the widget. 639 640 Example return: 641 642 ``` 643 Container(Label(value="This is a label", padding=0), 644 Button(label="This is a button", padding=0), **attrs) 645 ``` 646 647 """ 648 649 constructor = "(" 650 for name in signature(getattr(self, "__init__")).parameters: 651 current = "" 652 if name == "attrs": 653 current += "**attrs" 654 continue 655 656 if len(constructor) > 1: 657 current += ", " 658 659 current += name 660 661 attr = getattr(self, name, None) 662 if attr is None: 663 continue 664 665 current += "=" 666 667 if isinstance(attr, str): 668 current += f'"{attr}"' 669 else: 670 current += str(attr) 671 672 constructor += current 673 674 constructor += ")" 675 676 return type(self).__name__ + constructor 677 678 679class Label(Widget): 680 """A Widget to display a string 681 682 By default, this widget uses `pytermgui.widgets.styles.MARKUP`. This 683 allows it to house markup text that is parsed before display, such as: 684 685 ```python3 686 import pytermgui as ptg 687 688 with ptg.alt_buffer(): 689 root = ptg.Container( 690 ptg.Label("[italic 141 bold]This is some [green]fancy [white inverse]text!") 691 ) 692 root.print() 693 ptg.getch() 694 ``` 695 696 <p style="text-align: center"> 697 <img 698 src="https://github.com/bczsalba/pytermgui/blob/master/assets/docs/widgets/label.png?raw=true" 699 width=100%> 700 </p> 701 """ 702 703 serialized = Widget.serialized + ["*value", "align", "padding"] 704 styles = w_styles.StyleManager(value=w_styles.MARKUP) 705 706 def __init__( 707 self, 708 value: str = "", 709 style: str | w_styles.StyleValue = "", 710 padding: int = 0, 711 non_first_padding: int = 0, 712 **attrs: Any, 713 ) -> None: 714 """Initializes a Label. 715 716 Args: 717 value: The value of this string. Using the default value style 718 (`pytermgui.widgets.styles.MARKUP`), 719 style: A pre-set value for self.styles.value. 720 padding: The number of space (" ") characters to prepend to every line after 721 line breaking. 722 non_first_padding: The number of space characters to prepend to every 723 non-first line of `get_lines`. This is applied on top of `padding`. 724 """ 725 726 super().__init__(**attrs) 727 728 self.value = value 729 self.padding = padding 730 self.non_first_padding = non_first_padding 731 self.width = real_length(value) + self.padding 732 733 if style != "": 734 self.styles.value = style 735 736 def get_lines(self) -> list[str]: 737 """Get lines representing this Label, breaking lines as necessary""" 738 739 lines = [] 740 limit = self.width - self.padding 741 broken = break_line( 742 self.styles.value(self.value), 743 limit=limit, 744 non_first_limit=limit - self.non_first_padding, 745 ) 746 747 for i, line in enumerate(broken): 748 if i == 0: 749 lines.append(self.padding * " " + line) 750 continue 751 752 lines.append(self.padding * " " + self.non_first_padding * " " + line) 753 754 return lines or [""] 755 756 757class ScrollableWidget(Widget): 758 """A widget with some scrolling helper methods. 759 760 This is not an implementation of the scrolling behaviour itself, just the 761 user-facing API for it. 762 763 It provides a `_scroll_offset` attribute, which is an integer describing the current 764 scroll state offset from the top, as well as some methods to modify the state.""" 765 766 def __init__(self, **attrs: Any) -> None: 767 """Initializes the scrollable widget.""" 768 769 super().__init__(**attrs) 770 771 self._max_scroll = 0 772 self._scroll_offset = 0 773 774 def scroll(self, offset: int) -> bool: 775 """Scrolls to given offset, returns the new scroll_offset. 776 777 Args: 778 offset: The amount to scroll by. Positive offsets scroll down, 779 negative up. 780 781 Returns: 782 True if the scroll offset changed, False otherwise. 783 """ 784 785 base = self._scroll_offset 786 787 self._scroll_offset = min( 788 max(0, self._scroll_offset + offset), self._max_scroll 789 ) 790 791 return base != self._scroll_offset 792 793 def scroll_end(self, end: int) -> int: 794 """Scrolls to either top or bottom end of this object. 795 796 Args: 797 end: The offset to scroll to. 0 goes to the very top, -1 to the 798 very bottom. 799 800 Returns: 801 True if the scroll offset changed, False otherwise. 802 """ 803 804 base = self._scroll_offset 805 806 if end == 0: 807 self._scroll_offset = 0 808 809 elif end == -1: 810 self._scroll_offset = self._max_scroll 811 812 return base != self._scroll_offset 813 814 def get_lines(self) -> list[str]: 815 ...
78class Widget: # pylint: disable=too-many-public-methods 79 """The base of the Widget system""" 80 81 set_style = classmethod(_set_obj_or_cls_style) 82 set_char = classmethod(_set_obj_or_cls_char) 83 84 styles = w_styles.StyleManager() 85 """Default styles for this class""" 86 87 chars: dict[str, w_styles.CharType] = {} 88 """Default characters for this class""" 89 90 keys: dict[str, set[str]] = {} 91 """Groups of keys that are used in `handle_key`""" 92 93 serialized: list[str] = [ 94 "id", 95 "pos", 96 "depth", 97 "width", 98 "height", 99 "selected_index", 100 "selectables_length", 101 ] 102 """Fields of widget that shall be serialized by `pytermgui.serializer.Serializer`""" 103 104 # This class is loaded after this module, 105 # and thus mypy doesn't see its existence. 106 _id_manager: Optional["_IDManager"] = None # type: ignore 107 108 is_bindable = False 109 """Allow binding support""" 110 111 size_policy = SizePolicy.get_default() 112 """`pytermgui.enums.SizePolicy` to set widget's width according to""" 113 114 parent_align = HorizontalAlignment.get_default() 115 """`pytermgui.enums.HorizontalAlignment` to align widget by""" 116 117 from_data: Callable[..., Widget | list[Widget] | None] 118 119 # We cannot import boxes here due to cyclic imports. 120 box: Any 121 122 def __init__(self, **attrs: Any) -> None: 123 """Initialize object""" 124 125 self.set_style = lambda key, value: _set_obj_or_cls_style(self, key, value) 126 self.set_char = lambda key, value: _set_obj_or_cls_char(self, key, value) 127 128 self.width = 1 129 self.height = 1 130 self.pos = self.terminal.origin 131 132 self.depth = 0 133 134 self.styles = type(self).styles.branch(self) 135 self.chars = type(self).chars.copy() 136 137 self.parent: Widget | None = None 138 self.selected_index: int | None = None 139 140 self._selectables_length = 0 141 self._id: Optional[str] = None 142 self._serialized_fields = type(self).serialized 143 self._bindings: dict[str | Type[MouseEvent], tuple[BoundCallback, str]] = {} 144 self._relative_width: float | None = None 145 self._previous_state: tuple[tuple[int, int], list[str]] | None = None 146 147 self.positioned_line_buffer: list[tuple[tuple[int, int], str]] = [] 148 149 for attr, value in attrs.items(): 150 setattr(self, attr, value) 151 152 def __repr__(self) -> str: 153 """Return repr string of this widget. 154 155 Returns: 156 Whatever this widget's `debug` method gives. 157 """ 158 159 return self.debug() 160 161 def __fancy_repr__(self) -> Generator[str, None, None]: 162 """Yields the repr of this object, then a preview of it.""" 163 164 yield self.debug() 165 yield "\n\n" 166 yield { 167 "text": "\n".join((line + reset() for line in self.get_lines())), 168 "highlight": False, 169 } 170 171 def __iter__(self) -> Iterator[Widget]: 172 """Return self for iteration""" 173 174 yield self 175 176 @property 177 def bindings(self) -> dict[str | Type[MouseEvent], tuple[BoundCallback, str]]: 178 """Gets a copy of the bindings internal dictionary. 179 180 Returns: 181 A copy of the internal bindings dictionary, such as: 182 183 ``` 184 { 185 "*": (star_callback, "This is a callback activated when '*' is pressed.") 186 } 187 ``` 188 """ 189 190 return self._bindings.copy() 191 192 @property 193 def id(self) -> Optional[str]: # pylint: disable=invalid-name 194 """Gets this widget's id property 195 196 Returns: 197 The id string if one is present, None otherwise. 198 """ 199 200 return self._id 201 202 @id.setter 203 def id(self, value: str) -> None: # pylint: disable=invalid-name 204 """Registers a widget to the Widget._id_manager. 205 206 If this widget already had an id, the old value is deregistered 207 before the new one is assigned. 208 209 Args: 210 value: The new id this widget will be registered as. 211 """ 212 213 if self._id == value: 214 return 215 216 manager = Widget._id_manager 217 assert manager is not None 218 219 old = manager.get_id(self) 220 if old is not None: 221 manager.deregister(old) 222 223 self._id = value 224 manager.register(self) 225 226 @property 227 def selectables_length(self) -> int: 228 """Gets how many selectables this widget contains. 229 230 Returns: 231 An integer describing the amount of selectables in this widget. 232 """ 233 234 return self._selectables_length 235 236 @property 237 def selectables(self) -> list[tuple[Widget, int]]: 238 """Gets a list of all selectables within this widget 239 240 Returns: 241 A list of tuples. In the default implementation this will be 242 a list of one tuple, containing a reference to `self`, as well 243 as the lowest index, 0. 244 """ 245 246 return [(self, 0)] 247 248 @property 249 def is_selectable(self) -> bool: 250 """Determines whether this widget has any selectables. 251 252 Returns: 253 A boolean, representing `self.selectables_length != 0`. 254 """ 255 256 return self.selectables_length != 0 257 258 @property 259 def static_width(self) -> int: 260 """Allows for a shorter way of setting a width, and SizePolicy.STATIC. 261 262 Args: 263 value: The new width integer. 264 265 Returns: 266 None, as this is setter only. 267 """ 268 269 return None # type: ignore 270 271 @static_width.setter 272 def static_width(self, value: int) -> None: 273 """See the static_width getter.""" 274 275 self.width = value 276 self.size_policy = SizePolicy.STATIC 277 278 @property 279 def relative_width(self) -> float | None: 280 """Sets this widget's relative width, and changes size_policy to RELATIVE. 281 282 The value is clamped to 1.0. 283 284 If a Container holds a width of 30, and it has a subwidget with a relative 285 width of 0.5, it will be resized to 15. 286 287 Args: 288 value: The multiplier to apply to the parent's width. 289 290 Returns: 291 The current relative_width. 292 """ 293 294 return self._relative_width 295 296 @relative_width.setter 297 def relative_width(self, value: float) -> None: 298 """See the relative_width getter.""" 299 300 self.size_policy = SizePolicy.RELATIVE 301 self._relative_width = min(1.0, value) 302 303 @property 304 def terminal(self) -> Terminal: 305 """Returns the current global terminal instance.""" 306 307 return get_terminal() 308 309 def get_change(self) -> WidgetChange | None: 310 """Determines whether widget lines changed since the last call to this function.""" 311 312 lines = self.get_lines() 313 314 if self._previous_state is None: 315 self._previous_state = (self.width, self.height), lines 316 return WidgetChange.LINES 317 318 lines = self.get_lines() 319 (old_width, old_height), old_lines = self._previous_state 320 321 self._previous_state = (self.width, self.height), lines 322 323 if old_width != self.width and old_height != self.height: 324 return WidgetChange.SIZE 325 326 if old_width != self.width: 327 return WidgetChange.WIDTH 328 329 if old_height != self.height: 330 return WidgetChange.HEIGHT 331 332 if old_lines != lines: 333 return WidgetChange.LINES 334 335 return None 336 337 def contains(self, pos: tuple[int, int]) -> bool: 338 """Determines whether widget contains `pos`. 339 340 Args: 341 pos: Position to compare. 342 343 Returns: 344 Boolean describing whether the position is inside 345 this widget. 346 """ 347 348 rect = self.pos, ( 349 self.pos[0] + self.width, 350 self.pos[1] + self.height, 351 ) 352 353 (left, top), (right, bottom) = rect 354 355 return left <= pos[0] < right and top <= pos[1] < bottom 356 357 def handle_mouse(self, event: MouseEvent) -> bool: 358 """Tries to call the most specific mouse handler function available. 359 360 This function looks for a set of mouse action handlers. Each handler follows 361 the format 362 363 on_{event_name} 364 365 For example, the handler triggered on MouseAction.LEFT_CLICK would be 366 `on_left_click`. If no handler is found nothing is done. 367 368 You can also define more general handlers, for example to group left & right 369 clicks you can use `on_click`, and to catch both up and down scroll you can use 370 `on_scroll`. General handlers are only used if they are the most specific ones, 371 i.e. there is no "specific" handler. 372 373 Args: 374 event: The event to handle. 375 376 Returns: 377 Whether the parent of this widget should treat it as one to "stick" events 378 to, e.g. to keep sending mouse events to it. One can "unstick" a widget by 379 returning False in the handler. 380 """ 381 382 def _get_names(action: MouseAction) -> tuple[str, ...]: 383 if action.value in ["hover", "release"]: 384 return (action.value,) 385 386 parts = action.value.split("_") 387 388 # left click & right click 389 if parts[0] in ["left", "right"]: 390 return (action.value, parts[1]) 391 392 # scroll up & down 393 return (action.value, parts[0]) 394 395 possible_names = _get_names(event.action) 396 for name in possible_names: 397 if hasattr(self, f"on_{name}"): 398 handle = getattr(self, f"on_{name}") 399 400 return handle(event) 401 402 return False 403 404 def handle_key(self, key: str) -> bool: 405 """Handles a mouse event, returning its success. 406 407 Args: 408 key: String representation of input string. 409 The `pytermgui.input.keys` object can be 410 used to retrieve special keys. 411 412 Returns: 413 A boolean describing whether the key was handled. 414 """ 415 416 return False and hasattr(self, key) 417 418 def serialize(self) -> dict[str, Any]: 419 """Serializes a widget. 420 421 The fields looked at are defined `Widget.serialized`. Note that 422 this method is not very commonly used at the moment, so it might 423 not have full functionality in non-nuclear widgets. 424 425 Returns: 426 Dictionary of widget attributes. The dictionary will always 427 have a `type` field. Any styles are converted into markup 428 strings during serialization, so they can be loaded again in 429 their original form. 430 431 Example return: 432 ``` 433 { 434 "type": "Label", 435 "value": "[210 bold]I am a title", 436 "parent_align": 0, 437 ... 438 } 439 ``` 440 """ 441 442 fields = self._serialized_fields 443 444 out: dict[str, Any] = {"type": type(self).__name__} 445 for key in fields: 446 # Detect styled values 447 if key.startswith("*"): 448 style = True 449 key = key[1:] 450 else: 451 style = False 452 453 value = getattr(self, key) 454 455 # Convert styled value into markup 456 if style: 457 style_call = self._get_style(key) 458 if isinstance(value, list): 459 out[key] = [markup.get_markup(style_call(char)) for char in value] 460 else: 461 out[key] = markup.get_markup(style_call(value)) 462 463 continue 464 465 out[key] = value 466 467 # The chars need to be handled separately 468 out["chars"] = {} 469 for key, value in self.chars.items(): 470 style_call = self._get_style(key) 471 472 if isinstance(value, list): 473 out["chars"][key] = [ 474 markup.get_markup(style_call(char)) for char in value 475 ] 476 else: 477 out["chars"][key] = markup.get_markup(style_call(value)) 478 479 return out 480 481 def copy(self) -> Widget: 482 """Creates a deep copy of this widget""" 483 484 return deepcopy(self) 485 486 def _get_style(self, key: str) -> w_styles.DepthlessStyleType: 487 """Gets style call from its key. 488 489 This is analogous to using `self.styles.{key}` 490 491 Args: 492 key: A key into the widget's style manager. 493 494 Returns: 495 A `pytermgui.styles.StyleCall` object containing the referenced 496 style. StyleCall objects should only be used internally inside a 497 widget. 498 499 Raises: 500 KeyError: Style key is invalid. 501 """ 502 503 return self.styles[key] 504 505 def _get_char(self, key: str) -> w_styles.CharType: 506 """Gets character from its key. 507 508 Args: 509 key: A key into the widget's chars dictionary. 510 511 Returns: 512 Either a `list[str]` or a simple `str`, depending on the character. 513 514 Raises: 515 KeyError: Style key is invalid. 516 """ 517 518 chars = self.chars[key] 519 if isinstance(chars, str): 520 return chars 521 522 return chars.copy() 523 524 def get_lines(self) -> list[str]: 525 """Gets lines representing this widget. 526 527 These lines have to be equal to the widget in length. All 528 widgets must provide this method. Make sure to keep it performant, 529 as it will be called very often, often multiple times per WindowManager frame. 530 531 Any longer actions should be done outside of this method, and only their 532 result should be looked up here. 533 534 Returns: 535 Nothing by default. 536 537 Raises: 538 NotImplementedError: As this method is required for **all** widgets, not 539 having it defined will raise NotImplementedError. 540 """ 541 542 raise NotImplementedError(f"get_lines() is not defined for type {type(self)}.") 543 544 def bind( 545 self, key: str, action: BoundCallback, description: Optional[str] = None 546 ) -> None: 547 """Binds an action to a keypress. 548 549 This function is only called by implementations above this layer. To use this 550 functionality use `pytermgui.window_manager.WindowManager`, or write your own 551 custom layer. 552 553 Special keys: 554 - keys.ANY_KEY: Any and all keypresses execute this binding. 555 - keys.MouseAction: Any and all mouse inputs execute this binding. 556 557 Args: 558 key: The key that the action will be bound to. 559 action: The action executed when the key is pressed. 560 description: An optional description for this binding. It is not really 561 used anywhere, but you can provide a helper menu and display them. 562 563 Raises: 564 TypeError: This widget is not bindable, i.e. widget.is_bindable == False. 565 """ 566 567 if not self.is_bindable: 568 raise TypeError(f"Widget of type {type(self)} does not accept bindings.") 569 570 if description is None: 571 description = f"Binding of {key} to {action}" 572 573 self._bindings[key] = (action, description) 574 575 def unbind(self, key: str) -> None: 576 """Unbinds the given key.""" 577 578 del self._bindings[key] 579 580 def execute_binding(self, key: Any) -> bool: 581 """Executes a binding belonging to key, when present. 582 583 Use this method inside custom widget `handle_keys` methods, or to run a callback 584 without its corresponding key having been pressed. 585 586 Args: 587 key: Usually a string, indexing into the `_bindings` dictionary. These are the 588 same strings as defined in `Widget.bind`. 589 590 Returns: 591 True if the binding was found, False otherwise. Bindings will always be 592 executed if they are found. 593 """ 594 595 # Execute special binding 596 if keys.ANY_KEY in self._bindings: 597 method, _ = self._bindings[keys.ANY_KEY] 598 method(self, key) 599 600 if key in self._bindings: 601 method, _ = self._bindings[key] 602 method(self, key) 603 604 return True 605 606 return False 607 608 def select(self, index: int | None = None) -> None: 609 """Selects a part of this Widget. 610 611 Args: 612 index: The index to select. 613 614 Raises: 615 TypeError: This widget has no selectables, i.e. widget.is_selectable == False. 616 """ 617 618 if not self.is_selectable: 619 raise TypeError(f"Object of type {type(self)} has no selectables.") 620 621 if index is not None: 622 index = min(max(0, index), self.selectables_length - 1) 623 self.selected_index = index 624 625 def print(self) -> None: 626 """Prints this widget""" 627 628 for line in self.get_lines(): 629 print(line) 630 631 def debug(self) -> str: 632 """Returns identifiable information about this widget. 633 634 This method is used to easily differentiate between widgets. By default, all widget's 635 __repr__ method is an alias to this. The signature of each widget is used to generate 636 the return value. 637 638 Returns: 639 A string almost exactly matching the line of code that could have defined the widget. 640 641 Example return: 642 643 ``` 644 Container(Label(value="This is a label", padding=0), 645 Button(label="This is a button", padding=0), **attrs) 646 ``` 647 648 """ 649 650 constructor = "(" 651 for name in signature(getattr(self, "__init__")).parameters: 652 current = "" 653 if name == "attrs": 654 current += "**attrs" 655 continue 656 657 if len(constructor) > 1: 658 current += ", " 659 660 current += name 661 662 attr = getattr(self, name, None) 663 if attr is None: 664 continue 665 666 current += "=" 667 668 if isinstance(attr, str): 669 current += f'"{attr}"' 670 else: 671 current += str(attr) 672 673 constructor += current 674 675 constructor += ")" 676 677 return type(self).__name__ + constructor
The base of the Widget system
122 def __init__(self, **attrs: Any) -> None: 123 """Initialize object""" 124 125 self.set_style = lambda key, value: _set_obj_or_cls_style(self, key, value) 126 self.set_char = lambda key, value: _set_obj_or_cls_char(self, key, value) 127 128 self.width = 1 129 self.height = 1 130 self.pos = self.terminal.origin 131 132 self.depth = 0 133 134 self.styles = type(self).styles.branch(self) 135 self.chars = type(self).chars.copy() 136 137 self.parent: Widget | None = None 138 self.selected_index: int | None = None 139 140 self._selectables_length = 0 141 self._id: Optional[str] = None 142 self._serialized_fields = type(self).serialized 143 self._bindings: dict[str | Type[MouseEvent], tuple[BoundCallback, str]] = {} 144 self._relative_width: float | None = None 145 self._previous_state: tuple[tuple[int, int], list[str]] | None = None 146 147 self.positioned_line_buffer: list[tuple[tuple[int, int], str]] = [] 148 149 for attr, value in attrs.items(): 150 setattr(self, attr, value)
Initialize object
31def _set_obj_or_cls_style( 32 obj_or_cls: Type[Widget] | Widget, key: str, value: w_styles.StyleType 33) -> Type[Widget] | Widget: 34 """Sets a style for an object or class 35 36 Args: 37 obj_or_cls: The Widget instance or type to update. 38 key: The style key. 39 value: The new style. 40 41 Returns: 42 Type[Widget] | Widget: The updated class. 43 44 Raises: 45 See `pytermgui.widgets.styles.StyleManager`. 46 """ 47 48 obj_or_cls.styles[key] = value 49 50 return obj_or_cls
Sets a style for an object or class
Args
- obj_or_cls: The Widget instance or type to update.
- key: The style key.
- value: The new style.
Returns
Type[Widget] | Widget: The updated class.
Raises
53def _set_obj_or_cls_char( 54 obj_or_cls: Type[Widget] | Widget, key: str, value: w_styles.CharType 55) -> Type[Widget] | Widget: 56 """Sets a char for an object or class 57 58 Args: 59 obj_or_cls: The Widget instance or type to update. 60 key: The char key. 61 value: The new char. 62 63 Returns: 64 Type[Widget] | Widget: The updated class. 65 66 Raises: 67 KeyError: The char key provided is invalid. 68 """ 69 70 if not key in obj_or_cls.chars.keys(): 71 raise KeyError(f"Char {key} is not valid for {obj_or_cls}!") 72 73 obj_or_cls.chars[key] = value 74 75 return obj_or_cls
Sets a char for an object or class
Args
- obj_or_cls: The Widget instance or type to update.
- key: The char key.
- value: The new char.
Returns
Type[Widget] | Widget: The updated class.
Raises
- KeyError: The char key provided is invalid.
Fields of widget that shall be serialized by pytermgui.serializer.Serializer
pytermgui.enums.HorizontalAlignment
to align widget by
49def auto(data: Any, **widget_args: Any) -> Optional[Widget | list[Splitter]]: 50 """Creates a widget from specific data structures. 51 52 This conversion includes various widget classes, as well as some shorthands for 53 more complex objects. This method is called implicitly whenever a non-widget is 54 attempted to be added to a Widget. 55 56 57 Args: 58 data: The structure to convert. See below for formats. 59 **widget_args: Arguments passed straight to the widget constructor. 60 61 Returns: 62 The widget or list of widgets created, or None if the passed structure could 63 not be converted. 64 65 <br> 66 <details style="text-align: left"> 67 <summary style="all: revert; cursor: pointer">Data structures:</summary> 68 69 `pytermgui.widgets.base.Label`: 70 71 * Created from `str` 72 * Syntax example: `"Label value"` 73 74 `pytermgui.widgets.extra.Splitter`: 75 76 * Created from `tuple[Any]` 77 * Syntax example: `(YourWidget(), "auto_syntax", ...)` 78 79 `pytermgui.widgets.extra.Splitter` prompt: 80 81 * Created from `dict[Any, Any]` 82 * Syntax example: `{YourWidget(): "auto_syntax"}` 83 84 `pytermgui.widgets.buttons.Button`: 85 86 * Created from `list[str, pytermgui.widgets.buttons.MouseCallback]` 87 * Syntax example: `["Button label", lambda target, caller: ...]` 88 89 `pytermgui.widgets.buttons.Checkbox`: 90 91 * Created from `list[bool, Callable[[bool], Any]]` 92 * Syntax example: `[True, lambda checked: ...]` 93 94 `pytermgui.widgets.buttons.Toggle`: 95 96 * Created from `list[tuple[str, str], Callable[[str], Any]]` 97 * Syntax example: `[("On", "Off"), lambda new_value: ...]` 98 </details> 99 100 Example: 101 102 ```python3 103 from pytermgui import Container 104 form = ( 105 Container(id="form") 106 + "[157 bold]This is a title" 107 + "" 108 + {"[72 italic]Label1": "[210]Button1"} 109 + {"[72 italic]Label2": "[210]Button2"} 110 + {"[72 italic]Label3": "[210]Button3"} 111 + "" 112 + ["Submit", lambda _, button, your_submit_handler(button.parent)] 113 ) 114 ``` 115 """ 116 # In my opinion, returning immediately after construction is much more readable. 117 # pylint: disable=too-many-return-statements 118 119 # Nothing to do. 120 if isinstance(data, Widget): 121 # Set all **widget_args 122 for key, value in widget_args.items(): 123 setattr(data, key, value) 124 125 return data 126 127 # Label 128 if isinstance(data, str): 129 return Label(data, **widget_args) 130 131 # Splitter 132 if isinstance(data, tuple): 133 return Splitter(*data, **widget_args) 134 135 # buttons 136 if isinstance(data, list): 137 label = data[0] 138 onclick = None 139 if len(data) > 1: 140 onclick = data[1] 141 142 # Checkbox 143 if isinstance(label, bool): 144 return Checkbox(onclick, checked=label, **widget_args) 145 146 # Toggle 147 if isinstance(label, tuple): 148 assert len(label) == 2 149 return Toggle(label, onclick, **widget_args) 150 151 return Button(label, onclick, **widget_args) 152 153 # prompt splitter 154 if isinstance(data, dict): 155 rows: list[Splitter] = [] 156 157 for key, value in data.items(): 158 left = auto(key, parent_align=HorizontalAlignment.LEFT) 159 right = auto(value, parent_align=HorizontalAlignment.RIGHT) 160 161 rows.append(Splitter(left, right, **widget_args)) 162 163 if len(rows) == 1: 164 return rows[0] 165 166 return rows 167 168 return None
Creates a widget from specific data structures.
This conversion includes various widget classes, as well as some shorthands for more complex objects. This method is called implicitly whenever a non-widget is attempted to be added to a Widget.
Args
- data: The structure to convert. See below for formats.
- **widget_args: Arguments passed straight to the widget constructor.
Returns
The widget or list of widgets created, or None if the passed structure could not be converted.
Data structures:
- Created from
str
- Syntax example:
"Label value"
pytermgui.widgets.extra.Splitter
:
- Created from
tuple[Any]
- Syntax example:
(YourWidget(), "auto_syntax", ...)
pytermgui.widgets.extra.Splitter
prompt:
- Created from
dict[Any, Any]
- Syntax example:
{YourWidget(): "auto_syntax"}
pytermgui.widgets.buttons.Button
:
- Created from
list[str, pytermgui.widgets.buttons.MouseCallback]
- Syntax example:
["Button label", lambda target, caller: ...]
pytermgui.widgets.buttons.Checkbox
:
- Created from
list[bool, Callable[[bool], Any]]
- Syntax example:
[True, lambda checked: ...]
pytermgui.widgets.buttons.Toggle
:
- Created from
list[tuple[str, str], Callable[[str], Any]]
- Syntax example:
[("On", "Off"), lambda new_value: ...]
Example:
from pytermgui import Container
form = (
Container(id="form")
+ "[157 bold]This is a title"
+ ""
+ {"[72 italic]Label1": "[210]Button1"}
+ {"[72 italic]Label2": "[210]Button2"}
+ {"[72 italic]Label3": "[210]Button3"}
+ ""
+ ["Submit", lambda _, button, your_submit_handler(button.parent)]
)
Gets a copy of the bindings internal dictionary.
Returns
A copy of the internal bindings dictionary, such as:
{ "*": (star_callback, "This is a callback activated when '*' is pressed.") }
Gets this widget's id property
Returns
The id string if one is present, None otherwise.
Gets how many selectables this widget contains.
Returns
An integer describing the amount of selectables in this widget.
Gets a list of all selectables within this widget
Returns
A list of tuples. In the default implementation this will be a list of one tuple, containing a reference to
self
, as well as the lowest index, 0.
Determines whether this widget has any selectables.
Returns
A boolean, representing
self.selectables_length != 0
.
Allows for a shorter way of setting a width, and SizePolicy.STATIC.
Args
- value: The new width integer.
Returns
None, as this is setter only.
Sets this widget's relative width, and changes size_policy to RELATIVE.
The value is clamped to 1.0.
If a Container holds a width of 30, and it has a subwidget with a relative width of 0.5, it will be resized to 15.
Args
- value: The multiplier to apply to the parent's width.
Returns
The current relative_width.
309 def get_change(self) -> WidgetChange | None: 310 """Determines whether widget lines changed since the last call to this function.""" 311 312 lines = self.get_lines() 313 314 if self._previous_state is None: 315 self._previous_state = (self.width, self.height), lines 316 return WidgetChange.LINES 317 318 lines = self.get_lines() 319 (old_width, old_height), old_lines = self._previous_state 320 321 self._previous_state = (self.width, self.height), lines 322 323 if old_width != self.width and old_height != self.height: 324 return WidgetChange.SIZE 325 326 if old_width != self.width: 327 return WidgetChange.WIDTH 328 329 if old_height != self.height: 330 return WidgetChange.HEIGHT 331 332 if old_lines != lines: 333 return WidgetChange.LINES 334 335 return None
Determines whether widget lines changed since the last call to this function.
337 def contains(self, pos: tuple[int, int]) -> bool: 338 """Determines whether widget contains `pos`. 339 340 Args: 341 pos: Position to compare. 342 343 Returns: 344 Boolean describing whether the position is inside 345 this widget. 346 """ 347 348 rect = self.pos, ( 349 self.pos[0] + self.width, 350 self.pos[1] + self.height, 351 ) 352 353 (left, top), (right, bottom) = rect 354 355 return left <= pos[0] < right and top <= pos[1] < bottom
Determines whether widget contains pos
.
Args
- pos: Position to compare.
Returns
Boolean describing whether the position is inside this widget.
357 def handle_mouse(self, event: MouseEvent) -> bool: 358 """Tries to call the most specific mouse handler function available. 359 360 This function looks for a set of mouse action handlers. Each handler follows 361 the format 362 363 on_{event_name} 364 365 For example, the handler triggered on MouseAction.LEFT_CLICK would be 366 `on_left_click`. If no handler is found nothing is done. 367 368 You can also define more general handlers, for example to group left & right 369 clicks you can use `on_click`, and to catch both up and down scroll you can use 370 `on_scroll`. General handlers are only used if they are the most specific ones, 371 i.e. there is no "specific" handler. 372 373 Args: 374 event: The event to handle. 375 376 Returns: 377 Whether the parent of this widget should treat it as one to "stick" events 378 to, e.g. to keep sending mouse events to it. One can "unstick" a widget by 379 returning False in the handler. 380 """ 381 382 def _get_names(action: MouseAction) -> tuple[str, ...]: 383 if action.value in ["hover", "release"]: 384 return (action.value,) 385 386 parts = action.value.split("_") 387 388 # left click & right click 389 if parts[0] in ["left", "right"]: 390 return (action.value, parts[1]) 391 392 # scroll up & down 393 return (action.value, parts[0]) 394 395 possible_names = _get_names(event.action) 396 for name in possible_names: 397 if hasattr(self, f"on_{name}"): 398 handle = getattr(self, f"on_{name}") 399 400 return handle(event) 401 402 return False
Tries to call the most specific mouse handler function available.
This function looks for a set of mouse action handlers. Each handler follows the format
on_{event_name}
For example, the handler triggered on MouseAction.LEFT_CLICK would be
on_left_click
. If no handler is found nothing is done.
You can also define more general handlers, for example to group left & right
clicks you can use on_click
, and to catch both up and down scroll you can use
on_scroll
. General handlers are only used if they are the most specific ones,
i.e. there is no "specific" handler.
Args
- event: The event to handle.
Returns
Whether the parent of this widget should treat it as one to "stick" events to, e.g. to keep sending mouse events to it. One can "unstick" a widget by returning False in the handler.
404 def handle_key(self, key: str) -> bool: 405 """Handles a mouse event, returning its success. 406 407 Args: 408 key: String representation of input string. 409 The `pytermgui.input.keys` object can be 410 used to retrieve special keys. 411 412 Returns: 413 A boolean describing whether the key was handled. 414 """ 415 416 return False and hasattr(self, key)
Handles a mouse event, returning its success.
Args
- key: String representation of input string.
The
pytermgui.input.keys
object can be used to retrieve special keys.
Returns
A boolean describing whether the key was handled.
418 def serialize(self) -> dict[str, Any]: 419 """Serializes a widget. 420 421 The fields looked at are defined `Widget.serialized`. Note that 422 this method is not very commonly used at the moment, so it might 423 not have full functionality in non-nuclear widgets. 424 425 Returns: 426 Dictionary of widget attributes. The dictionary will always 427 have a `type` field. Any styles are converted into markup 428 strings during serialization, so they can be loaded again in 429 their original form. 430 431 Example return: 432 ``` 433 { 434 "type": "Label", 435 "value": "[210 bold]I am a title", 436 "parent_align": 0, 437 ... 438 } 439 ``` 440 """ 441 442 fields = self._serialized_fields 443 444 out: dict[str, Any] = {"type": type(self).__name__} 445 for key in fields: 446 # Detect styled values 447 if key.startswith("*"): 448 style = True 449 key = key[1:] 450 else: 451 style = False 452 453 value = getattr(self, key) 454 455 # Convert styled value into markup 456 if style: 457 style_call = self._get_style(key) 458 if isinstance(value, list): 459 out[key] = [markup.get_markup(style_call(char)) for char in value] 460 else: 461 out[key] = markup.get_markup(style_call(value)) 462 463 continue 464 465 out[key] = value 466 467 # The chars need to be handled separately 468 out["chars"] = {} 469 for key, value in self.chars.items(): 470 style_call = self._get_style(key) 471 472 if isinstance(value, list): 473 out["chars"][key] = [ 474 markup.get_markup(style_call(char)) for char in value 475 ] 476 else: 477 out["chars"][key] = markup.get_markup(style_call(value)) 478 479 return out
Serializes a widget.
The fields looked at are defined Widget.serialized
. Note that
this method is not very commonly used at the moment, so it might
not have full functionality in non-nuclear widgets.
Returns
Dictionary of widget attributes. The dictionary will always have a
type
field. Any styles are converted into markup strings during serialization, so they can be loaded again in their original form.Example return:
{ "type": "Label", "value": "[210 bold]I am a title", "parent_align": 0, ... }
481 def copy(self) -> Widget: 482 """Creates a deep copy of this widget""" 483 484 return deepcopy(self)
Creates a deep copy of this widget
524 def get_lines(self) -> list[str]: 525 """Gets lines representing this widget. 526 527 These lines have to be equal to the widget in length. All 528 widgets must provide this method. Make sure to keep it performant, 529 as it will be called very often, often multiple times per WindowManager frame. 530 531 Any longer actions should be done outside of this method, and only their 532 result should be looked up here. 533 534 Returns: 535 Nothing by default. 536 537 Raises: 538 NotImplementedError: As this method is required for **all** widgets, not 539 having it defined will raise NotImplementedError. 540 """ 541 542 raise NotImplementedError(f"get_lines() is not defined for type {type(self)}.")
Gets lines representing this widget.
These lines have to be equal to the widget in length. All widgets must provide this method. Make sure to keep it performant, as it will be called very often, often multiple times per WindowManager frame.
Any longer actions should be done outside of this method, and only their result should be looked up here.
Returns
Nothing by default.
Raises
- NotImplementedError: As this method is required for all widgets, not having it defined will raise NotImplementedError.
544 def bind( 545 self, key: str, action: BoundCallback, description: Optional[str] = None 546 ) -> None: 547 """Binds an action to a keypress. 548 549 This function is only called by implementations above this layer. To use this 550 functionality use `pytermgui.window_manager.WindowManager`, or write your own 551 custom layer. 552 553 Special keys: 554 - keys.ANY_KEY: Any and all keypresses execute this binding. 555 - keys.MouseAction: Any and all mouse inputs execute this binding. 556 557 Args: 558 key: The key that the action will be bound to. 559 action: The action executed when the key is pressed. 560 description: An optional description for this binding. It is not really 561 used anywhere, but you can provide a helper menu and display them. 562 563 Raises: 564 TypeError: This widget is not bindable, i.e. widget.is_bindable == False. 565 """ 566 567 if not self.is_bindable: 568 raise TypeError(f"Widget of type {type(self)} does not accept bindings.") 569 570 if description is None: 571 description = f"Binding of {key} to {action}" 572 573 self._bindings[key] = (action, description)
Binds an action to a keypress.
This function is only called by implementations above this layer. To use this
functionality use pytermgui.window_manager.WindowManager
, or write your own
custom layer.
Special keys:
- keys.ANY_KEY: Any and all keypresses execute this binding.
- keys.MouseAction: Any and all mouse inputs execute this binding.
Args
- key: The key that the action will be bound to.
- action: The action executed when the key is pressed.
- description: An optional description for this binding. It is not really used anywhere, but you can provide a helper menu and display them.
Raises
- TypeError: This widget is not bindable, i.e. widget.is_bindable == False.
575 def unbind(self, key: str) -> None: 576 """Unbinds the given key.""" 577 578 del self._bindings[key]
Unbinds the given key.
580 def execute_binding(self, key: Any) -> bool: 581 """Executes a binding belonging to key, when present. 582 583 Use this method inside custom widget `handle_keys` methods, or to run a callback 584 without its corresponding key having been pressed. 585 586 Args: 587 key: Usually a string, indexing into the `_bindings` dictionary. These are the 588 same strings as defined in `Widget.bind`. 589 590 Returns: 591 True if the binding was found, False otherwise. Bindings will always be 592 executed if they are found. 593 """ 594 595 # Execute special binding 596 if keys.ANY_KEY in self._bindings: 597 method, _ = self._bindings[keys.ANY_KEY] 598 method(self, key) 599 600 if key in self._bindings: 601 method, _ = self._bindings[key] 602 method(self, key) 603 604 return True 605 606 return False
Executes a binding belonging to key, when present.
Use this method inside custom widget handle_keys
methods, or to run a callback
without its corresponding key having been pressed.
Args
- key: Usually a string, indexing into the
_bindings
dictionary. These are the same strings as defined inWidget.bind
.
Returns
True if the binding was found, False otherwise. Bindings will always be executed if they are found.
608 def select(self, index: int | None = None) -> None: 609 """Selects a part of this Widget. 610 611 Args: 612 index: The index to select. 613 614 Raises: 615 TypeError: This widget has no selectables, i.e. widget.is_selectable == False. 616 """ 617 618 if not self.is_selectable: 619 raise TypeError(f"Object of type {type(self)} has no selectables.") 620 621 if index is not None: 622 index = min(max(0, index), self.selectables_length - 1) 623 self.selected_index = index
Selects a part of this Widget.
Args
- index: The index to select.
Raises
- TypeError: This widget has no selectables, i.e. widget.is_selectable == False.
625 def print(self) -> None: 626 """Prints this widget""" 627 628 for line in self.get_lines(): 629 print(line)
Prints this widget
631 def debug(self) -> str: 632 """Returns identifiable information about this widget. 633 634 This method is used to easily differentiate between widgets. By default, all widget's 635 __repr__ method is an alias to this. The signature of each widget is used to generate 636 the return value. 637 638 Returns: 639 A string almost exactly matching the line of code that could have defined the widget. 640 641 Example return: 642 643 ``` 644 Container(Label(value="This is a label", padding=0), 645 Button(label="This is a button", padding=0), **attrs) 646 ``` 647 648 """ 649 650 constructor = "(" 651 for name in signature(getattr(self, "__init__")).parameters: 652 current = "" 653 if name == "attrs": 654 current += "**attrs" 655 continue 656 657 if len(constructor) > 1: 658 current += ", " 659 660 current += name 661 662 attr = getattr(self, name, None) 663 if attr is None: 664 continue 665 666 current += "=" 667 668 if isinstance(attr, str): 669 current += f'"{attr}"' 670 else: 671 current += str(attr) 672 673 constructor += current 674 675 constructor += ")" 676 677 return type(self).__name__ + constructor
Returns identifiable information about this widget.
This method is used to easily differentiate between widgets. By default, all widget's __repr__ method is an alias to this. The signature of each widget is used to generate the return value.
Returns
A string almost exactly matching the line of code that could have defined the widget.
Example return:
Container(Label(value="This is a label", padding=0), Button(label="This is a button", padding=0), **attrs)
680class Label(Widget): 681 """A Widget to display a string 682 683 By default, this widget uses `pytermgui.widgets.styles.MARKUP`. This 684 allows it to house markup text that is parsed before display, such as: 685 686 ```python3 687 import pytermgui as ptg 688 689 with ptg.alt_buffer(): 690 root = ptg.Container( 691 ptg.Label("[italic 141 bold]This is some [green]fancy [white inverse]text!") 692 ) 693 root.print() 694 ptg.getch() 695 ``` 696 697 <p style="text-align: center"> 698 <img 699 src="https://github.com/bczsalba/pytermgui/blob/master/assets/docs/widgets/label.png?raw=true" 700 width=100%> 701 </p> 702 """ 703 704 serialized = Widget.serialized + ["*value", "align", "padding"] 705 styles = w_styles.StyleManager(value=w_styles.MARKUP) 706 707 def __init__( 708 self, 709 value: str = "", 710 style: str | w_styles.StyleValue = "", 711 padding: int = 0, 712 non_first_padding: int = 0, 713 **attrs: Any, 714 ) -> None: 715 """Initializes a Label. 716 717 Args: 718 value: The value of this string. Using the default value style 719 (`pytermgui.widgets.styles.MARKUP`), 720 style: A pre-set value for self.styles.value. 721 padding: The number of space (" ") characters to prepend to every line after 722 line breaking. 723 non_first_padding: The number of space characters to prepend to every 724 non-first line of `get_lines`. This is applied on top of `padding`. 725 """ 726 727 super().__init__(**attrs) 728 729 self.value = value 730 self.padding = padding 731 self.non_first_padding = non_first_padding 732 self.width = real_length(value) + self.padding 733 734 if style != "": 735 self.styles.value = style 736 737 def get_lines(self) -> list[str]: 738 """Get lines representing this Label, breaking lines as necessary""" 739 740 lines = [] 741 limit = self.width - self.padding 742 broken = break_line( 743 self.styles.value(self.value), 744 limit=limit, 745 non_first_limit=limit - self.non_first_padding, 746 ) 747 748 for i, line in enumerate(broken): 749 if i == 0: 750 lines.append(self.padding * " " + line) 751 continue 752 753 lines.append(self.padding * " " + self.non_first_padding * " " + line) 754 755 return lines or [""]
A Widget to display a string
By default, this widget uses pytermgui.widgets.styles.MARKUP
. This
allows it to house markup text that is parsed before display, such as:
import pytermgui as ptg
with ptg.alt_buffer():
root = ptg.Container(
ptg.Label("[italic 141 bold]This is some [green]fancy [white inverse]text!")
)
root.print()
ptg.getch()
707 def __init__( 708 self, 709 value: str = "", 710 style: str | w_styles.StyleValue = "", 711 padding: int = 0, 712 non_first_padding: int = 0, 713 **attrs: Any, 714 ) -> None: 715 """Initializes a Label. 716 717 Args: 718 value: The value of this string. Using the default value style 719 (`pytermgui.widgets.styles.MARKUP`), 720 style: A pre-set value for self.styles.value. 721 padding: The number of space (" ") characters to prepend to every line after 722 line breaking. 723 non_first_padding: The number of space characters to prepend to every 724 non-first line of `get_lines`. This is applied on top of `padding`. 725 """ 726 727 super().__init__(**attrs) 728 729 self.value = value 730 self.padding = padding 731 self.non_first_padding = non_first_padding 732 self.width = real_length(value) + self.padding 733 734 if style != "": 735 self.styles.value = style
Initializes a Label.
Args
- value: The value of this string. Using the default value style
(
pytermgui.widgets.styles.MARKUP
), - style: A pre-set value for self.styles.value.
- padding: The number of space (" ") characters to prepend to every line after line breaking.
- non_first_padding: The number of space characters to prepend to every
non-first line of
get_lines
. This is applied on top ofpadding
.
Fields of widget that shall be serialized by pytermgui.serializer.Serializer
737 def get_lines(self) -> list[str]: 738 """Get lines representing this Label, breaking lines as necessary""" 739 740 lines = [] 741 limit = self.width - self.padding 742 broken = break_line( 743 self.styles.value(self.value), 744 limit=limit, 745 non_first_limit=limit - self.non_first_padding, 746 ) 747 748 for i, line in enumerate(broken): 749 if i == 0: 750 lines.append(self.padding * " " + line) 751 continue 752 753 lines.append(self.padding * " " + self.non_first_padding * " " + line) 754 755 return lines or [""]
Get lines representing this Label, breaking lines as necessary