6

The default appearance of a button in urwid is very functional, but in my eyes not very pretty. It can also be irritating when several buttons are side by side in a row.

How can I achieve that a button is displayed with a frame and centered text and changes its color when it gets the focus?

enter image description here

AFoeee
  • 731
  • 7
  • 24

2 Answers2

10

You can draw anything with Urwid that's drawable in the terminal with pure Unicode text, with foreground and background colors assigned per character.

Considering this, it's impossible to draw exactly the same as it is in your mockup, because to draw borders you need to use the Unicode box drawing characters, which would take up more space.

I started writing an example, but sadly don't have time to polish it right now.

I'm sharing here in its unfinished state (working, but selection appearance is buggy), in the hopes that you might find it useful and maybe serve as a good enough starting point for you to fiddle with.

Screenshots:

enter image description here

enter image description here

Code:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from __future__ import print_function, absolute_import, division
import urwid


PALETTE = [
    ('normal', '', ''),
    ('bold', 'bold', ''),
    ('blue', 'bold', 'dark blue'),
    ('highlight', 'black', 'dark blue'),
]


def show_or_exit(key):
    if key in ('q', 'Q', 'esc'):
        raise urwid.ExitMainLoop()


class CustomButton(urwid.Button):
    button_left = urwid.Text('[')
    button_right = urwid.Text(']')


def custom_button(*args, **kwargs):
    b = CustomButton(*args, **kwargs)
    b = urwid.AttrMap(b, '', 'highlight')
    b = urwid.Padding(b, left=4, right=4)
    return b


class BoxButton(urwid.WidgetWrap):
    _border_char = u'─'
    def __init__(self, label, on_press=None, user_data=None):
        padding_size = 2
        border = self._border_char * (len(label) + padding_size * 2)
        cursor_position = len(border) + padding_size

        self.top = u'┌' + border + u'┐\n'
        self.middle = u'│  ' + label + u'  │\n'
        self.bottom = u'└' + border + u'┘'

        # self.widget = urwid.Text([self.top, self.middle, self.bottom])
        self.widget = urwid.Pile([
            urwid.Text(self.top[:-1]),
            urwid.Text(self.middle[:-1]),
            urwid.Text(self.bottom),
        ])

        self.widget = urwid.AttrMap(self.widget, '', 'highlight')

        # self.widget = urwid.Padding(self.widget, 'center')
        # self.widget = urwid.Filler(self.widget)

        # here is a lil hack: use a hidden button for evt handling
        self._hidden_btn = urwid.Button('hidden %s' % label, on_press, user_data)

        super(BoxButton, self).__init__(self.widget)

    def selectable(self):
        return True

    def keypress(self, *args, **kw):
        return self._hidden_btn.keypress(*args, **kw)

    def mouse_event(self, *args, **kw):
        return self._hidden_btn.mouse_event(*args, **kw)


if __name__ == '__main__':
    header = urwid.Text('Header')
    footer = urwid.Text('Footer')
    onclick = lambda w: footer.set_text('clicked: %r' % w)
    widget = urwid.Pile([
        header,
        urwid.Text('Simple custom buttons:'),
        urwid.Columns([
            custom_button('OK', on_press=onclick),
            custom_button('Cancel', on_press=onclick),
        ]),
        urwid.Text('Box bordered buttons:'),
        urwid.Columns([
            urwid.Padding(BoxButton('OK', on_press=onclick), left=4, right=4),
            BoxButton('Cancel', on_press=onclick),
        ]),
        footer,
    ])
    widget = urwid.Filler(widget, 'top')
    loop = urwid.MainLoop(widget, PALETTE, unhandled_input=show_or_exit)
    loop.run()
Elias Dorneles
  • 22,556
  • 11
  • 85
  • 107
  • This works, but where do `button_left` and `button_right` are documented? – cYrus Nov 29 '20 at 16:01
  • I guess they're not documented, but you can see it in the code here: https://github.com/urwid/urwid/blob/4c739b6be21b0c98324e7b7780d8e712f3ad6db3/urwid/wimp.py#L448-L449 – Elias Dorneles Nov 29 '20 at 21:50
  • Thanks, my question was more like, can we rely on them? But I guess only the devs know. :) – cYrus Nov 30 '20 at 10:29
  • 1
    I wouldn't worry about it, because: 1) they're public attributes, so, meant to be extended 2) [the code that uses it is trivial](https://github.com/urwid/urwid/blob/4c739b6be21b0c98324e7b7780d8e712f3ad6db3/urwid/wimp.py#L478-L483), and therefore easy to reproduce if needed – Elias Dorneles Nov 30 '20 at 10:32
3

Building on the excellent answer by Elias, I took his BoxButton class and simplified it a bit by using a LineBox instead of manually drawing the border:

class BoxButton(urwid.WidgetWrap):
    """ Taken from https://stackoverflow.com/a/65871001/778272
    """
    def __init__(self, label, on_click):
        label_widget = urwid.Text(label, align='center')
        self.widget = urwid.LineBox(label_widget)
        self.hidden_button = urwid.Button('hidden button', on_click)
        super(BoxButton, self).__init__(self.widget)

    def selectable(self):
        return True

    def keypress(self, *args, **kwargs):
        return self.hidden_button.keypress(*args, **kwargs)

    def mouse_event(self, *args, **kwargs):
        return self.hidden_button.mouse_event(*args, **kwargs)

It is up to the container to set the button's width. The button label will be automatically centered. Example using columns to display two buttons in a row:

urwid.Columns([
    (14, BoxButton('Deploy', on_deploy_clicked)),
    (1, urwid.Text(' ')),  # quick hack to add gap
    (14, BoxButton('Restart', on_restart_clicked)),
])

enter image description here

BoxButton intentionally does not decorate itself. To decorate it:

urwid.AttrMap(BoxButton('Deploy', on_clicked), 'normal', 'highlight')
Lucio Paiva
  • 19,015
  • 11
  • 82
  • 104