urwid 2.6.0.post0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of urwid might be problematic. Click here for more details.

Files changed (75) hide show
  1. urwid/__init__.py +333 -0
  2. urwid/canvas.py +1413 -0
  3. urwid/command_map.py +137 -0
  4. urwid/container.py +59 -0
  5. urwid/decoration.py +65 -0
  6. urwid/display/__init__.py +97 -0
  7. urwid/display/_posix_raw_display.py +413 -0
  8. urwid/display/_raw_display_base.py +914 -0
  9. urwid/display/_web.css +12 -0
  10. urwid/display/_web.js +462 -0
  11. urwid/display/_win32.py +171 -0
  12. urwid/display/_win32_raw_display.py +269 -0
  13. urwid/display/common.py +1219 -0
  14. urwid/display/curses.py +690 -0
  15. urwid/display/escape.py +624 -0
  16. urwid/display/html_fragment.py +251 -0
  17. urwid/display/lcd.py +518 -0
  18. urwid/display/raw.py +37 -0
  19. urwid/display/web.py +636 -0
  20. urwid/event_loop/__init__.py +55 -0
  21. urwid/event_loop/abstract_loop.py +175 -0
  22. urwid/event_loop/asyncio_loop.py +231 -0
  23. urwid/event_loop/glib_loop.py +294 -0
  24. urwid/event_loop/main_loop.py +721 -0
  25. urwid/event_loop/select_loop.py +230 -0
  26. urwid/event_loop/tornado_loop.py +206 -0
  27. urwid/event_loop/trio_loop.py +302 -0
  28. urwid/event_loop/twisted_loop.py +269 -0
  29. urwid/event_loop/zmq_loop.py +275 -0
  30. urwid/font.py +695 -0
  31. urwid/graphics.py +96 -0
  32. urwid/highlight.css +19 -0
  33. urwid/listbox.py +1899 -0
  34. urwid/monitored_list.py +522 -0
  35. urwid/numedit.py +376 -0
  36. urwid/signals.py +330 -0
  37. urwid/split_repr.py +130 -0
  38. urwid/str_util.py +358 -0
  39. urwid/text_layout.py +632 -0
  40. urwid/treetools.py +515 -0
  41. urwid/util.py +557 -0
  42. urwid/version.py +16 -0
  43. urwid/vterm.py +1806 -0
  44. urwid/widget/__init__.py +181 -0
  45. urwid/widget/attr_map.py +161 -0
  46. urwid/widget/attr_wrap.py +140 -0
  47. urwid/widget/bar_graph.py +649 -0
  48. urwid/widget/big_text.py +77 -0
  49. urwid/widget/box_adapter.py +126 -0
  50. urwid/widget/columns.py +1145 -0
  51. urwid/widget/constants.py +574 -0
  52. urwid/widget/container.py +227 -0
  53. urwid/widget/divider.py +110 -0
  54. urwid/widget/edit.py +718 -0
  55. urwid/widget/filler.py +403 -0
  56. urwid/widget/frame.py +539 -0
  57. urwid/widget/grid_flow.py +539 -0
  58. urwid/widget/line_box.py +194 -0
  59. urwid/widget/overlay.py +829 -0
  60. urwid/widget/padding.py +597 -0
  61. urwid/widget/pile.py +971 -0
  62. urwid/widget/popup.py +170 -0
  63. urwid/widget/progress_bar.py +141 -0
  64. urwid/widget/scrollable.py +597 -0
  65. urwid/widget/solid_fill.py +44 -0
  66. urwid/widget/text.py +354 -0
  67. urwid/widget/widget.py +852 -0
  68. urwid/widget/widget_decoration.py +166 -0
  69. urwid/widget/wimp.py +792 -0
  70. urwid/wimp.py +23 -0
  71. urwid-2.6.0.post0.dist-info/COPYING +504 -0
  72. urwid-2.6.0.post0.dist-info/METADATA +332 -0
  73. urwid-2.6.0.post0.dist-info/RECORD +75 -0
  74. urwid-2.6.0.post0.dist-info/WHEEL +5 -0
  75. urwid-2.6.0.post0.dist-info/top_level.txt +1 -0
