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/wimp.py
ADDED
|
@@ -0,0 +1,792 @@
|
|
|
1
|
+
# Urwid Window-Icon-Menu-Pointer-style widget classes
|
|
2
|
+
# Copyright (C) 2004-2011 Ian Ward
|
|
3
|
+
#
|
|
4
|
+
# This library is free software; you can redistribute it and/or
|
|
5
|
+
# modify it under the terms of the GNU Lesser General Public
|
|
6
|
+
# License as published by the Free Software Foundation; either
|
|
7
|
+
# version 2.1 of the License, or (at your option) any later version.
|
|
8
|
+
#
|
|
9
|
+
# This library is distributed in the hope that it will be useful,
|
|
10
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
12
|
+
# Lesser General Public License for more details.
|
|
13
|
+
#
|
|
14
|
+
# You should have received a copy of the GNU Lesser General Public
|
|
15
|
+
# License along with this library; if not, write to the Free Software
|
|
16
|
+
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
17
|
+
#
|
|
18
|
+
# Urwid web site: https://urwid.org/
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import typing
|
|
24
|
+
|
|
25
|
+
from urwid.canvas import CompositeCanvas
|
|
26
|
+
from urwid.command_map import Command
|
|
27
|
+
from urwid.signals import connect_signal
|
|
28
|
+
from urwid.text_layout import calc_coords
|
|
29
|
+
from urwid.util import is_mouse_press
|
|
30
|
+
|
|
31
|
+
from .columns import Columns
|
|
32
|
+
from .constants import Align, WrapMode
|
|
33
|
+
from .text import Text
|
|
34
|
+
from .widget import WidgetError, WidgetWrap
|
|
35
|
+
|
|
36
|
+
if typing.TYPE_CHECKING:
|
|
37
|
+
from collections.abc import Callable, Hashable, MutableSequence
|
|
38
|
+
|
|
39
|
+
from typing_extensions import Literal, Self
|
|
40
|
+
|
|
41
|
+
from urwid.canvas import TextCanvas
|
|
42
|
+
from urwid.text_layout import TextLayout
|
|
43
|
+
|
|
44
|
+
_T = typing.TypeVar("_T")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class SelectableIcon(Text):
|
|
48
|
+
ignore_focus = False
|
|
49
|
+
_selectable = True
|
|
50
|
+
|
|
51
|
+
def __init__(
|
|
52
|
+
self,
|
|
53
|
+
text: str | tuple[Hashable, str] | list[str | tuple[Hashable, str]],
|
|
54
|
+
cursor_position: int = 0,
|
|
55
|
+
align: Literal["left", "center", "right"] | Align = Align.LEFT,
|
|
56
|
+
wrap: Literal["space", "any", "clip", "ellipsis"] | WrapMode = WrapMode.SPACE,
|
|
57
|
+
layout: TextLayout | None = None,
|
|
58
|
+
) -> None:
|
|
59
|
+
"""
|
|
60
|
+
:param text: markup for this widget; see :class:`Text` for
|
|
61
|
+
description of text markup
|
|
62
|
+
:param cursor_position: position the cursor will appear in the
|
|
63
|
+
text when this widget is in focus
|
|
64
|
+
:param align: typically ``'left'``, ``'center'`` or ``'right'``
|
|
65
|
+
:type align: text alignment mode
|
|
66
|
+
:param wrap: typically ``'space'``, ``'any'``, ``'clip'`` or ``'ellipsis'``
|
|
67
|
+
:type wrap: text wrapping mode
|
|
68
|
+
:param layout: defaults to a shared :class:`StandardTextLayout` instance
|
|
69
|
+
:type layout: text layout instance
|
|
70
|
+
|
|
71
|
+
This is a text widget that is selectable. A cursor
|
|
72
|
+
displayed at a fixed location in the text when in focus.
|
|
73
|
+
This widget has no special handling of keyboard or mouse input.
|
|
74
|
+
"""
|
|
75
|
+
super().__init__(text, align=align, wrap=wrap, layout=layout)
|
|
76
|
+
self._cursor_position = cursor_position
|
|
77
|
+
|
|
78
|
+
def render(
|
|
79
|
+
self,
|
|
80
|
+
size: tuple[int] | tuple[()],
|
|
81
|
+
focus: bool = False,
|
|
82
|
+
) -> TextCanvas | CompositeCanvas: # type: ignore[override]
|
|
83
|
+
"""
|
|
84
|
+
Render the text content of this widget with a cursor when
|
|
85
|
+
in focus.
|
|
86
|
+
|
|
87
|
+
>>> si = SelectableIcon(u"[!]")
|
|
88
|
+
>>> si
|
|
89
|
+
<SelectableIcon selectable fixed/flow widget '[!]'>
|
|
90
|
+
>>> si.render((4,), focus=True).cursor
|
|
91
|
+
(0, 0)
|
|
92
|
+
>>> si = SelectableIcon("((*))", 2)
|
|
93
|
+
>>> si.render((8,), focus=True).cursor
|
|
94
|
+
(2, 0)
|
|
95
|
+
>>> si.render((2,), focus=True).cursor
|
|
96
|
+
(0, 1)
|
|
97
|
+
>>> si.render(()).cursor
|
|
98
|
+
>>> si.render(()).text
|
|
99
|
+
[b'((*))']
|
|
100
|
+
>>> si.render((), focus=True).cursor
|
|
101
|
+
(2, 0)
|
|
102
|
+
"""
|
|
103
|
+
c: TextCanvas | CompositeCanvas = super().render(size, focus)
|
|
104
|
+
if focus:
|
|
105
|
+
# create a new canvas so we can add a cursor
|
|
106
|
+
c = CompositeCanvas(c)
|
|
107
|
+
c.cursor = self.get_cursor_coords(size)
|
|
108
|
+
return c
|
|
109
|
+
|
|
110
|
+
def get_cursor_coords(self, size: tuple[int] | tuple[()]) -> tuple[int, int] | None:
|
|
111
|
+
"""
|
|
112
|
+
Return the position of the cursor if visible. This method
|
|
113
|
+
is required for widgets that display a cursor.
|
|
114
|
+
"""
|
|
115
|
+
if self._cursor_position > len(self.text):
|
|
116
|
+
return None
|
|
117
|
+
# find out where the cursor will be displayed based on
|
|
118
|
+
# the text layout
|
|
119
|
+
if size:
|
|
120
|
+
(maxcol,) = size
|
|
121
|
+
else:
|
|
122
|
+
maxcol, _ = self.pack()
|
|
123
|
+
trans = self.get_line_translation(maxcol)
|
|
124
|
+
x, y = calc_coords(self.text, trans, self._cursor_position)
|
|
125
|
+
if maxcol <= x:
|
|
126
|
+
return None
|
|
127
|
+
return x, y
|
|
128
|
+
|
|
129
|
+
def keypress(self, size: tuple[int] | tuple[()], key: str) -> str:
|
|
130
|
+
"""
|
|
131
|
+
No keys are handled by this widget. This method is
|
|
132
|
+
required for selectable widgets.
|
|
133
|
+
"""
|
|
134
|
+
return key
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class CheckBoxError(WidgetError):
|
|
138
|
+
pass
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class CheckBox(WidgetWrap[Columns]):
|
|
142
|
+
states: typing.ClassVar[dict[bool | Literal["mixed"], SelectableIcon]] = {
|
|
143
|
+
True: SelectableIcon("[X]", 1),
|
|
144
|
+
False: SelectableIcon("[ ]", 1),
|
|
145
|
+
"mixed": SelectableIcon("[#]", 1),
|
|
146
|
+
}
|
|
147
|
+
reserve_columns = 4
|
|
148
|
+
|
|
149
|
+
# allow users of this class to listen for change events
|
|
150
|
+
# sent when the state of this widget is modified
|
|
151
|
+
# (this variable is picked up by the MetaSignals metaclass)
|
|
152
|
+
signals: typing.ClassVar[list[str]] = ["change", "postchange"]
|
|
153
|
+
|
|
154
|
+
@typing.overload
|
|
155
|
+
def __init__(
|
|
156
|
+
self,
|
|
157
|
+
label: str | tuple[Hashable, str] | list[str | tuple[Hashable, str]],
|
|
158
|
+
state: bool = False,
|
|
159
|
+
has_mixed: typing.Literal[False] = False,
|
|
160
|
+
on_state_change: Callable[[Self, bool, _T], typing.Any] | None = None,
|
|
161
|
+
user_data: _T = ...,
|
|
162
|
+
checked_symbol: str | None = ...,
|
|
163
|
+
) -> None: ...
|
|
164
|
+
|
|
165
|
+
@typing.overload
|
|
166
|
+
def __init__(
|
|
167
|
+
self,
|
|
168
|
+
label: str | tuple[Hashable, str] | list[str | tuple[Hashable, str]],
|
|
169
|
+
state: bool = False,
|
|
170
|
+
has_mixed: typing.Literal[False] = False,
|
|
171
|
+
on_state_change: Callable[[Self, bool], typing.Any] | None = None,
|
|
172
|
+
user_data: None = None,
|
|
173
|
+
checked_symbol: str | None = ...,
|
|
174
|
+
) -> None: ...
|
|
175
|
+
|
|
176
|
+
@typing.overload
|
|
177
|
+
def __init__(
|
|
178
|
+
self,
|
|
179
|
+
label: str | tuple[Hashable, str] | list[str | tuple[Hashable, str]],
|
|
180
|
+
state: typing.Literal["mixed"] | bool = False,
|
|
181
|
+
has_mixed: typing.Literal[True] = True,
|
|
182
|
+
on_state_change: Callable[[Self, bool | typing.Literal["mixed"], _T], typing.Any] | None = None,
|
|
183
|
+
user_data: _T = ...,
|
|
184
|
+
checked_symbol: str | None = ...,
|
|
185
|
+
) -> None: ...
|
|
186
|
+
|
|
187
|
+
@typing.overload
|
|
188
|
+
def __init__(
|
|
189
|
+
self,
|
|
190
|
+
label: str | tuple[Hashable, str] | list[str | tuple[Hashable, str]],
|
|
191
|
+
state: typing.Literal["mixed"] | bool = False,
|
|
192
|
+
has_mixed: typing.Literal[True] = True,
|
|
193
|
+
on_state_change: Callable[[Self, bool | typing.Literal["mixed"]], typing.Any] | None = None,
|
|
194
|
+
user_data: None = None,
|
|
195
|
+
checked_symbol: str | None = ...,
|
|
196
|
+
) -> None: ...
|
|
197
|
+
|
|
198
|
+
def __init__(
|
|
199
|
+
self,
|
|
200
|
+
label: str | tuple[Hashable, str] | list[str | tuple[Hashable, str]],
|
|
201
|
+
state: bool | Literal["mixed"] = False,
|
|
202
|
+
has_mixed: typing.Literal[False, True] = False, # MyPy issue: Literal[True, False] is not equal `bool`
|
|
203
|
+
on_state_change: (
|
|
204
|
+
Callable[[Self, bool, _T], typing.Any]
|
|
205
|
+
| Callable[[Self, bool], typing.Any]
|
|
206
|
+
| Callable[[Self, bool | typing.Literal["mixed"], _T], typing.Any]
|
|
207
|
+
| Callable[[Self, bool | typing.Literal["mixed"]], typing.Any]
|
|
208
|
+
| None
|
|
209
|
+
) = None,
|
|
210
|
+
user_data: _T | None = None,
|
|
211
|
+
checked_symbol: str | None = None,
|
|
212
|
+
):
|
|
213
|
+
"""
|
|
214
|
+
:param label: markup for check box label
|
|
215
|
+
:param state: False, True or "mixed"
|
|
216
|
+
:param has_mixed: True if "mixed" is a state to cycle through
|
|
217
|
+
:param on_state_change: shorthand for connect_signal()
|
|
218
|
+
function call for a single callback
|
|
219
|
+
:param user_data: user_data for on_state_change
|
|
220
|
+
|
|
221
|
+
..note:: `pack` method expect, that `Columns` backend widget is not modified from outside
|
|
222
|
+
|
|
223
|
+
Signals supported: ``'change'``, ``"postchange"``
|
|
224
|
+
|
|
225
|
+
Register signal handler with::
|
|
226
|
+
|
|
227
|
+
urwid.connect_signal(check_box, 'change', callback, user_data)
|
|
228
|
+
|
|
229
|
+
where callback is callback(check_box, new_state [,user_data])
|
|
230
|
+
Unregister signal handlers with::
|
|
231
|
+
|
|
232
|
+
urwid.disconnect_signal(check_box, 'change', callback, user_data)
|
|
233
|
+
|
|
234
|
+
>>> CheckBox("Confirm")
|
|
235
|
+
<CheckBox selectable fixed/flow widget 'Confirm' state=False>
|
|
236
|
+
>>> CheckBox("Yogourt", "mixed", True)
|
|
237
|
+
<CheckBox selectable fixed/flow widget 'Yogourt' state='mixed'>
|
|
238
|
+
>>> cb = CheckBox("Extra onions", True)
|
|
239
|
+
>>> cb
|
|
240
|
+
<CheckBox selectable fixed/flow widget 'Extra onions' state=True>
|
|
241
|
+
>>> cb.render((20,), focus=True).text
|
|
242
|
+
[b'[X] Extra onions ']
|
|
243
|
+
>>> CheckBox("Test", None)
|
|
244
|
+
Traceback (most recent call last):
|
|
245
|
+
...
|
|
246
|
+
ValueError: None not in (True, False, 'mixed')
|
|
247
|
+
"""
|
|
248
|
+
if state not in self.states:
|
|
249
|
+
raise ValueError(f"{state!r} not in {tuple(self.states.keys())}")
|
|
250
|
+
|
|
251
|
+
self._label = Text(label)
|
|
252
|
+
self.has_mixed = has_mixed
|
|
253
|
+
|
|
254
|
+
self._state = state
|
|
255
|
+
if checked_symbol:
|
|
256
|
+
self.states[True] = SelectableIcon(f"[{checked_symbol}]", 1)
|
|
257
|
+
# The old way of listening for a change was to pass the callback
|
|
258
|
+
# in to the constructor. Just convert it to the new way:
|
|
259
|
+
if on_state_change:
|
|
260
|
+
connect_signal(self, "change", on_state_change, user_data)
|
|
261
|
+
|
|
262
|
+
# Initial create expect no callbacks call, create explicit
|
|
263
|
+
super().__init__(
|
|
264
|
+
Columns(
|
|
265
|
+
[(self.reserve_columns, self.states[state]), self._label],
|
|
266
|
+
focus_column=0,
|
|
267
|
+
),
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
def pack(self, size: tuple[()] | tuple[int] | None = None, focus: bool = False) -> tuple[str, str]:
|
|
271
|
+
"""Pack for widget.
|
|
272
|
+
|
|
273
|
+
:param size: size data. Special case: None - get minimal widget size to fit
|
|
274
|
+
:param focus: widget is focused
|
|
275
|
+
|
|
276
|
+
>>> cb = CheckBox("test")
|
|
277
|
+
>>> cb.pack((10,))
|
|
278
|
+
(10, 1)
|
|
279
|
+
>>> cb.pack()
|
|
280
|
+
(8, 1)
|
|
281
|
+
>>> ml_cb = CheckBox("Multi\\nline\\ncheckbox")
|
|
282
|
+
>>> ml_cb.pack()
|
|
283
|
+
(12, 3)
|
|
284
|
+
>>> ml_cb.pack((), True)
|
|
285
|
+
(12, 3)
|
|
286
|
+
"""
|
|
287
|
+
return super().pack(size or (), focus)
|
|
288
|
+
|
|
289
|
+
def _repr_words(self) -> list[str]:
|
|
290
|
+
return [*super()._repr_words(), repr(self.label)]
|
|
291
|
+
|
|
292
|
+
def _repr_attrs(self) -> dict[str, typing.Any]:
|
|
293
|
+
return {**super()._repr_attrs(), "state": self.state}
|
|
294
|
+
|
|
295
|
+
def set_label(self, label: str | tuple[Hashable, str] | list[str | tuple[Hashable, str]]):
|
|
296
|
+
"""
|
|
297
|
+
Change the check box label.
|
|
298
|
+
|
|
299
|
+
label -- markup for label. See Text widget for description
|
|
300
|
+
of text markup.
|
|
301
|
+
|
|
302
|
+
>>> cb = CheckBox(u"foo")
|
|
303
|
+
>>> cb
|
|
304
|
+
<CheckBox selectable fixed/flow widget 'foo' state=False>
|
|
305
|
+
>>> cb.set_label(('bright_attr', u"bar"))
|
|
306
|
+
>>> cb
|
|
307
|
+
<CheckBox selectable fixed/flow widget 'bar' state=False>
|
|
308
|
+
"""
|
|
309
|
+
self._label.set_text(label)
|
|
310
|
+
# no need to call self._invalidate(). WidgetWrap takes care of
|
|
311
|
+
# that when self.w changes
|
|
312
|
+
|
|
313
|
+
def get_label(self):
|
|
314
|
+
"""
|
|
315
|
+
Return label text.
|
|
316
|
+
|
|
317
|
+
>>> cb = CheckBox(u"Seriously")
|
|
318
|
+
>>> print(cb.get_label())
|
|
319
|
+
Seriously
|
|
320
|
+
>>> print(cb.label)
|
|
321
|
+
Seriously
|
|
322
|
+
>>> cb.set_label([('bright_attr', u"flashy"), u" normal"])
|
|
323
|
+
>>> print(cb.label) # only text is returned
|
|
324
|
+
flashy normal
|
|
325
|
+
"""
|
|
326
|
+
return self._label.text
|
|
327
|
+
|
|
328
|
+
label = property(get_label)
|
|
329
|
+
|
|
330
|
+
def set_state(
|
|
331
|
+
self,
|
|
332
|
+
state: bool | Literal["mixed"],
|
|
333
|
+
do_callback: bool = True,
|
|
334
|
+
) -> None:
|
|
335
|
+
"""
|
|
336
|
+
Set the CheckBox state.
|
|
337
|
+
|
|
338
|
+
state -- True, False or "mixed"
|
|
339
|
+
do_callback -- False to suppress signal from this change
|
|
340
|
+
|
|
341
|
+
>>> from urwid import disconnect_signal
|
|
342
|
+
>>> changes = []
|
|
343
|
+
>>> def callback_a(user_data, cb, state):
|
|
344
|
+
... changes.append("A %r %r" % (state, user_data))
|
|
345
|
+
>>> def callback_b(cb, state):
|
|
346
|
+
... changes.append("B %r" % state)
|
|
347
|
+
>>> cb = CheckBox('test', False, False)
|
|
348
|
+
>>> key1 = connect_signal(cb, 'change', callback_a, user_args=("user_a",))
|
|
349
|
+
>>> key2 = connect_signal(cb, 'change', callback_b)
|
|
350
|
+
>>> cb.set_state(True) # both callbacks will be triggered
|
|
351
|
+
>>> cb.state
|
|
352
|
+
True
|
|
353
|
+
>>> disconnect_signal(cb, 'change', callback_a, user_args=("user_a",))
|
|
354
|
+
>>> cb.state = False
|
|
355
|
+
>>> cb.state
|
|
356
|
+
False
|
|
357
|
+
>>> cb.set_state(True)
|
|
358
|
+
>>> cb.state
|
|
359
|
+
True
|
|
360
|
+
>>> cb.set_state(False, False) # don't send signal
|
|
361
|
+
>>> changes
|
|
362
|
+
["A True 'user_a'", 'B True', 'B False', 'B True']
|
|
363
|
+
"""
|
|
364
|
+
if self._state == state:
|
|
365
|
+
return
|
|
366
|
+
|
|
367
|
+
if state not in self.states:
|
|
368
|
+
raise CheckBoxError(f"{self!r} Invalid state: {state!r}")
|
|
369
|
+
|
|
370
|
+
# self._state is None is a special case when the CheckBox
|
|
371
|
+
# has just been created
|
|
372
|
+
old_state = self._state
|
|
373
|
+
if do_callback:
|
|
374
|
+
self._emit("change", state)
|
|
375
|
+
self._state = state
|
|
376
|
+
# rebuild the display widget with the new state
|
|
377
|
+
self._w = Columns([(self.reserve_columns, self.states[state]), self._label], focus_column=0)
|
|
378
|
+
if do_callback:
|
|
379
|
+
self._emit("postchange", old_state)
|
|
380
|
+
|
|
381
|
+
def get_state(self) -> bool | Literal["mixed"]:
|
|
382
|
+
"""Return the state of the checkbox."""
|
|
383
|
+
return self._state
|
|
384
|
+
|
|
385
|
+
state = property(get_state, set_state)
|
|
386
|
+
|
|
387
|
+
def keypress(self, size: tuple[int], key: str) -> str | None:
|
|
388
|
+
"""
|
|
389
|
+
Toggle state on 'activate' command.
|
|
390
|
+
|
|
391
|
+
>>> assert CheckBox._command_map[' '] == 'activate'
|
|
392
|
+
>>> assert CheckBox._command_map['enter'] == 'activate'
|
|
393
|
+
>>> size = (10,)
|
|
394
|
+
>>> cb = CheckBox('press me')
|
|
395
|
+
>>> cb.state
|
|
396
|
+
False
|
|
397
|
+
>>> cb.keypress(size, ' ')
|
|
398
|
+
>>> cb.state
|
|
399
|
+
True
|
|
400
|
+
>>> cb.keypress(size, ' ')
|
|
401
|
+
>>> cb.state
|
|
402
|
+
False
|
|
403
|
+
"""
|
|
404
|
+
if self._command_map[key] != Command.ACTIVATE:
|
|
405
|
+
return key
|
|
406
|
+
|
|
407
|
+
self.toggle_state()
|
|
408
|
+
return None
|
|
409
|
+
|
|
410
|
+
def toggle_state(self) -> None:
|
|
411
|
+
"""
|
|
412
|
+
Cycle to the next valid state.
|
|
413
|
+
|
|
414
|
+
>>> cb = CheckBox("3-state", has_mixed=True)
|
|
415
|
+
>>> cb.state
|
|
416
|
+
False
|
|
417
|
+
>>> cb.toggle_state()
|
|
418
|
+
>>> cb.state
|
|
419
|
+
True
|
|
420
|
+
>>> cb.toggle_state()
|
|
421
|
+
>>> cb.state
|
|
422
|
+
'mixed'
|
|
423
|
+
>>> cb.toggle_state()
|
|
424
|
+
>>> cb.state
|
|
425
|
+
False
|
|
426
|
+
"""
|
|
427
|
+
if self.state is False:
|
|
428
|
+
self.set_state(True)
|
|
429
|
+
elif self.state is True:
|
|
430
|
+
if self.has_mixed:
|
|
431
|
+
self.set_state("mixed")
|
|
432
|
+
else:
|
|
433
|
+
self.set_state(False)
|
|
434
|
+
elif self.state == "mixed":
|
|
435
|
+
self.set_state(False)
|
|
436
|
+
|
|
437
|
+
def mouse_event(self, size: tuple[int], event: str, button: int, x: int, y: int, focus: bool) -> bool:
|
|
438
|
+
"""
|
|
439
|
+
Toggle state on button 1 press.
|
|
440
|
+
|
|
441
|
+
>>> size = (20,)
|
|
442
|
+
>>> cb = CheckBox("clickme")
|
|
443
|
+
>>> cb.state
|
|
444
|
+
False
|
|
445
|
+
>>> cb.mouse_event(size, 'mouse press', 1, 2, 0, True)
|
|
446
|
+
True
|
|
447
|
+
>>> cb.state
|
|
448
|
+
True
|
|
449
|
+
"""
|
|
450
|
+
if button != 1 or not is_mouse_press(event):
|
|
451
|
+
return False
|
|
452
|
+
self.toggle_state()
|
|
453
|
+
return True
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
class RadioButton(CheckBox):
|
|
457
|
+
states: typing.ClassVar[dict[bool | Literal["mixed"], SelectableIcon]] = {
|
|
458
|
+
True: SelectableIcon("(X)", 1),
|
|
459
|
+
False: SelectableIcon("( )", 1),
|
|
460
|
+
"mixed": SelectableIcon("(#)", 1),
|
|
461
|
+
}
|
|
462
|
+
reserve_columns = 4
|
|
463
|
+
|
|
464
|
+
@typing.overload
|
|
465
|
+
def __init__(
|
|
466
|
+
self,
|
|
467
|
+
group: MutableSequence[RadioButton],
|
|
468
|
+
label: str | tuple[Hashable, str] | list[str | tuple[Hashable, str]],
|
|
469
|
+
state: bool | Literal["first True"] = ...,
|
|
470
|
+
on_state_change: Callable[[Self, bool, _T], typing.Any] | None = None,
|
|
471
|
+
user_data: _T = ...,
|
|
472
|
+
) -> None: ...
|
|
473
|
+
|
|
474
|
+
@typing.overload
|
|
475
|
+
def __init__(
|
|
476
|
+
self,
|
|
477
|
+
group: MutableSequence[RadioButton],
|
|
478
|
+
label: str | tuple[Hashable, str] | list[str | tuple[Hashable, str]],
|
|
479
|
+
state: bool | Literal["first True"] = ...,
|
|
480
|
+
on_state_change: Callable[[Self, bool], typing.Any] | None = None,
|
|
481
|
+
user_data: None = None,
|
|
482
|
+
) -> None: ...
|
|
483
|
+
|
|
484
|
+
def __init__(
|
|
485
|
+
self,
|
|
486
|
+
group: MutableSequence[RadioButton],
|
|
487
|
+
label: str | tuple[Hashable, str] | list[str | tuple[Hashable, str]],
|
|
488
|
+
state: bool | Literal["first True"] = "first True",
|
|
489
|
+
on_state_change: Callable[[Self, bool, _T], typing.Any] | Callable[[Self, bool], typing.Any] | None = None,
|
|
490
|
+
user_data: _T | None = None,
|
|
491
|
+
) -> None:
|
|
492
|
+
"""
|
|
493
|
+
:param group: list for radio buttons in same group
|
|
494
|
+
:param label: markup for radio button label
|
|
495
|
+
:param state: False, True, "mixed" or "first True"
|
|
496
|
+
:param on_state_change: shorthand for connect_signal()
|
|
497
|
+
function call for a single 'change' callback
|
|
498
|
+
:param user_data: user_data for on_state_change
|
|
499
|
+
|
|
500
|
+
..note:: `pack` method expect, that `Columns` backend widget is not modified from outside
|
|
501
|
+
|
|
502
|
+
This function will append the new radio button to group.
|
|
503
|
+
"first True" will set to True if group is empty.
|
|
504
|
+
|
|
505
|
+
Signals supported: ``'change'``, ``"postchange"``
|
|
506
|
+
|
|
507
|
+
Register signal handler with::
|
|
508
|
+
|
|
509
|
+
urwid.connect_signal(radio_button, 'change', callback, user_data)
|
|
510
|
+
|
|
511
|
+
where callback is callback(radio_button, new_state [,user_data])
|
|
512
|
+
Unregister signal handlers with::
|
|
513
|
+
|
|
514
|
+
urwid.disconnect_signal(radio_button, 'change', callback, user_data)
|
|
515
|
+
|
|
516
|
+
>>> bgroup = [] # button group
|
|
517
|
+
>>> b1 = RadioButton(bgroup, u"Agree")
|
|
518
|
+
>>> b2 = RadioButton(bgroup, u"Disagree")
|
|
519
|
+
>>> len(bgroup)
|
|
520
|
+
2
|
|
521
|
+
>>> b1
|
|
522
|
+
<RadioButton selectable fixed/flow widget 'Agree' state=True>
|
|
523
|
+
>>> b2
|
|
524
|
+
<RadioButton selectable fixed/flow widget 'Disagree' state=False>
|
|
525
|
+
>>> b2.render((15,), focus=True).text # ... = b in Python 3
|
|
526
|
+
[...'( ) Disagree ']
|
|
527
|
+
"""
|
|
528
|
+
if state == "first True":
|
|
529
|
+
state = not group
|
|
530
|
+
|
|
531
|
+
self.group = group
|
|
532
|
+
super().__init__(label, state, False, on_state_change, user_data) # type: ignore[call-overload]
|
|
533
|
+
group.append(self)
|
|
534
|
+
|
|
535
|
+
def set_state(self, state: bool | Literal["mixed"], do_callback: bool = True) -> None:
|
|
536
|
+
"""
|
|
537
|
+
Set the RadioButton state.
|
|
538
|
+
|
|
539
|
+
state -- True, False or "mixed"
|
|
540
|
+
|
|
541
|
+
do_callback -- False to suppress signal from this change
|
|
542
|
+
|
|
543
|
+
If state is True all other radio buttons in the same button
|
|
544
|
+
group will be set to False.
|
|
545
|
+
|
|
546
|
+
>>> bgroup = [] # button group
|
|
547
|
+
>>> b1 = RadioButton(bgroup, u"Agree")
|
|
548
|
+
>>> b2 = RadioButton(bgroup, u"Disagree")
|
|
549
|
+
>>> b3 = RadioButton(bgroup, u"Unsure")
|
|
550
|
+
>>> b1.state, b2.state, b3.state
|
|
551
|
+
(True, False, False)
|
|
552
|
+
>>> b2.set_state(True)
|
|
553
|
+
>>> b1.state, b2.state, b3.state
|
|
554
|
+
(False, True, False)
|
|
555
|
+
>>> def relabel_button(radio_button, new_state):
|
|
556
|
+
... radio_button.set_label(u"Think Harder!")
|
|
557
|
+
>>> key = connect_signal(b3, 'change', relabel_button)
|
|
558
|
+
>>> b3
|
|
559
|
+
<RadioButton selectable fixed/flow widget 'Unsure' state=False>
|
|
560
|
+
>>> b3.set_state(True) # this will trigger the callback
|
|
561
|
+
>>> b3
|
|
562
|
+
<RadioButton selectable fixed/flow widget 'Think Harder!' state=True>
|
|
563
|
+
"""
|
|
564
|
+
if self._state == state:
|
|
565
|
+
return
|
|
566
|
+
|
|
567
|
+
super().set_state(state, do_callback)
|
|
568
|
+
|
|
569
|
+
# if we're clearing the state we don't have to worry about
|
|
570
|
+
# other buttons in the button group
|
|
571
|
+
if state is not True:
|
|
572
|
+
return
|
|
573
|
+
|
|
574
|
+
# clear the state of each other radio button
|
|
575
|
+
for cb in self.group:
|
|
576
|
+
if cb is self:
|
|
577
|
+
continue
|
|
578
|
+
if cb.state:
|
|
579
|
+
cb.state = False
|
|
580
|
+
|
|
581
|
+
def toggle_state(self) -> None:
|
|
582
|
+
"""
|
|
583
|
+
Set state to True.
|
|
584
|
+
|
|
585
|
+
>>> bgroup = [] # button group
|
|
586
|
+
>>> b1 = RadioButton(bgroup, "Agree")
|
|
587
|
+
>>> b2 = RadioButton(bgroup, "Disagree")
|
|
588
|
+
>>> b1.state, b2.state
|
|
589
|
+
(True, False)
|
|
590
|
+
>>> b2.toggle_state()
|
|
591
|
+
>>> b1.state, b2.state
|
|
592
|
+
(False, True)
|
|
593
|
+
>>> b2.toggle_state()
|
|
594
|
+
>>> b1.state, b2.state
|
|
595
|
+
(False, True)
|
|
596
|
+
"""
|
|
597
|
+
self.set_state(True)
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
class Button(WidgetWrap[Columns]):
|
|
601
|
+
button_left = Text("<")
|
|
602
|
+
button_right = Text(">")
|
|
603
|
+
|
|
604
|
+
signals: typing.ClassVar[list[str]] = ["click"]
|
|
605
|
+
|
|
606
|
+
@typing.overload
|
|
607
|
+
def __init__(
|
|
608
|
+
self,
|
|
609
|
+
label: str | tuple[Hashable, str] | list[str | tuple[Hashable, str]],
|
|
610
|
+
on_press: Callable[[Self, _T], typing.Any] | None = None,
|
|
611
|
+
user_data: _T = ...,
|
|
612
|
+
*,
|
|
613
|
+
align: Literal["left", "center", "right"] | Align = ...,
|
|
614
|
+
wrap: Literal["space", "any", "clip", "ellipsis"] | WrapMode = ...,
|
|
615
|
+
layout: TextLayout | None = ...,
|
|
616
|
+
) -> None: ...
|
|
617
|
+
|
|
618
|
+
@typing.overload
|
|
619
|
+
def __init__(
|
|
620
|
+
self,
|
|
621
|
+
label: str | tuple[Hashable, str] | list[str | tuple[Hashable, str]],
|
|
622
|
+
on_press: Callable[[Self], typing.Any] | None = None,
|
|
623
|
+
user_data: None = None,
|
|
624
|
+
*,
|
|
625
|
+
align: Literal["left", "center", "right"] | Align = ...,
|
|
626
|
+
wrap: Literal["space", "any", "clip", "ellipsis"] | WrapMode = ...,
|
|
627
|
+
layout: TextLayout | None = ...,
|
|
628
|
+
) -> None: ...
|
|
629
|
+
|
|
630
|
+
def __init__(
|
|
631
|
+
self,
|
|
632
|
+
label: str | tuple[Hashable, str] | list[str | tuple[Hashable, str]],
|
|
633
|
+
on_press: Callable[[Self, _T], typing.Any] | Callable[[Self], typing.Any] | None = None,
|
|
634
|
+
user_data: _T | None = None,
|
|
635
|
+
*,
|
|
636
|
+
align: Literal["left", "center", "right"] | Align = Align.LEFT,
|
|
637
|
+
wrap: Literal["space", "any", "clip", "ellipsis"] | WrapMode = WrapMode.SPACE,
|
|
638
|
+
layout: TextLayout | None = None,
|
|
639
|
+
) -> None:
|
|
640
|
+
"""
|
|
641
|
+
:param label: markup for button label
|
|
642
|
+
:param on_press: shorthand for connect_signal()
|
|
643
|
+
function call for a single callback
|
|
644
|
+
:param user_data: user_data for on_press
|
|
645
|
+
:param align: typically ``'left'``, ``'center'`` or ``'right'``
|
|
646
|
+
:type align: label alignment mode
|
|
647
|
+
:param wrap: typically ``'space'``, ``'any'``, ``'clip'`` or ``'ellipsis'``
|
|
648
|
+
:type wrap: label wrapping mode
|
|
649
|
+
:param layout: defaults to a shared :class:`StandardTextLayout` instance
|
|
650
|
+
:type layout: text layout instance
|
|
651
|
+
|
|
652
|
+
..note:: `pack` method expect, that `Columns` backend widget is not modified from outside
|
|
653
|
+
|
|
654
|
+
Signals supported: ``'click'``
|
|
655
|
+
|
|
656
|
+
Register signal handler with::
|
|
657
|
+
|
|
658
|
+
urwid.connect_signal(button, 'click', callback, user_data)
|
|
659
|
+
|
|
660
|
+
where callback is callback(button [,user_data])
|
|
661
|
+
Unregister signal handlers with::
|
|
662
|
+
|
|
663
|
+
urwid.disconnect_signal(button, 'click', callback, user_data)
|
|
664
|
+
|
|
665
|
+
>>> from urwid.util import set_temporary_encoding
|
|
666
|
+
>>> Button(u"Ok")
|
|
667
|
+
<Button selectable fixed/flow widget 'Ok'>
|
|
668
|
+
>>> b = Button("Cancel")
|
|
669
|
+
>>> b.render((15,), focus=True).text # ... = b in Python 3
|
|
670
|
+
[b'< Cancel >']
|
|
671
|
+
>>> aligned_button = Button("Test", align=Align.CENTER)
|
|
672
|
+
>>> aligned_button.render((10,), focus=True).text
|
|
673
|
+
[b'< Test >']
|
|
674
|
+
>>> wrapped_button = Button("Long label", wrap=WrapMode.ELLIPSIS)
|
|
675
|
+
>>> with set_temporary_encoding("utf-8"):
|
|
676
|
+
... wrapped_button.render((7,), focus=False).text[0].decode('utf-8')
|
|
677
|
+
'< Lo… >'
|
|
678
|
+
"""
|
|
679
|
+
self._label = SelectableIcon(label, 0, align=align, wrap=wrap, layout=layout)
|
|
680
|
+
cols = Columns(
|
|
681
|
+
[(1, self.button_left), self._label, (1, self.button_right)],
|
|
682
|
+
dividechars=1,
|
|
683
|
+
)
|
|
684
|
+
super().__init__(cols)
|
|
685
|
+
|
|
686
|
+
# The old way of listening for a change was to pass the callback
|
|
687
|
+
# in to the constructor. Just convert it to the new way:
|
|
688
|
+
if on_press:
|
|
689
|
+
connect_signal(self, "click", on_press, user_data)
|
|
690
|
+
|
|
691
|
+
def pack(self, size: tuple[()] | tuple[int] | None = None, focus: bool = False) -> tuple[int, int]:
|
|
692
|
+
"""Pack for widget.
|
|
693
|
+
|
|
694
|
+
:param size: size data. Special case: None - get minimal widget size to fit
|
|
695
|
+
:param focus: widget is focused
|
|
696
|
+
|
|
697
|
+
>>> btn = Button("Some button")
|
|
698
|
+
>>> btn.pack((10,))
|
|
699
|
+
(10, 2)
|
|
700
|
+
>>> btn.pack()
|
|
701
|
+
(15, 1)
|
|
702
|
+
>>> btn.pack((), True)
|
|
703
|
+
(15, 1)
|
|
704
|
+
"""
|
|
705
|
+
return super().pack(size or (), focus)
|
|
706
|
+
|
|
707
|
+
def _repr_words(self) -> list[str]:
|
|
708
|
+
# include button.label in repr(button)
|
|
709
|
+
return [*super()._repr_words(), repr(self.label)]
|
|
710
|
+
|
|
711
|
+
def set_label(self, label: str | tuple[Hashable, str] | list[str | tuple[Hashable, str]]) -> None:
|
|
712
|
+
"""
|
|
713
|
+
Change the button label.
|
|
714
|
+
|
|
715
|
+
label -- markup for button label
|
|
716
|
+
|
|
717
|
+
>>> b = Button("Ok")
|
|
718
|
+
>>> b.set_label(u"Yup yup")
|
|
719
|
+
>>> b
|
|
720
|
+
<Button selectable fixed/flow widget 'Yup yup'>
|
|
721
|
+
"""
|
|
722
|
+
self._label.set_text(label)
|
|
723
|
+
|
|
724
|
+
def get_label(self) -> str:
|
|
725
|
+
"""
|
|
726
|
+
Return label text.
|
|
727
|
+
|
|
728
|
+
>>> b = Button(u"Ok")
|
|
729
|
+
>>> print(b.get_label())
|
|
730
|
+
Ok
|
|
731
|
+
>>> print(b.label)
|
|
732
|
+
Ok
|
|
733
|
+
"""
|
|
734
|
+
return self._label.text
|
|
735
|
+
|
|
736
|
+
label = property(get_label)
|
|
737
|
+
|
|
738
|
+
def keypress(self, size: tuple[int], key: str) -> str | None:
|
|
739
|
+
"""
|
|
740
|
+
Send 'click' signal on 'activate' command.
|
|
741
|
+
|
|
742
|
+
>>> assert Button._command_map[' '] == 'activate'
|
|
743
|
+
>>> assert Button._command_map['enter'] == 'activate'
|
|
744
|
+
>>> size = (15,)
|
|
745
|
+
>>> b = Button(u"Cancel")
|
|
746
|
+
>>> clicked_buttons = []
|
|
747
|
+
>>> def handle_click(button):
|
|
748
|
+
... clicked_buttons.append(button.label)
|
|
749
|
+
>>> key = connect_signal(b, 'click', handle_click)
|
|
750
|
+
>>> b.keypress(size, 'enter')
|
|
751
|
+
>>> b.keypress(size, ' ')
|
|
752
|
+
>>> clicked_buttons # ... = u in Python 2
|
|
753
|
+
[...'Cancel', ...'Cancel']
|
|
754
|
+
"""
|
|
755
|
+
if self._command_map[key] != Command.ACTIVATE:
|
|
756
|
+
return key
|
|
757
|
+
|
|
758
|
+
self._emit("click")
|
|
759
|
+
return None
|
|
760
|
+
|
|
761
|
+
def mouse_event(self, size: tuple[int], event: str, button: int, x: int, y: int, focus: bool) -> bool:
|
|
762
|
+
"""
|
|
763
|
+
Send 'click' signal on button 1 press.
|
|
764
|
+
|
|
765
|
+
>>> size = (15,)
|
|
766
|
+
>>> b = Button(u"Ok")
|
|
767
|
+
>>> clicked_buttons = []
|
|
768
|
+
>>> def handle_click(button):
|
|
769
|
+
... clicked_buttons.append(button.label)
|
|
770
|
+
>>> key = connect_signal(b, 'click', handle_click)
|
|
771
|
+
>>> b.mouse_event(size, 'mouse press', 1, 4, 0, True)
|
|
772
|
+
True
|
|
773
|
+
>>> b.mouse_event(size, 'mouse press', 2, 4, 0, True) # ignored
|
|
774
|
+
False
|
|
775
|
+
>>> clicked_buttons # ... = u in Python 2
|
|
776
|
+
[...'Ok']
|
|
777
|
+
"""
|
|
778
|
+
if button != 1 or not is_mouse_press(event):
|
|
779
|
+
return False
|
|
780
|
+
|
|
781
|
+
self._emit("click")
|
|
782
|
+
return True
|
|
783
|
+
|
|
784
|
+
|
|
785
|
+
def _test():
|
|
786
|
+
import doctest
|
|
787
|
+
|
|
788
|
+
doctest.testmod()
|
|
789
|
+
|
|
790
|
+
|
|
791
|
+
if __name__ == "__main__":
|
|
792
|
+
_test()
|