I am writing an application in python with urwid.
I need an Edit
widget with autocompletion. Haven't seen one in the documentation so I have tried implementing it on my own based on the pop_up example.
However, I am faced with the fact that the pop_up widget has the focus which is a problem because:
- The cursor in the edit widget is not visible. When using the left and right arrow keys without counting how often you have pressed them you do not know where the next character will be inserted.
- All user input goes to the pop_up widget and not to the PopUpLauncher, although most of the key events are meant for the edit widget. I cannot call Edit.keypress because I don't know the size of the edit widget. Therefore I need to duplicate code from urwid.
How can I set the PopUpLauncher to have the focus?
From there on this answer might be helpful.
A strongly simplified version of my custom widget class:
#!/usr/bin/env python
import urwid
class AutoCompleteEdit(urwid.PopUpLauncher):
CMD_INSERT_SELECTED = "complete"
command_map = urwid.CommandMap()
command_map['tab'] = CMD_INSERT_SELECTED
def __init__(self, get_completions):
self.edit_widget = urwid.Edit()
self.__super.__init__(self.edit_widget)
self.get_completions = get_completions
# ------- user input ------
def keypress(self, size, key):
cmd = self.command_map[key]
if cmd is None:
out = self.__super.keypress(size, key)
self.update_completions()
return out
return self.__super.keypress(size, key)
def forwarded_keypress(self, key):
if self.edit_widget.valid_char(key):
#if (isinstance(key, text_type) and not isinstance(self._caption, text_type)):
# # screen is sending us unicode input, must be using utf-8
# # encoding because that's all we support, so convert it
# # to bytes to match our caption's type
# key = key.encode('utf-8')
self.edit_widget.insert_text(key)
self.update_completions()
return
cmd = self.command_map[key]
if cmd == self.CMD_INSERT_SELECTED:
self.insert_selected()
return
elif cmd == urwid.CURSOR_LEFT:
p = self.edit_widget.edit_pos
if p == 0:
return key
p = urwid.move_prev_char(self.edit_widget.edit_text, 0, p)
self.edit_widget.set_edit_pos(p)
elif cmd == urwid.CURSOR_RIGHT:
p = self.edit_widget.edit_pos
if p >= len(self.edit_widget.edit_text):
return key
p = urwid.move_next_char(self.edit_widget.edit_text, p, len(self.edit_widget.edit_text))
self.edit_widget.set_edit_pos(p)
elif key == "backspace":
self.edit_widget.pref_col_maxcol = None, None
if not self.edit_widget._delete_highlighted():
p = self.edit_widget.edit_pos
if p == 0:
return key
p = urwid.move_prev_char(self.edit_widget.edit_text,0,p)
self.edit_widget.set_edit_text(self.edit_widget.edit_text[:p] + self.edit_widget.edit_text[self.edit_widget.edit_pos:])
self.edit_widget.set_edit_pos(p)
elif key == "delete":
self.edit_widget.pref_col_maxcol = None, None
if not self.edit_widget._delete_highlighted():
p = self.edit_widget.edit_pos
if p >= len(self.edit_widget.edit_text):
return key
p = urwid.move_next_char(self.edit_widget.edit_text,p,len(self.edit_widget.edit_text))
self.edit_widget.set_edit_text(self.edit_widget.edit_text[:self.edit_widget.edit_pos] + self.edit_widget.edit_text[p:])
else:
return key
self.update_completions()
return key
def update_completions(self):
i = self.edit_widget.edit_pos
text = self.edit_widget.edit_text[:i]
prefix, completions = self.get_completions(text)
self.prefix = prefix
self.completions = completions
if not self.completions:
if self.is_open():
self.close_pop_up()
return
if not self.is_open():
self.open_pop_up()
self._pop_up_widget.update_completions(completions)
def insert_selected(self):
text = self._pop_up_widget.get_selected()
i = self.edit_widget.edit_pos - len(self.prefix)
assert i >= 0
text = text[i:]
self.edit_widget.insert_text(text)
self.close_pop_up()
# ------- internal ------
def is_open(self):
return self._pop_up_widget
# ------- implementation of abstract methods ------
def create_pop_up(self):
return PopUpList(self.forwarded_keypress)
def get_pop_up_parameters(self):
height = len(self.completions)
width = max(len(x) for x in self.completions)
return {'left':len(self.prefix), 'top':1, 'overlay_width':width, 'overlay_height':height}
class PopUpList(urwid.WidgetWrap):
ATTR = 'popup-button'
ATTR_FOCUS = 'popup-button-focus'
def __init__(self, keypress_callback):
self.body = urwid.SimpleListWalker([urwid.Text("")])
widget = urwid.ListBox(self.body)
widget = urwid.AttrMap(widget, self.ATTR)
self.__super.__init__(widget)
self.keypress_callback = keypress_callback
def update_completions(self, completions):
self.body.clear()
for x in completions:
widget = ListEntry(x)
widget = urwid.AttrMap(widget, self.ATTR, self.ATTR_FOCUS)
self.body.append(widget)
def get_selected(self):
focus_widget, focus_pos = self.body.get_focus()
return self.body[focus_pos].original_widget.text
def keypress(self, size, key):
key = self.keypress_callback(key)
if key:
return super().keypress(size, key)
class ListEntry(urwid.Text):
#https://stackoverflow.com/a/56759094
_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
if __name__ == '__main__':
palette = [
('popup-button', 'white', 'dark blue'),
('popup-button-focus', 'white,standout', 'dark blue'),
('error', 'dark red', 'default'),
]
completions = ["hello", "hi", "world", "earth", "universe"]
def get_completions(start):
i = start.rfind(" ")
if i == -1:
prefix = ""
else:
i += 1
prefix = start[:i]
start = start[i:]
return prefix, [word for word in completions if word.startswith(start)]
widget = AutoCompleteEdit(get_completions)
widget = urwid.Filler(widget)
#WARNING: note the pop_ups=True
urwid.MainLoop(widget, palette, pop_ups=True).run()