7

I'm using urwid, which is a Python "framework" for designing terminal user interfaces in ncurses. There's one thing though that I'm not able to do in urwid that was easy in curses - make the cursor invisible. As it is now, the cursor is visible when selecting buttons, and it just looks plain ugly. Is there a way to disable it?

makos
  • 157
  • 3
  • 14
  • `urwid.SelectableIcon` has similar behavior to button but no cursor; it may be worth a look. – anon01 Mar 30 '18 at 23:07

4 Answers4

6

I agree that the flashing cursor on an urwid.Button looks a bit lame, so I've come up with a solution to hide it. In urwid, the Button class is just a subclass of WidgetWrap containing a SelectableIcon and two Text widgets (the enclosing "<" and ">"). It's the SelectableIcon class that sets the cursor position to the first character of the label, by default. By subclassing SelectableIcon, modifying the cursor position and then wrapping it into an urwid.WidgetWrap subclass you can create your own custom button that can do all the tricks a built-in Button, or even more.

Here' what it looks like in my project.

enter image description here

import urwid

class ButtonLabel(urwid.SelectableIcon):
    def __init__(self, text):
        """
        Here's the trick: 
        we move the cursor out to the right of the label/text, so it doesn't show
        """
        curs_pos = len(text) + 1 
        urwid.SelectableIcon.__init__(self, text, cursor_position=curs_pos)

Next, you can wrap a ButtonLabel object along with any other objects into a WidgetWrap subclass that will be your custom button class.

class FixedButton(urwid.WidgetWrap):
    _selectable = True
    signals = ["click"]
    def __init__(self, label):
        self.label = ButtonLabel(label)
        # you could combine the ButtonLabel object with other widgets here
        display_widget = self.label 
        urwid.WidgetWrap.__init__(self, urwid.AttrMap(display_widget, None, focus_map="button_reversed"))

    def keypress(self, size, key):
        """
        catch all the keys you want to handle here
        and emit the click signal along with any data 
        """
        pass

    def set_label(self, new_label):
        # we can set the label at run time, if necessary
        self.label.set_text(str(new_label))

    def mouse_event(self, size, event, button, col, row, focus):
        """
        handle any mouse events here
        and emit the click signal along with any data 
        """
        pass

In this code, there is actually not much combination of widgets in the FixedButton WidgetWrap subclass, but you could add a "[" and "]" to the edges of the button, wrap it into a LineBox, etc. If all this is superfluous, you can just move the event handling functions into the ButtonLabel class, and make it emit a signal when it gets clicked.

To make the button reversed when the user moves on it, wrap it into AttrMap and set the focus_map to some palette entry ("button_reversed", in my case).

imrek
  • 2,930
  • 3
  • 20
  • 36
  • Hi @imrek, I needed a solution to this as well. Your's is a nice one. 2 things I found: 1) you don't really need to sublclass SelectableIcon - you could do everything from the self.label instance within FixedButton - checking the length and setting the cursor position. 2) your code will not work if 'label' is markup rather than plain text. Solution would be to use the urwid util function decompose_tagmarkup(). Full code in FixedButton.__init()__ looks like: text, attrib = decompose_tagmarkup(label) cursor_pos = len(text) + 1 self._label = urwid.SelectableIcon("", cursor_pos) – scottmac991 Aug 10 '20 at 14:46
3

Building upon Drunken Master's answer, I've cleaned up the solution as much as possible.

The urwid.SelectableIcon is basically an urwid.Text field with this ugly blinking cursor. So instead of overriding the urwid.SelectableIcon and packing it into an urwid.WidgetWrap, let's take an urwid.Text directly and make it selectable and react to button/mouse activation (inspired from urwid's simple menu tutorial):

import urwid

choices = u'Chapman Cleese Gilliam Idle Jones Palin'.split()

