#!/usr/bin/env python # -* coding: utf-8 -*- """ curses_misc.py: Module for various widgets that are used throughout wicd-curses. """ # Copyright (C) 2008-2009 Andrew Psaltis # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # MA 02110-1301, USA. import urwid from wicd.translations import _ from functools import reduce # Uses code that is towards the bottom def error(ui, parent, message): """Shows an error dialog (or something that resembles one)""" # /\ # /!!\ # /____\ dialog = TextDialog(message, 6, 40, ('important', 'ERROR')) return dialog.run(ui, parent) class SelText(urwid.Text): """A selectable text widget. See urwid.Text.""" def selectable(self): """Make widget selectable.""" return True def keypress(self, size, key): """Don't handle any keys.""" return key class NSelListBox(urwid.ListBox): """ Non-selectable ListBox. """ def selectable(self): return False # This class is annoying. :/ class DynWrap(urwid.AttrWrap): """ Makes an object have mutable selectivity. Attributes will change like those in an AttrWrap w = widget to wrap sensitive = current selectable state attrs = tuple of (attr_sens,attr_not_sens) attrfoc = attributes when in focus, defaults to nothing """ # pylint: disable-msg=W0231 def __init__(self, w, sensitive=True, attrs=('editbx', 'editnfc'), focus_attr='editfc'): self._attrs = attrs self._sensitive = sensitive if sensitive: cur_attr = attrs[0] else: cur_attr = attrs[1] # pylint: disable-msg=E1101 self.__super.__init__(w, cur_attr, focus_attr) def get_sensitive(self): """ Getter for sensitive property. """ return self._sensitive def set_sensitive(self, state): """ Setter for sensitive property. """ if state: self.set_attr(self._attrs[0]) else: self.set_attr(self._attrs[1]) self._sensitive = state property(get_sensitive, set_sensitive) def get_attrs(self): """ Getter for attrs property. """ return self._attrs def set_attrs(self, attrs): """ Setter for attrs property. """ self._attrs = attrs property(get_attrs, set_attrs) def selectable(self): return self._sensitive class DynEdit(DynWrap): """ Edit DynWrap'ed to the most common specifications. """ # pylint: disable-msg=W0231 def __init__(self, caption='', edit_text='', sensitive=True, attrs=('editbx', 'editnfc'), focus_attr='editfc'): caption = ('editcp', caption + ': ') edit = urwid.Edit(caption, edit_text) # pylint: disable-msg=E1101 self.__super.__init__(edit, sensitive, attrs, focus_attr) class DynIntEdit(DynWrap): """ IntEdit DynWrap'ed to the most common specifications. """ # pylint: disable-msg=W0231 def __init__(self, caption='', edit_text='', sensitive=True, attrs=('editbx', 'editnfc'), focus_attr='editfc'): caption = ('editcp', caption + ':') edit = urwid.IntEdit(caption, edit_text) # pylint: disable-msg=E1101 self.__super.__init__(edit, sensitive, attrs, focus_attr) class DynRadioButton(DynWrap): """ RadioButton DynWrap'ed to the most common specifications. """ # pylint: disable-msg=W0231 def __init__(self, group, label, state='first True', on_state_change=None, user_data=None, sensitive=True, attrs=('body', 'editnfc'), focus_attr='body'): #caption = ('editcp', caption + ':') button = urwid.RadioButton(group, label, state, on_state_change, user_data) # pylint: disable-msg=E1101 self.__super.__init__(button, sensitive, attrs, focus_attr) class MaskingEditException(Exception): """ Custom exception. """ pass # Password-style edit class MaskingEdit(urwid.Edit): """ mask_mode = one of: "always" : everything is a '*' all of the time "no_focus" : everything is a '*' only when not in focus "off" : everything is always unmasked mask_char = the single character that masks all other characters in the field """ # pylint: disable-msg=W0231 def __init__(self, caption="", edit_text="", multiline=False, align='left', wrap='space', allow_tab=False, edit_pos=None, layout=None, mask_mode="always", mask_char='*'): self.mask_mode = mask_mode if len(mask_char) > 1: raise MaskingEditException('Masks of more than one character are' + ' not supported!') self.mask_char = mask_char # pylint: disable-msg=E1101 self.__super.__init__(caption, edit_text, multiline, align, wrap, allow_tab, edit_pos, layout) def get_caption(self): """ Return caption. """ return self.caption def get_mask_mode(self): """ Getter for mask_mode property. """ return self.mask_mode def set_mask_mode(self, mode): """ Setter for mask_mode property.""" self.mask_mode = mode def get_masked_text(self): """ Get masked out text. """ return self.mask_char * len(self.get_edit_text()) def render(self, xxx_todo_changeme, focus=False): """ Render edit widget and return canvas. Include cursor when in focus. """ (maxcol, ) = xxx_todo_changeme if self.mask_mode == "off" or (self.mask_mode == 'no_focus' and focus): # pylint: disable-msg=E1101 canv = self.__super.render((maxcol, ), focus) # The cache messes this thing up, because I am totally changing what # is displayed. self._invalidate() return canv # Else, we have a slight mess to deal with... self._shift_view_to_cursor = not not focus # force bool text, attr = self.get_text() text = text[:len(self.caption)] + self.get_masked_text() trans = self.get_line_translation(maxcol, (text, attr)) canv = urwid.canvas.apply_text_layout(text, attr, trans, maxcol) if focus: canv = urwid.CompositeCanvas(canv) canv.cursor = self.get_cursor_coords((maxcol, )) return canv class TabColumns(urwid.WidgetWrap): """ Tabbed interface, mostly for use in the Preferences Dialog titles_dict = dictionary of tab_contents (a SelText) : tab_widget (box) attr = normal attributes attrsel = attribute when active """ # FIXME Make the bottom_part optional # pylint: disable-msg=W0231 def __init__(self, tab_str, tab_wid, title, bottom_part=None, attr=('body', 'focus'), attrsel='tab active', attrtitle='header'): #self.bottom_part = bottom_part #title_wid = urwid.Text((attrtitle, title), align='right') column_list = [] for w in tab_str: text, trash = w.get_text() column_list.append(('fixed', len(text), w)) column_list.append(urwid.Text((attrtitle, title), align='right')) self.tab_map = dict(list(zip(tab_str, tab_wid))) self.active_tab = tab_str[0] self.columns = urwid.Columns(column_list, dividechars=1) #walker = urwid.SimpleListWalker([self.columns, tab_wid[0]]) #self.listbox = urwid.ListBox(walker) self.gen_pile(tab_wid[0], True) self.frame = urwid.Frame(self.pile) # pylint: disable-msg=E1101 self.__super.__init__(self.frame) def gen_pile(self, lbox, firstrun=False): """ Make the pile in the middle. """ self.pile = urwid.Pile([ ('fixed', 1, urwid.Filler(self.columns, 'top')), urwid.Filler(lbox, 'top', height=('relative', 99)), #('fixed', 1, urwid.Filler(self.bottom_part, 'bottom')) ]) if not firstrun: self.frame.set_body(self.pile) self._w = self.frame self._invalidate() def selectable(self): """ Return whether the widget is selectable. """ return True def keypress(self, size, key): """ Handle keypresses. """ # If the key is page up or page down, move focus to the tabs and call # left or right on the tabs. if key == "page up" or key == "page down": self._w.get_body().set_focus(0) if key == "page up": newK = 'left' else: newK = 'right' self.keypress(size, newK) self._w.get_body().set_focus(1) else: key = self._w.keypress(size, key) wid = self.pile.get_focus().get_body() if wid == self.columns: self.active_tab.set_attr('body') self.columns.get_focus().set_attr('tab active') self.active_tab = self.columns.get_focus() self.gen_pile(self.tab_map[self.active_tab]) return key def mouse_event(self, size, event, button, x, y, focus): """ Handle mouse events. """ wid = self.pile.get_focus().get_body() if wid == self.columns: self.active_tab.set_attr('body') self._w.mouse_event(size, event, button, x, y, focus) if wid == self.columns: self.active_tab.set_attr('body') self.columns.get_focus().set_attr('tab active') self.active_tab = self.columns.get_focus() self.gen_pile(self.tab_map[self.active_tab]) class ComboBoxException(Exception): """ Custom exception. """ pass # A "combo box" of SelTexts # I based this off of the code found here: # http://excess.org/urwid/browser/contrib/trunk/rbreu_menus.py # This is a hack/kludge. It isn't without quirks, but it more or less works. # We need to wait for changes in urwid's Canvas API before we can actually # make a real ComboBox. class ComboBox(urwid.WidgetWrap): """A ComboBox of text objects""" class ComboSpace(urwid.WidgetWrap): """The actual menu-like space that comes down from the ComboBox""" # pylint: disable-msg=W0231 def __init__(self, l, body, ui, show_first, pos=(0, 0), attr=('body', 'focus')): """ body : parent widget l : stuff to include in the combobox ui : the screen show_first: index of the element in the list to pick first pos : a tuple of (row,col) where to put the list attr : a tuple of (attr_no_focus,attr_focus) """ #Calculate width and height of the menu widget: height = len(l) width = 0 for entry in l: if len(entry) > width: width = len(entry) content = [urwid.AttrWrap(SelText(w), attr[0], attr[1]) for w in l] self._listbox = urwid.ListBox(content) self._listbox.set_focus(show_first) overlay = urwid.Overlay(self._listbox, body, ('fixed left', pos[0]), width + 2, ('fixed top', pos[1]), height) # pylint: disable-msg=E1101 self.__super.__init__(overlay) def show(self, ui, display): """ Show widget. """ dim = ui.get_cols_rows() keys = True #Event loop: while True: if keys: ui.draw_screen(dim, self.render(dim, True)) keys = ui.get_input() if "window resize" in keys: dim = ui.get_cols_rows() if "esc" in keys: return None if "enter" in keys: (wid, pos) = self._listbox.get_focus() (text, attr) = wid.get_text() return text for k in keys: #Send key to underlying widget: self._w.keypress(dim, k) #def get_size(self): # pylint: disable-msg=W0231 def __init__(self, label='', l=None, attrs=('body', 'editnfc'), focus_attr='focus', use_enter=True, focus=0, callback=None, user_args=None): """ label : bit of text that preceeds the combobox. If it is "", then ignore it l : stuff to include in the combobox body : parent widget ui : the screen row : where this object is to be found onscreen focus : index of the element in the list to pick first callback : function that takes (combobox,sel_index,user_args=None) user_args : user_args in the callback """ self.DOWN_ARROW = ' vvv' self.label = urwid.Text(label) self.attrs = attrs self.focus_attr = focus_attr if l is None: l = [] self.list = l s, trash = self.label.get_text() self.overlay = None self.cbox = DynWrap(SelText(self.DOWN_ARROW), attrs=attrs, focus_attr=focus_attr) # Unicode will kill me sooner or later. if label != '': w = urwid.Columns( [('fixed', len(s), self.label), self.cbox], dividechars=1 ) else: w = urwid.Columns([self.cbox]) # pylint: disable-msg=E1101 self.__super.__init__(w) # We need this to pick our keypresses self.use_enter = use_enter if urwid.VERSION < (1, 1, 0): self.focus = focus else: self._w.focus_position = focus self.callback = callback self.user_args = user_args # Widget references to simplify some things self.parent = None self.ui = None self.row = None def set_list(self, l): """ Populate widget list. """ self.list = l def set_focus(self, index): """ Set widget focus. """ if urwid.VERSION < (1, 1, 0): self.focus = index else: try: self._w.focus_position = index except IndexError: pass # API changed between urwid 0.9.8.4 and 0.9.9 try: self.cbox.set_w(SelText(self.list[index] + self.DOWN_ARROW)) except AttributeError: self.cbox._w = SelText(self.list[index] + self.DOWN_ARROW) if self.overlay: self.overlay._listbox.set_focus(index) def rebuild_combobox(self): """ Rebuild combobox. """ self.build_combobox(self.parent, self.ui, self.row) def build_combobox(self, parent, ui, row): """ Build combobox. """ s, trash = self.label.get_text() if urwid.VERSION < (1, 1, 0): index = self.focus else: index = self._w.focus_position # pylint: disable-msg=E1103 self.cbox = DynWrap(SelText([self.list[index] + self.DOWN_ARROW]), attrs=self.attrs, focus_attr=self.focus_attr) if str != '': w = urwid.Columns([('fixed', len(s), self.label), self.cbox], dividechars=1) self.overlay = self.ComboSpace(self.list, parent, ui, index, pos=(len(s) + 1, row)) else: w = urwid.Columns([self.cbox]) self.overlay = self.ComboSpace(self.list, parent, ui, index, pos=(0, row)) self._w = w self._invalidate() self.parent = parent self.ui = ui self.row = row # If we press space or enter, be a combo box! def keypress(self, size, key): """ Handle keypresses. """ activate = key == ' ' if self.use_enter: activate = activate or key == 'enter' if activate: # Die if the user didn't prepare the combobox overlay if self.overlay is None: raise ComboBoxException('ComboBox must be built before use!') retval = self.overlay.show(self.ui, self.parent) if retval is not None: self.set_focus(self.list.index(retval)) #self.cbox.set_w(SelText(retval+' vvv')) if self.callback is not None: self.callback(self, self.overlay._listbox.get_focus()[1], self.user_args) return self._w.keypress(size, key) def selectable(self): """ Return whether the widget is selectable. """ return self.cbox.selectable() def get_focus(self): """ Return widget focus. """ if self.overlay: return self.overlay._listbox.get_focus() else: if urwid.VERSION < (1, 1, 0): return None, self.focus else: return None, self._w.focus_position # pylint: disable-msg=E1103 def get_sensitive(self): """ Return widget sensitivity. """ return self.cbox.get_sensitive() def set_sensitive(self, state): """ Set widget sensitivity. """ self.cbox.set_sensitive(state) # This is a h4x3d copy of some of the code in Ian Ward's dialog.py example. class DialogExit(Exception): """ Custom exception. """ pass class Dialog2(urwid.WidgetWrap): """ Base class for other dialogs. """ def __init__(self, text, height, width, body=None): self.buttons = None self.width = int(width) if width <= 0: self.width = ('relative', 80) self.height = int(height) if height <= 0: self.height = ('relative', 80) self.body = body if body is None: # fill space with nothing body = urwid.Filler(urwid.Divider(), 'top') self.frame = urwid.Frame(body, focus_part='footer') if text is not None: self.frame.header = urwid.Pile([ urwid.Text(text, align='right'), urwid.Divider() ]) w = self.frame self.view = w # buttons: tuple of name,exitcode def add_buttons(self, buttons): """ Add buttons. """ l = [] maxlen = 0 for name, exitcode in buttons: b = urwid.Button(name, self.button_press) b.exitcode = exitcode b = urwid.AttrWrap(b, 'body', 'focus') l.append(b) maxlen = max(len(name), maxlen) maxlen += 4 # because of '< ... >' self.buttons = urwid.GridFlow(l, maxlen, 3, 1, 'center') self.frame.footer = urwid.Pile([ urwid.Divider(), self.buttons ], focus_item=1) def button_press(self, button): """ Handle button press. """ raise DialogExit(button.exitcode) def run(self, ui, parent): """ Run the UI. """ ui.set_mouse_tracking() size = ui.get_cols_rows() overlay = urwid.Overlay( urwid.LineBox(self.view), parent, 'center', self.width, 'middle', self.height ) try: while True: canvas = overlay.render(size, focus=True) ui.draw_screen(size, canvas) keys = None while not keys: keys = ui.get_input() for k in keys: if urwid.VERSION < (1, 0, 0): check_mouse_event = urwid.is_mouse_event else: check_mouse_event = urwid.util.is_mouse_event if check_mouse_event(k): event, button, col, row = k overlay.mouse_event(size, event, button, col, row, focus=True) else: if k == 'window resize': size = ui.get_cols_rows() k = self.view.keypress(size, k) if k == 'esc': raise DialogExit(-1) if k: self.unhandled_key(size, k) except DialogExit as e: return self.on_exit(e.args[0]) def on_exit(self, exitcode): """ Handle dialog exit. """ return exitcode, "" def unhandled_key(self, size, key): """ Handle keypresses. """ pass class TextDialog(Dialog2): """ Simple dialog with text and "OK" button. """ def __init__(self, text, height, width, header=None, align='left', buttons=(_('OK'), 1)): l = [urwid.Text(text)] body = urwid.ListBox(l) body = urwid.AttrWrap(body, 'body') Dialog2.__init__(self, header, height + 2, width + 2, body) if type(buttons) == list: self.add_buttons(buttons) else: self.add_buttons([buttons]) def unhandled_key(self, size, k): """ Handle keys. """ if k in ('up', 'page up', 'down', 'page down'): self.frame.set_focus('body') self.view.keypress(size, k) self.frame.set_focus('footer') class InputDialog(Dialog2): """ Simple dialog with text and entry. """ def __init__(self, text, height, width, ok_name=_('OK'), edit_text=''): self.edit = urwid.Edit(wrap='clip', edit_text=edit_text) body = urwid.ListBox([self.edit]) body = urwid.AttrWrap(body, 'editbx', 'editfc') Dialog2.__init__(self, text, height, width, body) self.frame.set_focus('body') self.add_buttons([(ok_name, 0), (_('Cancel'), -1)]) def unhandled_key(self, size, k): """ Handle keys. """ if k in ('up', 'page up'): self.frame.set_focus('body') if k in ('down', 'page down'): self.frame.set_focus('footer') if k == 'enter': # pass enter to the "ok" button self.frame.set_focus('footer') self.view.keypress(size, k) def on_exit(self, exitcode): """ Handle dialog exit. """ return exitcode, self.edit.get_edit_text() class ClickCols(urwid.WidgetWrap): """ Clickable menubar. """ # pylint: disable-msg=W0231 def __init__(self, items, callback=None, args=None): cols = urwid.Columns(items) # pylint: disable-msg=E1101 self.__super.__init__(cols) self.callback = callback self.args = args def mouse_event(self, size, event, button, x, y, focus): """ Handle mouse events. """ if event == "mouse press": # The keypress dealie in wicd-curses.py expects a list of keystrokes self.callback([self.args]) class OptCols(urwid.WidgetWrap): """ Htop-style menubar on the bottom of the screen. """ # tuples = [(key,desc)], on_event gets passed a key # attrs = (attr_key,attr_desc) # handler = function passed the key of the "button" pressed # mentions of 'left' and right will be converted to <- and -> respectively # pylint: disable-msg=W0231 def __init__(self, tuples, handler, attrs=('body', 'infobar'), debug=False): # Find the longest string. Keys for this bar should be no greater than # 2 characters long (e.g., -> for left) #maxlen = 6 #for i in tuples: # newmax = len(i[0])+len(i[1]) # if newmax > maxlen: # maxlen = newmax # Construct the texts textList = [] i = 0 # callbacks map the text contents to its assigned callback. self.callbacks = [] for cmd in tuples: key = reduce(lambda s, (f, t): s.replace(f, t), [ ('ctrl ', 'Ctrl+'), ('meta ', 'Alt+'), ('left', '<-'), ('right', '->'), ('page up', 'Page Up'), ('page down', 'Page Down'), ('esc', 'ESC'), ('enter', 'Enter'), ('s', 'S')], cmd[0]) if debug: callback = self.debugClick args = cmd[1] else: callback = handler args = cmd[0] #self.callbacks.append(cmd[2]) col = ClickCols([ ('fixed', len(key) + 1, urwid.Text((attrs[0], key + ':'))), urwid.AttrWrap(urwid.Text(cmd[1]), attrs[1])], callback, args) textList.append(col) i += 1 if debug: self.debug = urwid.Text("DEBUG_MODE") textList.append(('fixed', 10, self.debug)) cols = urwid.Columns(textList) # pylint: disable-msg=E1101 self.__super.__init__(cols) def debugClick(self, args): """ Debug clicks. """ self.debug.set_text(args) def mouse_event(self, size, event, button, x, y, focus): """ Handle mouse events. """ # Widgets are evenly long (as of current), so... return self._w.mouse_event(size, event, button, x, y, focus)