urwid 2.6.0.post0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of urwid might be problematic. Click here for more details.

Files changed (75) hide show
  1. urwid/__init__.py +333 -0
  2. urwid/canvas.py +1413 -0
  3. urwid/command_map.py +137 -0
  4. urwid/container.py +59 -0
  5. urwid/decoration.py +65 -0
  6. urwid/display/__init__.py +97 -0
  7. urwid/display/_posix_raw_display.py +413 -0
  8. urwid/display/_raw_display_base.py +914 -0
  9. urwid/display/_web.css +12 -0
  10. urwid/display/_web.js +462 -0
  11. urwid/display/_win32.py +171 -0
  12. urwid/display/_win32_raw_display.py +269 -0
  13. urwid/display/common.py +1219 -0
  14. urwid/display/curses.py +690 -0
  15. urwid/display/escape.py +624 -0
  16. urwid/display/html_fragment.py +251 -0
  17. urwid/display/lcd.py +518 -0
  18. urwid/display/raw.py +37 -0
  19. urwid/display/web.py +636 -0
  20. urwid/event_loop/__init__.py +55 -0
  21. urwid/event_loop/abstract_loop.py +175 -0
  22. urwid/event_loop/asyncio_loop.py +231 -0
  23. urwid/event_loop/glib_loop.py +294 -0
  24. urwid/event_loop/main_loop.py +721 -0
  25. urwid/event_loop/select_loop.py +230 -0
  26. urwid/event_loop/tornado_loop.py +206 -0
  27. urwid/event_loop/trio_loop.py +302 -0
  28. urwid/event_loop/twisted_loop.py +269 -0
  29. urwid/event_loop/zmq_loop.py +275 -0
  30. urwid/font.py +695 -0
  31. urwid/graphics.py +96 -0
  32. urwid/highlight.css +19 -0
  33. urwid/listbox.py +1899 -0
  34. urwid/monitored_list.py +522 -0
  35. urwid/numedit.py +376 -0
  36. urwid/signals.py +330 -0
  37. urwid/split_repr.py +130 -0
  38. urwid/str_util.py +358 -0
  39. urwid/text_layout.py +632 -0
  40. urwid/treetools.py +515 -0
  41. urwid/util.py +557 -0
  42. urwid/version.py +16 -0
  43. urwid/vterm.py +1806 -0
  44. urwid/widget/__init__.py +181 -0
  45. urwid/widget/attr_map.py +161 -0
  46. urwid/widget/attr_wrap.py +140 -0
  47. urwid/widget/bar_graph.py +649 -0
  48. urwid/widget/big_text.py +77 -0
  49. urwid/widget/box_adapter.py +126 -0
  50. urwid/widget/columns.py +1145 -0
  51. urwid/widget/constants.py +574 -0
  52. urwid/widget/container.py +227 -0
  53. urwid/widget/divider.py +110 -0
  54. urwid/widget/edit.py +718 -0
  55. urwid/widget/filler.py +403 -0
  56. urwid/widget/frame.py +539 -0
  57. urwid/widget/grid_flow.py +539 -0
  58. urwid/widget/line_box.py +194 -0
  59. urwid/widget/overlay.py +829 -0
  60. urwid/widget/padding.py +597 -0
  61. urwid/widget/pile.py +971 -0
  62. urwid/widget/popup.py +170 -0
  63. urwid/widget/progress_bar.py +141 -0
  64. urwid/widget/scrollable.py +597 -0
  65. urwid/widget/solid_fill.py +44 -0
  66. urwid/widget/text.py +354 -0
  67. urwid/widget/widget.py +852 -0
  68. urwid/widget/widget_decoration.py +166 -0
  69. urwid/widget/wimp.py +792 -0
  70. urwid/wimp.py +23 -0
  71. urwid-2.6.0.post0.dist-info/COPYING +504 -0
  72. urwid-2.6.0.post0.dist-info/METADATA +332 -0
  73. urwid-2.6.0.post0.dist-info/RECORD +75 -0
  74. urwid-2.6.0.post0.dist-info/WHEEL +5 -0
  75. urwid-2.6.0.post0.dist-info/top_level.txt +1 -0
urwid/widget/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()