class ListEntry(urwid.Text):
    _selectable = True

    signals = ["click"]

    def keypress(self, size, key):
        """
        Send 'click' signal on 'activate' command.
        """
        if self._command_map[key] != urwid.ACTIVATE:
            return key

        self._emit('click')

    def mouse_event(self, size, event, button, x, y, focus):
        """
        Send 'click' signal on button 1 press.
        """
        if button != 1 or not urwid.util.is_mouse_press(event):
            return False

        self._emit('click')
        return True

def menu(title, choices):
    body = [urwid.Text(title), urwid.Divider()]
    for c in choices:
        button = ListEntry(c)
        urwid.connect_signal(button, 'click', item_chosen, c)
        body.append(urwid.AttrMap(button, None, focus_map='reversed'))
    return urwid.ListBox(urwid.SimpleFocusListWalker(body))

def item_chosen(button, choice):
    response = urwid.Text([u'You chose ', choice, u'\n'])
    done = ListEntry(u'Ok')
    urwid.connect_signal(done, 'click', exit_program)
    main.original_widget = urwid.Filler(urwid.Pile([response,
                                                    urwid.AttrMap(done, None, focus_map='reversed')]))

def exit_program(button):
    raise urwid.ExitMainLoop()

main = urwid.Padding(menu(u'Pythons', choices), left=2, right=2)
top = urwid.Overlay(main, urwid.SolidFill(u'\N{MEDIUM SHADE}'),
                    align='center', width=('relative', 60),
                    valign='middle', height=('relative', 60),
                    min_width=20, min_height=9)
urwid.MainLoop(top, palette=[('reversed', 'standout', '')]).run()

Works like a charm:

enter image description here

orzechow
  • 1,210
  • 2
  • 13
  • 18
  • Do you have a full working example? I'm not sure how to use your code as-is. If you know of a better solution 18 months on, even better. This Question is the top result when looking to hide the cursor (and have the pretty inversed colours for showing focus), so would benefit from a full example. – Etzeitet Feb 22 '21 at 16:32
  • 1
    Hi @Etzeitet, have a look into my edited answer. You can use the `ListEntry` class simply as replacement of `urwid.Button` class. – orzechow Feb 24 '21 at 10:33
  • fantastic! I can confirm the example works. I'm slowly getting the hang of urwid, and this certain helps. Thanks for the update! – Etzeitet Feb 24 '21 at 15:07
2

urwid uses the curs_set function, but does not expose it as a class method anywhere. Someone could modify urwid to allow using this method; otherwise there's no reliable method of doing this.

You might report it as an issue.

Thomas Dickey
  • 51,086
  • 7
  • 70
  • 105
  • 1
    I see. I posted an issue there, hopefully urwid devs will consider this. – makos Jan 09 '16 at 02:36
  • Exposing the func is insufficient as urwid will just turn the cursor back on. makos reported this upstream: https://github.com/urwid/urwid/issues/170 – Mike Frysinger Dec 19 '21 at 01:06
1

Along the lines of Drunken Master's answer, but with "minimally invasive surgery":

class ButtonLabel(urwid.SelectableIcon):
    '''
    use Drunken Master's trick to move the cursor out of view
    '''
    def set_text(self, label):
        '''
        set_text is invoked by Button.set_label
        '''
        self.__super.set_text(label)
        self._cursor_position = len(label) + 1


class MyButton(urwid.Button):
    '''
    - override __init__ to use our ButtonLabel instead of urwid.SelectableIcon

    - make button_left and button_right plain strings and variable width -
      any string, including an empty string, can be set and displayed

    - otherwise, we leave Button behaviour unchanged
    '''
    button_left = "["
    button_right = "]"

    def __init__(self, label, on_press=None, user_data=None):
        self._label = ButtonLabel("")
        cols = urwid.Columns([
            ('fixed', len(self.button_left), urwid.Text(self.button_left)),
            self._label,
            ('fixed', len(self.button_right), urwid.Text(self.button_right))],
            dividechars=1)
        super(urwid.Button, self).__init__(cols)

        if on_press:
            urwid.connect_signal(self, 'click', on_press, user_data)

        self.set_label(label)

Here, we only modify the button's appearance but otherwise leave its behaviour unchanged.