urwid/widget/widget.py ADDED
@@ -0,0 +1,852 @@
1
+ # Urwid basic widget classes
2
+ # Copyright (C) 2004-2012 Ian Ward
3
+ #
4
+ # This library is free software; you can redistribute it and/or
5
+ # modify it under the terms of the GNU Lesser General Public
6
+ # License as published by the Free Software Foundation; either
7
+ # version 2.1 of the License, or (at your option) any later version.
8
+ #
9
+ # This library is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12
+ # Lesser General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU Lesser General Public
15
+ # License along with this library; if not, write to the Free Software
16
+ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17
+ #
18
+ # Urwid web site: https://urwid.org/
19
+
20
+
21
+ from __future__ import annotations
22
+
23
+ import functools
24
+ import logging
25
+ import typing
26
+ import warnings
27
+ from operator import attrgetter
28
+
29
+ from urwid import signals
30
+ from urwid.canvas import Canvas, CanvasCache, CompositeCanvas
31
+ from urwid.command_map import command_map
32
+ from urwid.split_repr import split_repr
33
+ from urwid.util import MetaSuper
34
+
35
+ from .constants import Sizing
36
+
37
+ if typing.TYPE_CHECKING:
38
+ from collections.abc import Callable, Hashable
39
+
40
+ WrappedWidget = typing.TypeVar("WrappedWidget")
41
+ LOGGER = logging.getLogger(__name__)
42
+
43
+
44
+ class WidgetMeta(MetaSuper, signals.MetaSignals):
45
+ """
46
+ Bases: :class:`MetaSuper`, :class:`MetaSignals`
47
+
48
+ Automatic caching of render and rows methods.
49
+
50
+ Class variable *no_cache* is a list of names of methods to not cache
51
+ automatically. Valid method names for *no_cache* are ``'render'`` and
52
+ ``'rows'``.
53
+
54
+ Class variable *ignore_focus* if defined and set to ``True`` indicates
55
+ that the canvas this widget renders is not affected by the focus
56
+ parameter, so it may be ignored when caching.
57
+ """
58
+
59
+ def __init__(cls, name, bases, d):
60
+ no_cache = d.get("no_cache", [])
61
+
62
+ super().__init__(name, bases, d)
63
+
64
+ if "render" in d:
65
+ if "render" not in no_cache:
66
+ render_fn = cache_widget_render(cls)
67
+ else:
68
+ render_fn = nocache_widget_render(cls)
69
+ cls.render = render_fn
70
+
71
+ if "rows" in d and "rows" not in no_cache:
72
+ cls.rows = cache_widget_rows(cls)
73
+ if "no_cache" in d:
74
+ del cls.no_cache
75
+ if "ignore_focus" in d:
76
+ del cls.ignore_focus
77
+
78
+
79
+ class WidgetError(Exception):
80
+ """Widget specific errors."""
81
+
82
+
83
+ class WidgetWarning(Warning):
84
+ """Widget specific warnings."""
85
+
86
+
87
+ def validate_size(widget, size, canv):
88
+ """
89
+ Raise a WidgetError if a canv does not match size.
90
+ """
91
+ if (size and size[1:] != (0,) and size[0] != canv.cols()) or (len(size) > 1 and size[1] != canv.rows()):
92
+ raise WidgetError(
93
+ f"Widget {widget!r} rendered ({canv.cols():d} x {canv.rows():d}) canvas when passed size {size!r}!"
94
+ )
95
+
96
+
97
+ def cache_widget_render(cls):
98
+ """
99
+ Return a function that wraps the cls.render() method
100
+ and fetches and stores canvases with CanvasCache.
101
+ """
102
+ ignore_focus = bool(getattr(cls, "ignore_focus", False))
103
+ fn = cls.render
104
+
105
+ @functools.wraps(fn)
106
+ def cached_render(self, size, focus=False):
107
+ focus = focus and not ignore_focus
108
+ canv = CanvasCache.fetch(self, cls, size, focus)
109
+ if canv:
110
+ return canv
111
+
112
+ canv = fn(self, size, focus=focus)
113
+ validate_size(self, size, canv)
114
+ if canv.widget_info:
115
+ canv = CompositeCanvas(canv)
116
+ canv.finalize(self, size, focus)
117
+ CanvasCache.store(cls, canv)
118
+ return canv
119
+
120
+ cached_render.original_fn = fn
121
+ return cached_render
122
+
123
+
124
+ def nocache_widget_render(cls):
125
+ """
126
+ Return a function that wraps the cls.render() method
127
+ and finalizes the canvas that it returns.
128
+ """
129
+ fn = cls.render
130
+ if hasattr(fn, "original_fn"):
131
+ fn = fn.original_fn
132
+
133
+ @functools.wraps(fn)
134
+ def finalize_render(self, size, focus=False):
135
+ canv = fn(self, size, focus=focus)
136
+ if canv.widget_info:
137
+ canv = CompositeCanvas(canv)
138
+ validate_size(self, size, canv)
139
+ canv.finalize(self, size, focus)
140
+ return canv
141
+
142
+ finalize_render.original_fn = fn
143
+ return finalize_render
144
+
145
+
146
+ def nocache_widget_render_instance(self):
147
+ """
148
+ Return a function that wraps the cls.render() method
149
+ and finalizes the canvas that it returns, but does not
150
+ cache the canvas.
151
+ """
152
+ fn = self.render.original_fn
153
+
154
+ @functools.wraps(fn)
155
+ def finalize_render(size, focus=False):
156
+ canv = fn(self, size, focus=focus)
157
+ if canv.widget_info:
158
+ canv = CompositeCanvas(canv)
159
+ canv.finalize(self, size, focus)
160
+ return canv
161
+
162
+ finalize_render.original_fn = fn
163
+ return finalize_render
164
+
165
+
166
+ def cache_widget_rows(cls):
167
+ """
168
+ Return a function that wraps the cls.rows() method
169
+ and returns rows from the CanvasCache if available.
170
+ """
171
+ ignore_focus = bool(getattr(cls, "ignore_focus", False))
172
+ fn = cls.rows
173
+
174
+ @functools.wraps(fn)
175
+ def cached_rows(self, size: tuple[int], focus: bool = False) -> int:
176
+ focus = focus and not ignore_focus
177
+ canv = CanvasCache.fetch(self, cls, size, focus)
178
+ if canv:
179
+ return canv.rows()
180
+
181
+ return fn(self, size, focus)
182
+
183
+ return cached_rows
184
+
185
+
186
+ class Widget(metaclass=WidgetMeta):
187
+ """
188
+ Widget base class
189
+
190
+ .. attribute:: _selectable
191
+ :annotation: = False
192
+
193
+ The default :meth:`.selectable` method returns this
194
+ value.
195
+
196
+ .. attribute:: _sizing
197
+ :annotation: = frozenset(['flow', 'box', 'fixed'])
198
+
199
+ The default :meth:`.sizing` method returns this value.
200
+
201
+ .. attribute:: _command_map
202
+ :annotation: = urwid.command_map
203
+
204
+ A shared :class:`CommandMap` instance. May be redefined
205
+ in subclasses or widget instances.
206
+
207
+
208
+ .. method:: rows(size, focus=False)
209
+
210
+ .. note::
211
+
212
+ This method is not implemented in :class:`.Widget` but
213
+ must be implemented by any flow widget. See :meth:`.sizing`.
214
+
215
+ See :meth:`Widget.render` for parameter details.
216
+
217
+ :returns: The number of rows required for this widget given a number
218
+ of columns in *size*
219
+
220
+ This is the method flow widgets use to communicate their size to other
221
+ widgets without having to render a canvas. This should be a quick
222
+ calculation as this function may be called a number of times in normal
223
+ operation. If your implementation may take a long time you should add
224
+ your own caching here.
225
+
226
+ There is some metaclass magic defined in the :class:`Widget`
227
+ metaclass :class:`WidgetMeta` that causes the
228
+ result of this function to be retrieved from any
229
+ canvas cached by :class:`CanvasCache`, so if your widget
230
+ has been rendered you may not receive calls to this function. The class
231
+ variable :attr:`ignore_focus` may be defined and set to ``True`` if this
232
+ widget renders the same size regardless of the value of the *focus*
233
+ parameter.
234
+
235
+ .. method:: get_cursor_coords(size)
236
+
237
+ .. note::
238
+
239
+ This method is not implemented in :class:`.Widget` but
240
+ must be implemented by any widget that may return cursor
241
+ coordinates as part of the canvas that :meth:`render` returns.
242
+
243
+ :param size: See :meth:`Widget.render` for details.
244
+ :type size: widget size
245
+
246
+ :returns: (*col*, *row*) if this widget has a cursor, ``None`` otherwise
247
+
248
+ Return the cursor coordinates (*col*, *row*) of a cursor that will appear
249
+ as part of the canvas rendered by this widget when in focus, or ``None``
250
+ if no cursor is displayed.
251
+
252
+ The :class:`ListBox` widget
253
+ uses this method to make sure a cursor in the focus widget is not
254
+ scrolled out of view. It is a separate method to avoid having to render
255
+ the whole widget while calculating layout.
256
+
257
+ Container widgets will typically call the :meth:`.get_cursor_coords`
258
+ method on their focus widget.
259
+
260
+
261
+ .. method:: get_pref_col(size)
262
+
263
+ .. note::
264
+
265
+ This method is not implemented in :class:`.Widget` but
266
+ may be implemented by a subclass.
267
+
268
+ :param size: See :meth:`Widget.render` for details.
269
+ :type size: widget size
270
+
271
+ :returns: a column number or ``'left'`` for the leftmost available
272
+ column or ``'right'`` for the rightmost available column
273
+
274
+ Return the preferred column for the cursor to be displayed in this
275
+ widget. This value might not be the same as the column returned from
276
+ :meth:`get_cursor_coords`.
277
+
278
+ The :class:`ListBox` and :class:`Pile`
279
+ widgets call this method on a widget losing focus and use the value
280
+ returned to call :meth:`.move_cursor_to_coords` on the widget becoming
281
+ the focus. This allows the focus to move up and down through widgets
282
+ while keeping the cursor in approximately the same column on screen.
283
+
284
+
285
+ .. method:: move_cursor_to_coords(size, col, row)
286
+
287
+ .. note::
288
+
289
+ This method is not implemented in :class:`.Widget` but
290
+ may be implemented by a subclass. Not implementing this
291
+ method is equivalent to having a method that always returns
292
+ ``False``.
293
+
294
+ :param size: See :meth:`Widget.render` for details.
295
+ :type size: widget size
296
+ :param col: new column for the cursor, 0 is the left edge of this widget
297
+ :type col: int
298
+ :param row: new row for the cursor, 0 it the top row of this widget
299
+ :type row: int
300
+
301
+ :returns: ``True`` if the position was set successfully anywhere on
302
+ *row*, ``False`` otherwise
303
+ """
304
+
305
+ _selectable = False
306
+ _sizing = frozenset([Sizing.FLOW, Sizing.BOX, Sizing.FIXED])
307
+ _command_map = command_map
308
+
309
+ def __init__(self):
310
+ self.logger = logging.getLogger(f"{self.__class__.__module__}.{self.__class__.__name__}")
311
+
312
+ def _invalidate(self) -> None:
313
+ """
314
+ Mark cached canvases rendered by this widget as dirty so that
315
+ they will not be used again.
316
+ """
317
+ CanvasCache.invalidate(self)
318
+
319
+ def _emit(self, name: Hashable, *args) -> None:
320
+ """
321
+ Convenience function to emit signals with self as first
322
+ argument.
323
+ """
324
+ signals.emit_signal(self, name, self, *args)
325
+
326
+ def selectable(self) -> bool:
327
+ """
328
+ :returns: ``True`` if this is a widget that is designed to take the
329
+ focus, i.e. it contains something the user might want to
330
+ interact with, ``False`` otherwise,
331
+
332
+ This default implementation returns :attr:`._selectable`.
333
+ Subclasses may leave these is if the are not selectable,
334
+ or if they are always selectable they may
335
+ set the :attr:`_selectable` class variable to ``True``.
336
+
337
+ If this method returns ``True`` then the :meth:`.keypress` method
338
+ must be implemented.
339
+
340
+ Returning ``False`` does not guarantee that this widget will never be in
341
+ focus, only that this widget will usually be skipped over when changing
342
+ focus. It is still possible for non selectable widgets to have the focus
343
+ (typically when there are no other selectable widgets visible).
344
+ """
345
+ return self._selectable
346
+
347
+ def sizing(self) -> frozenset[Sizing]:
348
+ """
349
+ :returns: A frozenset including one or more of ``'box'``, ``'flow'`` and
350
+ ``'fixed'``. Default implementation returns the value of
351
+ :attr:`._sizing`, which for this class includes all three.
352
+
353
+ The sizing modes returned indicate the modes that may be
354
+ supported by this widget, but is not sufficient to know
355
+ that using that sizing mode will work. Subclasses should
356
+ make an effort to remove sizing modes they know will not
357
+ work given the state of the widget, but many do not yet
358
+ do this.
359
+
360
+ If a sizing mode is missing from the set then the widget
361
+ should fail when used in that mode.
362
+
363
+ If ``'flow'`` is among the values returned then the other
364
+ methods in this widget must be able to accept a
365
+ single-element tuple (*maxcol*,) to their ``size``
366
+ parameter, and the :meth:`rows` method must be defined.
367
+
368
+ If ``'box'`` is among the values returned then the other
369
+ methods must be able to accept a two-element tuple
370
+ (*maxcol*, *maxrow*) to their size parameter.
371
+
372
+ If ``'fixed'`` is among the values returned then the other
373
+ methods must be able to accept an empty tuple () to
374
+ their size parameter, and the :meth:`pack` method must
375
+ be defined.
376
+ """
377
+ return self._sizing
378
+
379
+ def pack(self, size: tuple[()] | tuple[int] | tuple[int, int], focus: bool = False) -> tuple[int, int]:
380
+ """
381
+ See :meth:`Widget.render` for parameter details.
382
+
383
+ :returns: A "packed" size (*maxcol*, *maxrow*) for this widget
384
+
385
+ Calculate and return a minimum
386
+ size where all content could still be displayed. Fixed widgets must
387
+ implement this method and return their size when ``()`` is passed as the
388
+ *size* parameter.
389
+
390
+ This default implementation returns the *size* passed, or the *maxcol*
391
+ passed and the value of :meth:`rows` as the *maxrow* when (*maxcol*,)
392
+ is passed as the *size* parameter.
393
+
394
+ .. note::
395
+
396
+ This is a new method that hasn't been fully implemented across the
397
+ standard widget types. In particular it has not yet been
398
+ implemented for container widgets.
399
+
400
+ :class:`Text` widgets have implemented this method.
401
+ You can use :meth:`Text.pack` to calculate the minimum
402
+ columns and rows required to display a text widget without wrapping,
403
+ or call it iteratively to calculate the minimum number of columns
404
+ required to display the text wrapped into a target number of rows.
405
+ """
406
+ if not size:
407
+ if Sizing.FIXED in self.sizing():
408
+ raise NotImplementedError("Fixed widgets must override Widget.pack()")
409
+ raise WidgetError(f"Cannot pack () size, this is not a fixed widget: {self!r}")
410
+
411
+ if len(size) == 1:
412
+ if Sizing.FLOW in self.sizing():
413
+ return (*size, self.rows(size, focus)) # pylint: disable=no-member # can not announce abstract
414
+
415
+ raise WidgetError(f"Cannot pack (maxcol,) size, this is not a flow widget: {self!r}")
416
+
417
+ return size
418
+
419
+ @property
420
+ def base_widget(self) -> Widget:
421
+ """
422
+ Read-only property that steps through decoration widgets
423
+ and returns the one at the base. This default implementation
424
+ returns self.
425
+ """
426
+ return self
427
+
428
+ @property
429
+ def focus(self) -> Widget | None:
430
+ """
431
+ Read-only property returning the child widget in focus for
432
+ container widgets. This default implementation
433
+ always returns ``None``, indicating that this widget has no children.
434
+ """
435
+ return None
436
+
437
+ def _not_a_container(self, val=None):
438
+ raise IndexError(f"No focus_position, {self!r} is not a container widget")
439
+
440
+ focus_position = property(
441
+ _not_a_container,
442
+ _not_a_container,
443
+ doc="""
444
+ Property for reading and setting the focus position for
445
+ container widgets. This default implementation raises
446
+ :exc:`IndexError`, making normal widgets fail the same way
447
+ accessing :attr:`.focus_position` on an empty container widget would.
448
+ """,
449
+ )
450
+
451
+ def __repr__(self):
452
+ """
453
+ A friendly __repr__ for widgets, designed to be extended
454
+ by subclasses with _repr_words and _repr_attr methods.
455
+ """
456
+ return split_repr(self)
457
+
458
+ def _repr_words(self) -> list[str]:
459
+ words = []
460
+ if self.selectable():
461
+ words = ["selectable", *words]
462
+ if self.sizing() and self.sizing() != frozenset([Sizing.FLOW, Sizing.BOX, Sizing.FIXED]):
463
+ words.append("/".join(sorted(self.sizing())))
464
+ return [*words, "widget"]
465
+
466
+ def _repr_attrs(self) -> dict[str, typing.Any]:
467
+ return {}
468
+
469
+ def keypress(self, size: tuple[()] | tuple[int] | tuple[int, int], key: str) -> str | None:
470
+ """Keyboard input handler.
471
+
472
+ :param size: See :meth:`Widget.render` for details
473
+ :type size: tuple[()] | tuple[int] | tuple[int, int]
474
+ :param key: a single keystroke value; see :ref:`keyboard-input`
475
+ :type key: str
476
+ :return: ``None`` if *key* was handled by *key* (the same value passed) if *key* was not handled
477
+ :rtype: str | None
478
+ """
479
+ if not self.selectable():
480
+ if hasattr(self, "logger"):
481
+ self.logger.debug(f"keypress sent to non selectable widget {self!r}")
482
+ else:
483
+ warnings.warn(
484
+ f"Widget {self.__class__.__name__} did not call 'super().__init__()",
485
+ WidgetWarning,
486
+ stacklevel=3,
487
+ )
488
+ LOGGER.debug(f"Widget {self!r} is not selectable")
489
+ return key
490
+
491
+ def mouse_event(
492
+ self,
493
+ size: tuple[()] | tuple[int] | tuple[int, int],
494
+ event: str,
495
+ button: int,
496
+ col: int,
497
+ row: int,
498
+ focus: bool,
499
+ ) -> bool | None:
500
+ """Mouse event handler.
501
+
502
+ :param size: See :meth:`Widget.render` for details.
503
+ :type size: tuple[()] | tuple[int] | tuple[int, int]
504
+ :param event: Values such as ``'mouse press'``, ``'ctrl mouse press'``,
505
+ ``'mouse release'``, ``'meta mouse release'``,
506
+ ``'mouse drag'``; see :ref:`mouse-input`
507
+ :type event: str
508
+ :param button: 1 through 5 for press events, often 0 for release events
509
+ (which button was released is often not known)
510
+ :type button: int
511
+ :param col: Column of the event, 0 is the left edge of this widget
512
+ :type col: int
513
+ :param row: Row of the event, 0 it the top row of this widget
514
+ :type row: int
515
+ :param focus: Set to ``True`` if this widget or one of its children is in focus
516
+ :type focus: bool
517
+ :return: ``True`` if the event was handled by this widget, ``False`` otherwise
518
+ :rtype: bool | None
519
+ """
520
+ if not self.selectable():
521
+ if hasattr(self, "logger"):
522
+ self.logger.debug(f"Widget {self!r} is not selectable")
523
+ else:
524
+ warnings.warn(
525
+ f"Widget {self.__class__.__name__} not called 'super().__init__()",
526
+ WidgetWarning,
527
+ stacklevel=3,
528
+ )
529
+ LOGGER.debug(f"Widget {self!r} is not selectable")
530
+ return False
531
+
532
+ def render(self, size: tuple[()] | tuple[int] | tuple[int, int], focus: bool = False) -> Canvas:
533
+ """Render widget and produce canvas
534
+
535
+ :param size: One of the following, *maxcol* and *maxrow* are integers > 0:
536
+
537
+ (*maxcol*, *maxrow*)
538
+ for box sizing -- the parent chooses the exact
539
+ size of this widget
540
+
541
+ (*maxcol*,)
542
+ for flow sizing -- the parent chooses only the
543
+ number of columns for this widget
544
+
545
+ ()
546
+ for fixed sizing -- this widget is a fixed size
547
+ which can't be adjusted by the parent
548
+ :type size: widget size
549
+ :param focus: set to ``True`` if this widget or one of its children is in focus
550
+ :type focus: bool
551
+
552
+ :returns: A :class:`Canvas` subclass instance containing the rendered content of this widget
553
+
554
+ :class:`Text` widgets return a :class:`TextCanvas` (arbitrary text and display attributes),
555
+ :class:`SolidFill` widgets return a :class:`SolidCanvas` (a single character repeated across the whole surface)
556
+ and container widgets return a :class:`CompositeCanvas` (one or more other canvases arranged arbitrarily).
557
+
558
+ If *focus* is ``False``, the returned canvas may not have a cursor position set.
559
+
560
+ There is some metaclass magic defined in the :class:`Widget` metaclass :class:`WidgetMeta`
561
+ that causes the result of this method to be cached by :class:`CanvasCache`.
562
+ Later calls will automatically look up the value in the cache first.
563
+
564
+ As a small optimization the class variable :attr:`ignore_focus`
565
+ may be defined and set to ``True`` if this widget renders the same
566
+ canvas regardless of the value of the *focus* parameter.
567
+
568
+ Any time the content of a widget changes it should call
569
+ :meth:`_invalidate` to remove any cached canvases, or the widget
570
+ may render the cached canvas instead of creating a new one.
571
+ """
572
+ raise NotImplementedError
573
+
574
+
575
+ class FlowWidget(Widget):
576
+ """
577
+ Deprecated. Inherit from Widget and add:
578
+
579
+ _sizing = frozenset(['flow'])
580
+
581
+ at the top of your class definition instead.
582
+
583
+ Base class of widgets that determine their rows from the number of
584
+ columns available.
585
+ """
586
+
587
+ _sizing = frozenset([Sizing.FLOW])
588
+
589
+ def __init__(self, *args, **kwargs):
590
+ warnings.warn(
591
+ """
592
+ FlowWidget is deprecated. Inherit from Widget and add:
593
+
594
+ _sizing = frozenset(['flow'])
595
+
596
+ at the top of your class definition instead.""",
597
+ DeprecationWarning,
598
+ stacklevel=3,
599
+ )
600
+ super().__init__()
601
+
602
+ def rows(self, size: int, focus: bool = False) -> int:
603
+ """
604
+ All flow widgets must implement this function.
605
+ """
606
+ raise NotImplementedError()
607
+
608
+ def render(self, size: tuple[int], focus: bool = False):
609
+ """
610
+ All widgets must implement this function.
611
+ """
612
+ raise NotImplementedError()
613
+
614
+
615
+ class BoxWidget(Widget):
616
+ """
617
+ Deprecated. Inherit from Widget and add:
618
+
619
+ _sizing = frozenset(['box'])
620
+ _selectable = True
621
+
622
+ at the top of your class definition instead.
623
+
624
+ Base class of width and height constrained widgets such as
625
+ the top level widget attached to the display object
626
+ """
627
+
628
+ _selectable = True
629
+ _sizing = frozenset([Sizing.BOX])
630
+
631
+ def __init__(self, *args, **kwargs):
632
+ warnings.warn(
633
+ """
634
+ BoxWidget is deprecated. Inherit from Widget and add:
635
+
636
+ _sizing = frozenset(['box'])
637
+ _selectable = True
638
+
639
+ at the top of your class definition instead.""",
640
+ DeprecationWarning,
641
+ stacklevel=3,
642
+ )
643
+ super().__init__()
644
+
645
+ def render(self, size: tuple[int, int], focus: bool = False):
646
+ """
647
+ All widgets must implement this function.
648
+ """
649
+ raise NotImplementedError()
650
+
651
+
652
+ def fixed_size(size):
653
+ """
654
+ raise ValueError if size != ().
655
+
656
+ Used by FixedWidgets to test size parameter.
657
+ """
658
+ if size != ():
659
+ raise ValueError(f"FixedWidget takes only () for size.passed: {size!r}")
660
+
661
+
662
+ class FixedWidget(Widget):
663
+ """
664
+ Deprecated. Inherit from Widget and add:
665
+
666
+ _sizing = frozenset(['fixed'])
667
+
668
+ at the top of your class definition instead.
669
+
670
+ Base class of widgets that know their width and height and
671
+ cannot be resized
672
+ """
673
+
674
+ _sizing = frozenset([Sizing.FIXED])
675
+
676
+ def __init__(self, *args, **kwargs):
677
+ warnings.warn(
678
+ """
679
+ FixedWidget is deprecated. Inherit from Widget and add:
680
+
681
+ _sizing = frozenset(['fixed'])
682
+
683
+ at the top of your class definition instead.""",
684
+ DeprecationWarning,
685
+ stacklevel=3,
686
+ )
687
+ super().__init__()
688
+
689
+ def render(self, size, focus=False):
690
+ """
691
+ All widgets must implement this function.
692
+ """
693
+ raise NotImplementedError()
694
+
695
+ def pack(self, size=None, focus=False):
696
+ """
697
+ All fixed widgets must implement this function.
698
+ """
699
+ raise NotImplementedError()
700
+
701
+
702
+ def delegate_to_widget_mixin(attribute_name: str) -> type[Widget]:
703
+ """
704
+ Return a mixin class that delegates all standard widget methods
705
+ to an attribute given by attribute_name.
706
+
707
+ This mixin is designed to be used as a superclass of another widget.
708
+ """
709
+ # FIXME: this is so common, let's add proper support for it
710
+ # when layout and rendering are separated
711
+
712
+ get_delegate = attrgetter(attribute_name)
713
+
714
+ class DelegateToWidgetMixin(Widget):
715
+ no_cache: typing.ClassVar[list[str]] = ["rows"] # crufty metaclass work-around
716
+
717
+ def render(self, size, focus: bool = False) -> CompositeCanvas:
718
+ canv = get_delegate(self).render(size, focus=focus)
719
+ return CompositeCanvas(canv)
720
+
721
+ @property
722
+ def selectable(self) -> Callable[[], bool]:
723
+ return get_delegate(self).selectable
724
+
725
+ @property
726
+ def get_cursor_coords(self) -> Callable[[tuple[()] | tuple[int] | tuple[int, int]], tuple[int, int] | None]:
727
+ # TODO(Aleksei): Get rid of property usage after getting rid of "if getattr"
728
+ return get_delegate(self).get_cursor_coords
729
+
730
+ @property
731
+ def get_pref_col(self) -> Callable[[tuple[()] | tuple[int] | tuple[int, int]], int | None]:
732
+ # TODO(Aleksei): Get rid of property usage after getting rid of "if getattr"
733
+ return get_delegate(self).get_pref_col
734
+
735
+ def keypress(self, size: tuple[()] | tuple[int] | tuple[int, int], key: str) -> str | None:
736
+ return get_delegate(self).keypress(size, key)
737
+
738
+ @property
739
+ def move_cursor_to_coords(self) -> Callable[[[tuple[()] | tuple[int] | tuple[int, int], int, int]], bool]:
740
+ # TODO(Aleksei): Get rid of property usage after getting rid of "if getattr"
741
+ return get_delegate(self).move_cursor_to_coords
742
+
743
+ @property
744
+ def rows(self) -> Callable[[tuple[int], bool], int]:
745
+ return get_delegate(self).rows
746
+
747
+ @property
748
+ def mouse_event(
749
+ self,
750
+ ) -> Callable[[tuple[()] | tuple[int] | tuple[int, int], str, int, int, int, bool], bool | None]:
751
+ # TODO(Aleksei): Get rid of property usage after getting rid of "if getattr"
752
+ return get_delegate(self).mouse_event
753
+
754
+ @property
755
+ def sizing(self) -> Callable[[], frozenset[Sizing]]:
756
+ return get_delegate(self).sizing
757
+
758
+ @property
759
+ def pack(self) -> Callable[[tuple[()] | tuple[int] | tuple[int, int], bool], tuple[int, int]]:
760
+ return get_delegate(self).pack
761
+
762
+ return DelegateToWidgetMixin
763
+
764
+
765
+ class WidgetWrapError(Exception):
766
+ pass
767
+
768
+
769
+ class WidgetWrap(delegate_to_widget_mixin("_wrapped_widget"), typing.Generic[WrappedWidget]):
770
+ def __init__(self, w: WrappedWidget) -> None:
771
+ """
772
+ w -- widget to wrap, stored as self._w
773
+
774
+ This object will pass the functions defined in Widget interface
775
+ definition to self._w.
776
+
777
+ The purpose of this widget is to provide a base class for
778
+ widgets that compose other widgets for their display and
779
+ behaviour. The details of that composition should not affect
780
+ users of the subclass. The subclass may decide to expose some
781
+ of the wrapped widgets by behaving like a ContainerWidget or
782
+ WidgetDecoration, or it may hide them from outside access.
783
+ """
784
+ super().__init__()
785
+ if not isinstance(w, Widget):
786
+ obj_class_path = f"{w.__class__.__module__}.{w.__class__.__name__}"
787
+ warnings.warn(
788
+ f"{obj_class_path} is not subclass of Widget",
789
+ DeprecationWarning,
790
+ stacklevel=2,
791
+ )
792
+ self._wrapped_widget = w
793
+
794
+ @property
795
+ def _w(self) -> WrappedWidget:
796
+ return self._wrapped_widget
797
+
798
+ @_w.setter
799
+ def _w(self, new_widget: WrappedWidget) -> None:
800
+ """
801
+ Change the wrapped widget. This is meant to be called
802
+ only by subclasses.
803
+
804
+ >>> size = (10,)
805
+ >>> ww = WidgetWrap(Edit("hello? ","hi"))
806
+ >>> ww.render(size).text # ... = b in Python 3
807
+ [...'hello? hi ']
808
+ >>> ww.selectable()
809
+ True
810
+ >>> ww._w = Text("goodbye") # calls _set_w()
811
+ >>> ww.render(size).text
812
+ [...'goodbye ']
813
+ >>> ww.selectable()
814
+ False
815
+ """
816
+ self._wrapped_widget = new_widget
817
+ self._invalidate()
818
+
819
+ def _set_w(self, w: WrappedWidget) -> None:
820
+ """
821
+ Change the wrapped widget. This is meant to be called
822
+ only by subclasses.
823
+ >>> from urwid import Edit, Text
824
+ >>> size = (10,)
825
+ >>> ww = WidgetWrap(Edit("hello? ","hi"))
826
+ >>> ww.render(size).text # ... = b in Python 3
827
+ [...'hello? hi ']
828
+ >>> ww.selectable()
829
+ True
830
+ >>> ww._w = Text("goodbye") # calls _set_w()
831
+ >>> ww.render(size).text
832
+ [...'goodbye ']
833
+ >>> ww.selectable()
834
+ False
835
+ """
836
+ warnings.warn(
837
+ "_set_w is deprecated. Please use 'WidgetWrap._w' property directly",
838
+ DeprecationWarning,
839
+ stacklevel=2,
840
+ )
841
+ self._wrapped_widget = w
842
+ self._invalidate()
843
+
844
+
845
+ def _test():
846
+ import doctest
847
+
848
+ doctest.testmod()
849
+
850
+
851
+ if __name__ == "__main__":
852
+ _test()