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.
- urwid/__init__.py +333 -0
- urwid/canvas.py +1413 -0
- urwid/command_map.py +137 -0
- urwid/container.py +59 -0
- urwid/decoration.py +65 -0
- urwid/display/__init__.py +97 -0
- urwid/display/_posix_raw_display.py +413 -0
- urwid/display/_raw_display_base.py +914 -0
- urwid/display/_web.css +12 -0
- urwid/display/_web.js +462 -0
- urwid/display/_win32.py +171 -0
- urwid/display/_win32_raw_display.py +269 -0
- urwid/display/common.py +1219 -0
- urwid/display/curses.py +690 -0
- urwid/display/escape.py +624 -0
- urwid/display/html_fragment.py +251 -0
- urwid/display/lcd.py +518 -0
- urwid/display/raw.py +37 -0
- urwid/display/web.py +636 -0
- urwid/event_loop/__init__.py +55 -0
- urwid/event_loop/abstract_loop.py +175 -0
- urwid/event_loop/asyncio_loop.py +231 -0
- urwid/event_loop/glib_loop.py +294 -0
- urwid/event_loop/main_loop.py +721 -0
- urwid/event_loop/select_loop.py +230 -0
- urwid/event_loop/tornado_loop.py +206 -0
- urwid/event_loop/trio_loop.py +302 -0
- urwid/event_loop/twisted_loop.py +269 -0
- urwid/event_loop/zmq_loop.py +275 -0
- urwid/font.py +695 -0
- urwid/graphics.py +96 -0
- urwid/highlight.css +19 -0
- urwid/listbox.py +1899 -0
- urwid/monitored_list.py +522 -0
- urwid/numedit.py +376 -0
- urwid/signals.py +330 -0
- urwid/split_repr.py +130 -0
- urwid/str_util.py +358 -0
- urwid/text_layout.py +632 -0
- urwid/treetools.py +515 -0
- urwid/util.py +557 -0
- urwid/version.py +16 -0
- urwid/vterm.py +1806 -0
- urwid/widget/__init__.py +181 -0
- urwid/widget/attr_map.py +161 -0
- urwid/widget/attr_wrap.py +140 -0
- urwid/widget/bar_graph.py +649 -0
- urwid/widget/big_text.py +77 -0
- urwid/widget/box_adapter.py +126 -0
- urwid/widget/columns.py +1145 -0
- urwid/widget/constants.py +574 -0
- urwid/widget/container.py +227 -0
- urwid/widget/divider.py +110 -0
- urwid/widget/edit.py +718 -0
- urwid/widget/filler.py +403 -0
- urwid/widget/frame.py +539 -0
- urwid/widget/grid_flow.py +539 -0
- urwid/widget/line_box.py +194 -0
- urwid/widget/overlay.py +829 -0
- urwid/widget/padding.py +597 -0
- urwid/widget/pile.py +971 -0
- urwid/widget/popup.py +170 -0
- urwid/widget/progress_bar.py +141 -0
- urwid/widget/scrollable.py +597 -0
- urwid/widget/solid_fill.py +44 -0
- urwid/widget/text.py +354 -0
- urwid/widget/widget.py +852 -0
- urwid/widget/widget_decoration.py +166 -0
- urwid/widget/wimp.py +792 -0
- urwid/wimp.py +23 -0
- urwid-2.6.0.post0.dist-info/COPYING +504 -0
- urwid-2.6.0.post0.dist-info/METADATA +332 -0
- urwid-2.6.0.post0.dist-info/RECORD +75 -0
- urwid-2.6.0.post0.dist-info/WHEEL +5 -0
- urwid-2.6.0.post0.dist-info/top_level.txt +1 -0
urwid/widget/columns.py
ADDED
|
@@ -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
|