Search code examples
pythonconsole-applicationurwid

Urwid keyboard triggered popup


I'm trying to display a help dialog over top my urwid application when a user presses the H key, but I can't seem to get it to go away. It displays without a problem. What am I missing? I've been at this for the good part of an entire day now.

I've looked at several examples describing different ways to implement this. I've played around with signals, but to no avail. I'd like to avoid having a visible button for help and rely solely on the keyboard shortcut.

import urwid

class Application(object):
    '''
    The console UI
    '''

    # The default color palette
    _palette = [
        ('banner', 'black', 'light gray'),
        ('selectable', 'white', 'black'),
        ('focus', 'black', 'light gray')
    ]

    def __init__(self):
        self._content = [
            urwid.Text('Initializing...', align = 'left')
        ]

        # Body
        self._body_walker = urwid.SimpleListWalker(self._content)
        self._body_list = urwid.ListBox(self._body_walker)
        self._body_padding = urwid.Padding(
            self._body_list,
            left = 1,
            right = 1
        )
        self._body = urwid.LineBox(self._body_padding)

        # Loop
        self._loop = urwid.MainLoop(
            self._body,
            self._palette,
            unhandled_input = self._handle_input
        )

    def reset_layout(self):
        '''
        Resets the console UI to the default layout
        '''

        self._loop.widget = self._body
        self._loop.draw_screen()

    def _handle_input(self, key):
        '''
        Handles user input to the console UI

        Args:
            key (object): A mouse or keyboard input sequence
        '''

        if type(key) == str:
            if key in ('q', 'Q'):
                raise urwid.ExitMainLoop()
            elif key in ('h', 'H'):
                self.dialog(
                    [
                        'Urwid v1.3.1\n',
                        '\n',
                        'Press Q to quit\n',
                        'Press H for help'
                    ]
                )
        elif type(key) == tuple:
            pass

    def print(self, string = '', align = 'left'):
        '''
        Prints a string to the console UI

        Args:
            string (str): The string to print
            align (str): The alignment of the printed text
        '''

        self._body_walker.append(
            urwid.Text(string, align = align)
        )

    def printf(self, *strings):
        '''
        Prints multiple strings with different alignment

        Args:
            strings (tuple): A string, alignment pair
        '''

        self._body_walker.append(
            urwid.Columns(
                [
                    urwid.Text(string, align = align)
                    for string, align in strings
                ]
            )
        )

    def start(self):
        '''
        Starts the console UI
        '''

        self._loop.run()

    def dialog(self, text = ['']):
        '''
        Overlays a dialog box on top of the console UI

        Args:
            test (list): A list of strings to display
        '''

        # Header
        header_text = urwid.Text(('banner', 'Help'), align = 'center')
        header = urwid.AttrMap(header_text, 'banner')

        # Body
        body_text = urwid.Text(text, align = 'center')
        body_filler = urwid.Filler(body_text, valign = 'top')
        body_padding = urwid.Padding(
            body_filler,
            left = 1,
            right = 1
        )
        body = urwid.LineBox(body_padding)

        # Footer
        footer = urwid.Button('Okay', self.reset_layout())
        footer = urwid.AttrWrap(footer, 'selectable', 'focus')
        footer = urwid.GridFlow([footer], 8, 1, 1, 'center')

        # Layout
        layout = urwid.Frame(
            body,
            header = header,
            footer = footer,
            focus_part = 'footer'
        )

        w = urwid.Overlay(
            urwid.LineBox(layout),
            self._body,
            align = 'center',
            width = 40,
            valign = 'middle',
            height = 10
        )

        self._loop.widget = w

Application().start()

Solution

  • Can you imagine how stupid I feel right now after realizing that all I needed to do was drop the parentheses on the button's callback?

    footer = urwid.Button('Okay', self.reset_layout)

    Here's some more sample code to play around with for whoever stumbles upon this in the future.

    import urwid
    
    class Application(object):
        '''
        The console UI
        '''
    
        # The default color palette
        _palette = [
            ('banner', 'black', 'light gray'),
            ('selectable', 'white', 'black'),
            ('focus', 'black', 'light gray')
        ]
    
        def __init__(self):
            self._body = urwid.SolidFill('.')
            self._pile = urwid.Pile(
                [
                    self.dialog()
                ]
            )
            self._over = urwid.Overlay(
                self._pile,
                self._body,
                align = 'center',
                valign = 'middle',
                width = 20,
                height = 10
            )
    
            # Loop
            self._loop = urwid.MainLoop(
                self._over,
                self._palette,
                unhandled_input = self._handle_input
            )
    
        def _handle_input(self, key):
            '''
            Handles user input to the console UI
    
            Args:
                key (object): A mouse or keyboard input sequence
            '''
    
            if type(key) == str:
                if key in ('q', 'Q'):
                    raise urwid.ExitMainLoop()
            elif type(key) == tuple:
                pass
    
        def start(self):
            '''
            Starts the console UI
            '''
    
            self._loop.run()
    
        def do(self, thing):
            self._loop.widget = self._body
            #self._pile.contents.clear()
    
        def dialog(self):
            '''
            Overlays a dialog box on top of the console UI
            '''
    
            # Header
            header_text = urwid.Text(('banner', 'Help'), align = 'center')
            header = urwid.AttrMap(header_text, 'banner')
    
            # Body
            body_text = urwid.Text('Hello world', align = 'center')
            body_filler = urwid.Filler(body_text, valign = 'top')
            body_padding = urwid.Padding(
                body_filler,
                left = 1,
                right = 1
            )
            body = urwid.LineBox(body_padding)
    
            # Footer
            footer = urwid.Button('Okay', self.do)
            footer = urwid.AttrWrap(footer, 'selectable', 'focus')
            footer = urwid.GridFlow([footer], 8, 1, 1, 'center')
    
            # Layout
            layout = urwid.Frame(
                body,
                header = header,
                footer = footer,
                focus_part = 'footer'
            )
    
            return layout
    
    Application().start()