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/pile.py ADDED
@@ -0,0 +1,971 @@
1
+ from __future__ import annotations
2
+
3
+ import typing
4
+ import warnings
5
+ from itertools import chain, repeat
6
+
7
+ from urwid.canvas import CanvasCombine, CompositeCanvas, SolidCanvas
8
+ from urwid.command_map import Command
9
+ from urwid.monitored_list import MonitoredFocusList, MonitoredList
10
+ from urwid.util import is_mouse_press
11
+
12
+ from .constants import Sizing, WHSettings
13
+ from .container import WidgetContainerListContentsMixin, WidgetContainerMixin, _ContainerElementSizingFlag
14
+ from .widget import Widget, WidgetError, WidgetWarning
15
+
16
+ if typing.TYPE_CHECKING:
17
+ from collections.abc import Iterable, Sequence
18
+
19
+ from typing_extensions import Literal
20
+
21
+
22
+ class PileError(WidgetError):
23
+ """Pile related errors."""
24
+
25
+
26
+ class PileWarning(WidgetWarning):
27
+ """Pile related warnings."""
28
+
29
+
30
+ class Pile(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin):
31
+ """
32
+ A pile of widgets stacked vertically from top to bottom
33
+ """
34
+
35
+ def sizing(self) -> frozenset[Sizing]:
36
+ """Sizing supported by widget.
37
+
38
+ :return: Calculated widget sizing
39
+ :rtype: frozenset[Sizing]
40
+
41
+ Due to the nature of container with mutable contents, this method cannot be cached.
42
+
43
+ Rules:
44
+ * WEIGHT BOX -> BOX
45
+ * GIVEN BOX -> FLOW (height is known) & BOX (can be shrinked/padded)
46
+ * PACK BOX -> Unsupported
47
+
48
+ * WEIGHT FLOW -> FLOW
49
+ * GIVEN FLOW -> Unsupported
50
+ * PACK FLOW -> FLOW
51
+
52
+ * WEIGHT FIXED -> Need also FLOW or/and BOX to properly render due to width calculation
53
+ * GIVEN FIXED -> Unsupported
54
+ * PACK FIXED -> FIXED (widget knows its size)
55
+
56
+ >>> from urwid import BigText, ProgressBar, SolidFill, Text, Thin3x3Font
57
+ >>> font = Thin3x3Font()
58
+
59
+ # BOX-only widget
60
+ >>> Pile((SolidFill("#"),))
61
+ <Pile box widget>
62
+
63
+ # GIVEN BOX -> BOX/FLOW
64
+ >>> Pile(((10, SolidFill("#")),))
65
+ <Pile box/flow widget>
66
+
67
+ # FLOW-only
68
+ >>> Pile((ProgressBar(None, None),))
69
+ <Pile flow widget>
70
+
71
+ # FIXED -> FIXED
72
+ >>> Pile(((WHSettings.PACK, BigText("0", font)),))
73
+ <Pile fixed widget>
74
+
75
+ # FLOW/FIXED -> FLOW/FIXED
76
+ >>> Pile(((WHSettings.PACK, Text("text")),))
77
+ <Pile fixed/flow widget>
78
+
79
+ # FLOW + FIXED widgets -> FLOW/FIXED
80
+ >>> Pile((ProgressBar(None, None), (WHSettings.PACK, BigText("0", font))))
81
+ <Pile fixed/flow widget>
82
+
83
+ # GIVEN BOX + FIXED widgets -> BOX/FLOW/FIXED (GIVEN BOX allows overriding its height & allows any width)
84
+ >>> Pile(((10, SolidFill("#")), (WHSettings.PACK, BigText("0", font))))
85
+ <Pile widget>
86
+
87
+ # Invalid sizing combination -> use fallback settings (and produce warning)
88
+ >>> Pile(((WHSettings.WEIGHT, 1, BigText("0", font)),))
89
+ <Pile box/flow widget>
90
+ """
91
+ strict_box = False
92
+ has_flow = False
93
+
94
+ has_fixed = False
95
+ supported: set[Sizing] = set()
96
+
97
+ box_flow_fixed = (
98
+ _ContainerElementSizingFlag.BOX | _ContainerElementSizingFlag.FLOW | _ContainerElementSizingFlag.FIXED
99
+ )
100
+ flow_fixed = _ContainerElementSizingFlag.FLOW | _ContainerElementSizingFlag.FIXED
101
+
102
+ for idx, (widget, (size_kind, _size_weight)) in enumerate(self.contents):
103
+ w_sizing = widget.sizing()
104
+
105
+ flag = _ContainerElementSizingFlag.NONE
106
+
107
+ if size_kind == WHSettings.WEIGHT:
108
+ flag |= _ContainerElementSizingFlag.WH_WEIGHT
109
+ if Sizing.BOX in w_sizing:
110
+ flag |= _ContainerElementSizingFlag.BOX
111
+ if Sizing.FLOW in w_sizing:
112
+ flag |= _ContainerElementSizingFlag.FLOW
113
+ if Sizing.FIXED in w_sizing and w_sizing & {Sizing.BOX, Sizing.FLOW}:
114
+ flag |= _ContainerElementSizingFlag.FIXED
115
+
116
+ elif size_kind == WHSettings.GIVEN:
117
+ flag |= _ContainerElementSizingFlag.WH_GIVEN
118
+ if Sizing.BOX in w_sizing:
119
+ flag |= _ContainerElementSizingFlag.BOX
120
+ flag |= _ContainerElementSizingFlag.FLOW
121
+
122
+ else:
123
+ flag = _ContainerElementSizingFlag.WH_PACK
124
+ if Sizing.FLOW in w_sizing:
125
+ flag |= _ContainerElementSizingFlag.FLOW
126
+ if Sizing.FIXED in w_sizing:
127
+ flag |= _ContainerElementSizingFlag.FIXED
128
+
129
+ if not flag & box_flow_fixed:
130
+ warnings.warn(
131
+ f"Sizing combination of widget {idx} not supported: "
132
+ f"{size_kind.name} {'|'.join(w_sizing).upper()}",
133
+ PileWarning,
134
+ stacklevel=3,
135
+ )
136
+ return frozenset((Sizing.BOX, Sizing.FLOW))
137
+
138
+ if flag & _ContainerElementSizingFlag.BOX:
139
+ supported.add(Sizing.BOX)
140
+ if not flag & flow_fixed:
141
+ strict_box = True
142
+ break
143
+
144
+ if flag & _ContainerElementSizingFlag.FLOW:
145
+ has_flow = True
146
+ if flag & _ContainerElementSizingFlag.FIXED:
147
+ has_fixed = True
148
+
149
+ if not strict_box:
150
+ if has_flow:
151
+ supported.add(Sizing.FLOW)
152
+ if has_fixed:
153
+ supported.add(Sizing.FIXED)
154
+
155
+ return frozenset(supported)
156
+
157
+ def __init__(
158
+ self,
159
+ widget_list: Iterable[
160
+ Widget
161
+ | tuple[Literal["pack", WHSettings.PACK] | int, Widget]
162
+ | tuple[Literal["weight", "given", WHSettings.WEIGHT, WHSettings.GIVEN], int, Widget]
163
+ ],
164
+ focus_item: Widget | int | None = None,
165
+ ) -> None:
166
+ """
167
+ :param widget_list: child widgets
168
+ :type widget_list: iterable
169
+ :param focus_item: child widget that gets the focus initially.
170
+ Chooses the first selectable widget if unset.
171
+ :type focus_item: Widget or int
172
+
173
+ *widget_list* may also contain tuples such as:
174
+
175
+ (*given_height*, *widget*)
176
+ always treat *widget* as a box widget and give it *given_height* rows,
177
+ where given_height is an int
178
+ (``'pack'``, *widget*)
179
+ allow *widget* to calculate its own height by calling its :meth:`rows`
180
+ method, ie. treat it as a flow widget.
181
+ (``'weight'``, *weight*, *widget*)
182
+ if the pile is treated as a box widget then treat widget as a box
183
+ widget with a height based on its relative weight value, otherwise
184
+ treat the same as (``'pack'``, *widget*).
185
+
186
+ Widgets not in a tuple are the same as (``'weight'``, ``1``, *widget*)`
187
+
188
+ .. note:: If the Pile is treated as a box widget there must be at least
189
+ one ``'weight'`` tuple in :attr:`widget_list`.
190
+ """
191
+ self._selectable = False
192
+ super().__init__()
193
+ self._contents: MonitoredFocusList[
194
+ Widget,
195
+ tuple[Literal[WHSettings.PACK], None] | tuple[Literal[WHSettings.GIVEN, WHSettings.WEIGHT], int],
196
+ ] = MonitoredFocusList()
197
+ self._contents.set_modified_callback(self._contents_modified)
198
+ self._contents.set_focus_changed_callback(lambda f: self._invalidate())
199
+ self._contents.set_validate_contents_modified(self._validate_contents_modified)
200
+
201
+ for i, original in enumerate(widget_list):
202
+ w = original
203
+ if not isinstance(w, tuple):
204
+ self.contents.append((w, (WHSettings.WEIGHT, 1)))
205
+ elif w[0] in {Sizing.FLOW, WHSettings.PACK}: # 'pack' used to be called 'flow'
206
+ f, w = w
207
+ self.contents.append((w, (WHSettings.PACK, None)))
208
+ elif len(w) == 2 or w[0] in {Sizing.FIXED, WHSettings.GIVEN}: # backwards compatibility
209
+ height, w = w[-2:]
210
+ self.contents.append((w, (WHSettings.GIVEN, height)))
211
+ elif w[0] == WHSettings.WEIGHT:
212
+ f, height, w = w
213
+ self.contents.append((w, (f, height)))
214
+ else:
215
+ raise PileError(f"initial widget list item invalid {original!r}")
216
+ if focus_item is None and w.selectable():
217
+ focus_item = i
218
+
219
+ if not isinstance(w, Widget):
220
+ warnings.warn(f"{w!r} is not a Widget", PileWarning, stacklevel=3)
221
+
222
+ if self.contents and focus_item is not None:
223
+ self.focus = focus_item
224
+
225
+ self.pref_col = 0
226
+
227
+ def _contents_modified(self) -> None:
228
+ """
229
+ Recalculate whether this widget should be selectable whenever the
230
+ contents has been changed.
231
+ """
232
+ self._selectable = any(w.selectable() for w, o in self.contents)
233
+ self._invalidate()
234
+
235
+ def _validate_contents_modified(self, slc, new_items):
236
+ for item in new_items:
237
+ try:
238
+ _w, (t, _n) = item
239
+ if t not in {WHSettings.PACK, WHSettings.GIVEN, WHSettings.WEIGHT}:
240
+ raise PileError(f"added content invalid: {item!r}")
241
+ except (TypeError, ValueError) as exc: # noqa: PERF203
242
+ raise PileError(f"added content invalid: {item!r}").with_traceback(exc.__traceback__) from exc
243
+
244
+ @property
245
+ def widget_list(self):
246
+ """
247
+ A list of the widgets in this Pile
248
+
249
+ .. note:: only for backwards compatibility. You should use the new
250
+ standard container property :attr:`contents`.
251
+ """
252
+ warnings.warn(
253
+ "only for backwards compatibility. You should use the new standard container property `contents`",
254
+ PendingDeprecationWarning,
255
+ stacklevel=2,
256
+ )
257
+ ml = MonitoredList(w for w, t in self.contents)
258
+
259
+ def user_modified():
260
+ self.widget_list = ml
261
+
262
+ ml.set_modified_callback(user_modified)
263
+ return ml
264
+
265
+ @widget_list.setter
266
+ def widget_list(self, widgets):
267
+ focus_position = self.focus_position
268
+ self.contents = [
269
+ (new, options)
270
+ for (new, (w, options)) in zip(
271
+ widgets,
272
+ # need to grow contents list if widgets is longer
273
+ chain(self.contents, repeat((None, (WHSettings.WEIGHT, 1)))),
274
+ )
275
+ ]
276
+ if focus_position < len(widgets):
277
+ self.focus_position = focus_position
278
+
279
+ @property
280
+ def item_types(self):
281
+ """
282
+ A list of the options values for widgets in this Pile.
283
+
284
+ .. note:: only for backwards compatibility. You should use the new
285
+ standard container property :attr:`contents`.
286
+ """
287
+ warnings.warn(
288
+ "only for backwards compatibility. You should use the new standard container property `contents`",
289
+ PendingDeprecationWarning,
290
+ stacklevel=2,
291
+ )
292
+ ml = MonitoredList(
293
+ # return the old item type names
294
+ ({WHSettings.GIVEN: Sizing.FIXED, WHSettings.PACK: Sizing.FLOW}.get(f, f), height)
295
+ for w, (f, height) in self.contents
296
+ )
297
+
298
+ def user_modified():
299
+ self.item_types = ml
300
+
301
+ ml.set_modified_callback(user_modified)
302
+ return ml
303
+
304
+ @item_types.setter
305
+ def item_types(self, item_types):
306
+ warnings.warn(
307
+ "only for backwards compatibility. You should use the new standard container property `contents`",
308
+ PendingDeprecationWarning,
309
+ stacklevel=2,
310
+ )
311
+ focus_position = self.focus_position
312
+ self.contents = [
313
+ (w, ({Sizing.FIXED: WHSettings.GIVEN, Sizing.FLOW: WHSettings.PACK}.get(new_t, new_t), new_height))
314
+ for ((new_t, new_height), (w, options)) in zip(item_types, self.contents)
315
+ ]
316
+ if focus_position < len(item_types):
317
+ self.focus_position = focus_position
318
+
319
+ @property
320
+ def contents(
321
+ self,
322
+ ) -> MonitoredFocusList[
323
+ Widget,
324
+ tuple[Literal[WHSettings.PACK], None] | tuple[Literal[WHSettings.GIVEN, WHSettings.WEIGHT], int],
325
+ ]:
326
+ """
327
+ The contents of this Pile as a list of (widget, options) tuples.
328
+
329
+ options currently may be one of
330
+
331
+ (``'pack'``, ``None``)
332
+ allow widget to calculate its own height by calling its
333
+ :meth:`rows <Widget.rows>` method, i.e. treat it as a flow widget.
334
+ (``'given'``, *n*)
335
+ Always treat widget as a box widget with a given height of *n* rows.
336
+ (``'weight'``, *w*)
337
+ If the Pile itself is treated as a box widget then
338
+ the value *w* will be used as a relative weight for assigning rows
339
+ to this box widget. If the Pile is being treated as a flow
340
+ widget then this is the same as (``'pack'``, ``None``) and the *w*
341
+ value is ignored.
342
+
343
+ If the Pile itself is treated as a box widget then at least one
344
+ widget must have a (``'weight'``, *w*) options value, or the Pile will
345
+ not be able to grow to fill the required number of rows.
346
+
347
+ This list may be modified like a normal list and the Pile widget
348
+ will updated automatically.
349
+
350
+ .. seealso:: Create new options tuples with the :meth:`options` method
351
+ """
352
+ return self._contents
353
+
354
+ @contents.setter
355
+ def contents(
356
+ self,
357
+ c: Sequence[
358
+ Widget,
359
+ tuple[Literal[WHSettings.PACK], None] | tuple[Literal[WHSettings.GIVEN, WHSettings.WEIGHT], int],
360
+ ],
361
+ ) -> None:
362
+ self._contents[:] = c
363
+
364
+ @staticmethod
365
+ def options(
366
+ height_type: Literal["pack", "given", "weight"] | WHSettings = WHSettings.WEIGHT,
367
+ height_amount: int | None = 1,
368
+ ) -> (
369
+ tuple[Literal[WHSettings.PACK], None]
370
+ | tuple[Literal["given", "weight", WHSettings.GIVEN, WHSettings.WEIGHT], int]
371
+ ):
372
+ """
373
+ Return a new options tuple for use in a Pile's :attr:`contents` list.
374
+
375
+ :param height_type: ``'pack'``, ``'given'`` or ``'weight'``
376
+ :param height_amount: ``None`` for ``'pack'``, a number of rows for
377
+ ``'fixed'`` or a weight value (number) for ``'weight'``
378
+ """
379
+
380
+ if height_type == WHSettings.PACK:
381
+ return (WHSettings.PACK, None)
382
+ if height_type not in {WHSettings.GIVEN, WHSettings.WEIGHT}:
383
+ raise PileError(f"invalid height_type: {height_type!r}")
384
+ return (height_type, height_amount)
385
+
386
+ @property
387
+ def focus(self) -> Widget | None:
388
+ """the child widget in focus or None when Pile is empty"""
389
+ if not self.contents:
390
+ return None
391
+ return self.contents[self.focus_position][0]
392
+
393
+ @focus.setter
394
+ def focus(self, item: Widget | int) -> None:
395
+ """
396
+ Set the item in focus, for backwards compatibility.
397
+
398
+ .. note:: only for backwards compatibility. You should use the new
399
+ standard container property :attr:`focus_position`.
400
+ to set the position by integer index instead.
401
+
402
+ :param item: element to focus
403
+ :type item: Widget or int
404
+ """
405
+ if isinstance(item, int):
406
+ self.focus_position = item
407
+ return
408
+ for i, (w, _options) in enumerate(self.contents):
409
+ if item == w:
410
+ self.focus_position = i
411
+ return
412
+ raise ValueError(f"Widget not found in Pile contents: {item!r}")
413
+
414
+ def _get_focus(self) -> Widget:
415
+ warnings.warn(
416
+ f"method `{self.__class__.__name__}._get_focus` is deprecated, "
417
+ f"please use `{self.__class__.__name__}.focus` property",
418
+ DeprecationWarning,
419
+ stacklevel=3,
420
+ )
421
+ if not self.contents:
422
+ return None
423
+ return self.contents[self.focus_position][0]
424
+
425
+ def get_focus(self) -> Widget | None:
426
+ """
427
+ Return the widget in focus, for backwards compatibility. You may
428
+ also use the new standard container property .focus to get the
429
+ child widget in focus.
430
+ """
431
+ warnings.warn(
432
+ "for backwards compatibility."
433
+ "You may also use the new standard container property .focus to get the child widget in focus.",
434
+ PendingDeprecationWarning,
435
+ stacklevel=2,
436
+ )
437
+ if not self.contents:
438
+ return None
439
+ return self.contents[self.focus_position][0]
440
+
441
+ def set_focus(self, item: Widget | int) -> None:
442
+ warnings.warn(
443
+ "for backwards compatibility."
444
+ "You may also use the new standard container property .focus to get the child widget in focus.",
445
+ PendingDeprecationWarning,
446
+ stacklevel=2,
447
+ )
448
+ if isinstance(item, int):
449
+ self.focus_position = item
450
+ return
451
+ for i, (w, _options) in enumerate(self.contents):
452
+ if item == w:
453
+ self.focus_position = i
454
+ return
455
+ raise ValueError(f"Widget not found in Pile contents: {item!r}")
456
+
457
+ @property
458
+ def focus_item(self):
459
+ warnings.warn(
460
+ "only for backwards compatibility."
461
+ "You should use the new standard container properties "
462
+ "`focus` and `focus_position` to get the child widget in focus or modify the focus position.",
463
+ DeprecationWarning,
464
+ stacklevel=2,
465
+ )
466
+ return self.focus
467
+
468
+ @focus_item.setter
469
+ def focus_item(self, new_item):
470
+ warnings.warn(
471
+ "only for backwards compatibility."
472
+ "You should use the new standard container properties "
473
+ "`focus` and `focus_position` to get the child widget in focus or modify the focus position.",
474
+ DeprecationWarning,
475
+ stacklevel=2,
476
+ )
477
+ self.focus = new_item
478
+
479
+ @property
480
+ def focus_position(self) -> int:
481
+ """
482
+ index of child widget in focus.
483
+ Raises :exc:`IndexError` if read when Pile is empty, or when set to an invalid index.
484
+ """
485
+ if not self.contents:
486
+ raise IndexError("No focus_position, Pile is empty")
487
+ return self.contents.focus
488
+
489
+ @focus_position.setter
490
+ def focus_position(self, position: int) -> None:
491
+ """
492
+ Set the widget in focus.
493
+
494
+ position -- index of child widget to be made focus
495
+ """
496
+ try:
497
+ if position < 0 or position >= len(self.contents):
498
+ raise IndexError(f"No Pile child widget at position {position}")
499
+ except TypeError as exc:
500
+ raise IndexError(f"No Pile child widget at position {position}").with_traceback(exc.__traceback__) from exc
501
+ self.contents.focus = position
502
+
503
+ def _get_focus_position(self) -> int | None:
504
+ warnings.warn(
505
+ f"method `{self.__class__.__name__}._get_focus_position` is deprecated, "
506
+ f"please use `{self.__class__.__name__}.focus_position` property",
507
+ DeprecationWarning,
508
+ stacklevel=3,
509
+ )
510
+ if not self.contents:
511
+ raise IndexError("No focus_position, Pile is empty")
512
+ return self.contents.focus
513
+
514
+ def _set_focus_position(self, position: int) -> None:
515
+ """
516
+ Set the widget in focus.
517
+
518
+ position -- index of child widget to be made focus
519
+ """
520
+ warnings.warn(
521
+ f"method `{self.__class__.__name__}._set_focus_position` is deprecated, "
522
+ f"please use `{self.__class__.__name__}.focus_position` property",
523
+ DeprecationWarning,
524
+ stacklevel=3,
525
+ )
526
+ try:
527
+ if position < 0 or position >= len(self.contents):
528
+ raise IndexError(f"No Pile child widget at position {position}")
529
+ except TypeError as exc:
530
+ raise IndexError(f"No Pile child widget at position {position}").with_traceback(exc.__traceback__) from exc
531
+ self.contents.focus = position
532
+
533
+ def get_pref_col(self, size: tuple[()] | tuple[int] | tuple[int, int]) -> int | None:
534
+ """Return the preferred column for the cursor, or None."""
535
+ if not self.selectable():
536
+ return None
537
+
538
+ if not self.contents:
539
+ return None
540
+
541
+ _, _, size_args = self.get_rows_sizes(size, focus=self.selectable())
542
+ self._update_pref_col_from_focus(size_args[self.focus_position])
543
+ return self.pref_col
544
+
545
+ def get_item_size(
546
+ self,
547
+ size: tuple[()] | tuple[int] | tuple[int, int],
548
+ i: int,
549
+ focus: bool,
550
+ item_rows: list[int] | None = None,
551
+ ) -> tuple[()] | tuple[int] | tuple[int, int]:
552
+ """
553
+ Return a size appropriate for passing to self.contents[i][0].render
554
+ """
555
+ _w, (f, height) = self.contents[i]
556
+ if f == WHSettings.PACK:
557
+ if not size:
558
+ return ()
559
+ return (size[0],)
560
+
561
+ if not size:
562
+ raise PileError(f"Element {i} using parameters {f} and do not have full size information")
563
+
564
+ maxcol = size[0]
565
+
566
+ if f == WHSettings.GIVEN:
567
+ return (maxcol, height)
568
+
569
+ if f == WHSettings.WEIGHT:
570
+ if len(size) == 2:
571
+ if not item_rows:
572
+ item_rows = self.get_item_rows(size, focus)
573
+ return (maxcol, item_rows[i])
574
+
575
+ return (maxcol,)
576
+
577
+ raise PileError(f"Unsupported item height rules: {f}")
578
+
579
+ def _get_fixed_rows_sizes(
580
+ self,
581
+ focus: bool = False,
582
+ ) -> tuple[Sequence[int], Sequence[int], Sequence[tuple[int] | tuple[()]]]:
583
+ if not self.contents:
584
+ return (), (), ()
585
+
586
+ widths: dict[int, int] = {}
587
+ heights: dict[int, int] = {}
588
+ w_h_args: dict[int, tuple[int, int] | tuple[int] | tuple[()]] = {}
589
+
590
+ flow: list[tuple[Widget, int, bool]] = []
591
+ box: list[int] = []
592
+ weighted: dict[int, list[int]] = {}
593
+ weights: list[int] = []
594
+ weight_max_sizes: dict[int, int] = {}
595
+
596
+ for idx, (widget, (size_kind, size_weight)) in enumerate(self.contents):
597
+ w_sizing = widget.sizing()
598
+ focused = focus and self.focus == widget
599
+ if size_kind == WHSettings.PACK:
600
+ if Sizing.FIXED in w_sizing:
601
+ widths[idx], heights[idx] = widget.pack((), focused)
602
+ w_h_args[idx] = ()
603
+ if Sizing.FLOW in w_sizing:
604
+ # re-calculate height at the end
605
+ flow.append((widget, idx, focused))
606
+ if not w_sizing & {Sizing.FIXED, Sizing.FLOW}:
607
+ raise PileError(f"Unsupported sizing {w_sizing} for {size_kind.upper()}")
608
+
609
+ elif size_kind == WHSettings.GIVEN:
610
+ heights[idx] = size_weight
611
+ if Sizing.BOX in w_sizing:
612
+ box.append(idx)
613
+ else:
614
+ raise PileError(f"Unsupported sizing {w_sizing} for {size_kind.upper()}")
615
+
616
+ elif size_weight <= 0:
617
+ widths[idx] = 0
618
+ heights[idx] = 0
619
+ if Sizing.FLOW in w_sizing:
620
+ w_h_args[idx] = (0,)
621
+ else:
622
+ w_sizing[idx] = (0, 0)
623
+
624
+ elif Sizing.FIXED in w_sizing and w_sizing & {Sizing.BOX, Sizing.FLOW}:
625
+ width, height = widget.pack((), focused)
626
+ widths[idx] = width # We're fitting everything in case of FIXED
627
+
628
+ if Sizing.BOX in w_sizing:
629
+ weighted.setdefault(size_weight, []).append(idx)
630
+ weights.append(size_weight)
631
+
632
+ weight_max_sizes.setdefault(size_weight, height)
633
+ weight_max_sizes[size_weight] = max(weight_max_sizes[size_weight], height)
634
+
635
+ else:
636
+ # width replace is allowed
637
+ flow.append((widget, idx, focused))
638
+
639
+ elif Sizing.FLOW in w_sizing:
640
+ # FLOW WEIGHT widgets are rendered the same as PACK WEIGHT
641
+ flow.append((widget, idx, focused))
642
+ else:
643
+ raise PileError(f"Unsupported combination of {size_kind}, {w_sizing}")
644
+
645
+ if not widths:
646
+ raise PileError("No widgets providing width information")
647
+
648
+ max_width = max(widths.values())
649
+
650
+ for widget, idx, focused in flow:
651
+ widths[idx] = max_width
652
+ heights[idx] = widget.rows((max_width,), focused)
653
+ w_h_args[idx] = (max_width,)
654
+
655
+ if weight_max_sizes:
656
+ max_weighted_coefficient = max(height / weight for weight, height in weight_max_sizes.items())
657
+
658
+ for weight in weight_max_sizes:
659
+ height = max(int(max_weighted_coefficient * weight + 0.5), 1)
660
+ for idx in weighted[weight]:
661
+ heights[idx] = height
662
+ w_h_args[idx] = (max_width, height)
663
+
664
+ for idx in box:
665
+ widths[idx] = max_width
666
+ w_h_args[idx] = (max_width, heights[idx])
667
+
668
+ return (
669
+ tuple(widths[idx] for idx in range(len(widths))),
670
+ tuple(heights[idx] for idx in range(len(heights))),
671
+ tuple(w_h_args[idx] for idx in range(len(w_h_args))),
672
+ )
673
+
674
+ def get_rows_sizes(
675
+ self,
676
+ size: tuple[int, int] | tuple[int] | tuple[()],
677
+ focus: bool = False,
678
+ ) -> tuple[Sequence[int], Sequence[int], Sequence[tuple[int, int] | tuple[int] | tuple[()]]]:
679
+ """Get rows widths, heights and render size parameters"""
680
+ if not size:
681
+ return self._get_fixed_rows_sizes(focus=focus)
682
+
683
+ maxcol = size[0]
684
+ item_rows = None
685
+
686
+ widths: list[int] = []
687
+ heights: list[int] = []
688
+ w_h_args: list[tuple[int, int] | tuple[int] | tuple[()]] = []
689
+
690
+ for i, (w, (f, height)) in enumerate(self.contents):
691
+ if isinstance(w, Widget):
692
+ w_sizing = w.sizing()
693
+ else:
694
+ warnings.warn(f"{w!r} is not a Widget", PileWarning, stacklevel=3)
695
+ w_sizing = frozenset((Sizing.FLOW, Sizing.BOX))
696
+
697
+ item_focus = focus and self.focus == w
698
+ widths.append(maxcol)
699
+
700
+ if f == WHSettings.GIVEN:
701
+ heights.append(height)
702
+ w_h_args.append((maxcol, height))
703
+ elif f == WHSettings.PACK or len(size) == 1:
704
+ if Sizing.FLOW in w_sizing:
705
+ w_h_arg = (maxcol,)
706
+ elif Sizing.FIXED in w_sizing and f == WHSettings.PACK:
707
+ w_h_arg = ()
708
+ else:
709
+ warnings.warn(
710
+ f"Unusual widget {i} sizing {w_sizing} for {f.upper()}). "
711
+ f"Assuming wrong sizing and using {Sizing.FLOW.upper()} for height calculation",
712
+ PileWarning,
713
+ stacklevel=3,
714
+ )
715
+ w_h_arg = (maxcol,)
716
+
717
+ heights.append(w.pack(w_h_arg, item_focus)[1])
718
+ w_h_args.append(w_h_arg)
719
+ else:
720
+ if item_rows is None:
721
+ item_rows = self.get_item_rows(size, focus)
722
+ rows = item_rows[i]
723
+ heights.append(rows)
724
+ w_h_args.append((maxcol, rows))
725
+
726
+ return (tuple(widths), tuple(heights), tuple(w_h_args))
727
+
728
+ def pack(self, size: tuple[()] | tuple[int] | tuple[int, int] = (), focus: bool = False) -> tuple[int, int]:
729
+ """Get packed sized for widget."""
730
+ if size:
731
+ return super().pack(size, focus)
732
+ widths, heights, _ = self.get_rows_sizes(size, focus)
733
+ return (max(widths), sum(heights))
734
+
735
+ def get_item_rows(self, size: tuple[()] | tuple[int] | tuple[int, int], focus: bool) -> list[int]:
736
+ """
737
+ Return a list of the number of rows used by each widget in self.contents
738
+ """
739
+ remaining = None
740
+ maxcol = size[0]
741
+ if len(size) == 2:
742
+ remaining = size[1]
743
+
744
+ rows_numbers = []
745
+
746
+ if remaining is None:
747
+ # pile is a flow widget
748
+ for i, (w, (f, height)) in enumerate(self.contents):
749
+ if isinstance(w, Widget):
750
+ w_sizing = w.sizing()
751
+ else:
752
+ warnings.warn(f"{w!r} is not a Widget", PileWarning, stacklevel=3)
753
+ w_sizing = frozenset((Sizing.FLOW, Sizing.BOX))
754
+
755
+ focused = focus and self.focus == w
756
+
757
+ if f == WHSettings.GIVEN:
758
+ rows_numbers.append(height)
759
+ elif Sizing.FLOW in w_sizing:
760
+ rows_numbers.append(w.rows((maxcol,), focus=focused))
761
+ elif Sizing.FIXED in w_sizing and f == WHSettings.PACK:
762
+ rows_numbers.append(w.pack((), focused)[0])
763
+ else:
764
+ warnings.warn(
765
+ f"Unusual widget {i} sizing {w_sizing} for {f.upper()}). "
766
+ f"Assuming wrong sizing and using {Sizing.FLOW.upper()} for height calculation",
767
+ PileWarning,
768
+ stacklevel=3,
769
+ )
770
+ rows_numbers.append(w.rows((maxcol,), focus=focused))
771
+ return rows_numbers
772
+
773
+ # pile is a box widget
774
+ # do an extra pass to calculate rows for each widget
775
+ wtotal = 0
776
+ for w, (f, height) in self.contents:
777
+ if f == WHSettings.PACK:
778
+ rows = w.rows((maxcol,), focus=focus and self.focus == w)
779
+ rows_numbers.append(rows)
780
+ remaining -= rows
781
+ elif f == WHSettings.GIVEN:
782
+ rows_numbers.append(height)
783
+ remaining -= height
784
+ elif height:
785
+ rows_numbers.append(None)
786
+ wtotal += height
787
+ else:
788
+ rows_numbers.append(0) # zero-weighted items treated as ('given', 0)
789
+
790
+ if wtotal == 0:
791
+ raise PileError("No weighted widgets found for Pile treated as a box widget")
792
+
793
+ remaining = max(remaining, 0)
794
+
795
+ for i, (_w, (_f, height)) in enumerate(self.contents):
796
+ li = rows_numbers[i]
797
+ if li is None:
798
+ rows = int(float(remaining) * height / wtotal + 0.5)
799
+ rows_numbers[i] = rows
800
+ remaining -= rows
801
+ wtotal -= height
802
+ return rows_numbers
803
+
804
+ def render(
805
+ self,
806
+ size: tuple[()] | tuple[int] | tuple[int, int],
807
+ focus: bool = False,
808
+ ) -> SolidCanvas | CompositeCanvas:
809
+ _widths, heights, size_args = self.get_rows_sizes(size, focus)
810
+
811
+ combinelist = []
812
+ for i, (height, w_size, (w, _)) in enumerate(zip(heights, size_args, self.contents)):
813
+ item_focus = self.focus == w
814
+ canv = None
815
+ if height > 0:
816
+ canv = w.render(w_size, focus=focus and item_focus)
817
+
818
+ if canv:
819
+ combinelist.append((canv, i, item_focus))
820
+
821
+ if not combinelist:
822
+ return SolidCanvas(" ", size[0], (size[1:] + (0,))[0])
823
+
824
+ out = CanvasCombine(combinelist)
825
+ if len(size) == 2 and size[1] != out.rows():
826
+ # flow/fixed widgets rendered too large/small
827
+ out = CompositeCanvas(out)
828
+ out.pad_trim_top_bottom(0, size[1] - out.rows())
829
+ return out
830
+
831
+ def get_cursor_coords(self, size: tuple[()] | tuple[int] | tuple[int, int]) -> tuple[int, int] | None:
832
+ """Return the cursor coordinates of the focus widget."""
833
+ if not self.selectable():
834
+ return None
835
+ if not hasattr(self.focus, "get_cursor_coords"):
836
+ return None
837
+
838
+ i = self.focus_position
839
+ _widths, heights, size_args = self.get_rows_sizes(size, focus=True)
840
+ coords = self.focus.get_cursor_coords(size_args[i])
841
+
842
+ if coords is None:
843
+ return None
844
+ x, y = coords
845
+ if i > 0:
846
+ for r in heights[:i]:
847
+ y += r
848
+ return x, y
849
+
850
+ def rows(self, size: tuple[int] | tuple[int, int], focus: bool = False) -> int:
851
+ return sum(self.get_item_rows(size, focus))
852
+
853
+ def keypress(self, size: tuple[()] | tuple[int] | tuple[int, int], key: str) -> str | None:
854
+ """Pass the keypress to the widget in focus.
855
+
856
+ Unhandled 'up' and 'down' keys may cause a focus change.
857
+ """
858
+ if not self.contents:
859
+ return key
860
+
861
+ i = self.focus_position
862
+ _widths, heights, size_args = self.get_rows_sizes(size, focus=self.selectable())
863
+ if self.selectable():
864
+ key = self.focus.keypress(size_args[i], key)
865
+ if self._command_map[key] not in {Command.UP, Command.DOWN}:
866
+ return key
867
+
868
+ if self._command_map[key] == Command.UP:
869
+ candidates = tuple(range(i - 1, -1, -1)) # count backwards to 0
870
+ else: # self._command_map[key] == 'cursor down'
871
+ candidates = tuple(range(i + 1, len(self.contents)))
872
+
873
+ for j in candidates:
874
+ if not self.contents[j][0].selectable():
875
+ continue
876
+
877
+ self._update_pref_col_from_focus(size_args[self.focus_position])
878
+ self.focus_position = j
879
+ if not hasattr(self.focus, "move_cursor_to_coords"):
880
+ return None
881
+
882
+ rows = heights[j]
883
+ if self._command_map[key] == Command.UP:
884
+ rowlist = tuple(range(rows - 1, -1, -1))
885
+ else: # self._command_map[key] == 'cursor down'
886
+ rowlist = tuple(range(rows))
887
+ for row in rowlist:
888
+ if self.focus.move_cursor_to_coords(size_args[self.focus_position], self.pref_col, row):
889
+ break
890
+ return None
891
+
892
+ # nothing to select
893
+ return key
894
+
895
+ def _update_pref_col_from_focus(self, w_size: tuple[()] | tuple[int] | tuple[int, int]) -> None:
896
+ """Update self.pref_col from the focus widget."""
897
+
898
+ if not hasattr(self.focus, "get_pref_col"):
899
+ return
900
+
901
+ pref_col = self.focus.get_pref_col(w_size)
902
+ if pref_col is not None:
903
+ self.pref_col = pref_col
904
+
905
+ def move_cursor_to_coords(
906
+ self,
907
+ size: tuple[()] | tuple[int] | tuple[int, int],
908
+ col: int,
909
+ row: int,
910
+ ) -> bool:
911
+ """Capture pref col and set new focus."""
912
+ self.pref_col = col
913
+
914
+ # FIXME guessing focus==True
915
+ focus = True
916
+ wrow = 0
917
+ _widths, heights, size_args = self.get_rows_sizes(size, focus=focus)
918
+ for i, (r, w_size, (w, _)) in enumerate(zip(heights, size_args, self.contents)): # noqa: B007
919
+ if wrow + r > row:
920
+ break
921
+ wrow += r
922
+ else:
923
+ return False
924
+
925
+ if not w.selectable():
926
+ return False
927
+
928
+ if hasattr(w, "move_cursor_to_coords"):
929
+ rval = w.move_cursor_to_coords(w_size, col, row - wrow)
930
+ if rval is False:
931
+ return False
932
+
933
+ self.focus_position = i
934
+ return True
935
+
936
+ def mouse_event(
937
+ self,
938
+ size: tuple[()] | tuple[int] | tuple[int, int],
939
+ event: str,
940
+ button: int,
941
+ col: int,
942
+ row: int,
943
+ focus: bool,
944
+ ) -> bool | None:
945
+ """Pass the event to the contained widget.
946
+
947
+ May change focus on button 1 press.
948
+ """
949
+ wrow = 0
950
+ _widths, heights, size_args = self.get_rows_sizes(size, focus=focus)
951
+
952
+ for i, (height, w_size, (w, _)) in enumerate(zip(heights, size_args, self.contents)): # noqa: B007
953
+ if wrow + height > row:
954
+ target_row = row - wrow
955
+ break
956
+ wrow += height
957
+ else:
958
+ return False
959
+
960
+ if is_mouse_press(event) and button == 1 and w.selectable():
961
+ self.focus_position = i
962
+
963
+ if not hasattr(w, "mouse_event"):
964
+ warnings.warn(
965
+ f"{w.__class__.__module__}.{w.__class__.__name__} is not subclass of Widget",
966
+ DeprecationWarning,
967
+ stacklevel=2,
968
+ )
969
+ return False
970
+
971
+ return w.mouse_event(w_size, event, button, col, target_row, focus and self.focus == w)