bigtext.pyΒΆ

  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()