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