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
@@ -0,0 +1,1145 @@
1
+ from __future__ import annotations
2
+
3
+ import typing
4
+ import warnings
5
+ from itertools import chain, repeat
6
+
7
+ import urwid
8
+ from urwid.canvas import Canvas, CanvasJoin, CompositeCanvas, SolidCanvas
9
+ from urwid.command_map import Command
10
+ from urwid.monitored_list import MonitoredFocusList, MonitoredList
11
+ from urwid.util import is_mouse_press
12
+
13
+ from .constants import Align, Sizing, WHSettings
14
+ from .container import WidgetContainerListContentsMixin, WidgetContainerMixin, _ContainerElementSizingFlag
15
+ from .widget import Widget, WidgetError, WidgetWarning
16
+
17
+ if typing.TYPE_CHECKING:
18
+ from collections.abc import Iterable, Sequence
19
+
20
+ from typing_extensions import Literal
21
+
22
+
23
+ class ColumnsError(WidgetError):
24
+ """Columns related errors."""
25
+
26
+
27
+ class ColumnsWarning(WidgetWarning):
28
+ """Columns related warnings."""
29
+
30
+
31
+ class Columns(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin):
32
+ """
33
+ Widgets arranged horizontally in columns from left to right
34
+ """
35
+
36
+ def sizing(self) -> frozenset[Sizing]:
37
+ """Sizing supported by widget.
38
+
39
+ :return: Calculated widget sizing
40
+ :rtype: frozenset[Sizing]
41
+
42
+ Due to the nature of container with mutable contents, this method cannot be cached.
43
+
44
+ Rules:
45
+ * WEIGHT BOX -> BOX
46
+ * GIVEN BOX -> Can be included in FIXED and FLOW depends on the other columns
47
+ * PACK BOX -> Unsupported
48
+ * BOX-only widget without `box_columns` disallow FLOW render
49
+
50
+ * WEIGHT FLOW -> FLOW
51
+ * GIVEN FLOW -> FIXED (known width and widget knows its height) + FLOW (historic)
52
+ * PACK FLOW -> FLOW (widget fit in provided size)
53
+
54
+ * WEIGHT FIXED -> Need also FLOW or/and BOX to properly render due to width calculation
55
+ * GIVEN FIXED -> Unsupported
56
+ * PACK FIXED -> FIXED (widget knows its size)
57
+
58
+ Backward compatibility rules:
59
+ * GIVEN BOX -> Allow BOX
60
+
61
+ BOX can be only if ALL widgets support BOX.
62
+ FIXED can be only if no BOX without "box_columns" flag and no strict FLOW.
63
+
64
+ >>> from urwid import BigText, Edit, SolidFill, Text, Thin3x3Font
65
+ >>> font = Thin3x3Font()
66
+
67
+ # BOX-only widget
68
+ >>> Columns((SolidFill("#"),))
69
+ <Columns box widget>
70
+
71
+ # BOX-only widget with "get height from max"
72
+ >>> Columns((SolidFill("#"),), box_columns=(0,))
73
+ <Columns box widget>
74
+
75
+ # FLOW-only
76
+ >>> Columns((Edit(),))
77
+ <Columns selectable flow widget>
78
+
79
+ # FLOW allowed by "box_columns"
80
+ >>> Columns((Edit(), SolidFill("#")), box_columns=(1,))
81
+ <Columns selectable flow widget>
82
+
83
+ # FLOW/FIXED
84
+ >>> Columns((Text("T"),))
85
+ <Columns fixed/flow widget>
86
+
87
+ # GIVEN BOX only -> BOX only
88
+ >>> Columns(((5, SolidFill("#")),), box_columns=(0,))
89
+ <Columns box widget>
90
+
91
+ # No FLOW - BOX only
92
+ >>> Columns(((5, SolidFill("#")), SolidFill("*")), box_columns=(0, 1))
93
+ <Columns box widget>
94
+
95
+ # FIXED only -> FIXED
96
+ >>> Columns(((WHSettings.PACK, BigText("1", font)),))
97
+ <Columns fixed widget>
98
+
99
+ # Invalid sizing combination -> use fallback settings (and produce warning)
100
+ >>> Columns(((WHSettings.PACK, SolidFill("#")),))
101
+ <Columns box/flow widget>
102
+ """
103
+ strict_box = False
104
+ has_flow = False
105
+
106
+ block_fixed = False
107
+ has_fixed = False
108
+ supported: set[Sizing] = set()
109
+
110
+ box_flow_fixed = (
111
+ _ContainerElementSizingFlag.BOX | _ContainerElementSizingFlag.FLOW | _ContainerElementSizingFlag.FIXED
112
+ )
113
+ flow_fixed = _ContainerElementSizingFlag.FLOW | _ContainerElementSizingFlag.FIXED
114
+ given_box = _ContainerElementSizingFlag.BOX | _ContainerElementSizingFlag.WH_GIVEN
115
+
116
+ flags: set[_ContainerElementSizingFlag] = set()
117
+
118
+ for idx, (widget, (size_kind, _size_weight, is_box)) in enumerate(self.contents):
119
+ w_sizing = widget.sizing()
120
+
121
+ flag = _ContainerElementSizingFlag.NONE
122
+
123
+ if size_kind == WHSettings.WEIGHT:
124
+ flag |= _ContainerElementSizingFlag.WH_WEIGHT
125
+ if Sizing.BOX in w_sizing:
126
+ flag |= _ContainerElementSizingFlag.BOX
127
+ if Sizing.FLOW in w_sizing:
128
+ flag |= _ContainerElementSizingFlag.FLOW
129
+ if Sizing.FIXED in w_sizing and w_sizing & {Sizing.BOX, Sizing.FLOW}:
130
+ flag |= _ContainerElementSizingFlag.FIXED
131
+
132
+ elif size_kind == WHSettings.GIVEN:
133
+ flag |= _ContainerElementSizingFlag.WH_GIVEN
134
+ if Sizing.BOX in w_sizing:
135
+ flag |= _ContainerElementSizingFlag.BOX
136
+ if Sizing.FLOW in w_sizing:
137
+ flag |= _ContainerElementSizingFlag.FIXED
138
+ flag |= _ContainerElementSizingFlag.FLOW
139
+
140
+ else:
141
+ flag |= _ContainerElementSizingFlag.WH_PACK
142
+ if Sizing.FIXED in w_sizing:
143
+ flag |= _ContainerElementSizingFlag.FIXED
144
+ if Sizing.FLOW in w_sizing:
145
+ flag |= _ContainerElementSizingFlag.FLOW
146
+
147
+ if not flag & box_flow_fixed:
148
+ warnings.warn(
149
+ f"Sizing combination of widget {widget} (position={idx}) not supported: "
150
+ f"{size_kind.name} box={is_box}",
151
+ ColumnsWarning,
152
+ stacklevel=3,
153
+ )
154
+ return frozenset((Sizing.BOX, Sizing.FLOW))
155
+
156
+ flags.add(flag)
157
+
158
+ if flag & _ContainerElementSizingFlag.BOX and not (is_box or flag & flow_fixed):
159
+ strict_box = True
160
+
161
+ if flag & _ContainerElementSizingFlag.FLOW:
162
+ has_flow = True
163
+ if flag & _ContainerElementSizingFlag.FIXED:
164
+ has_fixed = True
165
+ elif flag & given_box != given_box:
166
+ block_fixed = True
167
+
168
+ if all(flag & _ContainerElementSizingFlag.BOX for flag in flags):
169
+ # Only if ALL widgets can be rendered as BOX, widget can be rendered as BOX.
170
+ # Hacky "BOX" render for FLOW-only is still present,
171
+ # due to incorrected implementation can be used by downstream
172
+ supported.add(Sizing.BOX)
173
+
174
+ if not strict_box:
175
+ if has_flow:
176
+ supported.add(Sizing.FLOW)
177
+
178
+ if has_fixed and not block_fixed:
179
+ supported.add(Sizing.FIXED)
180
+
181
+ if not supported:
182
+ warnings.warn(
183
+ f"Columns widget contents flags not allow to determine supported render kind:\n"
184
+ f"{', '.join(sorted(flag.log_string for flag in flags))}\n"
185
+ f"Using fallback hardcoded BOX|FLOW sizing kind.",
186
+ ColumnsWarning,
187
+ stacklevel=3,
188
+ )
189
+ return frozenset((Sizing.BOX, Sizing.FLOW))
190
+
191
+ return frozenset(supported)
192
+
193
+ def __init__(
194
+ self,
195
+ widget_list: Iterable[
196
+ Widget
197
+ | tuple[Literal["pack", WHSettings.PACK] | int, Widget]
198
+ | tuple[Literal["weight", "given", WHSettings.WEIGHT, WHSettings.GIVEN], int, Widget]
199
+ ],
200
+ dividechars: int = 0,
201
+ focus_column: int | Widget | None = None,
202
+ min_width: int = 1,
203
+ box_columns: Iterable[int] | None = None,
204
+ ):
205
+ """
206
+ :param widget_list: iterable of flow or box widgets
207
+ :param dividechars: number of blank characters between columns
208
+ :param focus_column: index into widget_list of column in focus or focused widget instance,
209
+ if ``None`` the first selectable widget will be chosen.
210
+ :param min_width: minimum width for each column which is not
211
+ calling widget.pack() in *widget_list*.
212
+ :param box_columns: a list of column indexes containing box widgets
213
+ whose height is set to the maximum of the rows
214
+ required by columns not listed in *box_columns*.
215
+
216
+ *widget_list* may also contain tuples such as:
217
+
218
+ (*given_width*, *widget*)
219
+ make this column *given_width* screen columns wide, where *given_width*
220
+ is an int
221
+ (``'pack'``, *widget*)
222
+ call :meth:`pack() <Widget.pack>` to calculate the width of this column
223
+ (``'weight'``, *weight*, *widget*)
224
+ give this column a relative *weight* (number) to calculate its width from the
225
+ screen columns remaining
226
+
227
+ Widgets not in a tuple are the same as (``'weight'``, ``1``, *widget*)
228
+
229
+ If the Columns widget is treated as a box widget then all children
230
+ are treated as box widgets, and *box_columns* is ignored.
231
+
232
+ If the Columns widget is treated as a flow widget then the rows
233
+ are calculated as the largest rows() returned from all columns
234
+ except the ones listed in *box_columns*. The box widgets in
235
+ *box_columns* will be displayed with this calculated number of rows,
236
+ filling the full height.
237
+ """
238
+ self._selectable = False
239
+ self._cache_column_widths: list[int] = []
240
+ super().__init__()
241
+ self._contents: MonitoredFocusList[
242
+ Widget,
243
+ tuple[Literal[WHSettings.PACK], None, bool]
244
+ | tuple[Literal[WHSettings.GIVEN, WHSettings.WEIGHT], int, bool],
245
+ ] = MonitoredFocusList()
246
+ self._contents.set_modified_callback(self._contents_modified)
247
+ self._contents.set_focus_changed_callback(lambda f: self._invalidate())
248
+ self._contents.set_validate_contents_modified(self._validate_contents_modified)
249
+
250
+ box_columns = set(box_columns or ())
251
+
252
+ for i, original in enumerate(widget_list):
253
+ w = original
254
+ if not isinstance(w, tuple):
255
+ self.contents.append((w, (WHSettings.WEIGHT, 1, i in box_columns)))
256
+ elif w[0] in {Sizing.FLOW, WHSettings.PACK}: # 'pack' used to be called 'flow'
257
+ _ignored, w = w
258
+ self.contents.append((w, (WHSettings.PACK, None, i in box_columns)))
259
+ elif len(w) == 2 or w[0] in {Sizing.FIXED, WHSettings.GIVEN}: # backwards compatibility: FIXED -> GIVEN
260
+ width, w = w[-2:]
261
+ self.contents.append((w, (WHSettings.GIVEN, width, i in box_columns)))
262
+ elif w[0] == WHSettings.WEIGHT:
263
+ _ignored, width, w = w
264
+ self.contents.append((w, (WHSettings.WEIGHT, width, i in box_columns)))
265
+ else:
266
+ raise ColumnsError(f"initial widget list item invalid: {original!r}")
267
+ if focus_column == w or (focus_column is None and w.selectable()):
268
+ focus_column = i
269
+
270
+ if not isinstance(w, Widget):
271
+ warnings.warn(f"{w!r} is not a Widget", ColumnsWarning, stacklevel=3)
272
+
273
+ self.dividechars = dividechars
274
+
275
+ if self.contents and focus_column is not None:
276
+ self.focus_position = focus_column
277
+ self.pref_col = None
278
+ self.min_width = min_width
279
+ self._cache_maxcol = None
280
+
281
+ def _contents_modified(self) -> None:
282
+ """
283
+ Recalculate whether this widget should be selectable whenever the
284
+ contents has been changed.
285
+ """
286
+ self._selectable = any(w.selectable() for w, o in self.contents)
287
+ self._invalidate()
288
+
289
+ def _validate_contents_modified(self, slc, new_items) -> None:
290
+ for item in new_items:
291
+ try:
292
+ _w, (t, _n, _b) = item
293
+ if t not in {WHSettings.PACK, WHSettings.GIVEN, WHSettings.WEIGHT}:
294
+ raise ColumnsError(f"added content invalid {item!r}")
295
+ except (TypeError, ValueError) as exc: # noqa: PERF203
296
+ raise ColumnsError(f"added content invalid {item!r}").with_traceback(exc.__traceback__) from exc
297
+
298
+ @property
299
+ def widget_list(self) -> MonitoredList:
300
+ """
301
+ A list of the widgets in this Columns
302
+
303
+ .. note:: only for backwards compatibility. You should use the new
304
+ standard container property :attr:`contents`.
305
+ """
306
+ warnings.warn(
307
+ "only for backwards compatibility. You should use the new standard container `contents`",
308
+ PendingDeprecationWarning,
309
+ stacklevel=2,
310
+ )
311
+ ml = MonitoredList(w for w, t in self.contents)
312
+
313
+ def user_modified():
314
+ self.widget_list = ml
315
+
316
+ ml.set_modified_callback(user_modified)
317
+ return ml
318
+
319
+ @widget_list.setter
320
+ def widget_list(self, widgets):
321
+ warnings.warn(
322
+ "only for backwards compatibility. You should use the new standard container `contents`",
323
+ PendingDeprecationWarning,
324
+ stacklevel=2,
325
+ )
326
+ focus_position = self.focus_position
327
+ self.contents = [
328
+ # need to grow contents list if widgets is longer
329
+ (new, options)
330
+ for (new, (w, options)) in zip(
331
+ widgets,
332
+ chain(self.contents, repeat((None, (WHSettings.WEIGHT, 1, False)))),
333
+ )
334
+ ]
335
+ if focus_position < len(widgets):
336
+ self.focus_position = focus_position
337
+
338
+ @property
339
+ def column_types(self) -> MonitoredList:
340
+ """
341
+ A list of the old partial options values for widgets in this Pile,
342
+ for backwards compatibility only. You should use the new standard
343
+ container property .contents to modify Pile contents.
344
+ """
345
+ warnings.warn(
346
+ "for backwards compatibility only."
347
+ "You should use the new standard container property .contents to modify Pile contents.",
348
+ PendingDeprecationWarning,
349
+ stacklevel=2,
350
+ )
351
+ ml = MonitoredList(
352
+ # return the old column type names
353
+ ({WHSettings.GIVEN: Sizing.FIXED, WHSettings.PACK: Sizing.FLOW}.get(t, t), n)
354
+ for w, (t, n, b) in self.contents
355
+ )
356
+
357
+ def user_modified():
358
+ self.column_types = ml
359
+
360
+ ml.set_modified_callback(user_modified)
361
+ return ml
362
+
363
+ @column_types.setter
364
+ def column_types(self, column_types):
365
+ warnings.warn(
366
+ "for backwards compatibility only."
367
+ "You should use the new standard container property .contents to modify Pile contents.",
368
+ PendingDeprecationWarning,
369
+ stacklevel=2,
370
+ )
371
+ focus_position = self.focus_position
372
+ self.contents = [
373
+ (w, ({Sizing.FIXED: WHSettings.GIVEN, Sizing.FLOW: WHSettings.PACK}.get(new_t, new_t), new_n, b))
374
+ for ((new_t, new_n), (w, (t, n, b))) in zip(column_types, self.contents)
375
+ ]
376
+ if focus_position < len(column_types):
377
+ self.focus_position = focus_position
378
+
379
+ @property
380
+ def box_columns(self) -> MonitoredList:
381
+ """
382
+ A list of the indexes of the columns that are to be treated as
383
+ box widgets when the Columns is treated as a flow widget.
384
+
385
+ .. note:: only for backwards compatibility. You should use the new
386
+ standard container property :attr:`contents`.
387
+ """
388
+ warnings.warn(
389
+ "only for backwards compatibility.You should use the new standard container property `contents`",
390
+ PendingDeprecationWarning,
391
+ stacklevel=2,
392
+ )
393
+ ml = MonitoredList(i for i, (w, (t, n, b)) in enumerate(self.contents) if b)
394
+
395
+ def user_modified():
396
+ self.box_columns = ml
397
+
398
+ ml.set_modified_callback(user_modified)
399
+ return ml
400
+
401
+ @box_columns.setter
402
+ def box_columns(self, box_columns):
403
+ warnings.warn(
404
+ "only for backwards compatibility.You should use the new standard container property `contents`",
405
+ PendingDeprecationWarning,
406
+ stacklevel=2,
407
+ )
408
+ box_columns = set(box_columns)
409
+ self.contents = [(w, (t, n, i in box_columns)) for (i, (w, (t, n, b))) in enumerate(self.contents)]
410
+
411
+ @property
412
+ def has_flow_type(self) -> bool:
413
+ """
414
+ .. deprecated:: 1.0 Read values from :attr:`contents` instead.
415
+ """
416
+ warnings.warn(
417
+ ".has_flow_type is deprecated, read values from .contents instead.",
418
+ DeprecationWarning,
419
+ stacklevel=2,
420
+ )
421
+ return WHSettings.PACK in self.column_types
422
+
423
+ @has_flow_type.setter
424
+ def has_flow_type(self, value):
425
+ warnings.warn(
426
+ ".has_flow_type is deprecated, read values from .contents instead.",
427
+ DeprecationWarning,
428
+ stacklevel=2,
429
+ )
430
+
431
+ @property
432
+ def contents(
433
+ self,
434
+ ) -> MonitoredFocusList[
435
+ Widget,
436
+ tuple[Literal[WHSettings.PACK], None, bool] | tuple[Literal[WHSettings.GIVEN, WHSettings.WEIGHT], int, bool],
437
+ ]:
438
+ """
439
+ The contents of this Columns as a list of `(widget, options)` tuples.
440
+ This list may be modified like a normal list and the Columns
441
+ widget will update automatically.
442
+
443
+ .. seealso:: Create new options tuples with the :meth:`options` method
444
+ """
445
+ return self._contents
446
+
447
+ @contents.setter
448
+ def contents(
449
+ self,
450
+ c: Sequence[
451
+ Widget,
452
+ tuple[Literal[WHSettings.PACK], None, bool]
453
+ | tuple[Literal[WHSettings.GIVEN, WHSettings.WEIGHT], int, bool],
454
+ ],
455
+ ) -> None:
456
+ self._contents[:] = c
457
+
458
+ @staticmethod
459
+ def options(
460
+ width_type: Literal[
461
+ "pack", "given", "weight", WHSettings.PACK, WHSettings.GIVEN, WHSettings.WEIGHT
462
+ ] = WHSettings.WEIGHT,
463
+ width_amount: int | None = 1,
464
+ box_widget: bool = False,
465
+ ) -> tuple[Literal[WHSettings.PACK], None, bool] | tuple[Literal[WHSettings.GIVEN, WHSettings.WEIGHT], int, bool]:
466
+ """
467
+ Return a new options tuple for use in a Pile's .contents list.
468
+
469
+ This sets an entry's width type: one of the following:
470
+
471
+ ``'pack'``
472
+ Call the widget's :meth:`Widget.pack` method to determine how wide
473
+ this column should be. *width_amount* is ignored.
474
+ ``'given'``
475
+ Make column exactly width_amount screen-columns wide.
476
+ ``'weight'``
477
+ Allocate the remaining space to this column by using
478
+ *width_amount* as a weight value.
479
+
480
+ :param width_type: ``'pack'``, ``'given'`` or ``'weight'``
481
+ :param width_amount: ``None`` for ``'pack'``, a number of screen columns
482
+ for ``'given'`` or a weight value (number) for ``'weight'``
483
+ :param box_widget: set to `True` if this widget is to be treated as a box
484
+ widget when the Columns widget itself is treated as a flow widget.
485
+ :type box_widget: bool
486
+ """
487
+ if width_type == WHSettings.PACK:
488
+ width_amount = None
489
+ if width_type not in {WHSettings.PACK, WHSettings.GIVEN, WHSettings.WEIGHT}:
490
+ raise ColumnsError(f"invalid width_type: {width_type!r}")
491
+ return (WHSettings(width_type), width_amount, box_widget)
492
+
493
+ def _invalidate(self) -> None:
494
+ self._cache_maxcol = None
495
+ super()._invalidate()
496
+
497
+ def set_focus_column(self, num: int) -> None:
498
+ """
499
+ Set the column in focus by its index in :attr:`widget_list`.
500
+
501
+ :param num: index of focus-to-be entry
502
+ :type num: int
503
+
504
+ .. note:: only for backwards compatibility. You may also use the new
505
+ standard container property :attr:`focus_position` to set the focus.
506
+ """
507
+ warnings.warn(
508
+ "only for backwards compatibility.You may also use the new standard container property `focus_position`",
509
+ PendingDeprecationWarning,
510
+ stacklevel=2,
511
+ )
512
+ self.focus_position = num
513
+
514
+ def get_focus_column(self) -> int:
515
+ """
516
+ Return the focus column index.
517
+
518
+ .. note:: only for backwards compatibility. You may also use the new
519
+ standard container property :attr:`focus_position` to get the focus.
520
+ """
521
+ warnings.warn(
522
+ "only for backwards compatibility.You may also use the new standard container property `focus_position`",
523
+ PendingDeprecationWarning,
524
+ stacklevel=2,
525
+ )
526
+ return self.focus_position
527
+
528
+ def set_focus(self, item: Widget | int) -> None:
529
+ """
530
+ Set the item in focus
531
+
532
+ .. note:: only for backwards compatibility. You may also use the new
533
+ standard container property :attr:`focus_position` to get the focus.
534
+
535
+ :param item: widget or integer index"""
536
+ warnings.warn(
537
+ "only for backwards compatibility."
538
+ "You may also use the new standard container property `focus_position` to get the focus.",
539
+ PendingDeprecationWarning,
540
+ stacklevel=2,
541
+ )
542
+ if isinstance(item, int):
543
+ self.focus_position = item
544
+ return
545
+ for i, (w, _options) in enumerate(self.contents):
546
+ if item == w:
547
+ self.focus_position = i
548
+ return
549
+ raise ValueError(f"Widget not found in Columns contents: {item!r}")
550
+
551
+ @property
552
+ def focus(self) -> Widget | None:
553
+ """
554
+ the child widget in focus or None when Columns is empty
555
+
556
+ Return the widget in focus, for backwards compatibility. You may
557
+ also use the new standard container property .focus to get the
558
+ child widget in focus.
559
+ """
560
+ if not self.contents:
561
+ return None
562
+ return self.contents[self.focus_position][0]
563
+
564
+ def _get_focus(self) -> Widget:
565
+ warnings.warn(
566
+ f"method `{self.__class__.__name__}._get_focus` is deprecated, "
567
+ f"please use `{self.__class__.__name__}.focus` property",
568
+ DeprecationWarning,
569
+ stacklevel=3,
570
+ )
571
+ if not self.contents:
572
+ return None
573
+ return self.contents[self.focus_position][0]
574
+
575
+ def get_focus(self):
576
+ """
577
+ Return the widget in focus, for backwards compatibility.
578
+
579
+ .. note:: only for backwards compatibility. You may also use the new
580
+ standard container property :attr:`focus` to get the focus.
581
+ """
582
+ warnings.warn(
583
+ "only for backwards compatibility."
584
+ "You may also use the new standard container property `focus` to get the focus.",
585
+ PendingDeprecationWarning,
586
+ stacklevel=2,
587
+ )
588
+ if not self.contents:
589
+ return None
590
+ return self.contents[self.focus_position][0]
591
+
592
+ @property
593
+ def focus_position(self) -> int | None:
594
+ """
595
+ index of child widget in focus.
596
+ Raises :exc:`IndexError` if read when Columns is empty, or when set to an invalid index.
597
+ """
598
+ if not self.contents:
599
+ raise IndexError("No focus_position, Columns is empty")
600
+ return self.contents.focus
601
+
602
+ @focus_position.setter
603
+ def focus_position(self, position: int) -> None:
604
+ """
605
+ Set the widget in focus.
606
+
607
+ position -- index of child widget to be made focus
608
+ """
609
+ try:
610
+ if position < 0 or position >= len(self.contents):
611
+ raise IndexError(f"No Columns child widget at position {position}")
612
+ except TypeError as exc:
613
+ raise IndexError(f"No Columns child widget at position {position}").with_traceback(
614
+ exc.__traceback__
615
+ ) from exc
616
+ self.contents.focus = position
617
+
618
+ def _get_focus_position(self) -> int | None:
619
+ warnings.warn(
620
+ f"method `{self.__class__.__name__}._get_focus_position` is deprecated, "
621
+ f"please use `{self.__class__.__name__}.focus_position` property",
622
+ DeprecationWarning,
623
+ stacklevel=3,
624
+ )
625
+ if not self.contents:
626
+ raise IndexError("No focus_position, Columns is empty")
627
+ return self.contents.focus
628
+
629
+ def _set_focus_position(self, position: int) -> None:
630
+ """
631
+ Set the widget in focus.
632
+
633
+ position -- index of child widget to be made focus
634
+ """
635
+ warnings.warn(
636
+ f"method `{self.__class__.__name__}._set_focus_position` is deprecated, "
637
+ f"please use `{self.__class__.__name__}.focus_position` property",
638
+ DeprecationWarning,
639
+ stacklevel=3,
640
+ )
641
+ try:
642
+ if position < 0 or position >= len(self.contents):
643
+ raise IndexError(f"No Columns child widget at position {position}")
644
+ except TypeError as exc:
645
+ raise IndexError(f"No Columns child widget at position {position}").with_traceback(
646
+ exc.__traceback__
647
+ ) from exc
648
+ self.contents.focus = position
649
+
650
+ @property
651
+ def focus_col(self):
652
+ """
653
+ A property for reading and setting the index of the column in
654
+ focus.
655
+
656
+ .. note:: only for backwards compatibility. You may also use the new
657
+ standard container property :attr:`focus_position` to get the focus.
658
+ """
659
+ warnings.warn(
660
+ "only for backwards compatibility."
661
+ "You may also use the new standard container property `focus_position` to get the focus.",
662
+ PendingDeprecationWarning,
663
+ stacklevel=2,
664
+ )
665
+ return self.focus_position
666
+
667
+ @focus_col.setter
668
+ def focus_col(self, new_position) -> None:
669
+ warnings.warn(
670
+ "only for backwards compatibility."
671
+ "You may also use the new standard container property `focus_position` to get the focus.",
672
+ PendingDeprecationWarning,
673
+ stacklevel=2,
674
+ )
675
+ self.focus_position = new_position
676
+
677
+ def column_widths(self, size: tuple[int] | tuple[int, int], focus: bool = False) -> list[int]:
678
+ """
679
+ Return a list of column widths.
680
+
681
+ 0 values in the list means hide the corresponding column completely
682
+ """
683
+ maxcol = size[0]
684
+ # FIXME: get rid of this check and recalculate only when a 'pack' widget has been modified.
685
+ if maxcol == self._cache_maxcol and not any(t == WHSettings.PACK for w, (t, n, b) in self.contents):
686
+ return self._cache_column_widths
687
+
688
+ widths = []
689
+
690
+ weighted = []
691
+ shared = maxcol + self.dividechars
692
+
693
+ for i, (w, (t, width, b)) in enumerate(self.contents):
694
+ if t == WHSettings.GIVEN:
695
+ static_w = width
696
+ elif t == WHSettings.PACK:
697
+ if isinstance(w, Widget):
698
+ w_sizing = w.sizing()
699
+ else:
700
+ warnings.warn(f"{w!r} is not a Widget", ColumnsWarning, stacklevel=3)
701
+ w_sizing = frozenset((urwid.BOX, urwid.FLOW))
702
+
703
+ if w_sizing & frozenset((Sizing.FIXED, Sizing.FLOW)):
704
+ candidate_size = 0
705
+
706
+ if Sizing.FIXED in w_sizing:
707
+ candidate_size = w.pack((), focus and i == self.focus_position)[0]
708
+
709
+ if Sizing.FLOW in w_sizing and (not candidate_size or candidate_size > maxcol):
710
+ # FIXME: should be able to pack with a different maxcol value
711
+ candidate_size = w.pack((maxcol,), focus and i == self.focus_position)[0]
712
+
713
+ static_w = candidate_size
714
+
715
+ else:
716
+ warnings.warn(
717
+ f"Unusual widget {w} sizing for {t} (box={b}). "
718
+ f"Assuming wrong sizing and using {Sizing.FLOW.upper()} for width calculation",
719
+ ColumnsWarning,
720
+ stacklevel=3,
721
+ )
722
+ static_w = w.pack((maxcol,), focus and i == self.focus_position)[0]
723
+
724
+ else:
725
+ static_w = self.min_width
726
+
727
+ if shared < static_w + self.dividechars and i > self.focus_position:
728
+ break
729
+
730
+ widths.append(static_w)
731
+ shared -= static_w + self.dividechars
732
+ if t not in {WHSettings.GIVEN, WHSettings.PACK}:
733
+ weighted.append((width, i))
734
+
735
+ # drop columns on the left until we fit
736
+ for i, width_ in enumerate(widths):
737
+ if shared >= 0:
738
+ break
739
+ shared += width_ + self.dividechars
740
+ widths[i] = 0
741
+ if weighted and weighted[0][1] == i:
742
+ del weighted[0]
743
+
744
+ if shared:
745
+ # divide up the remaining space between weighted cols
746
+ wtotal = sum(weight for weight, i in weighted)
747
+ grow = shared + len(weighted) * self.min_width
748
+ for weight, i in sorted(weighted):
749
+ width = max(int(grow * weight / wtotal + 0.5), self.min_width)
750
+
751
+ widths[i] = width
752
+ grow -= width
753
+ wtotal -= weight
754
+
755
+ self._cache_maxcol = maxcol
756
+ self._cache_column_widths = widths
757
+ return widths
758
+
759
+ def _get_fixed_column_sizes(
760
+ self,
761
+ focus: bool = False,
762
+ ) -> tuple[Sequence[int], Sequence[int], Sequence[tuple[int] | tuple[()]]]:
763
+ """Get column widths, heights and render size parameters"""
764
+ widths: dict[int, int] = {}
765
+ heights: dict[int, int] = {}
766
+ w_h_args: dict[int, tuple[int, int] | tuple[int] | tuple[()]] = {}
767
+ box: list[int] = []
768
+ weighted: dict[int, list[tuple[Widget, int, bool, bool]]] = {}
769
+ weights: list[int] = []
770
+ weight_max_sizes: dict[int, int] = {}
771
+
772
+ for i, (widget, (size_kind, size_weight, is_box)) in enumerate(self.contents):
773
+ w_sizing = widget.sizing()
774
+ focused = focus and i == self.focus_position
775
+
776
+ if size_kind == WHSettings.GIVEN:
777
+ widths[i] = size_weight
778
+ if is_box:
779
+ box.append(i)
780
+ elif Sizing.FLOW in w_sizing:
781
+ heights[i] = widget.rows((size_weight,), focused)
782
+ w_h_args[i] = (size_weight,)
783
+ else:
784
+ raise ColumnsError(f"Unsupported combination of {size_kind} box={is_box!r} for {widget}")
785
+
786
+ elif size_kind == WHSettings.PACK and Sizing.FIXED in w_sizing and not is_box:
787
+ width, height = widget.pack((), focused)
788
+ widths[i] = width
789
+ heights[i] = height
790
+ w_h_args[i] = ()
791
+
792
+ elif size_weight <= 0:
793
+ widths[i] = 0
794
+ heights[i] = 1
795
+ if is_box:
796
+ box.append(i)
797
+ else:
798
+ w_h_args[i] = (0,)
799
+
800
+ elif Sizing.FLOW in w_sizing or is_box:
801
+ if Sizing.FIXED in w_sizing:
802
+ width, height = widget.pack((), focused)
803
+ else:
804
+ width = self.min_width
805
+
806
+ weighted.setdefault(size_weight, []).append((widget, i, is_box, focused))
807
+ weights.append(size_weight)
808
+ weight_max_sizes.setdefault(size_weight, width)
809
+ weight_max_sizes[size_weight] = max(weight_max_sizes[size_weight], width)
810
+ else:
811
+ raise ColumnsError(f"Unsupported combination of {size_kind} box={is_box!r} for {widget}")
812
+
813
+ if weight_max_sizes:
814
+ max_weighted_coefficient = max(width / weight for weight, width in weight_max_sizes.items())
815
+
816
+ for weight in weight_max_sizes:
817
+ width = max(int(max_weighted_coefficient * weight + 0.5), self.min_width)
818
+ for widget, i, is_box, focused in weighted[weight]:
819
+ widths[i] = width
820
+
821
+ if not is_box:
822
+ heights[i] = widget.rows((width,), focused)
823
+ w_h_args[i] = (width,)
824
+ else:
825
+ box.append(i)
826
+
827
+ if not heights:
828
+ raise ColumnsError(f"No height information for pack {self!r} as FIXED")
829
+
830
+ max_height = max(heights.values())
831
+ for idx in box:
832
+ heights[idx] = max_height
833
+ w_h_args[idx] = (widths[idx], max_height)
834
+
835
+ return (
836
+ tuple(widths[idx] for idx in range(len(widths))),
837
+ tuple(heights[idx] for idx in range(len(heights))),
838
+ tuple(w_h_args[idx] for idx in range(len(w_h_args))),
839
+ )
840
+
841
+ def get_column_sizes(
842
+ self,
843
+ size: tuple[int, int] | tuple[int] | tuple[()],
844
+ focus: bool = False,
845
+ ) -> tuple[Sequence[int], Sequence[int], Sequence[tuple[int, int] | tuple[int] | tuple[()]]]:
846
+ """Get column widths, heights and render size parameters"""
847
+ if not size:
848
+ return self._get_fixed_column_sizes(focus=focus)
849
+
850
+ widths = tuple(self.column_widths(size=size, focus=focus))
851
+ heights: dict[int, int] = {}
852
+ w_h_args: dict[int, tuple[int, int] | tuple[int] | tuple[()]] = {}
853
+ box: list[int] = []
854
+ box_need_height: list[int] = []
855
+
856
+ for i, (width, (widget, (size_kind, _size_weight, is_box))) in enumerate(zip(widths, self.contents)):
857
+ if isinstance(widget, Widget):
858
+ w_sizing = widget.sizing()
859
+ else:
860
+ warnings.warn(f"{widget!r} is not Widget.", ColumnsWarning, stacklevel=3)
861
+ # This branch should be fully deleted later.
862
+ w_sizing = frozenset((Sizing.FLOW, Sizing.BOX))
863
+
864
+ if len(size) == 2 and Sizing.BOX in w_sizing:
865
+ heights[i] = size[1]
866
+ w_h_args[i] = (width, size[1])
867
+
868
+ elif is_box:
869
+ box.append(i)
870
+
871
+ elif Sizing.FLOW in w_sizing:
872
+ heights[i] = widget.rows((width,), focus and i == self.focus_position)
873
+ w_h_args[i] = (width,)
874
+
875
+ elif size_kind == WHSettings.PACK:
876
+ heights[i] = widget.pack((), focus and i == self.focus_position)[1]
877
+ w_h_args[i] = ()
878
+
879
+ else:
880
+ box_need_height.append(i)
881
+
882
+ if len(size) == 1:
883
+ if heights:
884
+ max_height = max(heights.values())
885
+ if box_need_height:
886
+ warnings.warn(
887
+ f"Widgets in columns {box_need_height} "
888
+ f"({[self.contents[i][0] for i in box_need_height]}) "
889
+ f'are BOX widgets not marked "box_columns" while FLOW render is requested (size={size!r})',
890
+ ColumnsWarning,
891
+ stacklevel=3,
892
+ )
893
+ else:
894
+ max_height = 1
895
+ else:
896
+ max_height = size[1]
897
+
898
+ for idx in (*box, *box_need_height):
899
+ heights[idx] = max_height
900
+ w_h_args[idx] = (widths[idx], max_height)
901
+
902
+ return (
903
+ widths,
904
+ tuple(heights[idx] for idx in range(len(heights))),
905
+ tuple(w_h_args[idx] for idx in range(len(w_h_args))),
906
+ )
907
+
908
+ def pack(self, size: tuple[()] | tuple[int] | tuple[int, int] = (), focus: bool = False) -> tuple[int, int]:
909
+ """Get packed sized for widget."""
910
+ if size:
911
+ return super().pack(size, focus)
912
+ widths, heights, _ = self.get_column_sizes(size, focus)
913
+ return (sum(widths) + self.dividechars * max(len(widths) - 1, 0), max(heights))
914
+
915
+ def render(
916
+ self,
917
+ size: tuple[()] | tuple[int] | tuple[int, int],
918
+ focus: bool = False,
919
+ ) -> SolidCanvas | CompositeCanvas:
920
+ """
921
+ Render columns and return canvas.
922
+
923
+ :param size: see :meth:`Widget.render` for details
924
+ :param focus: ``True`` if this widget is in focus
925
+ :type focus: bool
926
+ """
927
+ widths, _, size_args = self.get_column_sizes(size, focus)
928
+
929
+ data: list[tuple[Canvas, int, bool, int]] = []
930
+ for i, (width, w_size, (w, _)) in enumerate(zip(widths, size_args, self.contents)):
931
+ # if the widget has a width of 0, hide it
932
+ if width <= 0:
933
+ continue
934
+
935
+ if i < len(widths) - 1:
936
+ width += self.dividechars # noqa: PLW2901
937
+ data.append(
938
+ (
939
+ w.render(w_size, focus=focus and self.focus_position == i),
940
+ i,
941
+ self.focus_position == i,
942
+ width,
943
+ )
944
+ )
945
+
946
+ if not data:
947
+ if size:
948
+ return SolidCanvas(" ", size[0], (size[1:] + (1,))[0])
949
+ raise ColumnsError("No data to render")
950
+
951
+ canvas = CanvasJoin(data)
952
+ if size and canvas.cols() < size[0]:
953
+ canvas.pad_trim_left_right(0, size[0] - canvas.cols())
954
+ return canvas
955
+
956
+ def get_cursor_coords(self, size: tuple[()] | tuple[int] | tuple[int, int]) -> tuple[int, int] | None:
957
+ """Return the cursor coordinates from the focus widget."""
958
+ w, _ = self.contents[self.focus_position]
959
+
960
+ if not w.selectable():
961
+ return None
962
+ if not hasattr(w, "get_cursor_coords"):
963
+ return None
964
+
965
+ widths, _, size_args = self.get_column_sizes(size, focus=True)
966
+ if len(widths) <= self.focus_position:
967
+ return None
968
+
969
+ coords = w.get_cursor_coords(size_args[self.focus_position])
970
+ if coords is None:
971
+ return None
972
+
973
+ x, y = coords
974
+ x += sum(self.dividechars + wc for wc in widths[: self.focus_position] if wc > 0)
975
+ return x, y
976
+
977
+ def move_cursor_to_coords(
978
+ self,
979
+ size: tuple[()] | tuple[int] | tuple[int, int],
980
+ col: int | Literal["left", "right"],
981
+ row: int,
982
+ ) -> bool:
983
+ """
984
+ Choose a selectable column to focus based on the coords.
985
+
986
+ see :meth:`Widget.move_cursor_coords` for details
987
+ """
988
+ try:
989
+ widths, _, size_args = self.get_column_sizes(size, focus=True)
990
+ except Exception as exc:
991
+ raise ValueError(self.contents, size, col, row) from exc
992
+
993
+ best = None
994
+ x = 0
995
+ for i, (width, (w, _options)) in enumerate(zip(widths, self.contents)):
996
+ end = x + width
997
+ if w.selectable():
998
+ if col != Align.RIGHT and (col == Align.LEFT or x > col) and best is None:
999
+ # no other choice
1000
+ best = i, x, end, w
1001
+ break
1002
+ if col != Align.RIGHT and x > col and col - best[2] < x - col:
1003
+ # choose one on left
1004
+ break
1005
+ best = i, x, end, w
1006
+ if col != Align.RIGHT and col < end:
1007
+ # choose this one
1008
+ break
1009
+ x = end + self.dividechars
1010
+
1011
+ if best is None:
1012
+ return False
1013
+ i, x, end, w = best
1014
+ if hasattr(w, "move_cursor_to_coords"):
1015
+ if isinstance(col, int):
1016
+ move_x = min(max(0, col - x), end - x - 1)
1017
+ else:
1018
+ move_x = col
1019
+ rval = w.move_cursor_to_coords(size_args[i], move_x, row)
1020
+ if rval is False:
1021
+ return False
1022
+
1023
+ self.focus_position = i
1024
+ self.pref_col = col
1025
+ return True
1026
+
1027
+ def mouse_event(
1028
+ self,
1029
+ size: tuple[()] | tuple[int] | tuple[int, int],
1030
+ event: str,
1031
+ button: int,
1032
+ col: int,
1033
+ row: int,
1034
+ focus: bool,
1035
+ ) -> bool | None:
1036
+ """
1037
+ Send event to appropriate column.
1038
+ May change focus on button 1 press.
1039
+ """
1040
+ widths, _, size_args = self.get_column_sizes(size, focus=focus)
1041
+
1042
+ x = 0
1043
+ for i, (width, w_size, (w, _)) in enumerate(zip(widths, size_args, self.contents)):
1044
+ if col < x:
1045
+ return False
1046
+ w = self.contents[i][0] # noqa: PLW2901
1047
+ end = x + width
1048
+
1049
+ if col >= end:
1050
+ x = end + self.dividechars
1051
+ continue
1052
+
1053
+ focus = focus and self.focus_position == i
1054
+ if is_mouse_press(event) and button == 1 and w.selectable():
1055
+ self.focus_position = i
1056
+
1057
+ if not hasattr(w, "mouse_event"):
1058
+ warnings.warn(
1059
+ f"{w.__class__.__module__}.{w.__class__.__name__} is not subclass of Widget",
1060
+ DeprecationWarning,
1061
+ stacklevel=2,
1062
+ )
1063
+ return False
1064
+
1065
+ return w.mouse_event(w_size, event, button, col - x, row, focus)
1066
+ return False
1067
+
1068
+ def get_pref_col(self, size: tuple[()] | tuple[int] | tuple[int, int]) -> int:
1069
+ """Return the pref col from the column in focus."""
1070
+ widths, _, size_args = self.get_column_sizes(size, focus=True)
1071
+
1072
+ w, _ = self.contents[self.focus_position]
1073
+ if len(widths) <= self.focus_position:
1074
+ return 0
1075
+ col = None
1076
+ cwidth = widths[self.focus_position]
1077
+
1078
+ if hasattr(w, "get_pref_col"):
1079
+ col = w.get_pref_col(size_args[self.focus_position])
1080
+ if isinstance(col, int):
1081
+ col += self.focus_position * self.dividechars
1082
+ col += sum(widths[: self.focus_position])
1083
+ if col is None:
1084
+ col = self.pref_col
1085
+
1086
+ if col is None and w.selectable():
1087
+ col = cwidth // 2
1088
+ col += self.focus_position * self.dividechars
1089
+ col += sum(widths[: self.focus_position])
1090
+ return col
1091
+
1092
+ def rows(self, size: tuple[int] | tuple[int, int], focus: bool = False) -> int:
1093
+ """
1094
+ Return the number of rows required by the columns.
1095
+ This only makes sense if :attr:`widget_list` contains flow widgets.
1096
+
1097
+ see :meth:`Widget.rows` for details
1098
+ """
1099
+ widths = self.column_widths(size, focus)
1100
+
1101
+ rows = 1
1102
+ for i, (mc, (w, (_t, _n, b))) in enumerate(zip(widths, self.contents)):
1103
+ if b or mc <= 0:
1104
+ continue
1105
+ rows = max(rows, w.rows((mc,), focus=focus and self.focus_position == i))
1106
+ return rows
1107
+
1108
+ def keypress(self, size: tuple[()] | tuple[int] | tuple[int, int], key: str) -> str | None:
1109
+ """
1110
+ Pass keypress to the focus column.
1111
+
1112
+ :param size: Widget size correct for the supported sizing
1113
+ :type size: tuple[()] | tuple[int] | tuple[int, int]
1114
+ :param key: a single keystroke value
1115
+ :type key: str
1116
+ """
1117
+ if self.focus_position is None:
1118
+ return key
1119
+
1120
+ widths, _, size_args = self.get_column_sizes(size, focus=True)
1121
+ if self.focus_position >= len(widths):
1122
+ return key
1123
+
1124
+ i = self.focus_position
1125
+ w, _ = self.contents[i]
1126
+ if self._command_map[key] not in {Command.UP, Command.DOWN, Command.PAGE_UP, Command.PAGE_DOWN}:
1127
+ self.pref_col = None
1128
+ if w.selectable():
1129
+ key = w.keypress(size_args[i], key)
1130
+
1131
+ if self._command_map[key] not in {Command.LEFT, Command.RIGHT}:
1132
+ return key
1133
+
1134
+ if self._command_map[key] == Command.LEFT:
1135
+ candidates = list(range(i - 1, -1, -1)) # count backwards to 0
1136
+ else: # key == 'right'
1137
+ candidates = list(range(i + 1, len(self.contents)))
1138
+
1139
+ for j in candidates:
1140
+ if not self.contents[j][0].selectable():
1141
+ continue
1142
+
1143
+ self.focus_position = j
1144
+ return None
1145
+ return key