#!/usr/bin/python # # Urwid Window-Icon-Menu-Pointer-style widget classes # Copyright (C) 2004-2011 Ian Ward # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library 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 # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Urwid web site: http://excess.org/urwid/ from __future__ import division, print_function from urwid.widget import (Text, WidgetWrap, delegate_to_widget_mixin, BOX, FLOW) from urwid.canvas import CompositeCanvas from urwid.signals import connect_signal from urwid.container import Columns, Overlay from urwid.util import is_mouse_press from urwid.text_layout import calc_coords from urwid.signals import disconnect_signal # doctests from urwid.split_repr import python3_repr from urwid.decoration import WidgetDecoration from urwid.command_map import ACTIVATE class SelectableIcon(Text): _selectable = True def __init__(self, text, cursor_position=1): """ :param text: markup for this widget; see :class:`Text` for description of text markup :param cursor_position: position the cursor will appear in the text when this widget is in focus This is a text widget that is selectable. A cursor displayed at a fixed location in the text when in focus. This widget has no special handling of keyboard or mouse input. """ self.__super.__init__(text) self._cursor_position = cursor_position def render(self, size, focus=False): """ Render the text content of this widget with a cursor when in focus. >>> si = SelectableIcon(u"[!]") >>> si >>> si.render((4,), focus=True).cursor (1, 0) >>> si = SelectableIcon("((*))", 2) >>> si.render((8,), focus=True).cursor (2, 0) >>> si.render((2,), focus=True).cursor (0, 1) """ c = self.__super.render(size, focus) if focus: # create a new canvas so we can add a cursor c = CompositeCanvas(c) c.cursor = self.get_cursor_coords(size) return c def get_cursor_coords(self, size): """ Return the position of the cursor if visible. This method is required for widgets that display a cursor. """ if self._cursor_position > len(self.text): return None # find out where the cursor will be displayed based on # the text layout (maxcol,) = size trans = self.get_line_translation(maxcol) x, y = calc_coords(self.text, trans, self._cursor_position) if maxcol <= x: return None return x, y def keypress(self, size, key): """ No keys are handled by this widget. This method is required for selectable widgets. """ return key class CheckBoxError(Exception): pass class CheckBox(WidgetWrap): def sizing(self): return frozenset([FLOW]) states = { True: SelectableIcon("[X]"), False: SelectableIcon("[ ]"), 'mixed': SelectableIcon("[#]") } reserve_columns = 4 # allow users of this class to listen for change events # sent when the state of this widget is modified # (this variable is picked up by the MetaSignals metaclass) signals = ["change", 'postchange'] def __init__(self, label, state=False, has_mixed=False, on_state_change=None, user_data=None): """ :param label: markup for check box label :param state: False, True or "mixed" :param has_mixed: True if "mixed" is a state to cycle through :param on_state_change: shorthand for connect_signal() function call for a single callback :param user_data: user_data for on_state_change Signals supported: ``'change'``, ``"postchange"`` Register signal handler with:: urwid.connect_signal(check_box, 'change', callback, user_data) where callback is callback(check_box, new_state [,user_data]) Unregister signal handlers with:: urwid.disconnect_signal(check_box, 'change', callback, user_data) >>> CheckBox(u"Confirm") >>> CheckBox(u"Yogourt", "mixed", True) >>> cb = CheckBox(u"Extra onions", True) >>> cb >>> cb.render((20,), focus=True).text # ... = b in Python 3 [...'[X] Extra onions '] """ self.__super.__init__(None) # self.w set by set_state below self._label = Text("") self.has_mixed = has_mixed self._state = None # The old way of listening for a change was to pass the callback # in to the constructor. Just convert it to the new way: if on_state_change: connect_signal(self, 'change', on_state_change, user_data) self.set_label(label) self.set_state(state) def _repr_words(self): return self.__super._repr_words() + [ python3_repr(self.label)] def _repr_attrs(self): return dict(self.__super._repr_attrs(), state=self.state) def set_label(self, label): """ Change the check box label. label -- markup for label. See Text widget for description of text markup. >>> cb = CheckBox(u"foo") >>> cb >>> cb.set_label(('bright_attr', u"bar")) >>> cb """ self._label.set_text(label) # no need to call self._invalidate(). WidgetWrap takes care of # that when self.w changes def get_label(self): """ Return label text. >>> cb = CheckBox(u"Seriously") >>> print(cb.get_label()) Seriously >>> print(cb.label) Seriously >>> cb.set_label([('bright_attr', u"flashy"), u" normal"]) >>> print(cb.label) # only text is returned flashy normal """ return self._label.text label = property(get_label) def set_state(self, state, do_callback=True): """ Set the CheckBox state. state -- True, False or "mixed" do_callback -- False to suppress signal from this change >>> changes = [] >>> def callback_a(cb, state, user_data): ... changes.append("A %r %r" % (state, user_data)) >>> def callback_b(cb, state): ... changes.append("B %r" % state) >>> cb = CheckBox('test', False, False) >>> key1 = connect_signal(cb, 'change', callback_a, "user_a") >>> key2 = connect_signal(cb, 'change', callback_b) >>> cb.set_state(True) # both callbacks will be triggered >>> cb.state True >>> disconnect_signal(cb, 'change', callback_a, "user_a") >>> cb.state = False >>> cb.state False >>> cb.set_state(True) >>> cb.state True >>> cb.set_state(False, False) # don't send signal >>> changes ["A True 'user_a'", 'B True', 'B False', 'B True'] """ if self._state == state: return if state not in self.states: raise CheckBoxError("%s Invalid state: %s" % ( repr(self), repr(state))) # self._state is None is a special case when the CheckBox # has just been created old_state = self._state if do_callback and old_state is not None: self._emit('change', state) self._state = state # rebuild the display widget with the new state self._w = Columns( [ ('fixed', self.reserve_columns, self.states[state] ), self._label ] ) self._w.focus_col = 0 if do_callback and old_state is not None: self._emit('postchange', old_state) def get_state(self): """Return the state of the checkbox.""" return self._state state = property(get_state, set_state) def keypress(self, size, key): """ Toggle state on 'activate' command. >>> assert CheckBox._command_map[' '] == 'activate' >>> assert CheckBox._command_map['enter'] == 'activate' >>> size = (10,) >>> cb = CheckBox('press me') >>> cb.state False >>> cb.keypress(size, ' ') >>> cb.state True >>> cb.keypress(size, ' ') >>> cb.state False """ if self._command_map[key] != ACTIVATE: return key self.toggle_state() def toggle_state(self): """ Cycle to the next valid state. >>> cb = CheckBox("3-state", has_mixed=True) >>> cb.state False >>> cb.toggle_state() >>> cb.state True >>> cb.toggle_state() >>> cb.state 'mixed' >>> cb.toggle_state() >>> cb.state False """ if self.state == False: self.set_state(True) elif self.state == True: if self.has_mixed: self.set_state('mixed') else: self.set_state(False) elif self.state == 'mixed': self.set_state(False) def mouse_event(self, size, event, button, x, y, focus): """ Toggle state on button 1 press. >>> size = (20,) >>> cb = CheckBox("clickme") >>> cb.state False >>> cb.mouse_event(size, 'mouse press', 1, 2, 0, True) True >>> cb.state True """ if button != 1 or not is_mouse_press(event): return False self.toggle_state() return True class RadioButton(CheckBox): states = { True: SelectableIcon("(X)"), False: SelectableIcon("( )"), 'mixed': SelectableIcon("(#)") } reserve_columns = 4 def __init__(self, group, label, state="first True", on_state_change=None, user_data=None): """ :param group: list for radio buttons in same group :param label: markup for radio button label :param state: False, True, "mixed" or "first True" :param on_state_change: shorthand for connect_signal() function call for a single 'change' callback :param user_data: user_data for on_state_change This function will append the new radio button to group. "first True" will set to True if group is empty. Signals supported: ``'change'``, ``"postchange"`` Register signal handler with:: urwid.connect_signal(radio_button, 'change', callback, user_data) where callback is callback(radio_button, new_state [,user_data]) Unregister signal handlers with:: urwid.disconnect_signal(radio_button, 'change', callback, user_data) >>> bgroup = [] # button group >>> b1 = RadioButton(bgroup, u"Agree") >>> b2 = RadioButton(bgroup, u"Disagree") >>> len(bgroup) 2 >>> b1 >>> b2 >>> b2.render((15,), focus=True).text # ... = b in Python 3 [...'( ) Disagree '] """ if state=="first True": state = not group self.group = group self.__super.__init__(label, state, False, on_state_change, user_data) group.append(self) def set_state(self, state, do_callback=True): """ Set the RadioButton state. state -- True, False or "mixed" do_callback -- False to suppress signal from this change If state is True all other radio buttons in the same button group will be set to False. >>> bgroup = [] # button group >>> b1 = RadioButton(bgroup, u"Agree") >>> b2 = RadioButton(bgroup, u"Disagree") >>> b3 = RadioButton(bgroup, u"Unsure") >>> b1.state, b2.state, b3.state (True, False, False) >>> b2.set_state(True) >>> b1.state, b2.state, b3.state (False, True, False) >>> def relabel_button(radio_button, new_state): ... radio_button.set_label(u"Think Harder!") >>> key = connect_signal(b3, 'change', relabel_button) >>> b3 >>> b3.set_state(True) # this will trigger the callback >>> b3 """ if self._state == state: return self.__super.set_state(state, do_callback) # if we're clearing the state we don't have to worry about # other buttons in the button group if state is not True: return # clear the state of each other radio button for cb in self.group: if cb is self: continue if cb._state: cb.set_state(False) def toggle_state(self): """ Set state to True. >>> bgroup = [] # button group >>> b1 = RadioButton(bgroup, "Agree") >>> b2 = RadioButton(bgroup, "Disagree") >>> b1.state, b2.state (True, False) >>> b2.toggle_state() >>> b1.state, b2.state (False, True) >>> b2.toggle_state() >>> b1.state, b2.state (False, True) """ self.set_state(True) class Button(WidgetWrap): def sizing(self): return frozenset([FLOW]) button_left = Text("<") button_right = Text(">") signals = ["click"] def __init__(self, label, on_press=None, user_data=None): """ :param label: markup for button label :param on_press: shorthand for connect_signal() function call for a single callback :param user_data: user_data for on_press Signals supported: ``'click'`` Register signal handler with:: urwid.connect_signal(button, 'click', callback, user_data) where callback is callback(button [,user_data]) Unregister signal handlers with:: urwid.disconnect_signal(button, 'click', callback, user_data) >>> Button(u"Ok")