1#!/usr/bin/env python
2#
3# Urwid BigText example program
4# Copyright (C) 2004-2009 Ian Ward
5#
6# This library is free software; you can redistribute it and/or
7# modify it under the terms of the GNU Lesser General Public
8# License as published by the Free Software Foundation; either
9# version 2.1 of the License, or (at your option) any later version.
10#
11# This library is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14# Lesser General Public License for more details.
15#
16# You should have received a copy of the GNU Lesser General Public
17# License along with this library; if not, write to the Free Software
18# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
19#
20# Urwid web site: https://urwid.org/
21
22"""Urwid example demonstrating use of the BigText widget."""
23
24from __future__ import annotations
25
26import typing
27
28import urwid
29
30if typing.TYPE_CHECKING:
31 from collections.abc import Callable
32
33
34class SwitchingPadding(urwid.Padding[urwid.BigText]):
35 """Padding that switches its alignment based on the available width."""
36
37 def padding_values(
38 self,
39 size: tuple[int], # type: ignore[override]
40 focus: bool,
41 ) -> tuple[int, int]:
42 """Return the left and right padding, aligning left or right depending on the available width.
43
44 :param size: render size passed by the parent widget
45 :param focus: whether the wrapped widget is in focus
46 :returns: left and right padding widths
47 """
48 maxcol = size[0]
49 width, _height = self.original_widget.pack((), focus=focus) # urwid.BigText is FIXED size widget
50 if maxcol > width:
51 self.align = urwid.LEFT
52 else:
53 self.align = urwid.RIGHT
54 return super().padding_values(size, focus)
55
56
57class BigTextDisplay:
58 """Interactive demo application for the :class:`urwid.BigText` widget."""
59
60 palette: typing.ClassVar[list[tuple[str, str, str] | tuple[str, str, str, str]]] = [
61 ("body", "black", "light gray", "standout"),
62 ("header", "white", "dark red", "bold"),
63 ("button normal", "light gray", "dark blue", "standout"),
64 ("button select", "white", "dark green"),
65 ("button disabled", "dark gray", "dark blue"),
66 ("edit", "light gray", "dark blue"),
67 ("bigtext", "white", "black"),
68 ("chars", "light gray", "black"),
69 ("exit", "white", "dark cyan"),
70 ]
71
72 def create_radio_button(
73 self,
74 g: list[urwid.RadioButton],
75 name: str,
76 font: urwid.Font,
77 fn: Callable[[urwid.RadioButton, bool, urwid.Font], typing.Any],
78 ) -> urwid.AttrMap[urwid.RadioButton]:
79 """Create a radio button for selecting a font.
80
81 :param g: radio button group to add the new button to
82 :param name: label for the radio button
83 :param font: font this button selects
84 :param fn: callback connected to the button's ``change`` signal, with *font* as its user data
85 :returns: the radio button wrapped in an :class:`urwid.AttrMap`
86 """
87 w = urwid.RadioButton(g, name, False, on_state_change=fn, user_data=font)
88 return urwid.AttrMap(w, "button normal", "button select")
89
90 def create_disabled_radio_button(self, name: str) -> urwid.AttrMap[urwid.Text]:
91 """Create a disabled placeholder shown for fonts unavailable in the current encoding mode.
92
93 :param name: name of the unavailable font
94 :returns: the placeholder text wrapped in an :class:`urwid.AttrMap`
95 """
96 return urwid.AttrMap(urwid.Text(f" {name} (UTF-8 mode required)"), "button disabled")
97
98 def create_edit(
99 self,
100 label: str,
101 text: str,
102 fn: Callable[[urwid.Edit, str], typing.Any],
103 ) -> urwid.AttrMap[urwid.Edit]:
104 """Create an edit widget and connect it to a change callback.
105
106 :param label: caption shown before the editable text
107 :param text: initial contents of the edit widget
108 :param fn: callback connected to the widget's ``change`` signal, also invoked immediately
109 :returns: the edit widget wrapped in an :class:`urwid.AttrMap`
110 """
111 w = urwid.Edit(label, text)
112 urwid.connect_signal(w, "change", fn)
113 fn(w, text)
114 return urwid.AttrMap(w, "edit")
115
116 def set_font_event(self, w: urwid.RadioButton, state: bool, font: urwid.Font) -> None:
117 """Switch the displayed :class:`urwid.BigText` to the selected font.
118
119 :param w: radio button whose state changed
120 :param state: new state of the radio button
121 :param font: font the radio button selects
122 """
123 if state:
124 self.bigtext.set_font(font)
125 self.chars_avail.set_text(font.characters())
126
127 def edit_change_event(self, widget: urwid.Edit, text: str) -> None:
128 """Update the :class:`urwid.BigText` display with the edited text.
129
130 :param widget: edit widget that changed
131 :param text: new contents of the edit widget
132 """
133 self.bigtext.set_text(text)
134
135 def setup_view(
136 self,
137 ) -> tuple[
138 urwid.Frame[urwid.AttrMap[urwid.ListBox[int]], urwid.AttrMap[urwid.Text], None],
139 urwid.Overlay[urwid.BigText, urwid.Frame[urwid.AttrMap[urwid.ListBox[int]], urwid.AttrMap[urwid.Text], None]],
140 ]:
141 """Build the main view and the exit-confirmation overlay.
142
143 :returns: the main frame, and an overlay of the exit prompt on top of that frame
144 """
145 fonts = urwid.get_all_fonts()
146 # setup mode radio buttons
147 self.font_buttons: list[urwid.AttrMap[urwid.RadioButton] | urwid.AttrMap[urwid.Text]] = []
148 group: list[urwid.RadioButton] = []
149 utf8 = urwid.get_encoding_mode() == "utf8"
150 for name, fontcls in fonts:
151 font = fontcls()
152 rb: urwid.AttrMap[urwid.RadioButton] | urwid.AttrMap[urwid.Text]
153 if font.utf8_required and not utf8:
154 rb = self.create_disabled_radio_button(name)
155 else:
156 radio = self.create_radio_button(group, name, font, self.set_font_event)
157 rb = radio
158 if fontcls == urwid.Thin6x6Font:
159 chosen_font_rb = radio
160 exit_font = font
161 self.font_buttons.append(rb)
162
163 # Create BigText
164 self.bigtext = urwid.BigText("", None) # type: ignore[arg-type] # font assigned before render
165 bt = urwid.BoxAdapter(
166 urwid.Filler(
167 urwid.AttrMap(
168 SwitchingPadding(self.bigtext, urwid.LEFT, None),
169 "bigtext",
170 ),
171 urwid.BOTTOM,
172 None,
173 7,
174 ),
175 7,
176 )
177
178 # Create chars_avail
179 cah = urwid.Text("Characters Available:")
180 self.chars_avail = urwid.Text("", wrap=urwid.ANY)
181 ca = urwid.AttrMap(self.chars_avail, "chars")
182
183 chosen_font_rb.original_widget.set_state(True) # causes set_font_event call
184
185 # Create Edit widget
186 edit = self.create_edit("", "Urwid BigText example", self.edit_change_event)
187
188 # ListBox
189 chars = urwid.Pile([cah, ca])
190 # select_navigation lets TAB / SHIFT TAB move between the edit and the font buttons
191 fonts_pile = urwid.Pile([urwid.Text("Fonts:"), *self.font_buttons], focus_item=1, select_navigation=True)
192 col = urwid.Columns([(16, chars), fonts_pile], 3, focus_column=1, select_navigation=True)
193 bt_pile = urwid.Pile([bt, edit], focus_item=1, select_navigation=True)
194 lines: list[urwid.widget.AbstractFlowWidget] = [bt_pile, urwid.Divider(), col]
195 listbox = urwid.ListBox(urwid.SimpleListWalker(lines), select_navigation=True)
196
197 # Frame
198 w: urwid.Frame[
199 urwid.AttrMap[urwid.ListBox[int]],
200 urwid.AttrMap[urwid.Text],
201 None,
202 ] = urwid.Frame(
203 body=urwid.AttrMap(listbox, "body"),
204 header=urwid.AttrMap(
205 urwid.Text("Urwid BigText example program - TAB / SHIFT TAB move focus, F8 exits."), "header"
206 ),
207 )
208
209 # Exit message
210 exit_w = urwid.Overlay(
211 urwid.BigText(("exit", " Quit? "), exit_font),
212 w,
213 urwid.CENTER,
214 None,
215 urwid.MIDDLE,
216 None,
217 )
218 return w, exit_w
219
220 def main(self) -> None:
221 """Build the view and run the main loop until the user exits."""
222 self.view, self.exit_view = self.setup_view()
223 self.loop = urwid.MainLoop(self.view, self.palette, unhandled_input=self.unhandled_input)
224 self.loop.run()
225
226 def unhandled_input(self, key: str | tuple[str, int, int, int]) -> bool | None:
227 """Handle keys not consumed by the widgets: :kbd:`f8` to confirm exit, :kbd:`y`/:kbd:`n` to answer it.
228
229 :param key: unhandled key or mouse event
230 :returns: ``True`` if the key was handled, ``None`` otherwise
231 :raises urwid.ExitMainLoop: when the user confirms the exit prompt with :kbd:`y`
232 """
233 if key == "f8":
234 self.loop.widget = self.exit_view
235 return True
236 if self.loop.widget != self.exit_view:
237 return None
238 if key in {"y", "Y"}:
239 raise urwid.ExitMainLoop
240 if key in {"n", "N"}:
241 self.loop.widget = self.view
242 return True
243 return None
244
245
246def main() -> None:
247 """Run the BigText example program."""
248 BigTextDisplay().main()
249
250
251if __name__ == "__main__":
252 main()