urwid-2.0.1/0000755000175000017500000000000013231170705014307 5ustar andersonanderson00000000000000urwid-2.0.1/setup.py0000644000175000017500000000651113231170672016027 0ustar andersonanderson00000000000000#!/usr/bin/env python # # Urwid setup.py exports the useful bits # Copyright (C) 2004-2014 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/ try: PYTHON3 = not str is bytes except NameError: PYTHON3 = False try: from setuptools import setup, Extension # distribute required for Python 3 have_setuptools = True except ImportError: if PYTHON3: raise from distutils.core import setup, Extension have_setuptools = False import os exec(open(os.path.join("urwid","version.py")).read()) release = __version__ setup_d = { 'name':"urwid", 'version':release, 'author':"Ian Ward", 'author_email':"ian@excess.org", 'ext_modules':[Extension('urwid.str_util', sources=['source/str_util.c'])], 'packages':['urwid', 'urwid.tests'], 'url':"http://urwid.org/", 'license':"LGPL", 'keywords':"curses ui widget scroll listbox user interface text layout console ncurses", 'platforms':"unix-like", 'description': "A full-featured console (xterm et al.) user interface library", 'long_description':open("README.rst").read().split('.. content-start\n',1)[1], 'classifiers':[ "Development Status :: 5 - Production/Stable", "Environment :: Console", "Environment :: Console :: Curses", "Intended Audience :: Developers", "License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)", "Operating System :: POSIX", "Operating System :: Unix", "Operating System :: MacOS :: MacOS X", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Software Development :: Widget Sets", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.2", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: Implementation :: PyPy", ], } if have_setuptools: setup_d['zip_safe'] = False setup_d['test_suite'] = 'urwid.tests' if __name__ == "__main__": try: setup(**setup_d) except (IOError, SystemExit) as e: import sys if "test" in sys.argv: raise import traceback traceback.print_exc() print("Couldn't build the extension module, trying without it...") del setup_d["ext_modules"] setup(**setup_d) urwid-2.0.1/urwid.egg-info/0000755000175000017500000000000013231170705017133 5ustar andersonanderson00000000000000urwid-2.0.1/urwid.egg-info/top_level.txt0000644000175000017500000000000613231170704021660 0ustar andersonanderson00000000000000urwid urwid-2.0.1/urwid.egg-info/SOURCES.txt0000644000175000017500000001300113231170704021011 0ustar andersonanderson00000000000000COPYING MANIFEST.in README.rst setup.py docs/Makefile docs/changelog.rst docs/conf.py docs/index.rst docs/urwid-logo.png docs/examples/bigtext.py docs/examples/bigtext.py.xdotool docs/examples/bigtext1.png docs/examples/bigtext2.png docs/examples/bigtext3.png docs/examples/browse.py docs/examples/browse.py.xdotool docs/examples/browse1.png docs/examples/browse2.png docs/examples/edit.py docs/examples/edit.py.xdotool docs/examples/edit1.png docs/examples/edit2.png docs/examples/graph.py docs/examples/graph.py.xdotool docs/examples/graph1.png docs/examples/graph2.png docs/examples/index.rst docs/examples/palette_test.py docs/examples/palette_test.py.xdotool docs/examples/palette_test1.png docs/examples/palette_test2.png docs/examples/pop_up.py docs/examples/pop_up.py.xdotool docs/examples/pop_up1.png docs/examples/pop_up2.png docs/examples/real_browse.py docs/examples/real_edit.py docs/examples/subproc.py docs/examples/subproc.py.xdotool docs/examples/subproc1.png docs/examples/subproc2.png docs/examples/subproc2.py docs/examples/tour.py docs/examples/tour.py.xdotool docs/examples/tour1.png docs/examples/tour2.png docs/manual/bright_combinations.py docs/manual/bright_combinations.py.xdotool docs/manual/bright_combinations1.png docs/manual/canvascache.rst docs/manual/displayattributes.rst docs/manual/displaymodules.rst docs/manual/encodings.rst docs/manual/index.rst docs/manual/mainloop.rst docs/manual/overview.rst docs/manual/safe_combinations.py docs/manual/safe_combinations.py.xdotool docs/manual/safe_combinations1.png docs/manual/textlayout.rst docs/manual/userinput.rst docs/manual/wanat.py docs/manual/wanat_multi.py docs/manual/wcur1.py docs/manual/wcur2.py docs/manual/widgets.rst docs/manual/wmod.py docs/manual/wsel.py docs/manual/images/display_modules.png docs/manual/images/introduction.png docs/manual/images/urwid_widgets_1.png docs/manual/images/urwid_widgets_2.png docs/manual/images/widget_layout.png docs/reference/attrspec.rst docs/reference/canvas.rst docs/reference/command_map.rst docs/reference/constants.rst docs/reference/deprecated.rst docs/reference/display_modules.rst docs/reference/exceptions.rst docs/reference/global_settings.rst docs/reference/index.rst docs/reference/list_walkers.rst docs/reference/main_loop.rst docs/reference/meta.rst docs/reference/signals.rst docs/reference/text_layout.rst docs/reference/widget.rst docs/tools/compile_pngs.sh docs/tools/screenshots.sh docs/tools/templates/indexcontent.html docs/tools/templates/indexsidebar.html docs/tools/templates/localtoc.html docs/tutorial/adventure.py docs/tutorial/adventure.py.xdotool docs/tutorial/adventure1.png docs/tutorial/adventure2.png docs/tutorial/adventure3.png docs/tutorial/adventure4.png docs/tutorial/attr.py docs/tutorial/attr.py.xdotool docs/tutorial/attr1.png docs/tutorial/attr2.png docs/tutorial/attr3.png docs/tutorial/attr4.png docs/tutorial/cmenu.py docs/tutorial/cmenu.py.xdotool docs/tutorial/cmenu1.png docs/tutorial/cmenu2.png docs/tutorial/cmenu3.png docs/tutorial/cmenu4.png docs/tutorial/highcolors.py docs/tutorial/highcolors.py.xdotool docs/tutorial/highcolors1.png docs/tutorial/hmenu.py docs/tutorial/hmenu.py.xdotool docs/tutorial/hmenu1.png docs/tutorial/hmenu2.png docs/tutorial/hmenu3.png docs/tutorial/hmenu4.png docs/tutorial/index.rst docs/tutorial/input.py docs/tutorial/input.py.xdotool docs/tutorial/input1.png docs/tutorial/input2.png docs/tutorial/input3.png docs/tutorial/input4.png docs/tutorial/input5.png docs/tutorial/menu25.png docs/tutorial/minimal.py docs/tutorial/minimal.py.xdotool docs/tutorial/minimal1.png docs/tutorial/multiple.py docs/tutorial/multiple.py.xdotool docs/tutorial/multiple1.png docs/tutorial/multiple2.png docs/tutorial/multiple3.png docs/tutorial/multiple4.png docs/tutorial/qa.py docs/tutorial/qa.py.xdotool docs/tutorial/qa1.png docs/tutorial/qa2.png docs/tutorial/qa3.png docs/tutorial/sig.py docs/tutorial/sig.py.xdotool docs/tutorial/sig1.png docs/tutorial/sig2.png docs/tutorial/sig3.png docs/tutorial/sig4.png docs/tutorial/smenu.py docs/tutorial/smenu.py.xdotool docs/tutorial/smenu1.png docs/tutorial/smenu2.png docs/tutorial/smenu3.png examples/asyncio_socket_server.py examples/bigtext.py examples/browse.py examples/calc.py examples/dialog.py examples/edit.py examples/fib.py examples/graph.py examples/input_test.py examples/lcd_cf635.py examples/palette_test.py examples/pop_up.py examples/subproc.py examples/subproc2.py examples/terminal.py examples/tour.py examples/treesample.py examples/twisted_serve_ssh.py examples/twisted_serve_ssh.tac source/str_util.c urwid/__init__.py urwid/canvas.py urwid/command_map.py urwid/compat.py urwid/container.py urwid/curses_display.py urwid/decoration.py urwid/display_common.py urwid/escape.py urwid/font.py urwid/graphics.py urwid/html_fragment.py urwid/lcd_display.py urwid/listbox.py urwid/main_loop.py urwid/monitored_list.py urwid/old_str_util.py urwid/raw_display.py urwid/signals.py urwid/split_repr.py urwid/text_layout.py urwid/treetools.py urwid/util.py urwid/version.py urwid/vterm.py urwid/web_display.py urwid/widget.py urwid/wimp.py urwid.egg-info/PKG-INFO urwid.egg-info/SOURCES.txt urwid.egg-info/dependency_links.txt urwid.egg-info/not-zip-safe urwid.egg-info/top_level.txt urwid/tests/__init__.py urwid/tests/test_canvas.py urwid/tests/test_container.py urwid/tests/test_decoration.py urwid/tests/test_doctests.py urwid/tests/test_event_loops.py urwid/tests/test_graphics.py urwid/tests/test_listbox.py urwid/tests/test_str_util.py urwid/tests/test_text_layout.py urwid/tests/test_util.py urwid/tests/test_vterm.py urwid/tests/test_widget.py urwid/tests/util.pyurwid-2.0.1/urwid.egg-info/PKG-INFO0000644000175000017500000001016213231170704020227 0ustar andersonanderson00000000000000Metadata-Version: 1.1 Name: urwid Version: 2.0.1 Summary: A full-featured console (xterm et al.) user interface library Home-page: http://urwid.org/ Author: Ian Ward Author-email: ian@excess.org License: LGPL Description-Content-Type: UNKNOWN Description: About ===== Urwid is a console user interface library for Python. It includes many features useful for text console application developers including: - Applications resize quickly and smoothly - Automatic, programmable text alignment and wrapping - Simple markup for setting text attributes within blocks of text - Powerful list box with programmable content for scrolling all widget types - Your choice of event loops: Twisted, Glib, Tornado or select-based loop - Pre-built widgets include edit boxes, buttons, check boxes and radio buttons - Display modules include raw, curses, and experimental LCD and web displays - Support for UTF-8, simple 8-bit and CJK encodings - 256 and 88 color mode support - Compatible with Python 2.6, 2.7, 3.2+ and PyPy Home Page: http://urwid.org/ Testing ======= To run tests locally, install & run `tox`. You must have appropriate Python versions installed to run `tox` for each of them. To test code in all Python versions: .. code:: bash tox # Test all versions specified in tox.ini: tox -e py36 # Test Python 3.6 only tox -e py27,py36,pypy # Test Python 2.7, Python 3.6 & pypy Contributors ============ - `wardi `_ - `aszlig `_ - `mgiusti `_ - `and3rson `_ - `pazz `_ - `wackywendell `_ - `eevee `_ - `marienz `_ - `rndusr `_ - `matthijskooijman `_ - `Julian `_ - `techtonik `_ - `garrison `_ - `ivanov `_ - `abadger `_ - `aglyzov `_ - `ismail-s `_ - `horazont `_ - `robla `_ - `usrlocalben `_ - `geier `_ - `federicotdn `_ - `jwilk `_ - `rr- `_ - `tonycpsu `_ - `westurner `_ - `grugq `_ - `inducer `_ - `winbornejw `_ Keywords: curses ui widget scroll listbox user interface text layout console ncurses Platform: unix-like Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: Console Classifier: Environment :: Console :: Curses Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL) Classifier: Operating System :: POSIX Classifier: Operating System :: Unix Classifier: Operating System :: MacOS :: MacOS X Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Topic :: Software Development :: Widget Sets Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.2 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: Implementation :: PyPy urwid-2.0.1/urwid.egg-info/dependency_links.txt0000644000175000017500000000000113231170704023200 0ustar andersonanderson00000000000000 urwid-2.0.1/urwid.egg-info/not-zip-safe0000644000175000017500000000000113222654615021370 0ustar andersonanderson00000000000000 urwid-2.0.1/urwid/0000755000175000017500000000000013231170705015441 5ustar andersonanderson00000000000000urwid-2.0.1/urwid/treetools.py0000644000175000017500000003637213231170672020051 0ustar andersonanderson00000000000000#!/usr/bin/python # # Generic TreeWidget/TreeWalker class # Copyright (c) 2010 Rob Lanphier # Copyright (C) 2004-2010 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 """ Urwid tree view Features: - custom selectable widgets for trees - custom list walker for displaying widgets in a tree fashion """ import urwid from urwid.wimp import SelectableIcon class TreeWidgetError(RuntimeError): pass class TreeWidget(urwid.WidgetWrap): """A widget representing something in a nested tree display.""" indent_cols = 3 unexpanded_icon = SelectableIcon('+', 0) expanded_icon = SelectableIcon('-', 0) def __init__(self, node): self._node = node self._innerwidget = None self.is_leaf = not hasattr(node, 'get_first_child') self.expanded = True widget = self.get_indented_widget() self.__super.__init__(widget) def selectable(self): """ Allow selection of non-leaf nodes so children may be (un)expanded """ return not self.is_leaf def get_indented_widget(self): widget = self.get_inner_widget() if not self.is_leaf: widget = urwid.Columns([('fixed', 1, [self.unexpanded_icon, self.expanded_icon][self.expanded]), widget], dividechars=1) indent_cols = self.get_indent_cols() return urwid.Padding(widget, width=('relative', 100), left=indent_cols) def update_expanded_icon(self): """Update display widget text for parent widgets""" # icon is first element in columns indented widget self._w.base_widget.widget_list[0] = [ self.unexpanded_icon, self.expanded_icon][self.expanded] def get_indent_cols(self): return self.indent_cols * self.get_node().get_depth() def get_inner_widget(self): if self._innerwidget is None: self._innerwidget = self.load_inner_widget() return self._innerwidget def load_inner_widget(self): return urwid.Text(self.get_display_text()) def get_node(self): return self._node def get_display_text(self): return (self.get_node().get_key() + ": " + str(self.get_node().get_value())) def next_inorder(self): """Return the next TreeWidget depth first from this one.""" # first check if there's a child widget firstchild = self.first_child() if firstchild is not None: return firstchild # now we need to hunt for the next sibling thisnode = self.get_node() nextnode = thisnode.next_sibling() depth = thisnode.get_depth() while nextnode is None and depth > 0: # keep going up the tree until we find an ancestor next sibling thisnode = thisnode.get_parent() nextnode = thisnode.next_sibling() depth -= 1 assert depth == thisnode.get_depth() if nextnode is None: # we're at the end of the tree return None else: return nextnode.get_widget() def prev_inorder(self): """Return the previous TreeWidget depth first from this one.""" thisnode = self._node prevnode = thisnode.prev_sibling() if prevnode is not None: # we need to find the last child of the previous widget if its # expanded prevwidget = prevnode.get_widget() lastchild = prevwidget.last_child() if lastchild is None: return prevwidget else: return lastchild else: # need to hunt for the parent depth = thisnode.get_depth() if prevnode is None and depth == 0: return None elif prevnode is None: prevnode = thisnode.get_parent() return prevnode.get_widget() def keypress(self, size, key): """Handle expand & collapse requests (non-leaf nodes)""" if self.is_leaf: return key if key in ("+", "right"): self.expanded = True self.update_expanded_icon() elif key == "-": self.expanded = False self.update_expanded_icon() elif self._w.selectable(): return self.__super.keypress(size, key) else: return key def mouse_event(self, size, event, button, col, row, focus): if self.is_leaf or event != 'mouse press' or button!=1: return False if row == 0 and col == self.get_indent_cols(): self.expanded = not self.expanded self.update_expanded_icon() return True return False def first_child(self): """Return first child if expanded.""" if self.is_leaf or not self.expanded: return None else: if self._node.has_children(): firstnode = self._node.get_first_child() return firstnode.get_widget() else: return None def last_child(self): """Return last child if expanded.""" if self.is_leaf or not self.expanded: return None else: if self._node.has_children(): lastchild = self._node.get_last_child().get_widget() else: return None # recursively search down for the last descendant lastdescendant = lastchild.last_child() if lastdescendant is None: return lastchild else: return lastdescendant class TreeNode(object): """ Store tree contents and cache TreeWidget objects. A TreeNode consists of the following elements: * key: accessor token for parent nodes * value: subclass-specific data * parent: a TreeNode which contains a pointer back to this object * widget: The widget used to render the object """ def __init__(self, value, parent=None, key=None, depth=None): self._key = key self._parent = parent self._value = value self._depth = depth self._widget = None def get_widget(self, reload=False): """ Return the widget for this node.""" if self._widget is None or reload == True: self._widget = self.load_widget() return self._widget def load_widget(self): return TreeWidget(self) def get_depth(self): if self._depth is None and self._parent is None: self._depth = 0 elif self._depth is None: self._depth = self._parent.get_depth() + 1 return self._depth def get_index(self): if self.get_depth() == 0: return None else: key = self.get_key() parent = self.get_parent() return parent.get_child_index(key) def get_key(self): return self._key def set_key(self, key): self._key = key def change_key(self, key): self.get_parent().change_child_key(self._key, key) def get_parent(self): if self._parent == None and self.get_depth() > 0: self._parent = self.load_parent() return self._parent def load_parent(self): """Provide TreeNode with a parent for the current node. This function is only required if the tree was instantiated from a child node (virtual function)""" raise TreeWidgetError("virtual function. Implement in subclass") def get_value(self): return self._value def is_root(self): return self.get_depth() == 0 def next_sibling(self): if self.get_depth() > 0: return self.get_parent().next_child(self.get_key()) else: return None def prev_sibling(self): if self.get_depth() > 0: return self.get_parent().prev_child(self.get_key()) else: return None def get_root(self): root = self while root.get_parent() is not None: root = root.get_parent() return root class ParentNode(TreeNode): """Maintain sort order for TreeNodes.""" def __init__(self, value, parent=None, key=None, depth=None): TreeNode.__init__(self, value, parent=parent, key=key, depth=depth) self._child_keys = None self._children = {} def get_child_keys(self, reload=False): """Return a possibly ordered list of child keys""" if self._child_keys is None or reload == True: self._child_keys = self.load_child_keys() return self._child_keys def load_child_keys(self): """Provide ParentNode with an ordered list of child keys (virtual function)""" raise TreeWidgetError("virtual function. Implement in subclass") def get_child_widget(self, key): """Return the widget for a given key. Create if necessary.""" child = self.get_child_node(key) return child.get_widget() def get_child_node(self, key, reload=False): """Return the child node for a given key. Create if necessary.""" if key not in self._children or reload == True: self._children[key] = self.load_child_node(key) return self._children[key] def load_child_node(self, key): """Load the child node for a given key (virtual function)""" raise TreeWidgetError("virtual function. Implement in subclass") def set_child_node(self, key, node): """Set the child node for a given key. Useful for bottom-up, lazy population of a tree.""" self._children[key] = node def change_child_key(self, oldkey, newkey): if newkey in self._children: raise TreeWidgetError("%s is already in use" % newkey) self._children[newkey] = self._children.pop(oldkey) self._children[newkey].set_key(newkey) def get_child_index(self, key): try: return self.get_child_keys().index(key) except ValueError: errorstring = ("Can't find key %s in ParentNode %s\n" + "ParentNode items: %s") raise TreeWidgetError(errorstring % (key, self.get_key(), str(self.get_child_keys()))) def next_child(self, key): """Return the next child node in index order from the given key.""" index = self.get_child_index(key) # the given node may have just been deleted if index is None: return None index += 1 child_keys = self.get_child_keys() if index < len(child_keys): # get the next item at same level return self.get_child_node(child_keys[index]) else: return None def prev_child(self, key): """Return the previous child node in index order from the given key.""" index = self.get_child_index(key) if index is None: return None child_keys = self.get_child_keys() index -= 1 if index >= 0: # get the previous item at same level return self.get_child_node(child_keys[index]) else: return None def get_first_child(self): """Return the first TreeNode in the directory.""" child_keys = self.get_child_keys() return self.get_child_node(child_keys[0]) def get_last_child(self): """Return the last TreeNode in the directory.""" child_keys = self.get_child_keys() return self.get_child_node(child_keys[-1]) def has_children(self): """Does this node have any children?""" return len(self.get_child_keys())>0 class TreeWalker(urwid.ListWalker): """ListWalker-compatible class for displaying TreeWidgets positions are TreeNodes.""" def __init__(self, start_from): """start_from: TreeNode with the initial focus.""" self.focus = start_from def get_focus(self): widget = self.focus.get_widget() return widget, self.focus def set_focus(self, focus): self.focus = focus self._modified() def get_next(self, start_from): widget = start_from.get_widget() target = widget.next_inorder() if target is None: return None, None else: return target, target.get_node() def get_prev(self, start_from): widget = start_from.get_widget() target = widget.prev_inorder() if target is None: return None, None else: return target, target.get_node() class TreeListBox(urwid.ListBox): """A ListBox with special handling for navigation and collapsing of TreeWidgets""" def keypress(self, size, key): key = self.__super.keypress(size, key) return self.unhandled_input(size, key) def unhandled_input(self, size, input): """Handle macro-navigation keys""" if input == 'left': self.move_focus_to_parent(size) elif input == '-': self.collapse_focus_parent(size) elif input == 'home': self.focus_home(size) elif input == 'end': self.focus_end(size) else: return input def collapse_focus_parent(self, size): """Collapse parent directory.""" widget, pos = self.body.get_focus() self.move_focus_to_parent(size) pwidget, ppos = self.body.get_focus() if pos != ppos: self.keypress(size, "-") def move_focus_to_parent(self, size): """Move focus to parent of widget in focus.""" widget, pos = self.body.get_focus() parentpos = pos.get_parent() if parentpos is None: return middle, top, bottom = self.calculate_visible( size ) row_offset, focus_widget, focus_pos, focus_rows, cursor = middle trim_top, fill_above = top for widget, pos, rows in fill_above: row_offset -= rows if pos == parentpos: self.change_focus(size, pos, row_offset) return self.change_focus(size, pos.get_parent()) def focus_home(self, size): """Move focus to very top.""" widget, pos = self.body.get_focus() rootnode = pos.get_root() self.change_focus(size, rootnode) def focus_end( self, size ): """Move focus to far bottom.""" maxrow, maxcol = size widget, pos = self.body.get_focus() rootnode = pos.get_root() rootwidget = rootnode.get_widget() lastwidget = rootwidget.last_child() lastnode = lastwidget.get_node() self.change_focus(size, lastnode, maxrow-1) urwid-2.0.1/urwid/__init__.py0000644000175000017500000000772713231170672017572 0ustar andersonanderson00000000000000#!/usr/bin/python # # Urwid __init__.py - all the stuff you're likely to care about # # Copyright (C) 2004-2012 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.version import VERSION, __version__ from urwid.widget import (FLOW, BOX, FIXED, LEFT, RIGHT, CENTER, TOP, MIDDLE, BOTTOM, SPACE, ANY, CLIP, PACK, GIVEN, RELATIVE, RELATIVE_100, WEIGHT, WidgetMeta, WidgetError, Widget, FlowWidget, BoxWidget, fixed_size, FixedWidget, Divider, SolidFill, TextError, Text, EditError, Edit, IntEdit, delegate_to_widget_mixin, WidgetWrapError, WidgetWrap) from urwid.decoration import (WidgetDecoration, WidgetPlaceholder, AttrMapError, AttrMap, AttrWrap, BoxAdapterError, BoxAdapter, PaddingError, Padding, FillerError, Filler, WidgetDisable) from urwid.container import (GridFlowError, GridFlow, OverlayError, Overlay, FrameError, Frame, PileError, Pile, ColumnsError, Columns, WidgetContainerMixin) from urwid.wimp import (SelectableIcon, CheckBoxError, CheckBox, RadioButton, Button, PopUpLauncher, PopUpTarget) from urwid.listbox import (ListWalkerError, ListWalker, PollingListWalker, SimpleListWalker, SimpleFocusListWalker, ListBoxError, ListBox) from urwid.graphics import (BigText, LineBox, BarGraphMeta, BarGraphError, BarGraph, GraphVScale, ProgressBar, scale_bar_values) from urwid.canvas import (CanvasCache, CanvasError, Canvas, TextCanvas, BlankCanvas, SolidCanvas, CompositeCanvas, CanvasCombine, CanvasOverlay, CanvasJoin) from urwid.font import (get_all_fonts, Font, Thin3x3Font, Thin4x3Font, HalfBlock5x4Font, HalfBlock6x5Font, HalfBlockHeavy6x5Font, Thin6x6Font, HalfBlock7x7Font) from urwid.signals import (MetaSignals, Signals, emit_signal, register_signal, connect_signal, disconnect_signal) from urwid.monitored_list import MonitoredList, MonitoredFocusList from urwid.command_map import (CommandMap, command_map, REDRAW_SCREEN, CURSOR_UP, CURSOR_DOWN, CURSOR_LEFT, CURSOR_RIGHT, CURSOR_PAGE_UP, CURSOR_PAGE_DOWN, CURSOR_MAX_LEFT, CURSOR_MAX_RIGHT, ACTIVATE) from urwid.main_loop import (ExitMainLoop, MainLoop, SelectEventLoop, GLibEventLoop, TornadoEventLoop, AsyncioEventLoop) try: from urwid.main_loop import TwistedEventLoop except ImportError: pass from urwid.text_layout import (TextLayout, StandardTextLayout, default_layout, LayoutSegment) from urwid.display_common import (UPDATE_PALETTE_ENTRY, DEFAULT, BLACK, DARK_RED, DARK_GREEN, BROWN, DARK_BLUE, DARK_MAGENTA, DARK_CYAN, LIGHT_GRAY, DARK_GRAY, LIGHT_RED, LIGHT_GREEN, YELLOW, LIGHT_BLUE, LIGHT_MAGENTA, LIGHT_CYAN, WHITE, AttrSpecError, AttrSpec, RealTerminal, ScreenError, BaseScreen) from urwid.util import (calc_text_pos, calc_width, is_wide_char, move_next_char, move_prev_char, within_double_byte, detected_encoding, set_encoding, get_encoding_mode, apply_target_encoding, supports_unicode, calc_trim_text, TagMarkupException, decompose_tagmarkup, MetaSuper, int_scale, is_mouse_event) from urwid.treetools import (TreeWidgetError, TreeWidget, TreeNode, ParentNode, TreeWalker, TreeListBox) from urwid.vterm import (TermModes, TermCharset, TermScroller, TermCanvas, Terminal) from urwid import raw_display urwid-2.0.1/urwid/raw_display.py0000644000175000017500000010650313231170672020341 0ustar andersonanderson00000000000000#!/usr/bin/python # # Urwid raw display module # Copyright (C) 2004-2009 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 """ Direct terminal UI implementation """ import os import select import struct import sys import signal try: import fcntl import termios import tty except ImportError: pass # windows from urwid import util from urwid import escape from urwid.display_common import BaseScreen, RealTerminal, \ UPDATE_PALETTE_ENTRY, AttrSpec, UNPRINTABLE_TRANS_TABLE, \ INPUT_DESCRIPTORS_CHANGED from urwid import signals from urwid.compat import PYTHON3, bytes, B from subprocess import Popen, PIPE class Screen(BaseScreen, RealTerminal): def __init__(self, input=sys.stdin, output=sys.stdout): """Initialize a screen that directly prints escape codes to an output terminal. """ super(Screen, self).__init__() self._pal_escape = {} self._pal_attrspec = {} signals.connect_signal(self, UPDATE_PALETTE_ENTRY, self._on_update_palette_entry) self.colors = 16 # FIXME: detect this self.has_underline = True # FIXME: detect this self._keyqueue = [] self.prev_input_resize = 0 self.set_input_timeouts() self.screen_buf = None self._screen_buf_canvas = None self._resized = False self.maxrow = None self.gpm_mev = None self.gpm_event_pending = False self._mouse_tracking_enabled = False self.last_bstate = 0 self._setup_G1_done = False self._rows_used = None self._cy = 0 self.term = os.environ.get('TERM', '') self.fg_bright_is_bold = not self.term.startswith("xterm") self.bg_bright_is_blink = (self.term == "linux") self.back_color_erase = not self.term.startswith("screen") self.register_palette_entry( None, 'default','default') self._next_timeout = None self.signal_handler_setter = signal.signal # Our connections to the world self._term_output_file = output self._term_input_file = input # pipe for signalling external event loops about resize events self._resize_pipe_rd, self._resize_pipe_wr = os.pipe() fcntl.fcntl(self._resize_pipe_rd, fcntl.F_SETFL, os.O_NONBLOCK) def _on_update_palette_entry(self, name, *attrspecs): # copy the attribute to a dictionary containing the escape seqences a = attrspecs[{16:0,1:1,88:2,256:3}[self.colors]] self._pal_attrspec[name] = a self._pal_escape[name] = self._attrspec_to_escape(a) def set_input_timeouts(self, max_wait=None, complete_wait=0.125, resize_wait=0.125): """ Set the get_input timeout values. All values are in floating point numbers of seconds. max_wait -- amount of time in seconds to wait for input when there is no input pending, wait forever if None complete_wait -- amount of time in seconds to wait when get_input detects an incomplete escape sequence at the end of the available input resize_wait -- amount of time in seconds to wait for more input after receiving two screen resize requests in a row to stop Urwid from consuming 100% cpu during a gradual window resize operation """ self.max_wait = max_wait if max_wait is not None: if self._next_timeout is None: self._next_timeout = max_wait else: self._next_timeout = min(self._next_timeout, self.max_wait) self.complete_wait = complete_wait self.resize_wait = resize_wait def _sigwinch_handler(self, signum, frame=None): """ frame -- will always be None when the GLib event loop is being used. """ if not self._resized: os.write(self._resize_pipe_wr, B('R')) self._resized = True self.screen_buf = None def _sigcont_handler(self, signum, frame=None): """ frame -- will always be None when the GLib event loop is being used. """ self.stop() self.start() self._sigwinch_handler(None, None) def signal_init(self): """ Called in the startup of run wrapper to set the SIGWINCH and SIGCONT signal handlers. Override this function to call from main thread in threaded applications. """ self.signal_handler_setter(signal.SIGWINCH, self._sigwinch_handler) self.signal_handler_setter(signal.SIGCONT, self._sigcont_handler) def signal_restore(self): """ Called in the finally block of run wrapper to restore the SIGWINCH and SIGCONT signal handlers. Override this function to call from main thread in threaded applications. """ self.signal_handler_setter(signal.SIGCONT, signal.SIG_DFL) self.signal_handler_setter(signal.SIGWINCH, signal.SIG_DFL) def set_mouse_tracking(self, enable=True): """ Enable (or disable) mouse tracking. After calling this function get_input will include mouse click events along with keystrokes. """ enable = bool(enable) if enable == self._mouse_tracking_enabled: return self._mouse_tracking(enable) self._mouse_tracking_enabled = enable def _mouse_tracking(self, enable): if enable: self.write(escape.MOUSE_TRACKING_ON) self._start_gpm_tracking() else: self.write(escape.MOUSE_TRACKING_OFF) self._stop_gpm_tracking() def _start_gpm_tracking(self): if not os.path.isfile("/usr/bin/mev"): return if not os.environ.get('TERM',"").lower().startswith("linux"): return if not Popen: return m = Popen(["/usr/bin/mev","-e","158"], stdin=PIPE, stdout=PIPE, close_fds=True) fcntl.fcntl(m.stdout.fileno(), fcntl.F_SETFL, os.O_NONBLOCK) self.gpm_mev = m def _stop_gpm_tracking(self): if not self.gpm_mev: return os.kill(self.gpm_mev.pid, signal.SIGINT) os.waitpid(self.gpm_mev.pid, 0) self.gpm_mev = None def _start(self, alternate_buffer=True): """ Initialize the screen and input mode. alternate_buffer -- use alternate screen buffer """ if alternate_buffer: self.write(escape.SWITCH_TO_ALTERNATE_BUFFER) self._rows_used = None else: self._rows_used = 0 fd = self._term_input_file.fileno() if os.isatty(fd): self._old_termios_settings = termios.tcgetattr(fd) tty.setcbreak(fd) self.signal_init() self._alternate_buffer = alternate_buffer self._next_timeout = self.max_wait if not self._signal_keys_set: self._old_signal_keys = self.tty_signal_keys(fileno=fd) signals.emit_signal(self, INPUT_DESCRIPTORS_CHANGED) # restore mouse tracking to previous state self._mouse_tracking(self._mouse_tracking_enabled) return super(Screen, self)._start() def _stop(self): """ Restore the screen. """ self.clear() signals.emit_signal(self, INPUT_DESCRIPTORS_CHANGED) self.signal_restore() fd = self._term_input_file.fileno() if os.isatty(fd): termios.tcsetattr(fd, termios.TCSADRAIN, self._old_termios_settings) self._mouse_tracking(False) move_cursor = "" if self._alternate_buffer: move_cursor = escape.RESTORE_NORMAL_BUFFER elif self.maxrow is not None: move_cursor = escape.set_cursor_position( 0, self.maxrow) self.write( self._attrspec_to_escape(AttrSpec('','')) + escape.SI + move_cursor + escape.SHOW_CURSOR) self.flush() if self._old_signal_keys: self.tty_signal_keys(*(self._old_signal_keys + (fd,))) super(Screen, self)._stop() def write(self, data): """Write some data to the terminal. You may wish to override this if you're using something other than regular files for input and output. """ self._term_output_file.write(data) def flush(self): """Flush the output buffer. You may wish to override this if you're using something other than regular files for input and output. """ self._term_output_file.flush() def get_input(self, raw_keys=False): """Return pending input as a list. raw_keys -- return raw keycodes as well as translated versions This function will immediately return all the input since the last time it was called. If there is no input pending it will wait before returning an empty list. The wait time may be configured with the set_input_timeouts function. If raw_keys is False (default) this function will return a list of keys pressed. If raw_keys is True this function will return a ( keys pressed, raw keycodes ) tuple instead. Examples of keys returned: * ASCII printable characters: " ", "a", "0", "A", "-", "/" * ASCII control characters: "tab", "enter" * Escape sequences: "up", "page up", "home", "insert", "f1" * Key combinations: "shift f1", "meta a", "ctrl b" * Window events: "window resize" When a narrow encoding is not enabled: * "Extended ASCII" characters: "\\xa1", "\\xb2", "\\xfe" When a wide encoding is enabled: * Double-byte characters: "\\xa1\\xea", "\\xb2\\xd4" When utf8 encoding is enabled: * Unicode characters: u"\\u00a5", u'\\u253c" Examples of mouse events returned: * Mouse button press: ('mouse press', 1, 15, 13), ('meta mouse press', 2, 17, 23) * Mouse drag: ('mouse drag', 1, 16, 13), ('mouse drag', 1, 17, 13), ('ctrl mouse drag', 1, 18, 13) * Mouse button release: ('mouse release', 0, 18, 13), ('ctrl mouse release', 0, 17, 23) """ assert self._started self._wait_for_input_ready(self._next_timeout) keys, raw = self.parse_input(None, None, self.get_available_raw_input()) # Avoid pegging CPU at 100% when slowly resizing if keys==['window resize'] and self.prev_input_resize: while True: self._wait_for_input_ready(self.resize_wait) keys, raw2 = self.parse_input(None, None, self.get_available_raw_input()) raw += raw2 #if not keys: # keys, raw2 = self._get_input( # self.resize_wait) # raw += raw2 if keys!=['window resize']: break if keys[-1:]!=['window resize']: keys.append('window resize') if keys==['window resize']: self.prev_input_resize = 2 elif self.prev_input_resize == 2 and not keys: self.prev_input_resize = 1 else: self.prev_input_resize = 0 if raw_keys: return keys, raw return keys def get_input_descriptors(self): """ Return a list of integer file descriptors that should be polled in external event loops to check for user input. Use this method if you are implementing your own event loop. """ if not self._started: return [] fd_list = [self._term_input_file.fileno(), self._resize_pipe_rd] if self.gpm_mev is not None: fd_list.append(self.gpm_mev.stdout.fileno()) return fd_list _current_event_loop_handles = () def unhook_event_loop(self, event_loop): """ Remove any hooks added by hook_event_loop. """ for handle in self._current_event_loop_handles: event_loop.remove_watch_file(handle) if self._input_timeout: event_loop.remove_alarm(self._input_timeout) self._input_timeout = None def hook_event_loop(self, event_loop, callback): """ Register the given callback with the event loop, to be called with new input whenever it's available. The callback should be passed a list of processed keys and a list of unprocessed keycodes. Subclasses may wish to use parse_input to wrap the callback. """ if hasattr(self, 'get_input_nonblocking'): wrapper = self._make_legacy_input_wrapper(event_loop, callback) else: wrapper = lambda: self.parse_input( event_loop, callback, self.get_available_raw_input()) fds = self.get_input_descriptors() handles = [event_loop.watch_file(fd, wrapper) for fd in fds] self._current_event_loop_handles = handles _input_timeout = None _partial_codes = None def _make_legacy_input_wrapper(self, event_loop, callback): """ Support old Screen classes that still have a get_input_nonblocking and expect it to work. """ def wrapper(): if self._input_timeout: event_loop.remove_alarm(self._input_timeout) self._input_timeout = None timeout, keys, raw = self.get_input_nonblocking() if timeout is not None: self._input_timeout = event_loop.alarm(timeout, wrapper) callback(keys, raw) return wrapper def get_available_raw_input(self): """ Return any currently-available input. Does not block. This method is only used by the default `hook_event_loop` implementation; you can safely ignore it if you implement your own. """ codes = self._get_gpm_codes() + self._get_keyboard_codes() if self._partial_codes: codes = self._partial_codes + codes self._partial_codes = None # clean out the pipe used to signal external event loops # that a resize has occurred try: while True: os.read(self._resize_pipe_rd, 1) except OSError: pass return codes def parse_input(self, event_loop, callback, codes, wait_for_more=True): """ Read any available input from get_available_raw_input, parses it into keys, and calls the given callback. The current implementation tries to avoid any assumptions about what the screen or event loop look like; it only deals with parsing keycodes and setting a timeout when an incomplete one is detected. `codes` should be a sequence of keycodes, i.e. bytes. A bytearray is appropriate, but beware of using bytes, which only iterates as integers on Python 3. """ # Note: event_loop may be None for 100% synchronous support, only used # by get_input. Not documented because you shouldn't be doing it. if self._input_timeout and event_loop: event_loop.remove_alarm(self._input_timeout) self._input_timeout = None original_codes = codes processed = [] try: while codes: run, codes = escape.process_keyqueue( codes, wait_for_more) processed.extend(run) except escape.MoreInputRequired: # Set a timer to wait for the rest of the input; if it goes off # without any new input having come in, use the partial input k = len(original_codes) - len(codes) processed_codes = original_codes[:k] self._partial_codes = codes def _parse_incomplete_input(): self._input_timeout = None self._partial_codes = None self.parse_input( event_loop, callback, codes, wait_for_more=False) if event_loop: self._input_timeout = event_loop.alarm( self.complete_wait, _parse_incomplete_input) else: processed_codes = original_codes self._partial_codes = None if self._resized: processed.append('window resize') self._resized = False if callback: callback(processed, processed_codes) else: # For get_input return processed, processed_codes def _get_keyboard_codes(self): codes = [] while True: code = self._getch_nodelay() if code < 0: break codes.append(code) return codes def _get_gpm_codes(self): codes = [] try: while self.gpm_mev is not None and self.gpm_event_pending: codes.extend(self._encode_gpm_event()) except IOError as e: if e.args[0] != 11: raise return codes def _wait_for_input_ready(self, timeout): ready = None fd_list = [self._term_input_file.fileno()] if self.gpm_mev is not None: fd_list.append(self.gpm_mev.stdout.fileno()) while True: try: if timeout is None: ready,w,err = select.select( fd_list, [], fd_list) else: ready,w,err = select.select( fd_list,[],fd_list, timeout) break except select.error as e: if e.args[0] != 4: raise if self._resized: ready = [] break return ready def _getch(self, timeout): ready = self._wait_for_input_ready(timeout) if self.gpm_mev is not None: if self.gpm_mev.stdout.fileno() in ready: self.gpm_event_pending = True if self._term_input_file.fileno() in ready: return ord(os.read(self._term_input_file.fileno(), 1)) return -1 def _encode_gpm_event( self ): self.gpm_event_pending = False s = self.gpm_mev.stdout.readline().decode('ascii') l = s.split(",") if len(l) != 6: # unexpected output, stop tracking self._stop_gpm_tracking() signals.emit_signal(self, INPUT_DESCRIPTORS_CHANGED) return [] ev, x, y, ign, b, m = s.split(",") ev = int( ev.split("x")[-1], 16) x = int( x.split(" ")[-1] ) y = int( y.lstrip().split(" ")[0] ) b = int( b.split(" ")[-1] ) m = int( m.split("x")[-1].rstrip(), 16 ) # convert to xterm-like escape sequence last = next = self.last_bstate l = [] mod = 0 if m & 1: mod |= 4 # shift if m & 10: mod |= 8 # alt if m & 4: mod |= 16 # ctrl def append_button( b ): b |= mod l.extend([ 27, ord('['), ord('M'), b+32, x+32, y+32 ]) def determine_button_release( flag ): if b & 4 and last & 1: append_button( 0 + flag ) next |= 1 if b & 2 and last & 2: append_button( 1 + flag ) next |= 2 if b & 1 and last & 4: append_button( 2 + flag ) next |= 4 if ev == 20 or ev == 36 or ev == 52: # press if b & 4 and last & 1 == 0: append_button( 0 ) next |= 1 if b & 2 and last & 2 == 0: append_button( 1 ) next |= 2 if b & 1 and last & 4 == 0: append_button( 2 ) next |= 4 elif ev == 146: # drag if b & 4: append_button( 0 + escape.MOUSE_DRAG_FLAG ) elif b & 2: append_button( 1 + escape.MOUSE_DRAG_FLAG ) elif b & 1: append_button( 2 + escape.MOUSE_DRAG_FLAG ) else: # release if b & 4 and last & 1: append_button( 0 + escape.MOUSE_RELEASE_FLAG ) next &= ~ 1 if b & 2 and last & 2: append_button( 1 + escape.MOUSE_RELEASE_FLAG ) next &= ~ 2 if b & 1 and last & 4: append_button( 2 + escape.MOUSE_RELEASE_FLAG ) next &= ~ 4 if ev == 40: # double click (release) if b & 4 and last & 1: append_button( 0 + escape.MOUSE_MULTIPLE_CLICK_FLAG ) if b & 2 and last & 2: append_button( 1 + escape.MOUSE_MULTIPLE_CLICK_FLAG ) if b & 1 and last & 4: append_button( 2 + escape.MOUSE_MULTIPLE_CLICK_FLAG ) elif ev == 52: if b & 4 and last & 1: append_button( 0 + escape.MOUSE_MULTIPLE_CLICK_FLAG*2 ) if b & 2 and last & 2: append_button( 1 + escape.MOUSE_MULTIPLE_CLICK_FLAG*2 ) if b & 1 and last & 4: append_button( 2 + escape.MOUSE_MULTIPLE_CLICK_FLAG*2 ) self.last_bstate = next return l def _getch_nodelay(self): return self._getch(0) def get_cols_rows(self): """Return the terminal dimensions (num columns, num rows).""" y, x = 24, 80 try: buf = fcntl.ioctl(self._term_output_file.fileno(), termios.TIOCGWINSZ, ' '*4) y, x = struct.unpack('hh', buf) except IOError: # Term size could not be determined pass self.maxrow = y return x, y def _setup_G1(self): """ Initialize the G1 character set to graphics mode if required. """ if self._setup_G1_done: return while True: try: self.write(escape.DESIGNATE_G1_SPECIAL) self.flush() break except IOError: pass self._setup_G1_done = True def draw_screen(self, maxres, r ): """Paint screen with rendered canvas.""" (maxcol, maxrow) = maxres assert self._started assert maxrow == r.rows() # quick return if nothing has changed if self.screen_buf and r is self._screen_buf_canvas: return self._setup_G1() if self._resized: # handle resize before trying to draw screen return o = [escape.HIDE_CURSOR, self._attrspec_to_escape(AttrSpec('',''))] def partial_display(): # returns True if the screen is in partial display mode # ie. only some rows belong to the display return self._rows_used is not None if not partial_display(): o.append(escape.CURSOR_HOME) if self.screen_buf: osb = self.screen_buf else: osb = [] sb = [] cy = self._cy y = -1 def set_cursor_home(): if not partial_display(): return escape.set_cursor_position(0, 0) return (escape.CURSOR_HOME_COL + escape.move_cursor_up(cy)) def set_cursor_row(y): if not partial_display(): return escape.set_cursor_position(0, y) return escape.move_cursor_down(y - cy) def set_cursor_position(x, y): if not partial_display(): return escape.set_cursor_position(x, y) if cy > y: return ('\b' + escape.CURSOR_HOME_COL + escape.move_cursor_up(cy - y) + escape.move_cursor_right(x)) return ('\b' + escape.CURSOR_HOME_COL + escape.move_cursor_down(y - cy) + escape.move_cursor_right(x)) def is_blank_row(row): if len(row) > 1: return False if row[0][2].strip(): return False return True def attr_to_escape(a): if a in self._pal_escape: return self._pal_escape[a] elif isinstance(a, AttrSpec): return self._attrspec_to_escape(a) # undefined attributes use default/default # TODO: track and report these return self._attrspec_to_escape( AttrSpec('default','default')) def using_standout_or_underline(a): a = self._pal_attrspec.get(a, a) return isinstance(a, AttrSpec) and (a.standout or a.underline) ins = None o.append(set_cursor_home()) cy = 0 for row in r.content(): y += 1 if osb and y < len(osb) and osb[y] == row: # this row of the screen buffer matches what is # currently displayed, so we can skip this line sb.append( osb[y] ) continue sb.append(row) # leave blank lines off display when we are using # the default screen buffer (allows partial screen) if partial_display() and y > self._rows_used: if is_blank_row(row): continue self._rows_used = y if y or partial_display(): o.append(set_cursor_position(0, y)) # after updating the line we will be just over the # edge, but terminals still treat this as being # on the same line cy = y whitespace_at_end = False if row: a, cs, run = row[-1] if (run[-1:] == B(' ') and self.back_color_erase and not using_standout_or_underline(a)): whitespace_at_end = True row = row[:-1] + [(a, cs, run.rstrip(B(' ')))] elif y == maxrow-1 and maxcol > 1: row, back, ins = self._last_row(row) first = True lasta = lastcs = None for (a,cs, run) in row: assert isinstance(run, bytes) # canvases should render with bytes if cs != 'U': run = run.translate(UNPRINTABLE_TRANS_TABLE) if first or lasta != a: o.append(attr_to_escape(a)) lasta = a if first or lastcs != cs: assert cs in [None, "0", "U"], repr(cs) if lastcs == "U": o.append( escape.IBMPC_OFF ) if cs is None: o.append( escape.SI ) elif cs == "U": o.append( escape.IBMPC_ON ) else: o.append( escape.SO ) lastcs = cs o.append( run ) first = False if ins: (inserta, insertcs, inserttext) = ins ias = attr_to_escape(inserta) assert insertcs in [None, "0", "U"], repr(insertcs) if cs is None: icss = escape.SI elif cs == "U": icss = escape.IBMPC_ON else: icss = escape.SO o += [ "\x08"*back, ias, icss, escape.INSERT_ON, inserttext, escape.INSERT_OFF ] if cs == "U": o.append(escape.IBMPC_OFF) if whitespace_at_end: o.append(escape.ERASE_IN_LINE_RIGHT) if r.cursor is not None: x,y = r.cursor o += [set_cursor_position(x, y), escape.SHOW_CURSOR ] self._cy = y if self._resized: # handle resize before trying to draw screen return try: for l in o: if isinstance(l, bytes) and PYTHON3: l = l.decode('utf-8', 'replace') self.write(l) self.flush() except IOError as e: # ignore interrupted syscall if e.args[0] != 4: raise self.screen_buf = sb self._screen_buf_canvas = r def _last_row(self, row): """On the last row we need to slide the bottom right character into place. Calculate the new line, attr and an insert sequence to do that. eg. last row: XXXXXXXXXXXXXXXXXXXXYZ Y will be drawn after Z, shifting Z into position. """ new_row = row[:-1] z_attr, z_cs, last_text = row[-1] last_cols = util.calc_width(last_text, 0, len(last_text)) last_offs, z_col = util.calc_text_pos(last_text, 0, len(last_text), last_cols-1) if last_offs == 0: z_text = last_text del new_row[-1] # we need another segment y_attr, y_cs, nlast_text = row[-2] nlast_cols = util.calc_width(nlast_text, 0, len(nlast_text)) z_col += nlast_cols nlast_offs, y_col = util.calc_text_pos(nlast_text, 0, len(nlast_text), nlast_cols-1) y_text = nlast_text[nlast_offs:] if nlast_offs: new_row.append((y_attr, y_cs, nlast_text[:nlast_offs])) else: z_text = last_text[last_offs:] y_attr, y_cs = z_attr, z_cs nlast_cols = util.calc_width(last_text, 0, last_offs) nlast_offs, y_col = util.calc_text_pos(last_text, 0, last_offs, nlast_cols-1) y_text = last_text[nlast_offs:last_offs] if nlast_offs: new_row.append((y_attr, y_cs, last_text[:nlast_offs])) new_row.append((z_attr, z_cs, z_text)) return new_row, z_col-y_col, (y_attr, y_cs, y_text) def clear(self): """ Force the screen to be completely repainted on the next call to draw_screen(). """ self.screen_buf = None self.setup_G1 = True def _attrspec_to_escape(self, a): """ Convert AttrSpec instance a to an escape sequence for the terminal >>> s = Screen() >>> s.set_terminal_properties(colors=256) >>> a2e = s._attrspec_to_escape >>> a2e(s.AttrSpec('brown', 'dark green')) '\\x1b[0;33;42m' >>> a2e(s.AttrSpec('#fea,underline', '#d0d')) '\\x1b[0;38;5;229;4;48;5;164m' """ if self.term == 'fbterm': fg = escape.ESC + '[1;%d}' % (a.foreground_number,) bg = escape.ESC + '[2;%d}' % (a.background_number,) return fg + bg if a.foreground_high: fg = "38;5;%d" % a.foreground_number elif a.foreground_basic: if a.foreground_number > 7: if self.fg_bright_is_bold: fg = "1;%d" % (a.foreground_number - 8 + 30) else: fg = "%d" % (a.foreground_number - 8 + 90) else: fg = "%d" % (a.foreground_number + 30) else: fg = "39" st = ("1;" * a.bold + "3;" * a.italics + "4;" * a.underline + "5;" * a.blink + "7;" * a.standout + "9;" * a.strikethrough) if a.background_high: bg = "48;5;%d" % a.background_number elif a.background_basic: if a.background_number > 7: if self.bg_bright_is_blink: bg = "5;%d" % (a.background_number - 8 + 40) else: # this doesn't work on most terminals bg = "%d" % (a.background_number - 8 + 100) else: bg = "%d" % (a.background_number + 40) else: bg = "49" return escape.ESC + "[0;%s;%s%sm" % (fg, st, bg) def set_terminal_properties(self, colors=None, bright_is_bold=None, has_underline=None): """ colors -- number of colors terminal supports (1, 16, 88 or 256) or None to leave unchanged bright_is_bold -- set to True if this terminal uses the bold setting to create bright colors (numbers 8-15), set to False if this Terminal can create bright colors without bold or None to leave unchanged has_underline -- set to True if this terminal can use the underline setting, False if it cannot or None to leave unchanged """ if colors is None: colors = self.colors if bright_is_bold is None: bright_is_bold = self.fg_bright_is_bold if has_underline is None: has_underline = self.has_underline if colors == self.colors and bright_is_bold == self.fg_bright_is_bold \ and has_underline == self.has_underline: return self.colors = colors self.fg_bright_is_bold = bright_is_bold self.has_underline = has_underline self.clear() self._pal_escape = {} for p,v in self._palette.items(): self._on_update_palette_entry(p, *v) def reset_default_terminal_palette(self): """ Attempt to set the terminal palette to default values as taken from xterm. Uses number of colors from current set_terminal_properties() screen setting. """ if self.colors == 1: return def rgb_values(n): if self.colors == 16: aspec = AttrSpec("h%d"%n, "", 256) else: aspec = AttrSpec("h%d"%n, "", self.colors) return aspec.get_rgb_values()[:3] entries = [(n,) + rgb_values(n) for n in range(self.colors)] self.modify_terminal_palette(entries) def modify_terminal_palette(self, entries): """ entries - list of (index, red, green, blue) tuples. Attempt to set part of the terminal palette (this does not work on all terminals.) The changes are sent as a single escape sequence so they should all take effect at the same time. 0 <= index < 256 (some terminals will only have 16 or 88 colors) 0 <= red, green, blue < 256 """ if self.term == 'fbterm': modify = ["%d;%d;%d;%d" % (index, red, green, blue) for index, red, green, blue in entries] self.write("\x1b[3;"+";".join(modify)+"}") else: modify = ["%d;rgb:%02x/%02x/%02x" % (index, red, green, blue) for index, red, green, blue in entries] self.write("\x1b]4;"+";".join(modify)+"\x1b\\") self.flush() # shortcut for creating an AttrSpec with this screen object's # number of colors AttrSpec = lambda self, fg, bg: AttrSpec(fg, bg, self.colors) def _test(): import doctest doctest.testmod() if __name__=='__main__': _test() urwid-2.0.1/urwid/graphics.py0000755000175000017500000007675313231170672017643 0ustar andersonanderson00000000000000#!/usr/bin/python # -*- coding: utf-8 -*- # # Urwid graphics widgets # 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.compat import with_metaclass from urwid.util import decompose_tagmarkup, get_encoding_mode from urwid.canvas import CompositeCanvas, CanvasJoin, TextCanvas, \ CanvasCombine, SolidCanvas from urwid.widget import WidgetMeta, Widget, BOX, FIXED, FLOW, \ nocache_widget_render, nocache_widget_render_instance, fixed_size, \ WidgetWrap, Divider, SolidFill, Text, CENTER, CLIP from urwid.container import Pile, Columns from urwid.display_common import AttrSpec from urwid.decoration import WidgetDecoration class BigText(Widget): _sizing = frozenset([FIXED]) def __init__(self, markup, font): """ markup -- same as Text widget markup font -- instance of a Font class """ self.set_font(font) self.set_text(markup) def set_text(self, markup): self.text, self.attrib = decompose_tagmarkup(markup) self._invalidate() def get_text(self): """ Returns (text, attributes). """ return self.text, self.attrib def set_font(self, font): self.font = font self._invalidate() def pack(self, size=None, focus=False): rows = self.font.height cols = 0 for c in self.text: cols += self.font.char_width(c) return cols, rows def render(self, size, focus=False): fixed_size(size) # complain if parameter is wrong a = None ai = ak = 0 o = [] rows = self.font.height attrib = self.attrib + [(None, len(self.text))] for ch in self.text: if not ak: a, ak = attrib[ai] ai += 1 ak -= 1 width = self.font.char_width(ch) if not width: # ignore invalid characters continue c = self.font.render(ch) if a is not None: c = CompositeCanvas(c) c.fill_attr(a) o.append((c, None, False, width)) if o: canv = CanvasJoin(o) else: canv = TextCanvas([""] * rows, maxcol=0, check_width=False) canv = CompositeCanvas(canv) canv.set_depends([]) return canv class LineBox(WidgetDecoration, WidgetWrap): def __init__(self, original_widget, title="", title_align="center", tlcorner=u'┌', tline=u'─', lline=u'│', trcorner=u'â”', blcorner=u'â””', rline=u'│', bline=u'─', brcorner=u'┘'): """ Draw a line around original_widget. Use 'title' to set an initial title text with will be centered on top of the box. Use `title_align` to align the title to the 'left', 'right', or 'center'. The default is 'center'. You can also override the widgets used for the lines/corners: tline: top line bline: bottom line lline: left line rline: right line tlcorner: top left corner trcorner: top right corner blcorner: bottom left corner brcorner: bottom right corner If empty string is specified for one of the lines/corners, then no character will be output there. This allows for seamless use of adjoining LineBoxes. """ if tline: tline = Divider(tline) if bline: bline = Divider(bline) if lline: lline = SolidFill(lline) if rline: rline = SolidFill(rline) tlcorner, trcorner = Text(tlcorner), Text(trcorner) blcorner, brcorner = Text(blcorner), Text(brcorner) if not tline and title: raise ValueError('Cannot have a title when tline is empty string') self.title_widget = Text(self.format_title(title)) if tline: if title_align not in ('left', 'center', 'right'): raise ValueError('title_align must be one of "left", "right", or "center"') if title_align == 'left': tline_widgets = [('flow', self.title_widget), tline] else: tline_widgets = [tline, ('flow', self.title_widget)] if title_align == 'center': tline_widgets.append(tline) self.tline_widget = Columns(tline_widgets) top = Columns([ ('fixed', 1, tlcorner), self.tline_widget, ('fixed', 1, trcorner) ]) else: self.tline_widget = None top = None middle_widgets = [] if lline: middle_widgets.append(('fixed', 1, lline)) else: # Note: We need to define a fixed first widget (even if it's 0 width) so that the other # widgets have something to anchor onto middle_widgets.append(('fixed', 0, SolidFill(u""))) middle_widgets.append(original_widget) focus_col = len(middle_widgets) - 1 if rline: middle_widgets.append(('fixed', 1, rline)) middle = Columns(middle_widgets, box_columns=[0, 2], focus_column=focus_col) if bline: bottom = Columns([ ('fixed', 1, blcorner), bline, ('fixed', 1, brcorner) ]) else: bottom = None pile_widgets = [] if top: pile_widgets.append(('flow', top)) pile_widgets.append(middle) focus_pos = len(pile_widgets) - 1 if bottom: pile_widgets.append(('flow', bottom)) pile = Pile(pile_widgets, focus_item=focus_pos) WidgetDecoration.__init__(self, original_widget) WidgetWrap.__init__(self, pile) def format_title(self, text): if len(text) > 0: return " %s " % text else: return "" def set_title(self, text): if not self.title_widget: raise ValueError('Cannot set title when tline is unset') self.title_widget.set_text(self.format_title(text)) self.tline_widget._invalidate() class BarGraphMeta(WidgetMeta): """ Detect subclass get_data() method and dynamic change to get_data() method and disable caching in these cases. This is for backwards compatibility only, new programs should use set_data() instead of overriding get_data(). """ def __init__(cls, name, bases, d): super(BarGraphMeta, cls).__init__(name, bases, d) if "get_data" in d: cls.render = nocache_widget_render(cls) cls._get_data = cls.get_data cls.get_data = property( lambda self: self._get_data, nocache_bargraph_get_data) def nocache_bargraph_get_data(self, get_data_fn): """ Disable caching on this bargraph because get_data_fn needs to be polled to get the latest data. """ self.render = nocache_widget_render_instance(self) self._get_data = get_data_fn class BarGraphError(Exception): pass class BarGraph(with_metaclass(BarGraphMeta, Widget)): _sizing = frozenset([BOX]) ignore_focus = True eighths = u' â–▂▃▄▅▆▇' hlines = u'_⎺⎻─⎼⎽' def __init__(self, attlist, hatt=None, satt=None): """ Create a bar graph with the passed display characteristics. see set_segment_attributes for a description of the parameters. """ self.set_segment_attributes(attlist, hatt, satt) self.set_data([], 1, None) self.set_bar_width(None) def set_segment_attributes(self, attlist, hatt=None, satt=None): """ :param attlist: list containing display attribute or (display attribute, character) tuple for background, first segment, and optionally following segments. ie. len(attlist) == num segments+1 character defaults to ' ' if not specified. :param hatt: list containing attributes for horizontal lines. First element is for lines on background, second is for lines on first segment, third is for lines on second segment etc. :param satt: dictionary containing attributes for smoothed transitions of bars in UTF-8 display mode. The values are in the form: (fg,bg) : attr fg and bg are integers where 0 is the graph background, 1 is the first segment, 2 is the second, ... fg > bg in all values. attr is an attribute with a foreground corresponding to fg and a background corresponding to bg. If satt is not None and the bar graph is being displayed in a terminal using the UTF-8 encoding then the character cell that is shared between the segments specified will be smoothed with using the UTF-8 vertical eighth characters. eg: set_segment_attributes( ['no', ('unsure',"?"), 'yes'] ) will use the attribute 'no' for the background (the area from the top of the graph to the top of the bar), question marks with the attribute 'unsure' will be used for the topmost segment of the bar, and the attribute 'yes' will be used for the bottom segment of the bar. """ self.attr = [] self.char = [] if len(attlist) < 2: raise BarGraphError("attlist must include at least background and seg1: %r" % (attlist,)) assert len(attlist) >= 2, 'must at least specify bg and fg!' for a in attlist: if type(a) != tuple: self.attr.append(a) self.char.append(' ') else: attr, ch = a self.attr.append(attr) self.char.append(ch) self.hatt = [] if hatt is None: hatt = [self.attr[0]] elif type(hatt) != list: hatt = [hatt] self.hatt = hatt if satt is None: satt = {} for i in satt.items(): try: (fg, bg), attr = i except ValueError: raise BarGraphError("satt not in (fg,bg:attr) form: %r" % (i,)) if type(fg) != int or fg >= len(attlist): raise BarGraphError("fg not valid integer: %r" % (fg,)) if type(bg) != int or bg >= len(attlist): raise BarGraphError("bg not valid integer: %r" % (fg,)) if fg <= bg: raise BarGraphError("fg (%s) not > bg (%s)" % (fg, bg)) self.satt = satt def set_data(self, bardata, top, hlines=None): """ Store bar data, bargraph top and horizontal line positions. bardata -- a list of bar values. top -- maximum value for segments within bardata hlines -- None or a bar value marking horizontal line positions bar values are [ segment1, segment2, ... ] lists where top is the maximal value corresponding to the top of the bar graph and segment1, segment2, ... are the values for the top of each segment of this bar. Simple bar graphs will only have one segment in each bar value. Eg: if top is 100 and there is a bar value of [ 80, 30 ] then the top of this bar will be at 80% of full height of the graph and it will have a second segment that starts at 30%. """ if hlines is not None: hlines = hlines[:] # shallow copy hlines.sort() hlines.reverse() self.data = bardata, top, hlines self._invalidate() def _get_data(self, size): """ Return (bardata, top, hlines) This function is called by render to retrieve the data for the graph. It may be overloaded to create a dynamic bar graph. This implementation will truncate the bardata list returned if not all bars will fit within maxcol. """ (maxcol, maxrow) = size bardata, top, hlines = self.data widths = self.calculate_bar_widths((maxcol, maxrow), bardata) if len(bardata) > len(widths): return bardata[:len(widths)], top, hlines return bardata, top, hlines def set_bar_width(self, width): """ Set a preferred bar width for calculate_bar_widths to use. width -- width of bar or None for automatic width adjustment """ assert width is None or width > 0 self.bar_width = width self._invalidate() def calculate_bar_widths(self, size, bardata): """ Return a list of bar widths, one for each bar in data. If self.bar_width is None this implementation will stretch the bars across the available space specified by maxcol. """ (maxcol, maxrow) = size if self.bar_width is not None: return [self.bar_width] * min( len(bardata), maxcol / self.bar_width) if len(bardata) >= maxcol: return [1] * maxcol widths = [] grow = maxcol remain = len(bardata) for row in bardata: w = int(float(grow) / remain + 0.5) widths.append(w) grow -= w remain -= 1 return widths def selectable(self): """ Return False. """ return False def use_smoothed(self): return self.satt and get_encoding_mode() == "utf8" def calculate_display(self, size): """ Calculate display data. """ (maxcol, maxrow) = size bardata, top, hlines = self.get_data((maxcol, maxrow)) widths = self.calculate_bar_widths((maxcol, maxrow), bardata) if self.use_smoothed(): disp = calculate_bargraph_display(bardata, top, widths, maxrow * 8) disp = self.smooth_display(disp) else: disp = calculate_bargraph_display(bardata, top, widths, maxrow) if hlines: disp = self.hlines_display(disp, top, hlines, maxrow) return disp def hlines_display(self, disp, top, hlines, maxrow): """ Add hlines to display structure represented as bar_type tuple values: (bg, 0-5) bg is the segment that has the hline on it 0-5 is the hline graphic to use where 0 is a regular underscore and 1-5 are the UTF-8 horizontal scan line characters. """ if self.use_smoothed(): shiftr = 0 r = [(0.2, 1), (0.4, 2), (0.6, 3), (0.8, 4), (1.0, 5), ] else: shiftr = 0.5 r = [(1.0, 0), ] # reverse the hlines to match screen ordering rhl = [] for h in hlines: rh = float(top - h) * maxrow / top - shiftr if rh < 0: continue rhl.append(rh) # build a list of rows that will have hlines hrows = [] last_i = -1 for rh in rhl: i = int(rh) if i == last_i: continue f = rh - i for spl, chnum in r: if f < spl: hrows.append((i, chnum)) break last_i = i # fill hlines into disp data def fill_row(row, chnum): rout = [] for bar_type, width in row: if (type(bar_type) == int and len(self.hatt) > bar_type): rout.append(((bar_type, chnum), width)) continue rout.append((bar_type, width)) return rout o = [] k = 0 rnum = 0 for y_count, row in disp: if k >= len(hrows): o.append((y_count, row)) continue end_block = rnum + y_count while k < len(hrows) and hrows[k][0] < end_block: i, chnum = hrows[k] if i - rnum > 0: o.append((i - rnum, row)) o.append((1, fill_row(row, chnum))) rnum = i + 1 k += 1 if rnum < end_block: o.append((end_block - rnum, row)) rnum = end_block #assert 0, o return o def smooth_display(self, disp): """ smooth (col, row*8) display into (col, row) display using UTF vertical eighth characters represented as bar_type tuple values: ( fg, bg, 1-7 ) where fg is the lower segment, bg is the upper segment and 1-7 is the vertical eighth character to use. """ o = [] r = 0 # row remainder def seg_combine(a, b): (bt1, w1), (bt2, w2) = a, b if (bt1, w1) == (bt2, w2): return (bt1, w1), None, None wmin = min(w1, w2) l1 = l2 = None if w1 > w2: l1 = (bt1, w1 - w2) elif w2 > w1: l2 = (bt2, w2 - w1) if type(bt1) == tuple: return (bt1, wmin), l1, l2 if (bt2, bt1) not in self.satt: if r < 4: return (bt2, wmin), l1, l2 return (bt1, wmin), l1, l2 return ((bt2, bt1, 8 - r), wmin), l1, l2 def row_combine_last(count, row): o_count, o_row = o[-1] row = row[:] # shallow copy, so we don't destroy orig. o_row = o_row[:] l = [] while row: (bt, w), l1, l2 = seg_combine( o_row.pop(0), row.pop(0)) if l and l[-1][0] == bt: l[-1] = (bt, l[-1][1] + w) else: l.append((bt, w)) if l1: o_row = [l1] + o_row if l2: row = [l2] + row assert not o_row o[-1] = (o_count + count, l) # regroup into actual rows (8 disp rows == 1 actual row) for y_count, row in disp: if r: count = min(8 - r, y_count) row_combine_last(count, row) y_count -= count r += count r = r % 8 if not y_count: continue assert r == 0 # copy whole blocks if y_count > 7: o.append((y_count // 8 * 8, row)) y_count = y_count % 8 if not y_count: continue o.append((y_count, row)) r = y_count return [(y // 8, row) for (y, row) in o] def render(self, size, focus=False): """ Render BarGraph. """ (maxcol, maxrow) = size disp = self.calculate_display((maxcol, maxrow)) combinelist = [] for y_count, row in disp: l = [] for bar_type, width in row: if type(bar_type) == tuple: if len(bar_type) == 3: # vertical eighths fg, bg, k = bar_type a = self.satt[(fg, bg)] t = self.eighths[k] * width else: # horizontal lines bg, k = bar_type a = self.hatt[bg] t = self.hlines[k] * width else: a = self.attr[bar_type] t = self.char[bar_type] * width l.append((a, t)) c = Text(l).render((maxcol,)) assert c.rows() == 1, "Invalid characters in BarGraph!" combinelist += [(c, None, False)] * y_count canv = CanvasCombine(combinelist) return canv def calculate_bargraph_display(bardata, top, bar_widths, maxrow): """ Calculate a rendering of the bar graph described by data, bar_widths and height. bardata -- bar information with same structure as BarGraph.data top -- maximal value for bardata segments bar_widths -- list of integer column widths for each bar maxrow -- rows for display of bargraph Returns a structure as follows: [ ( y_count, [ ( bar_type, width), ... ] ), ... ] The outer tuples represent a set of identical rows. y_count is the number of rows in this set, the list contains the data to be displayed in the row repeated through the set. The inner tuple describes a run of width characters of bar_type. bar_type is an integer starting from 0 for the background, 1 for the 1st segment, 2 for the 2nd segment etc.. This function should complete in approximately O(n+m) time, where n is the number of bars displayed and m is the number of rows. """ assert len(bardata) == len(bar_widths) maxcol = sum(bar_widths) # build intermediate data structure rows = [None] * maxrow def add_segment(seg_num, col, row, width, rows=rows): if rows[row]: last_seg, last_col, last_end = rows[row][-1] if last_end > col: if last_col >= col: del rows[row][-1] else: rows[row][-1] = (last_seg, last_col, col) elif last_seg == seg_num and last_end == col: rows[row][-1] = (last_seg, last_col, last_end + width) return elif rows[row] is None: rows[row] = [] rows[row].append((seg_num, col, col + width)) col = 0 barnum = 0 for bar in bardata: width = bar_widths[barnum] if width < 1: continue # loop through in reverse order tallest = maxrow segments = scale_bar_values(bar, top, maxrow) for k in range(len(bar) - 1, -1, -1): s = segments[k] if s >= maxrow: continue if s < 0: s = 0 if s < tallest: # add only properly-overlapped bars tallest = s add_segment(k + 1, col, s, width) col += width barnum += 1 #print repr(rows) # build rowsets data structure rowsets = [] y_count = 0 last = [(0, maxcol)] for r in rows: if r is None: y_count = y_count + 1 continue if y_count: rowsets.append((y_count, last)) y_count = 0 i = 0 # index into "last" la, ln = last[i] # last attribute, last run length c = 0 # current column o = [] # output list to be added to rowsets for seg_num, start, end in r: while start > c + ln: o.append((la, ln)) i += 1 c += ln la, ln = last[i] if la == seg_num: # same attribute, can combine o.append((la, end - c)) else: if start - c > 0: o.append((la, start - c)) o.append((seg_num, end - start)) if end == maxcol: i = len(last) break # skip past old segments covered by new one while end >= c + ln: i += 1 c += ln la, ln = last[i] if la != seg_num: ln = c + ln - end c = end continue # same attribute, can extend oa, on = o[-1] on += c + ln - end o[-1] = oa, on i += 1 c += ln if c == maxcol: break assert i < len(last), repr((on, maxcol)) la, ln = last[i] if i < len(last): o += [(la, ln)] + last[i + 1:] last = o y_count += 1 if y_count: rowsets.append((y_count, last)) return rowsets class GraphVScale(Widget): _sizing = frozenset([BOX]) def __init__(self, labels, top): """ GraphVScale( [(label1 position, label1 markup),...], top ) label position -- 0 < position < top for the y position label markup -- text markup for this label top -- top y position This widget is a vertical scale for the BarGraph widget that can correspond to the BarGraph's horizontal lines """ self.set_scale(labels, top) def set_scale(self, labels, top): """ set_scale( [(label1 position, label1 markup),...], top ) label position -- 0 < position < top for the y position label markup -- text markup for this label top -- top y position """ labels = labels[:] # shallow copy labels.sort() labels.reverse() self.pos = [] self.txt = [] for y, markup in labels: self.pos.append(y) self.txt.append(Text(markup)) self.top = top def selectable(self): """ Return False. """ return False def render(self, size, focus=False): """ Render GraphVScale. """ (maxcol, maxrow) = size pl = scale_bar_values(self.pos, self.top, maxrow) combinelist = [] rows = 0 for p, t in zip(pl, self.txt): p -= 1 if p >= maxrow: break if p < rows: continue c = t.render((maxcol,)) if p > rows: run = p - rows c = CompositeCanvas(c) c.pad_trim_top_bottom(run, 0) rows += c.rows() combinelist.append((c, None, False)) if not combinelist: return SolidCanvas(" ", size[0], size[1]) c = CanvasCombine(combinelist) if maxrow - rows: c.pad_trim_top_bottom(0, maxrow - rows) return c def scale_bar_values( bar, top, maxrow ): """ Return a list of bar values aliased to integer values of maxrow. """ return [maxrow - int(float(v) * maxrow / top + 0.5) for v in bar] class ProgressBar(Widget): _sizing = frozenset([FLOW]) eighths = u' â–â–Žâ–▌▋▊▉' text_align = CENTER def __init__(self, normal, complete, current=0, done=100, satt=None): """ :param normal: display attribute for incomplete part of progress bar :param complete: display attribute for complete part of progress bar :param current: current progress :param done: progress amount at 100% :param satt: display attribute for smoothed part of bar where the foreground of satt corresponds to the normal part and the background corresponds to the complete part. If satt is ``None`` then no smoothing will be done. >>> pb = ProgressBar('a', 'b') >>> pb >>> print(pb.get_text()) 0 % >>> pb.set_completion(34.42) >>> print(pb.get_text()) 34 % >>> class CustomProgressBar(ProgressBar): ... def get_text(self): ... return u'Foobar' >>> cpb = CustomProgressBar('a', 'b') >>> print(cpb.get_text()) Foobar >>> for x in range(101): ... cpb.set_completion(x) ... s = cpb.render((10, )) >>> cpb2 = CustomProgressBar('a', 'b', satt='c') >>> for x in range(101): ... cpb2.set_completion(x) ... s = cpb2.render((10, )) """ self.normal = normal self.complete = complete self._current = current self._done = done self.satt = satt def set_completion(self, current): """ current -- current progress """ self._current = current self._invalidate() current = property(lambda self: self._current, set_completion) def _set_done(self, done): """ done -- progress amount at 100% """ self._done = done self._invalidate() done = property(lambda self: self._done, _set_done) def rows(self, size, focus=False): return 1 def get_text(self): """ Return the progress bar percentage text. You can override this method to display custom text. """ percent = min(100, max(0, int(self.current * 100 / self.done))) return str(percent) + " %" def render(self, size, focus=False): """ Render the progress bar. """ (maxcol,) = size txt = Text(self.get_text(), self.text_align, CLIP) c = txt.render((maxcol,)) cf = float(self.current) * maxcol / self.done ccol_dirty = int(cf) ccol = len(c._text[0][:ccol_dirty].decode( 'utf-8', 'ignore' ).encode( 'utf-8' )) cs = 0 if self.satt is not None: cs = int((cf - ccol) * 8) if ccol < 0 or (ccol == 0 and cs == 0): c._attr = [[(self.normal, maxcol)]] elif ccol >= maxcol: c._attr = [[(self.complete, maxcol)]] elif cs and c._text[0][ccol] == " ": t = c._text[0] cenc = self.eighths[cs].encode("utf-8") c._text[0] = t[:ccol] + cenc + t[ccol + 1:] a = [] if ccol > 0: a.append((self.complete, ccol)) a.append((self.satt, len(cenc))) if maxcol - ccol - 1 > 0: a.append((self.normal, maxcol - ccol - 1)) c._attr = [a] c._cs = [[(None, len(c._text[0]))]] else: c._attr = [[(self.complete, ccol), (self.normal, maxcol - ccol)]] return c class PythonLogo(Widget): _sizing = frozenset([FIXED]) def __init__(self): """ Create canvas containing an ASCII version of the Python Logo and store it. """ blu = AttrSpec('light blue', 'default') yel = AttrSpec('yellow', 'default') width = 17 self._canvas = Text([ (blu, " ______\n"), (blu, " _|_o__ |"), (yel, "__\n"), (blu, " | _____|"), (yel, " |\n"), (blu, " |__| "), (yel, "______|\n"), (yel, " |____o_|")]).render((width,)) def pack(self, size=None, focus=False): """ Return the size from our pre-rendered canvas. """ return self._canvas.cols(), self._canvas.rows() def render(self, size, focus=False): """ Return the pre-rendered canvas. """ fixed_size(size) return self._canvas def _test(): import doctest doctest.testmod() if __name__=='__main__': _test() urwid-2.0.1/urwid/widget.py0000644000175000017500000017044213231170672017311 0ustar andersonanderson00000000000000#!/usr/bin/python # # Urwid basic widget classes # Copyright (C) 2004-2012 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 operator import attrgetter from urwid.compat import text_type, with_metaclass from urwid.util import (MetaSuper, decompose_tagmarkup, calc_width, is_wide_char, move_prev_char, move_next_char) from urwid.text_layout import calc_pos, calc_coords, shift_line from urwid import signals from urwid import text_layout from urwid.canvas import (CanvasCache, CompositeCanvas, SolidCanvas, apply_text_layout) from urwid.command_map import (command_map, CURSOR_LEFT, CURSOR_RIGHT, CURSOR_UP, CURSOR_DOWN, CURSOR_MAX_LEFT, CURSOR_MAX_RIGHT) from urwid.split_repr import split_repr, remove_defaults, python3_repr # define some names for these constants to avoid misspellings in the source # and to document the constant strings we are using # Widget sizing methods FLOW = 'flow' BOX = 'box' FIXED = 'fixed' # Text alignment modes LEFT = 'left' RIGHT = 'right' CENTER = 'center' # Filler alignment TOP = 'top' MIDDLE = 'middle' BOTTOM = 'bottom' # Text wrapping modes SPACE = 'space' ANY = 'any' CLIP = 'clip' # Width and Height settings PACK = 'pack' GIVEN = 'given' RELATIVE = 'relative' RELATIVE_100 = (RELATIVE, 100) WEIGHT = 'weight' class WidgetMeta(MetaSuper, signals.MetaSignals): """ Bases: :class:`MetaSuper`, :class:`MetaSignals` Automatic caching of render and rows methods. Class variable *no_cache* is a list of names of methods to not cache automatically. Valid method names for *no_cache* are ``'render'`` and ``'rows'``. Class variable *ignore_focus* if defined and set to ``True`` indicates that the canvas this widget renders is not affected by the focus parameter, so it may be ignored when caching. """ def __init__(cls, name, bases, d): no_cache = d.get("no_cache", []) super(WidgetMeta, cls).__init__(name, bases, d) if "render" in d: if "render" not in no_cache: render_fn = cache_widget_render(cls) else: render_fn = nocache_widget_render(cls) cls.render = render_fn if "rows" in d and "rows" not in no_cache: cls.rows = cache_widget_rows(cls) if "no_cache" in d: del cls.no_cache if "ignore_focus" in d: del cls.ignore_focus class WidgetError(Exception): pass def validate_size(widget, size, canv): """ Raise a WidgetError if a canv does not match size size. """ if (size and size[1:] != (0,) and size[0] != canv.cols()) or \ (len(size)>1 and size[1] != canv.rows()): raise WidgetError("Widget %r rendered (%d x %d) canvas" " when passed size %r!" % (widget, canv.cols(), canv.rows(), size)) def update_wrapper(new_fn, fn): """ Copy as much of the function detail from fn to new_fn as we can. """ try: new_fn.__name__ = fn.__name__ new_fn.__dict__.update(fn.__dict__) new_fn.__doc__ = fn.__doc__ new_fn.__module__ = fn.__module__ except TypeError: pass # python2.3 ignore read-only attributes def cache_widget_render(cls): """ Return a function that wraps the cls.render() method and fetches and stores canvases with CanvasCache. """ ignore_focus = bool(getattr(cls, "ignore_focus", False)) fn = cls.render def cached_render(self, size, focus=False): focus = focus and not ignore_focus canv = CanvasCache.fetch(self, cls, size, focus) if canv: return canv canv = fn(self, size, focus=focus) validate_size(self, size, canv) if canv.widget_info: canv = CompositeCanvas(canv) canv.finalize(self, size, focus) CanvasCache.store(cls, canv) return canv cached_render.original_fn = fn update_wrapper(cached_render, fn) return cached_render def nocache_widget_render(cls): """ Return a function that wraps the cls.render() method and finalizes the canvas that it returns. """ fn = cls.render if hasattr(fn, "original_fn"): fn = fn.original_fn def finalize_render(self, size, focus=False): canv = fn(self, size, focus=focus) if canv.widget_info: canv = CompositeCanvas(canv) validate_size(self, size, canv) canv.finalize(self, size, focus) return canv finalize_render.original_fn = fn update_wrapper(finalize_render, fn) return finalize_render def nocache_widget_render_instance(self): """ Return a function that wraps the cls.render() method and finalizes the canvas that it returns, but does not cache the canvas. """ fn = self.render.original_fn def finalize_render(size, focus=False): canv = fn(self, size, focus=focus) if canv.widget_info: canv = CompositeCanvas(canv) canv.finalize(self, size, focus) return canv finalize_render.original_fn = fn update_wrapper(finalize_render, fn) return finalize_render def cache_widget_rows(cls): """ Return a function that wraps the cls.rows() method and returns rows from the CanvasCache if available. """ ignore_focus = bool(getattr(cls, "ignore_focus", False)) fn = cls.rows def cached_rows(self, size, focus=False): focus = focus and not ignore_focus canv = CanvasCache.fetch(self, cls, size, focus) if canv: return canv.rows() return fn(self, size, focus) update_wrapper(cached_rows, fn) return cached_rows class Widget(with_metaclass(WidgetMeta, object)): """ Widget base class .. attribute:: _selectable :annotation: = False The default :meth:`.selectable` method returns this value. .. attribute:: _sizing :annotation: = frozenset(['flow', 'box', 'fixed']) The default :meth:`.sizing` method returns this value. .. attribute:: _command_map :annotation: = urwid.command_map A shared :class:`CommandMap` instance. May be redefined in subclasses or widget instances. .. method:: render(size, focus=False) .. note:: This method is not implemented in :class:`.Widget` but must be implemented by any concrete subclass :param size: One of the following, *maxcol* and *maxrow* are integers > 0: (*maxcol*, *maxrow*) for box sizing -- the parent chooses the exact size of this widget (*maxcol*,) for flow sizing -- the parent chooses only the number of columns for this widget () for fixed sizing -- this widget is a fixed size which can't be adjusted by the parent :type size: widget size :param focus: set to ``True`` if this widget or one of its children is in focus :type focus: bool :returns: A :class:`Canvas` subclass instance containing the rendered content of this widget :class:`Text` widgets return a :class:`TextCanvas` (arbitrary text and display attributes), :class:`SolidFill` widgets return a :class:`SolidCanvas` (a single character repeated across the whole surface) and container widgets return a :class:`CompositeCanvas` (one or more other canvases arranged arbitrarily). If *focus* is ``False``, the returned canvas may not have a cursor position set. There is some metaclass magic defined in the :class:`Widget` metaclass :class:`WidgetMeta` that causes the result of this method to be cached by :class:`CanvasCache`. Later calls will automatically look up the value in the cache first. As a small optimization the class variable :attr:`ignore_focus` may be defined and set to ``True`` if this widget renders the same canvas regardless of the value of the *focus* parameter. Any time the content of a widget changes it should call :meth:`_invalidate` to remove any cached canvases, or the widget may render the cached canvas instead of creating a new one. .. method:: rows(size, focus=False) .. note:: This method is not implemented in :class:`.Widget` but must be implemented by any flow widget. See :meth:`.sizing`. See :meth:`Widget.render` for parameter details. :returns: The number of rows required for this widget given a number of columns in *size* This is the method flow widgets use to communicate their size to other widgets without having to render a canvas. This should be a quick calculation as this function may be called a number of times in normal operation. If your implementation may take a long time you should add your own caching here. There is some metaclass magic defined in the :class:`Widget` metaclass :class:`WidgetMeta` that causes the result of this function to be retrieved from any canvas cached by :class:`CanvasCache`, so if your widget has been rendered you may not receive calls to this function. The class variable :attr:`ignore_focus` may be defined and set to ``True`` if this widget renders the same size regardless of the value of the *focus* parameter. .. method:: keypress(size, key) .. note:: This method is not implemented in :class:`.Widget` but must be implemented by any selectable widget. See :meth:`.selectable`. :param size: See :meth:`Widget.render` for details :type size: widget size :param key: a single keystroke value; see :ref:`keyboard-input` :type key: bytes or unicode :returns: ``None`` if *key* was handled by this widget or *key* (the same value passed) if *key* was not handled by this widget Container widgets will typically call the :meth:`keypress` method on whichever of their children is set as the focus. The standard widgets use :attr:`_command_map` to determine what action should be performed for a given *key*. You may modify these values to your liking globally, at some level in the widget hierarchy or on individual widgets. See :class:`CommandMap` for the defaults. In your own widgets you may use whatever logic you like: filtering or translating keys, selectively passing along events etc. .. method:: mouse_event(size, event, button, col, row, focus) .. note:: This method is not implemented in :class:`.Widget` but may be implemented by a subclass. Not implementing this method is equivalent to having a method that always returns ``False``. :param size: See :meth:`Widget.render` for details. :type size: widget size :param event: Values such as ``'mouse press'``, ``'ctrl mouse press'``, ``'mouse release'``, ``'meta mouse release'``, ``'mouse drag'``; see :ref:`mouse-input` :type event: mouse event :param button: 1 through 5 for press events, often 0 for release events (which button was released is often not known) :type button: int :param col: Column of the event, 0 is the left edge of this widget :type col: int :param row: Row of the event, 0 it the top row of this widget :type row: int :param focus: Set to ``True`` if this widget or one of its children is in focus :type focus: bool :returns: ``True`` if the event was handled by this widget, ``False`` otherwise Container widgets will typically call the :meth:`mouse_event` method on whichever of their children is at the position (*col*, *row*). .. method:: get_cursor_coords(size) .. note:: This method is not implemented in :class:`.Widget` but must be implemented by any widget that may return cursor coordinates as part of the canvas that :meth:`render` returns. :param size: See :meth:`Widget.render` for details. :type size: widget size :returns: (*col*, *row*) if this widget has a cursor, ``None`` otherwise Return the cursor coordinates (*col*, *row*) of a cursor that will appear as part of the canvas rendered by this widget when in focus, or ``None`` if no cursor is displayed. The :class:`ListBox` widget uses this method to make sure a cursor in the focus widget is not scrolled out of view. It is a separate method to avoid having to render the whole widget while calculating layout. Container widgets will typically call the :meth:`.get_cursor_coords` method on their focus widget. .. method:: get_pref_col(size) .. note:: This method is not implemented in :class:`.Widget` but may be implemented by a subclass. :param size: See :meth:`Widget.render` for details. :type size: widget size :returns: a column number or ``'left'`` for the leftmost available column or ``'right'`` for the rightmost available column Return the preferred column for the cursor to be displayed in this widget. This value might not be the same as the column returned from :meth:`get_cursor_coords`. The :class:`ListBox` and :class:`Pile` widgets call this method on a widget losing focus and use the value returned to call :meth:`.move_cursor_to_coords` on the widget becoming the focus. This allows the focus to move up and down through widgets while keeping the cursor in approximately the same column on screen. .. method:: move_cursor_to_coords(size, col, row) .. note:: This method is not implemented in :class:`.Widget` but may be implemented by a subclass. Not implementing this method is equivalent to having a method that always returns ``False``. :param size: See :meth:`Widget.render` for details. :type size: widget size :param col: new column for the cursor, 0 is the left edge of this widget :type col: int :param row: new row for the cursor, 0 it the top row of this widget :type row: int :returns: ``True`` if the position was set successfully anywhere on *row*, ``False`` otherwise """ _selectable = False _sizing = frozenset([FLOW, BOX, FIXED]) _command_map = command_map def _invalidate(self): """ Mark cached canvases rendered by this widget as dirty so that they will not be used again. """ CanvasCache.invalidate(self) def _emit(self, name, *args): """ Convenience function to emit signals with self as first argument. """ signals.emit_signal(self, name, self, *args) def selectable(self): """ :returns: ``True`` if this is a widget that is designed to take the focus, i.e. it contains something the user might want to interact with, ``False`` otherwise, This default implementation returns :attr:`._selectable`. Subclasses may leave these is if the are not selectable, or if they are always selectable they may set the :attr:`_selectable` class variable to ``True``. If this method returns ``True`` then the :meth:`.keypress` method must be implemented. Returning ``False`` does not guarantee that this widget will never be in focus, only that this widget will usually be skipped over when changing focus. It is still possible for non selectable widgets to have the focus (typically when there are no other selectable widgets visible). """ return self._selectable def sizing(self): """ :returns: A frozenset including one or more of ``'box'``, ``'flow'`` and ``'fixed'``. Default implementation returns the value of :attr:`._sizing`, which for this class includes all three. The sizing modes returned indicate the modes that may be supported by this widget, but is not sufficient to know that using that sizing mode will work. Subclasses should make an effort to remove sizing modes they know will not work given the state of the widget, but many do not yet do this. If a sizing mode is missing from the set then the widget should fail when used in that mode. If ``'flow'`` is among the values returned then the other methods in this widget must be able to accept a single-element tuple (*maxcol*,) to their ``size`` parameter, and the :meth:`rows` method must be defined. If ``'box'`` is among the values returned then the other methods must be able to accept a two-element tuple (*maxcol*, *maxrow*) to their size parameter. If ``'fixed'`` is among the values returned then the other methods must be able to accept an empty tuple () to their size parameter, and the :meth:`pack` method must be defined. """ return self._sizing def pack(self, size, focus=False): """ See :meth:`Widget.render` for parameter details. :returns: A "packed" size (*maxcol*, *maxrow*) for this widget Calculate and return a minimum size where all content could still be displayed. Fixed widgets must implement this method and return their size when ``()`` is passed as the *size* parameter. This default implementation returns the *size* passed, or the *maxcol* passed and the value of :meth:`rows` as the *maxrow* when (*maxcol*,) is passed as the *size* parameter. .. note:: This is a new method that hasn't been fully implemented across the standard widget types. In particular it has not yet been implemented for container widgets. :class:`Text` widgets have implemented this method. You can use :meth:`Text.pack` to calculate the minimum columns and rows required to display a text widget without wrapping, or call it iteratively to calculate the minimum number of columns required to display the text wrapped into a target number of rows. """ if not size: if FIXED in self.sizing(): raise NotImplementedError('Fixed widgets must override' ' Widget.pack()') raise WidgetError('Cannot pack () size, this is not a fixed' ' widget: %s' % repr(self)) elif len(size) == 1: if FLOW in self.sizing(): return size + (self.rows(size, focus),) raise WidgetError('Cannot pack (maxcol,) size, this is not a' ' flow widget: %s' % repr(self)) return size base_widget = property(lambda self:self, doc=""" Read-only property that steps through decoration widgets and returns the one at the base. This default implementation returns self. """) focus = property(lambda self:None, doc=""" Read-only property returning the child widget in focus for container widgets. This default implementation always returns ``None``, indicating that this widget has no children. """) def _not_a_container(self, val=None): raise IndexError( "No focus_position, %r is not a container widget" % self) focus_position = property(_not_a_container, _not_a_container, doc=""" Property for reading and setting the focus position for container widgets. This default implementation raises :exc:`IndexError`, making normal widgets fail the same way accessing :attr:`.focus_position` on an empty container widget would. """) def __repr__(self): """ A friendly __repr__ for widgets, designed to be extended by subclasses with _repr_words and _repr_attr methods. """ return split_repr(self) def _repr_words(self): words = [] if self.selectable(): words = ["selectable"] + words if self.sizing() and self.sizing() != frozenset([FLOW, BOX, FIXED]): sizing_modes = list(self.sizing()) sizing_modes.sort() words.append("/".join(sizing_modes)) return words + ["widget"] def _repr_attrs(self): return {} class FlowWidget(Widget): """ Deprecated. Inherit from Widget and add: _sizing = frozenset(['flow']) at the top of your class definition instead. Base class of widgets that determine their rows from the number of columns available. """ _sizing = frozenset([FLOW]) def rows(self, size, focus=False): """ All flow widgets must implement this function. """ raise NotImplementedError() def render(self, size, focus=False): """ All widgets must implement this function. """ raise NotImplementedError() class BoxWidget(Widget): """ Deprecated. Inherit from Widget and add: _sizing = frozenset(['box']) _selectable = True at the top of your class definition instead. Base class of width and height constrained widgets such as the top level widget attached to the display object """ _selectable = True _sizing = frozenset([BOX]) def render(self, size, focus=False): """ All widgets must implement this function. """ raise NotImplementedError() def fixed_size(size): """ raise ValueError if size != (). Used by FixedWidgets to test size parameter. """ if size != (): raise ValueError("FixedWidget takes only () for size." \ "passed: %r" % (size,)) class FixedWidget(Widget): """ Deprecated. Inherit from Widget and add: _sizing = frozenset(['fixed']) at the top of your class definition instead. Base class of widgets that know their width and height and cannot be resized """ _sizing = frozenset([FIXED]) def render(self, size, focus=False): """ All widgets must implement this function. """ raise NotImplementedError() def pack(self, size=None, focus=False): """ All fixed widgets must implement this function. """ raise NotImplementedError() class Divider(Widget): """ Horizontal divider widget """ _sizing = frozenset([FLOW]) ignore_focus = True def __init__(self,div_char=u" ",top=0,bottom=0): """ :param div_char: character to repeat across line :type div_char: bytes or unicode :param top: number of blank lines above :type top: int :param bottom: number of blank lines below :type bottom: int >>> Divider() >>> Divider(u'-') >>> Divider(u'x', 1, 2) """ self.__super.__init__() self.div_char = div_char self.top = top self.bottom = bottom def _repr_words(self): return self.__super._repr_words() + [ python3_repr(self.div_char)] * (self.div_char != u" ") def _repr_attrs(self): attrs = dict(self.__super._repr_attrs()) if self.top: attrs['top'] = self.top if self.bottom: attrs['bottom'] = self.bottom return attrs def rows(self, size, focus=False): """ Return the number of lines that will be rendered. >>> Divider().rows((10,)) 1 >>> Divider(u'x', 1, 2).rows((10,)) 4 """ (maxcol,) = size return self.top + 1 + self.bottom def render(self, size, focus=False): """ Render the divider as a canvas and return it. >>> Divider().render((10,)).text # ... = b in Python 3 [...' '] >>> Divider(u'-', top=1).render((10,)).text [...' ', ...'----------'] >>> Divider(u'x', bottom=2).render((5,)).text [...'xxxxx', ...' ', ...' '] """ (maxcol,) = size canv = SolidCanvas(self.div_char, maxcol, 1) canv = CompositeCanvas(canv) if self.top or self.bottom: canv.pad_trim_top_bottom(self.top, self.bottom) return canv class SolidFill(BoxWidget): """ A box widget that fills an area with a single character """ _selectable = False ignore_focus = True def __init__(self, fill_char=" "): """ :param fill_char: character to fill area with :type fill_char: bytes or unicode >>> SolidFill(u'8') """ self.__super.__init__() self.fill_char = fill_char def _repr_words(self): return self.__super._repr_words() + [python3_repr(self.fill_char)] def render(self, size, focus=False ): """ Render the Fill as a canvas and return it. >>> SolidFill().render((4,2)).text # ... = b in Python 3 [...' ', ...' '] >>> SolidFill('#').render((5,3)).text [...'#####', ...'#####', ...'#####'] """ maxcol, maxrow = size return SolidCanvas(self.fill_char, maxcol, maxrow) class TextError(Exception): pass class Text(Widget): """ a horizontally resizeable text widget """ _sizing = frozenset([FLOW]) ignore_focus = True _repr_content_length_max = 140 def __init__(self, markup, align=LEFT, wrap=SPACE, layout=None): """ :param markup: content of text widget, one of: bytes or unicode text to be displayed (*display attribute*, *text markup*) *text markup* with *display attribute* applied to all parts of *text markup* with no display attribute already applied [*text markup*, *text markup*, ... ] all *text markup* in the list joined together :type markup: :ref:`text-markup` :param align: typically ``'left'``, ``'center'`` or ``'right'`` :type align: text alignment mode :param wrap: typically ``'space'``, ``'any'`` or ``'clip'`` :type wrap: text wrapping mode :param layout: defaults to a shared :class:`StandardTextLayout` instance :type layout: text layout instance >>> Text(u"Hello") >>> t = Text(('bold', u"stuff"), 'right', 'any') >>> t >>> print(t.text) stuff >>> t.attrib [('bold', 5)] """ self.__super.__init__() self._cache_maxcol = None self.set_text(markup) self.set_layout(align, wrap, layout) def _repr_words(self): """ Show the text in the repr in python3 format (b prefix for byte strings) and truncate if it's too long """ first = self.__super._repr_words() text = self.get_text()[0] rest = python3_repr(text) if len(rest) > self._repr_content_length_max: rest = (rest[:self._repr_content_length_max * 2 // 3 - 3] + '...' + rest[-self._repr_content_length_max // 3:]) return first + [rest] def _repr_attrs(self): attrs = dict(self.__super._repr_attrs(), align=self._align_mode, wrap=self._wrap_mode) return remove_defaults(attrs, Text.__init__) def _invalidate(self): self._cache_maxcol = None self.__super._invalidate() def set_text(self,markup): """ Set content of text widget. :param markup: see :class:`Text` for description. :type markup: text markup >>> t = Text(u"foo") >>> print(t.text) foo >>> t.set_text(u"bar") >>> print(t.text) bar >>> t.text = u"baz" # not supported because text stores text but set_text() takes markup Traceback (most recent call last): AttributeError: can't set attribute """ self._text, self._attrib = decompose_tagmarkup(markup) self._invalidate() def get_text(self): """ :returns: (*text*, *display attributes*) *text* complete bytes/unicode content of text widget *display attributes* run length encoded display attributes for *text*, eg. ``[('attr1', 10), ('attr2', 5)]`` >>> Text(u"Hello").get_text() # ... = u in Python 2 (...'Hello', []) >>> Text(('bright', u"Headline")).get_text() (...'Headline', [('bright', 8)]) >>> Text([('a', u"one"), u"two", ('b', u"three")]).get_text() (...'onetwothree', [('a', 3), (None, 3), ('b', 5)]) """ return self._text, self._attrib text = property(lambda self:self.get_text()[0], doc=""" Read-only property returning the complete bytes/unicode content of this widget """) attrib = property(lambda self:self.get_text()[1], doc=""" Read-only property returning the run-length encoded display attributes of this widget """) def set_align_mode(self, mode): """ Set text alignment mode. Supported modes depend on text layout object in use but defaults to a :class:`StandardTextLayout` instance :param mode: typically ``'left'``, ``'center'`` or ``'right'`` :type mode: text alignment mode >>> t = Text(u"word") >>> t.set_align_mode('right') >>> t.align 'right' >>> t.render((10,)).text # ... = b in Python 3 [...' word'] >>> t.align = 'center' >>> t.render((10,)).text [...' word '] >>> t.align = 'somewhere' Traceback (most recent call last): TextError: Alignment mode 'somewhere' not supported. """ if not self.layout.supports_align_mode(mode): raise TextError("Alignment mode %r not supported."% (mode,)) self._align_mode = mode self._invalidate() def set_wrap_mode(self, mode): """ Set text wrapping mode. Supported modes depend on text layout object in use but defaults to a :class:`StandardTextLayout` instance :param mode: typically ``'space'``, ``'any'`` or ``'clip'`` :type mode: text wrapping mode >>> t = Text(u"some words") >>> t.render((6,)).text # ... = b in Python 3 [...'some ', ...'words '] >>> t.set_wrap_mode('clip') >>> t.wrap 'clip' >>> t.render((6,)).text [...'some w'] >>> t.wrap = 'any' # Urwid 0.9.9 or later >>> t.render((6,)).text [...'some w', ...'ords '] >>> t.wrap = 'somehow' Traceback (most recent call last): TextError: Wrap mode 'somehow' not supported. """ if not self.layout.supports_wrap_mode(mode): raise TextError("Wrap mode %r not supported."%(mode,)) self._wrap_mode = mode self._invalidate() def set_layout(self, align, wrap, layout=None): """ Set the text layout object, alignment and wrapping modes at the same time. :type align: text alignment mode :param wrap: typically 'space', 'any' or 'clip' :type wrap: text wrapping mode :param layout: defaults to a shared :class:`StandardTextLayout` instance :type layout: text layout instance >>> t = Text(u"hi") >>> t.set_layout('right', 'clip') >>> t """ if layout is None: layout = text_layout.default_layout self._layout = layout self.set_align_mode(align) self.set_wrap_mode(wrap) align = property(lambda self:self._align_mode, set_align_mode) wrap = property(lambda self:self._wrap_mode, set_wrap_mode) layout = property(lambda self:self._layout) def render(self, size, focus=False): """ Render contents with wrapping and alignment. Return canvas. See :meth:`Widget.render` for parameter details. >>> Text(u"important things").render((18,)).text # ... = b in Python 3 [...'important things '] >>> Text(u"important things").render((11,)).text [...'important ', ...'things '] """ (maxcol,) = size text, attr = self.get_text() #assert isinstance(text, unicode) trans = self.get_line_translation( maxcol, (text,attr) ) return apply_text_layout(text, attr, trans, maxcol) def rows(self, size, focus=False): """ Return the number of rows the rendered text requires. See :meth:`Widget.rows` for parameter details. >>> Text(u"important things").rows((18,)) 1 >>> Text(u"important things").rows((11,)) 2 """ (maxcol,) = size return len(self.get_line_translation(maxcol)) def get_line_translation(self, maxcol, ta=None): """ Return layout structure used to map self.text to a canvas. This method is used internally, but may be useful for debugging custom layout classes. :param maxcol: columns available for display :type maxcol: int :param ta: ``None`` or the (*text*, *display attributes*) tuple returned from :meth:`.get_text` :type ta: text and display attributes """ if not self._cache_maxcol or self._cache_maxcol != maxcol: self._update_cache_translation(maxcol, ta) return self._cache_translation def _update_cache_translation(self,maxcol, ta): if ta: text, attr = ta else: text, attr = self.get_text() self._cache_maxcol = maxcol self._cache_translation = self._calc_line_translation( text, maxcol ) def _calc_line_translation(self, text, maxcol ): return self.layout.layout( text, self._cache_maxcol, self._align_mode, self._wrap_mode ) def pack(self, size=None, focus=False): """ Return the number of screen columns and rows required for this Text widget to be displayed without wrapping or clipping, as a single element tuple. :param size: ``None`` for unlimited screen columns or (*maxcol*,) to specify a maximum column size :type size: widget size >>> Text(u"important things").pack() (16, 1) >>> Text(u"important things").pack((15,)) (9, 2) >>> Text(u"important things").pack((8,)) (8, 2) """ text, attr = self.get_text() if size is not None: (maxcol,) = size if not hasattr(self.layout, "pack"): return size trans = self.get_line_translation( maxcol, (text,attr)) cols = self.layout.pack( maxcol, trans ) return (cols, len(trans)) i = 0 cols = 0 while i < len(text): j = text.find('\n', i) if j == -1: j = len(text) c = calc_width(text, i, j) if c>cols: cols = c i = j+1 return (cols, text.count('\n') + 1) class EditError(TextError): pass class Edit(Text): """ Text editing widget implements cursor movement, text insertion and deletion. A caption may prefix the editing area. Uses text class for text layout. Users of this class may listen for ``"change"`` or ``"postchange"`` events. See :func:``connect_signal``. * ``"change"`` is sent just before the value of edit_text changes. It receives the new text as an argument. Note that ``"change"`` cannot change the text in question as edit_text changes the text afterwards. * ``"postchange"`` is sent after the value of edit_text changes. It receives the old value of the text as an argument and thus is appropriate for changing the text. It is possible for a ``"postchange"`` event handler to get into a loop of changing the text and then being called when the event is re-emitted. It is up to the event handler to guard against this case (for instance, by not changing the text if it is signaled for for text that it has already changed once). """ # (this variable is picked up by the MetaSignals metaclass) signals = ["change", "postchange"] def valid_char(self, ch): """ Filter for text that may be entered into this widget by the user :param ch: character to be inserted :type ch: bytes or unicode This implementation returns True for all printable characters. """ return is_wide_char(ch,0) or (len(ch)==1 and ord(ch) >= 32) def selectable(self): return True def __init__(self, caption=u"", edit_text=u"", multiline=False, align=LEFT, wrap=SPACE, allow_tab=False, edit_pos=None, layout=None, mask=None): """ :param caption: markup for caption preceding edit_text, see :class:`Text` for description of text markup. :type caption: text markup :param edit_text: initial text for editing, type (bytes or unicode) must match the text in the caption :type edit_text: bytes or unicode :param multiline: True: 'enter' inserts newline False: return it :type multiline: bool :param align: typically 'left', 'center' or 'right' :type align: text alignment mode :param wrap: typically 'space', 'any' or 'clip' :type wrap: text wrapping mode :param allow_tab: True: 'tab' inserts 1-8 spaces False: return it :type allow_tab: bool :param edit_pos: initial position for cursor, None:end of edit_text :type edit_pos: int :param layout: defaults to a shared :class:`StandardTextLayout` instance :type layout: text layout instance :param mask: hide text entered with this character, None:disable mask :type mask: bytes or unicode >>> Edit() >>> Edit(u"Y/n? ", u"yes") >>> Edit(u"Name ", u"Smith", edit_pos=1) >>> Edit(u"", u"3.14", align='right') """ self.__super.__init__("", align, wrap, layout) self.multiline = multiline self.allow_tab = allow_tab self._edit_pos = 0 self.set_caption(caption) self._edit_text = '' self.set_edit_text(edit_text) if edit_pos is None: edit_pos = len(edit_text) self.set_edit_pos(edit_pos) self.set_mask(mask) self._shift_view_to_cursor = False def _repr_words(self): return self.__super._repr_words()[:-1] + [ python3_repr(self._edit_text)] + [ 'caption=' + python3_repr(self._caption)] * bool(self._caption) + [ 'multiline'] * (self.multiline is True) def _repr_attrs(self): attrs = dict(self.__super._repr_attrs(), edit_pos=self._edit_pos) return remove_defaults(attrs, Edit.__init__) def get_text(self): """ Returns ``(text, display attributes)``. See :meth:`Text.get_text` for details. Text returned includes the caption and edit_text, possibly masked. >>> Edit(u"What? ","oh, nothing.").get_text() # ... = u in Python 2 (...'What? oh, nothing.', []) >>> Edit(('bright',u"user@host:~$ "),"ls").get_text() (...'user@host:~$ ls', [('bright', 13)]) >>> Edit(u"password:", u"seekrit", mask=u"*").get_text() (...'password:*******', []) """ if self._mask is None: return self._caption + self._edit_text, self._attrib else: return self._caption + (self._mask * len(self._edit_text)), self._attrib def set_text(self, markup): """ Not supported by Edit widget. >>> Edit().set_text("test") Traceback (most recent call last): EditError: set_text() not supported. Use set_caption() or set_edit_text() instead. """ # FIXME: this smells. reimplement Edit as a WidgetWrap subclass to # clean this up # hack to let Text.__init__() work if not hasattr(self, '_text') and markup == "": self._text = None return raise EditError("set_text() not supported. Use set_caption()" " or set_edit_text() instead.") def get_pref_col(self, size): """ Return the preferred column for the cursor, or the current cursor x value. May also return ``'left'`` or ``'right'`` to indicate the leftmost or rightmost column available. This method is used internally and by other widgets when moving the cursor up or down between widgets so that the column selected is one that the user would expect. >>> size = (10,) >>> Edit().get_pref_col(size) 0 >>> e = Edit(u"", u"word") >>> e.get_pref_col(size) 4 >>> e.keypress(size, 'left') >>> e.get_pref_col(size) 3 >>> e.keypress(size, 'end') >>> e.get_pref_col(size) 'right' >>> e = Edit(u"", u"2\\nwords") >>> e.keypress(size, 'left') >>> e.keypress(size, 'up') >>> e.get_pref_col(size) 4 >>> e.keypress(size, 'left') >>> e.get_pref_col(size) 0 """ (maxcol,) = size pref_col, then_maxcol = self.pref_col_maxcol if then_maxcol != maxcol: return self.get_cursor_coords((maxcol,))[0] else: return pref_col def update_text(self): """ No longer supported. >>> Edit().update_text() Traceback (most recent call last): EditError: update_text() has been removed. Use set_caption() or set_edit_text() instead. """ raise EditError("update_text() has been removed. Use " "set_caption() or set_edit_text() instead.") def set_caption(self, caption): """ Set the caption markup for this widget. :param caption: markup for caption preceding edit_text, see :meth:`Text.__init__` for description of text markup. >>> e = Edit("") >>> e.set_caption("cap1") >>> print(e.caption) cap1 >>> e.set_caption(('bold', "cap2")) >>> print(e.caption) cap2 >>> e.attrib [('bold', 4)] >>> e.caption = "cap3" # not supported because caption stores text but set_caption() takes markup Traceback (most recent call last): AttributeError: can't set attribute """ self._caption, self._attrib = decompose_tagmarkup(caption) self._invalidate() caption = property(lambda self:self._caption, doc=""" Read-only property returning the caption for this widget. """) def set_edit_pos(self, pos): """ Set the cursor position with a self.edit_text offset. Clips pos to [0, len(edit_text)]. :param pos: cursor position :type pos: int >>> e = Edit(u"", u"word") >>> e.edit_pos 4 >>> e.set_edit_pos(2) >>> e.edit_pos 2 >>> e.edit_pos = -1 # Urwid 0.9.9 or later >>> e.edit_pos 0 >>> e.edit_pos = 20 >>> e.edit_pos 4 """ if pos < 0: pos = 0 if pos > len(self._edit_text): pos = len(self._edit_text) self.highlight = None self.pref_col_maxcol = None, None self._edit_pos = pos self._invalidate() edit_pos = property(lambda self:self._edit_pos, set_edit_pos, doc=""" Property controlling the edit position for this widget. """) def set_mask(self, mask): """ Set the character for masking text away. :param mask: hide text entered with this character, None:disable mask :type mask: bytes or unicode """ self._mask = mask self._invalidate() def set_edit_text(self, text): """ Set the edit text for this widget. :param text: text for editing, type (bytes or unicode) must match the text in the caption :type text: bytes or unicode >>> e = Edit() >>> e.set_edit_text(u"yes") >>> print(e.edit_text) yes >>> e >>> e.edit_text = u"no" # Urwid 0.9.9 or later >>> print(e.edit_text) no """ text = self._normalize_to_caption(text) self.highlight = None self._emit("change", text) old_text = self._edit_text self._edit_text = text if self.edit_pos > len(text): self.edit_pos = len(text) self._emit("postchange", old_text) self._invalidate() def get_edit_text(self): """ Return the edit text for this widget. >>> e = Edit(u"What? ", u"oh, nothing.") >>> print(e.get_edit_text()) oh, nothing. >>> print(e.edit_text) oh, nothing. """ return self._edit_text edit_text = property(get_edit_text, set_edit_text, doc=""" Property controlling the edit text for this widget. """) def insert_text(self, text): """ Insert text at the cursor position and update cursor. This method is used by the keypress() method when inserting one or more characters into edit_text. :param text: text for inserting, type (bytes or unicode) must match the text in the caption :type text: bytes or unicode >>> e = Edit(u"", u"42") >>> e.insert_text(u".5") >>> e >>> e.set_edit_pos(2) >>> e.insert_text(u"a") >>> print(e.edit_text) 42a.5 """ text = self._normalize_to_caption(text) result_text, result_pos = self.insert_text_result(text) self.set_edit_text(result_text) self.set_edit_pos(result_pos) self.highlight = None def _normalize_to_caption(self, text): """ Return text converted to the same type as self.caption (bytes or unicode) """ tu = isinstance(text, text_type) cu = isinstance(self._caption, text_type) if tu == cu: return text if tu: return text.encode('ascii') # follow python2's implicit conversion return text.decode('ascii') def insert_text_result(self, text): """ Return result of insert_text(text) without actually performing the insertion. Handy for pre-validation. :param text: text for inserting, type (bytes or unicode) must match the text in the caption :type text: bytes or unicode """ # if there's highlighted text, it'll get replaced by the new text text = self._normalize_to_caption(text) if self.highlight: start, stop = self.highlight btext, etext = self.edit_text[:start], self.edit_text[stop:] result_text = btext + etext result_pos = start else: result_text = self.edit_text result_pos = self.edit_pos try: result_text = (result_text[:result_pos] + text + result_text[result_pos:]) except: assert 0, repr((self.edit_text, result_text, text)) result_pos += len(text) return (result_text, result_pos) def keypress(self, size, key): """ Handle editing keystrokes, return others. >>> e, size = Edit(), (20,) >>> e.keypress(size, 'x') >>> e.keypress(size, 'left') >>> e.keypress(size, '1') >>> print(e.edit_text) 1x >>> e.keypress(size, 'backspace') >>> e.keypress(size, 'end') >>> e.keypress(size, '2') >>> print(e.edit_text) x2 >>> e.keypress(size, 'shift f1') 'shift f1' """ (maxcol,) = size p = self.edit_pos if self.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.insert_text(key) elif key=="tab" and self.allow_tab: key = " "*(8-(self.edit_pos%8)) self.insert_text(key) elif key=="enter" and self.multiline: key = "\n" self.insert_text(key) elif self._command_map[key] == CURSOR_LEFT: if p==0: return key p = move_prev_char(self.edit_text,0,p) self.set_edit_pos(p) elif self._command_map[key] == CURSOR_RIGHT: if p >= len(self.edit_text): return key p = move_next_char(self.edit_text,p,len(self.edit_text)) self.set_edit_pos(p) elif self._command_map[key] in (CURSOR_UP, CURSOR_DOWN): self.highlight = None x,y = self.get_cursor_coords((maxcol,)) pref_col = self.get_pref_col((maxcol,)) assert pref_col is not None #if pref_col is None: # pref_col = x if self._command_map[key] == CURSOR_UP: y -= 1 else: y += 1 if not self.move_cursor_to_coords((maxcol,),pref_col,y): return key elif key=="backspace": self.pref_col_maxcol = None, None if not self._delete_highlighted(): if p == 0: return key p = move_prev_char(self.edit_text,0,p) self.set_edit_text( self.edit_text[:p] + self.edit_text[self.edit_pos:] ) self.set_edit_pos( p ) elif key=="delete": self.pref_col_maxcol = None, None if not self._delete_highlighted(): if p >= len(self.edit_text): return key p = move_next_char(self.edit_text,p,len(self.edit_text)) self.set_edit_text( self.edit_text[:self.edit_pos] + self.edit_text[p:] ) elif self._command_map[key] in (CURSOR_MAX_LEFT, CURSOR_MAX_RIGHT): self.highlight = None self.pref_col_maxcol = None, None x,y = self.get_cursor_coords((maxcol,)) if self._command_map[key] == CURSOR_MAX_LEFT: self.move_cursor_to_coords((maxcol,), LEFT, y) else: self.move_cursor_to_coords((maxcol,), RIGHT, y) return else: # key wasn't handled return key def move_cursor_to_coords(self, size, x, y): """ Set the cursor position with (x,y) coordinates. Returns True if move succeeded, False otherwise. >>> size = (10,) >>> e = Edit("","edit\\ntext") >>> e.move_cursor_to_coords(size, 5, 0) True >>> e.edit_pos 4 >>> e.move_cursor_to_coords(size, 5, 3) False >>> e.move_cursor_to_coords(size, 0, 1) True >>> e.edit_pos 5 """ (maxcol,) = size trans = self.get_line_translation(maxcol) top_x, top_y = self.position_coords(maxcol, 0) if y < top_y or y >= len(trans): return False pos = calc_pos( self.get_text()[0], trans, x, y ) e_pos = pos - len(self.caption) if e_pos < 0: e_pos = 0 if e_pos > len(self.edit_text): e_pos = len(self.edit_text) self.edit_pos = e_pos self.pref_col_maxcol = x, maxcol self._invalidate() return True def mouse_event(self, size, event, button, x, y, focus): """ Move the cursor to the location clicked for button 1. >>> size = (20,) >>> e = Edit("","words here") >>> e.mouse_event(size, 'mouse press', 1, 2, 0, True) True >>> e.edit_pos 2 """ (maxcol,) = size if button==1: return self.move_cursor_to_coords( (maxcol,), x, y ) def _delete_highlighted(self): """ Delete all highlighted text and update cursor position, if any text is highlighted. """ if not self.highlight: return start, stop = self.highlight btext, etext = self.edit_text[:start], self.edit_text[stop:] self.set_edit_text( btext + etext ) self.edit_pos = start self.highlight = None return True def render(self, size, focus=False): """ Render edit widget and return canvas. Include cursor when in focus. >>> c = Edit("? ","yes").render((10,), focus=True) >>> c.text # ... = b in Python 3 [...'? yes '] >>> c.cursor (5, 0) """ (maxcol,) = size self._shift_view_to_cursor = bool(focus) canv = Text.render(self,(maxcol,)) if focus: canv = CompositeCanvas(canv) canv.cursor = self.get_cursor_coords((maxcol,)) # .. will need to FIXME if I want highlight to work again #if self.highlight: # hstart, hstop = self.highlight_coords() # d.coords['highlight'] = [ hstart, hstop ] return canv def get_line_translation(self, maxcol, ta=None ): trans = Text.get_line_translation(self, maxcol, ta) if not self._shift_view_to_cursor: return trans text, ignore = self.get_text() x,y = calc_coords( text, trans, self.edit_pos + len(self.caption) ) if x < 0: return ( trans[:y] + [shift_line(trans[y],-x)] + trans[y+1:] ) elif x >= maxcol: return ( trans[:y] + [shift_line(trans[y],-(x-maxcol+1))] + trans[y+1:] ) return trans def get_cursor_coords(self, size): """ Return the (*x*, *y*) coordinates of cursor within widget. >>> Edit("? ","yes").get_cursor_coords((10,)) (5, 0) """ (maxcol,) = size self._shift_view_to_cursor = True return self.position_coords(maxcol,self.edit_pos) def position_coords(self,maxcol,pos): """ Return (*x*, *y*) coordinates for an offset into self.edit_text. """ p = pos + len(self.caption) trans = self.get_line_translation(maxcol) x,y = calc_coords(self.get_text()[0], trans,p) return x,y class IntEdit(Edit): """Edit widget for integer values""" def valid_char(self, ch): """ Return true for decimal digits. """ return len(ch)==1 and ch in "0123456789" def __init__(self,caption="",default=None): """ caption -- caption markup default -- default edit value >>> IntEdit(u"", 42) """ if default is not None: val = str(default) else: val = "" self.__super.__init__(caption,val) def keypress(self, size, key): """ Handle editing keystrokes. Remove leading zeros. >>> e, size = IntEdit(u"", 5002), (10,) >>> e.keypress(size, 'home') >>> e.keypress(size, 'delete') >>> print(e.edit_text) 002 >>> e.keypress(size, 'end') >>> print(e.edit_text) 2 """ (maxcol,) = size unhandled = Edit.keypress(self,(maxcol,),key) if not unhandled: # trim leading zeros while self.edit_pos > 0 and self.edit_text[:1] == "0": self.set_edit_pos( self.edit_pos - 1) self.set_edit_text(self.edit_text[1:]) return unhandled def value(self): """ Return the numeric value of self.edit_text. >>> e, size = IntEdit(), (10,) >>> e.keypress(size, '5') >>> e.keypress(size, '1') >>> e.value() == 51 True """ if self.edit_text: return int(self.edit_text) else: return 0 def delegate_to_widget_mixin(attribute_name): """ Return a mixin class that delegates all standard widget methods to an attribute given by attribute_name. This mixin is designed to be used as a superclass of another widget. """ # FIXME: this is so common, let's add proper support for it # when layout and rendering are separated get_delegate = attrgetter(attribute_name) class DelegateToWidgetMixin(Widget): no_cache = ["rows"] # crufty metaclass work-around def render(self, size, focus=False): canv = get_delegate(self).render(size, focus=focus) return CompositeCanvas(canv) selectable = property(lambda self:get_delegate(self).selectable) get_cursor_coords = property( lambda self:get_delegate(self).get_cursor_coords) get_pref_col = property(lambda self:get_delegate(self).get_pref_col) keypress = property(lambda self:get_delegate(self).keypress) move_cursor_to_coords = property( lambda self:get_delegate(self).move_cursor_to_coords) rows = property(lambda self:get_delegate(self).rows) mouse_event = property(lambda self:get_delegate(self).mouse_event) sizing = property(lambda self:get_delegate(self).sizing) pack = property(lambda self:get_delegate(self).pack) return DelegateToWidgetMixin class WidgetWrapError(Exception): pass class WidgetWrap(delegate_to_widget_mixin('_wrapped_widget'), Widget): def __init__(self, w): """ w -- widget to wrap, stored as self._w This object will pass the functions defined in Widget interface definition to self._w. The purpose of this widget is to provide a base class for widgets that compose other widgets for their display and behaviour. The details of that composition should not affect users of the subclass. The subclass may decide to expose some of the wrapped widgets by behaving like a ContainerWidget or WidgetDecoration, or it may hide them from outside access. """ self._wrapped_widget = w def _set_w(self, w): """ Change the wrapped widget. This is meant to be called only by subclasses. >>> size = (10,) >>> ww = WidgetWrap(Edit("hello? ","hi")) >>> ww.render(size).text # ... = b in Python 3 [...'hello? hi '] >>> ww.selectable() True >>> ww._w = Text("goodbye") # calls _set_w() >>> ww.render(size).text [...'goodbye '] >>> ww.selectable() False """ self._wrapped_widget = w self._invalidate() _w = property(lambda self:self._wrapped_widget, _set_w) def _raise_old_name_error(self, val=None): raise WidgetWrapError("The WidgetWrap.w member variable has " "been renamed to WidgetWrap._w (not intended for use " "outside the class and its subclasses). " "Please update your code to use self._w " "instead of self.w.") w = property(_raise_old_name_error, _raise_old_name_error) def _test(): import doctest doctest.testmod() if __name__=='__main__': _test() urwid-2.0.1/urwid/util.py0000644000175000017500000003170113231170672016775 0ustar andersonanderson00000000000000#!/usr/bin/python # -*- coding: utf-8 -*- # # Urwid utility functions # 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 import escape from urwid.compat import bytes, text_type, text_types import codecs str_util = escape.str_util # bring str_util functions into our namespace calc_text_pos = str_util.calc_text_pos calc_width = str_util.calc_width is_wide_char = str_util.is_wide_char move_next_char = str_util.move_next_char move_prev_char = str_util.move_prev_char within_double_byte = str_util.within_double_byte def detect_encoding(): # Try to determine if using a supported double-byte encoding import locale try: try: locale.setlocale(locale.LC_ALL, "") except locale.Error: pass return locale.getlocale()[1] or "" except ValueError as e: # with invalid LANG value python will throw ValueError if e.args and e.args[0].startswith("unknown locale"): return "" else: raise if 'detected_encoding' not in locals(): detected_encoding = detect_encoding() else: assert 0, "It worked!" _target_encoding = None _use_dec_special = True def set_encoding( encoding ): """ Set the byte encoding to assume when processing strings and the encoding to use when converting unicode strings. """ encoding = encoding.lower() global _target_encoding, _use_dec_special if encoding in ( 'utf-8', 'utf8', 'utf' ): str_util.set_byte_encoding("utf8") _use_dec_special = False elif encoding in ( 'euc-jp' # JISX 0208 only , 'euc-kr', 'euc-cn', 'euc-tw' # CNS 11643 plain 1 only , 'gb2312', 'gbk', 'big5', 'cn-gb', 'uhc' # these shouldn't happen, should they? , 'eucjp', 'euckr', 'euccn', 'euctw', 'cncb' ): str_util.set_byte_encoding("wide") _use_dec_special = True else: str_util.set_byte_encoding("narrow") _use_dec_special = True # if encoding is valid for conversion from unicode, remember it _target_encoding = 'ascii' try: if encoding: u"".encode(encoding) _target_encoding = encoding except LookupError: pass def get_encoding_mode(): """ Get the mode Urwid is using when processing text strings. Returns 'narrow' for 8-bit encodings, 'wide' for CJK encodings or 'utf8' for UTF-8 encodings. """ return str_util.get_byte_encoding() def apply_target_encoding( s ): """ Return (encoded byte string, character set rle). """ if _use_dec_special and type(s) == text_type: # first convert drawing characters try: s = s.translate( escape.DEC_SPECIAL_CHARMAP ) except NotImplementedError: # python < 2.4 needs to do this the hard way.. for c, alt in zip(escape.DEC_SPECIAL_CHARS, escape.ALT_DEC_SPECIAL_CHARS): s = s.replace( c, escape.SO+alt+escape.SI ) if type(s) == text_type: s = s.replace(escape.SI+escape.SO, u"") # remove redundant shifts s = codecs.encode(s, _target_encoding, 'replace') assert isinstance(s, bytes) SO = escape.SO.encode('ascii') SI = escape.SI.encode('ascii') sis = s.split(SO) assert isinstance(sis[0], bytes) sis0 = sis[0].replace(SI, bytes()) sout = [] cout = [] if sis0: sout.append( sis0 ) cout.append( (None,len(sis0)) ) if len(sis)==1: return sis0, cout for sn in sis[1:]: assert isinstance(sn, bytes) assert isinstance(SI, bytes) sl = sn.split(SI, 1) if len(sl) == 1: sin = sl[0] assert isinstance(sin, bytes) sout.append(sin) rle_append_modify(cout, (escape.DEC_TAG.encode('ascii'), len(sin))) continue sin, son = sl son = son.replace(SI, bytes()) if sin: sout.append(sin) rle_append_modify(cout, (escape.DEC_TAG, len(sin))) if son: sout.append(son) rle_append_modify(cout, (None, len(son))) outstr = bytes().join(sout) return outstr, cout ###################################################################### # Try to set the encoding using the one detected by the locale module set_encoding( detected_encoding ) ###################################################################### def supports_unicode(): """ Return True if python is able to convert non-ascii unicode strings to the current encoding. """ return _target_encoding and _target_encoding != 'ascii' def calc_trim_text( text, start_offs, end_offs, start_col, end_col ): """ Calculate the result of trimming text. start_offs -- offset into text to treat as screen column 0 end_offs -- offset into text to treat as the end of the line start_col -- screen column to trim at the left end_col -- screen column to trim at the right Returns (start, end, pad_left, pad_right), where: start -- resulting start offset end -- resulting end offset pad_left -- 0 for no pad or 1 for one space to be added pad_right -- 0 for no pad or 1 for one space to be added """ spos = start_offs pad_left = pad_right = 0 if start_col > 0: spos, sc = calc_text_pos( text, spos, end_offs, start_col ) if sc < start_col: pad_left = 1 spos, sc = calc_text_pos( text, start_offs, end_offs, start_col+1 ) run = end_col - start_col - pad_left pos, sc = calc_text_pos( text, spos, end_offs, run ) if sc < run: pad_right = 1 return ( spos, pos, pad_left, pad_right ) def trim_text_attr_cs( text, attr, cs, start_col, end_col ): """ Return ( trimmed text, trimmed attr, trimmed cs ). """ spos, epos, pad_left, pad_right = calc_trim_text( text, 0, len(text), start_col, end_col ) attrtr = rle_subseg( attr, spos, epos ) cstr = rle_subseg( cs, spos, epos ) if pad_left: al = rle_get_at( attr, spos-1 ) rle_append_beginning_modify( attrtr, (al, 1) ) rle_append_beginning_modify( cstr, (None, 1) ) if pad_right: al = rle_get_at( attr, epos ) rle_append_modify( attrtr, (al, 1) ) rle_append_modify( cstr, (None, 1) ) return (bytes().rjust(pad_left) + text[spos:epos] + bytes().rjust(pad_right), attrtr, cstr) def rle_get_at( rle, pos ): """ Return the attribute at offset pos. """ x = 0 if pos < 0: return None for a, run in rle: if x+run > pos: return a x += run return None def rle_subseg( rle, start, end ): """Return a sub segment of an rle list.""" l = [] x = 0 for a, run in rle: if start: if start >= run: start -= run x += run continue x += start run -= start start = 0 if x >= end: break if x+run > end: run = end-x x += run l.append( (a, run) ) return l def rle_len( rle ): """ Return the number of characters covered by a run length encoded attribute list. """ run = 0 for v in rle: assert type(v) == tuple, repr(rle) a, r = v run += r return run def rle_append_beginning_modify(rle, a_r): """ Append (a, r) (unpacked from *a_r*) to BEGINNING of rle. Merge with first run when possible MODIFIES rle parameter contents. Returns None. """ a, r = a_r if not rle: rle[:] = [(a, r)] else: al, run = rle[0] if a == al: rle[0] = (a,run+r) else: rle[0:0] = [(al, r)] def rle_append_modify(rle, a_r): """ Append (a, r) (unpacked from *a_r*) to the rle list rle. Merge with last run when possible. MODIFIES rle parameter contents. Returns None. """ a, r = a_r if not rle or rle[-1][0] != a: rle.append( (a,r) ) return la,lr = rle[-1] rle[-1] = (a, lr+r) def rle_join_modify( rle, rle2 ): """ Append attribute list rle2 to rle. Merge last run of rle with first run of rle2 when possible. MODIFIES attr parameter contents. Returns None. """ if not rle2: return rle_append_modify(rle, rle2[0]) rle += rle2[1:] def rle_product( rle1, rle2 ): """ Merge the runs of rle1 and rle2 like this: eg. rle1 = [ ("a", 10), ("b", 5) ] rle2 = [ ("Q", 5), ("P", 10) ] rle_product: [ (("a","Q"), 5), (("a","P"), 5), (("b","P"), 5) ] rle1 and rle2 are assumed to cover the same total run. """ i1 = i2 = 1 # rle1, rle2 indexes if not rle1 or not rle2: return [] a1, r1 = rle1[0] a2, r2 = rle2[0] l = [] while r1 and r2: r = min(r1, r2) rle_append_modify( l, ((a1,a2),r) ) r1 -= r if r1 == 0 and i1< len(rle1): a1, r1 = rle1[i1] i1 += 1 r2 -= r if r2 == 0 and i2< len(rle2): a2, r2 = rle2[i2] i2 += 1 return l def rle_factor( rle ): """ Inverse of rle_product. """ rle1 = [] rle2 = [] for (a1, a2), r in rle: rle_append_modify( rle1, (a1, r) ) rle_append_modify( rle2, (a2, r) ) return rle1, rle2 class TagMarkupException(Exception): pass def decompose_tagmarkup(tm): """Return (text string, attribute list) for tagmarkup passed.""" tl, al = _tagmarkup_recurse(tm, None) # join as unicode or bytes based on type of first element text = tl[0][:0].join(tl) if al and al[-1][0] is None: del al[-1] return text, al def _tagmarkup_recurse( tm, attr ): """Return (text list, attribute list) for tagmarkup passed. tm -- tagmarkup attr -- current attribute or None""" if type(tm) == list: # for lists recurse to process each subelement rtl = [] ral = [] for element in tm: tl, al = _tagmarkup_recurse( element, attr ) if ral: # merge attributes when possible last_attr, last_run = ral[-1] top_attr, top_run = al[0] if last_attr == top_attr: ral[-1] = (top_attr, last_run + top_run) del al[-1] rtl += tl ral += al return rtl, ral if type(tm) == tuple: # tuples mark a new attribute boundary if len(tm) != 2: raise TagMarkupException("Tuples must be in the form (attribute, tagmarkup): %r" % (tm,)) attr, element = tm return _tagmarkup_recurse( element, attr ) if not isinstance(tm, text_types + (bytes,)): raise TagMarkupException("Invalid markup element: %r" % tm) # text return [tm], [(attr, len(tm))] def is_mouse_event( ev ): return type(ev) == tuple and len(ev)==4 and ev[0].find("mouse")>=0 def is_mouse_press( ev ): return ev.find("press")>=0 class MetaSuper(type): """adding .__super""" def __init__(cls, name, bases, d): super(MetaSuper, cls).__init__(name, bases, d) if hasattr(cls, "_%s__super" % name): raise AttributeError("Class has same name as one of its super classes") setattr(cls, "_%s__super" % name, super(cls)) def int_scale(val, val_range, out_range): """ Scale val in the range [0, val_range-1] to an integer in the range [0, out_range-1]. This implementation uses the "round-half-up" rounding method. >>> "%x" % int_scale(0x7, 0x10, 0x10000) '7777' >>> "%x" % int_scale(0x5f, 0x100, 0x10) '6' >>> int_scale(2, 6, 101) 40 >>> int_scale(1, 3, 4) 2 """ num = int(val * (out_range-1) * 2 + (val_range-1)) dem = ((val_range-1) * 2) # if num % dem == 0 then we are exactly half-way and have rounded up. return num // dem class StoppingContext(object): """Context manager that calls ``stop`` on a given object on exit. Used to make the ``start`` method on `MainLoop` and `BaseScreen` optionally act as context managers. """ def __init__(self, wrapped): self._wrapped = wrapped def __enter__(self): return self def __exit__(self, *exc_info): self._wrapped.stop() urwid-2.0.1/urwid/old_str_util.py0000755000175000017500000002351713231170672020534 0ustar andersonanderson00000000000000#!/usr/bin/python # -*- coding: utf-8 -*- # # Urwid unicode character processing tables # 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 import re from urwid.compat import bytes, B, ord2, text_type SAFE_ASCII_RE = re.compile(u"^[ -~]*$") SAFE_ASCII_BYTES_RE = re.compile(B("^[ -~]*$")) _byte_encoding = None # GENERATED DATA # generated from # http://www.unicode.org/Public/4.0-Update/EastAsianWidth-4.0.0.txt widths = [ (126, 1), (159, 0), (687, 1), (710, 0), (711, 1), (727, 0), (733, 1), (879, 0), (1154, 1), (1161, 0), (4347, 1), (4447, 2), (7467, 1), (7521, 0), (8369, 1), (8426, 0), (9000, 1), (9002, 2), (11021, 1), (12350, 2), (12351, 1), (12438, 2), (12442, 0), (19893, 2), (19967, 1), (55203, 2), (63743, 1), (64106, 2), (65039, 1), (65059, 0), (65131, 2), (65279, 1), (65376, 2), (65500, 1), (65510, 2), (120831, 1), (262141, 2), (1114109, 1), ] # ACCESSOR FUNCTIONS def get_width( o ): """Return the screen column width for unicode ordinal o.""" global widths if o == 0xe or o == 0xf: return 0 for num, wid in widths: if o <= num: return wid return 1 def decode_one( text, pos ): """ Return (ordinal at pos, next position) for UTF-8 encoded text. """ assert isinstance(text, bytes), text b1 = ord2(text[pos]) if not b1 & 0x80: return b1, pos+1 error = ord("?"), pos+1 lt = len(text) lt = lt-pos if lt < 2: return error if b1 & 0xe0 == 0xc0: b2 = ord2(text[pos+1]) if b2 & 0xc0 != 0x80: return error o = ((b1&0x1f)<<6)|(b2&0x3f) if o < 0x80: return error return o, pos+2 if lt < 3: return error if b1 & 0xf0 == 0xe0: b2 = ord2(text[pos+1]) if b2 & 0xc0 != 0x80: return error b3 = ord2(text[pos+2]) if b3 & 0xc0 != 0x80: return error o = ((b1&0x0f)<<12)|((b2&0x3f)<<6)|(b3&0x3f) if o < 0x800: return error return o, pos+3 if lt < 4: return error if b1 & 0xf8 == 0xf0: b2 = ord2(text[pos+1]) if b2 & 0xc0 != 0x80: return error b3 = ord2(text[pos+2]) if b3 & 0xc0 != 0x80: return error b4 = ord2(text[pos+2]) if b4 & 0xc0 != 0x80: return error o = ((b1&0x07)<<18)|((b2&0x3f)<<12)|((b3&0x3f)<<6)|(b4&0x3f) if o < 0x10000: return error return o, pos+4 return error def decode_one_uni(text, i): """ decode_one implementation for unicode strings """ return ord(text[i]), i+1 def decode_one_right(text, pos): """ Return (ordinal at pos, next position) for UTF-8 encoded text. pos is assumed to be on the trailing byte of a utf-8 sequence. """ assert isinstance(text, bytes), text error = ord("?"), pos-1 p = pos while p >= 0: if ord2(text[p])&0xc0 != 0x80: o, next = decode_one( text, p ) return o, p-1 p -=1 if p == p-4: return error def set_byte_encoding(enc): assert enc in ('utf8', 'narrow', 'wide') global _byte_encoding _byte_encoding = enc def get_byte_encoding(): return _byte_encoding def calc_text_pos(text, start_offs, end_offs, pref_col): """ Calculate the closest position to the screen column pref_col in text where start_offs is the offset into text assumed to be screen column 0 and end_offs is the end of the range to search. text may be unicode or a byte string in the target _byte_encoding Returns (position, actual_col). """ assert start_offs <= end_offs, repr((start_offs, end_offs)) utfs = isinstance(text, bytes) and _byte_encoding == "utf8" unis = not isinstance(text, bytes) if unis or utfs: decode = [decode_one, decode_one_uni][unis] i = start_offs sc = 0 n = 1 # number to advance by while i < end_offs: o, n = decode(text, i) w = get_width(o) if w+sc > pref_col: return i, sc i = n sc += w return i, sc assert type(text) == bytes, repr(text) # "wide" and "narrow" i = start_offs+pref_col if i >= end_offs: return end_offs, end_offs-start_offs if _byte_encoding == "wide": if within_double_byte(text, start_offs, i) == 2: i -= 1 return i, i-start_offs def calc_width(text, start_offs, end_offs): """ Return the screen column width of text between start_offs and end_offs. text may be unicode or a byte string in the target _byte_encoding Some characters are wide (take two columns) and others affect the previous character (take zero columns). Use the widths table above to calculate the screen column width of text[start_offs:end_offs] """ assert start_offs <= end_offs, repr((start_offs, end_offs)) utfs = isinstance(text, bytes) and _byte_encoding == "utf8" unis = not isinstance(text, bytes) if (unis and not SAFE_ASCII_RE.match(text) ) or (utfs and not SAFE_ASCII_BYTES_RE.match(text)): decode = [decode_one, decode_one_uni][unis] i = start_offs sc = 0 n = 1 # number to advance by while i < end_offs: o, n = decode(text, i) w = get_width(o) i = n sc += w return sc # "wide", "narrow" or all printable ASCII, just return the character count return end_offs - start_offs def is_wide_char(text, offs): """ Test if the character at offs within text is wide. text may be unicode or a byte string in the target _byte_encoding """ if isinstance(text, text_type): o = ord(text[offs]) return get_width(o) == 2 assert isinstance(text, bytes) if _byte_encoding == "utf8": o, n = decode_one(text, offs) return get_width(o) == 2 if _byte_encoding == "wide": return within_double_byte(text, offs, offs) == 1 return False def move_prev_char(text, start_offs, end_offs): """ Return the position of the character before end_offs. """ assert start_offs < end_offs if isinstance(text, text_type): return end_offs-1 assert isinstance(text, bytes) if _byte_encoding == "utf8": o = end_offs-1 while ord2(text[o])&0xc0 == 0x80: o -= 1 return o if _byte_encoding == "wide" and within_double_byte(text, start_offs, end_offs-1) == 2: return end_offs-2 return end_offs-1 def move_next_char(text, start_offs, end_offs): """ Return the position of the character after start_offs. """ assert start_offs < end_offs if isinstance(text, text_type): return start_offs+1 assert isinstance(text, bytes) if _byte_encoding == "utf8": o = start_offs+1 while o= 0x40 and v < 0x7f: # might be second half of big5, uhc or gbk encoding if pos == line_start: return 0 if ord2(text[pos-1]) >= 0x81: if within_double_byte(text, line_start, pos-1) == 1: return 2 return 0 if v < 0x80: return 0 i = pos -1 while i >= line_start: if ord2(text[i]) < 0x80: break i -= 1 if (pos - i) & 1: return 1 return 2 # TABLE GENERATION CODE def process_east_asian_width(): import sys out = [] last = None for line in sys.stdin.readlines(): if line[:1] == "#": continue line = line.strip() hex,rest = line.split(";",1) wid,rest = rest.split(" # ",1) word1 = rest.split(" ",1)[0] if "." in hex: hex = hex.split("..")[1] num = int(hex, 16) if word1 in ("COMBINING","MODIFIER",""): l = 0 elif wid in ("W", "F"): l = 2 else: l = 1 if last is None: out.append((0, l)) last = l if last == l: out[-1] = (num, l) else: out.append( (num, l) ) last = l print("widths = [") for o in out[1:]: # treat control characters same as ascii print("\t%r," % (o,)) print("]") if __name__ == "__main__": process_east_asian_width() urwid-2.0.1/urwid/container.py0000755000175000017500000024606213231170672020015 0ustar andersonanderson00000000000000#!/usr/bin/python # # Urwid container widget classes # Copyright (C) 2004-2012 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 itertools import chain, repeat from urwid.compat import xrange from urwid.util import is_mouse_press from urwid.widget import (Widget, Divider, FLOW, FIXED, PACK, BOX, WidgetWrap, GIVEN, WEIGHT, LEFT, RIGHT, RELATIVE, TOP, BOTTOM, CLIP, RELATIVE_100) from urwid.decoration import (Padding, Filler, calculate_left_right_padding, calculate_top_bottom_filler, normalize_align, normalize_width, normalize_valign, normalize_height, simplify_align, simplify_width, simplify_valign, simplify_height) from urwid.monitored_list import MonitoredList, MonitoredFocusList from urwid.canvas import (CompositeCanvas, CanvasOverlay, CanvasCombine, SolidCanvas, CanvasJoin) class WidgetContainerMixin(object): """ Mixin class for widget containers implementing common container methods """ def __getitem__(self, position): """ Container short-cut for self.contents[position][0].base_widget which means "give me the child widget at position without any widget decorations". This allows for concise traversal of nested container widgets such as: my_widget[position0][position1][position2] ... """ return self.contents[position][0].base_widget def get_focus_path(self): """ Return the .focus_position values starting from this container and proceeding along each child widget until reaching a leaf (non-container) widget. """ out = [] w = self while True: try: p = w.focus_position except IndexError: return out out.append(p) w = w.focus.base_widget def set_focus_path(self, positions): """ Set the .focus_position property starting from this container widget and proceeding along newly focused child widgets. Any failed assignment due do incompatible position types or invalid positions will raise an IndexError. This method may be used to restore a particular widget to the focus by passing in the value returned from an earlier call to get_focus_path(). positions -- sequence of positions """ w = self for p in positions: if p != w.focus_position: w.focus_position = p # modifies w.focus w = w.focus.base_widget def get_focus_widgets(self): """ Return the .focus values starting from this container and proceeding along each child widget until reaching a leaf (non-container) widget. Note that the list does not contain the topmost container widget (i.e., on which this method is called), but does include the lowest leaf widget. """ out = [] w = self while True: w = w.base_widget.focus if w is None: return out out.append(w) class WidgetContainerListContentsMixin(object): """ Mixin class for widget containers whose positions are indexes into a list available as self.contents. """ def __iter__(self): """ Return an iterable of positions for this container from first to last. """ return iter(xrange(len(self.contents))) def __reversed__(self): """ Return an iterable of positions for this container from last to first. """ return iter(xrange(len(self.contents) - 1, -1, -1)) class GridFlowError(Exception): pass class GridFlow(WidgetWrap, WidgetContainerMixin, WidgetContainerListContentsMixin): """ The GridFlow widget is a flow widget that renders all the widgets it contains the same width and it arranges them from left to right and top to bottom. """ def sizing(self): return frozenset([FLOW]) def __init__(self, cells, cell_width, h_sep, v_sep, align): """ :param cells: list of flow widgets to display :param cell_width: column width for each cell :param h_sep: blank columns between each cell horizontally :param v_sep: blank rows between cells vertically (if more than one row is required to display all the cells) :param align: horizontal alignment of cells, one of\: 'left', 'center', 'right', ('relative', percentage 0=left 100=right) """ self._contents = MonitoredFocusList([ (w, (GIVEN, cell_width)) for w in cells]) self._contents.set_modified_callback(self._invalidate) self._contents.set_focus_changed_callback(lambda f: self._invalidate()) self._contents.set_validate_contents_modified(self._contents_modified) self._cell_width = cell_width self.h_sep = h_sep self.v_sep = v_sep self.align = align self._cache_maxcol = None self.__super.__init__(None) # set self._w to something other than None self.get_display_widget(((h_sep+cell_width)*len(cells),)) def _invalidate(self): self._cache_maxcol = None self.__super._invalidate() def _contents_modified(self, slc, new_items): for item in new_items: try: w, (t, n) = item if t != GIVEN: raise ValueError except (TypeError, ValueError): raise GridFlowError("added content invalid %r" % (item,)) def _get_cells(self): ml = MonitoredList(w for w, t in self.contents) def user_modified(): self._set_cells(ml) ml.set_modified_callback(user_modified) return ml def _set_cells(self, widgets): focus_position = self.focus_position self.contents = [ (new, (GIVEN, self._cell_width)) for new in widgets] if focus_position < len(widgets): self.focus_position = focus_position cells = property(_get_cells, _set_cells, doc=""" A list of the widgets in this GridFlow .. note:: only for backwards compatibility. You should use the new use the new standard container property :attr:`contents` to modify GridFlow contents. """) def _get_cell_width(self): return self._cell_width def _set_cell_width(self, width): focus_position = self.focus_position self.contents = [ (w, (GIVEN, width)) for (w, options) in self.contents] self.focus_position = focus_position self._cell_width = width cell_width = property(_get_cell_width, _set_cell_width, doc=""" The width of each cell in the GridFlow. Setting this value affects all cells. """) def _get_contents(self): return self._contents def _set_contents(self, c): self._contents[:] = c contents = property(_get_contents, _set_contents, doc=""" The contents of this GridFlow as a list of (widget, options) tuples. options is currently a tuple in the form `('fixed', number)`. number is the number of screen columns to allocate to this cell. 'fixed' is the only type accepted at this time. This list may be modified like a normal list and the GridFlow widget will update automatically. .. seealso:: Create new options tuples with the :meth:`options` method. """) def options(self, width_type=GIVEN, width_amount=None): """ Return a new options tuple for use in a GridFlow's .contents list. width_type -- 'given' is the only value accepted width_amount -- None to use the default cell_width for this GridFlow """ if width_type != GIVEN: raise GridFlowError("invalid width_type: %r" % (width_type,)) if width_amount is None: width_amount = self._cell_width return (width_type, width_amount) def set_focus(self, cell): """ Set the cell in focus, for backwards compatibility. .. note:: only for backwards compatibility. You may also use the new standard container property :attr:`focus_position` to get the focus. :param cell: contained element to focus :type cell: Widget or int """ if isinstance(cell, int): return self._set_focus_position(cell) return self._set_focus_cell(cell) def get_focus(self): """ Return the widget in focus, for backwards compatibility. .. note:: only for backwards compatibility. You may also use the new standard container property :attr:`focus` to get the focus. """ if not self.contents: return None return self.contents[self.focus_position][0] focus = property(get_focus, doc="the child widget in focus or None when GridFlow is empty") def _set_focus_cell(self, cell): for i, (w, options) in enumerate(self.contents): if cell == w: self.focus_position = i return raise ValueError("Widget not found in GridFlow contents: %r" % (cell,)) focus_cell = property(get_focus, _set_focus_cell, doc=""" The widget in focus, for backwards compatibility. .. note:: only for backwards compatibility. You should use the new use the new standard container property :attr:`focus` to get the widget in focus and :attr:`focus_position` to get/set the cell in focus by index. """) def _get_focus_position(self): """ Return the index of the widget in focus or None if this GridFlow is empty. """ if not self.contents: raise IndexError("No focus_position, GridFlow is empty") return self.contents.focus def _set_focus_position(self, position): """ Set the widget in focus. position -- index of child widget to be made focus """ try: if position < 0 or position >= len(self.contents): raise IndexError except (TypeError, IndexError): raise IndexError("No GridFlow child widget at position %s" % (position,)) self.contents.focus = position focus_position = property(_get_focus_position, _set_focus_position, doc=""" index of child widget in focus. Raises :exc:`IndexError` if read when GridFlow is empty, or when set to an invalid index. """) def get_display_widget(self, size): """ Arrange the cells into columns (and possibly a pile) for display, input or to calculate rows, and update the display widget. """ (maxcol,) = size # use cache if possible if self._cache_maxcol == maxcol: return self._w self._cache_maxcol = maxcol self._w = self.generate_display_widget(size) return self._w def generate_display_widget(self, size): """ Actually generate display widget (ignoring cache) """ (maxcol,) = size divider = Divider() if not self.contents: return divider if self.v_sep > 1: # increase size of divider divider.top = self.v_sep-1 c = None p = Pile([]) used_space = 0 for i, (w, (width_type, width_amount)) in enumerate(self.contents): if c is None or maxcol - used_space < width_amount: # starting a new row if self.v_sep: p.contents.append((divider, p.options())) c = Columns([], self.h_sep) column_focused = False pad = Padding(c, self.align) # extra attribute to reference contents position pad.first_position = i p.contents.append((pad, p.options())) c.contents.append((w, c.options(GIVEN, width_amount))) if ((i == self.focus_position) or (not column_focused and w.selectable())): c.focus_position = len(c.contents) - 1 column_focused = True if i == self.focus_position: p.focus_position = len(p.contents) - 1 used_space = (sum(x[1][1] for x in c.contents) + self.h_sep * len(c.contents)) if width_amount > maxcol: # special case: display is too narrow for the given # width so we remove the Columns for better behaviour # FIXME: determine why this is necessary pad.original_widget=w pad.width = used_space - self.h_sep if self.v_sep: # remove first divider del p.contents[:1] return p def _set_focus_from_display_widget(self): """ Set the focus to the item in focus in the display widget. """ # display widget (self._w) is always built as: # # Pile([ # Padding( # Columns([ # possibly # cell, ...])), # Divider(), # possibly # ...]) pile_focus = self._w.focus if not pile_focus: return c = pile_focus.base_widget if c.focus: col_focus_position = c.focus_position else: col_focus_position = 0 # pad.first_position was set by generate_display_widget() above self.focus_position = pile_focus.first_position + col_focus_position def keypress(self, size, key): """ Pass keypress to display widget for handling. Captures focus changes. """ self.get_display_widget(size) key = self.__super.keypress(size, key) if key is None: self._set_focus_from_display_widget() return key def rows(self, size, focus=False): self.get_display_widget(size) return self.__super.rows(size, focus=focus) def render(self, size, focus=False ): self.get_display_widget(size) return self.__super.render(size, focus) def get_cursor_coords(self, size): """Get cursor from display widget.""" self.get_display_widget(size) return self.__super.get_cursor_coords(size) def move_cursor_to_coords(self, size, col, row): """Set the widget in focus based on the col + row.""" self.get_display_widget(size) rval = self.__super.move_cursor_to_coords(size, col, row) self._set_focus_from_display_widget() return rval def mouse_event(self, size, event, button, col, row, focus): self.get_display_widget(size) self.__super.mouse_event(size, event, button, col, row, focus) self._set_focus_from_display_widget() return True # at a minimum we adjusted our focus def get_pref_col(self, size): """Return pref col from display widget.""" self.get_display_widget(size) return self.__super.get_pref_col(size) class OverlayError(Exception): pass class Overlay(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin): """ Overlay contains two box widgets and renders one on top of the other """ _selectable = True _sizing = frozenset([BOX]) _DEFAULT_BOTTOM_OPTIONS = ( LEFT, None, RELATIVE, 100, None, 0, 0, TOP, None, RELATIVE, 100, None, 0, 0) def __init__(self, top_w, bottom_w, align, width, valign, height, min_width=None, min_height=None, left=0, right=0, top=0, bottom=0): """ :param top_w: a flow, box or fixed widget to overlay "on top" :type top_w: Widget :param bottom_w: a box widget to appear "below" previous widget :type bottom_w: Widget :param align: alignment, one of ``'left'``, ``'center'``, ``'right'`` or (``'relative'``, *percentage* 0=left 100=right) :type align: str :param width: width type, one of: ``'pack'`` if *top_w* is a fixed widget *given width* integer number of columns wide (``'relative'``, *percentage of total width*) make *top_w* width related to container width :param valign: alignment mode, one of ``'top'``, ``'middle'``, ``'bottom'`` or (``'relative'``, *percentage* 0=top 100=bottom) :param height: one of: ``'pack'`` if *top_w* is a flow or fixed widget *given height* integer number of rows high (``'relative'``, *percentage of total height*) make *top_w* height related to container height :param min_width: the minimum number of columns for *top_w* when width is not fixed :type min_width: int :param min_height: minimum number of rows for *top_w* when height is not fixed :type min_height: int :param left: a fixed number of columns to add on the left :type left: int :param right: a fixed number of columns to add on the right :type right: int :param top: a fixed number of rows to add on the top :type top: int :param bottom: a fixed number of rows to add on the bottom :type bottom: int Overlay widgets behave similarly to :class:`Padding` and :class:`Filler` widgets when determining the size and position of *top_w*. *bottom_w* is always rendered the full size available "below" *top_w*. """ self.__super.__init__() self.top_w = top_w self.bottom_w = bottom_w self.set_overlay_parameters(align, width, valign, height, min_width, min_height, left, right, top, bottom) def options(self, align_type, align_amount, width_type, width_amount, valign_type, valign_amount, height_type, height_amount, min_width=None, min_height=None, left=0, right=0, top=0, bottom=0): """ Return a new options tuple for use in this Overlay's .contents mapping. This is the common container API to create options for replacing the top widget of this Overlay. It is provided for completeness but is not necessarily the easiest way to change the overlay parameters. See also :meth:`.set_overlay_parameters` """ return (align_type, align_amount, width_type, width_amount, min_width, left, right, valign_type, valign_amount, height_type, height_amount, min_height, top, bottom) def set_overlay_parameters(self, align, width, valign, height, min_width=None, min_height=None, left=0, right=0, top=0, bottom=0): """ Adjust the overlay size and position parameters. See :class:`__init__() ` for a description of the parameters. """ # convert obsolete parameters 'fixed ...': if isinstance(align, tuple): if align[0] == 'fixed left': left = align[1] align = LEFT elif align[0] == 'fixed right': right = align[1] align = RIGHT if isinstance(width, tuple): if width[0] == 'fixed left': left = width[1] width = RELATIVE_100 elif width[0] == 'fixed right': right = width[1] width = RELATIVE_100 if isinstance(valign, tuple): if valign[0] == 'fixed top': top = valign[1] valign = TOP elif valign[0] == 'fixed bottom': bottom = valign[1] valign = BOTTOM if isinstance(height, tuple): if height[0] == 'fixed bottom': bottom = height[1] height = RELATIVE_100 elif height[0] == 'fixed top': top = height[1] height = RELATIVE_100 if width is None: # more obsolete values accepted width = PACK if height is None: height = PACK align_type, align_amount = normalize_align(align, OverlayError) width_type, width_amount = normalize_width(width, OverlayError) valign_type, valign_amount = normalize_valign(valign, OverlayError) height_type, height_amount = normalize_height(height, OverlayError) if height_type in (GIVEN, PACK): min_height = None # use container API to set the parameters self.contents[1] = (self.top_w, self.options( align_type, align_amount, width_type, width_amount, valign_type, valign_amount, height_type, height_amount, min_width, min_height, left, right, top, bottom)) def selectable(self): """Return selectable from top_w.""" return self.top_w.selectable() def keypress(self, size, key): """Pass keypress to top_w.""" return self.top_w.keypress(self.top_w_size(size, *self.calculate_padding_filler(size, True)), key) def _get_focus(self): """ Currently self.top_w is always the focus of an Overlay """ return self.top_w focus = property(_get_focus, doc="the top widget in this overlay is always in focus") def _get_focus_position(self): """ Return the top widget position (currently always 1). """ return 1 def _set_focus_position(self, position): """ Set the widget in focus. Currently only position 0 is accepted. position -- index of child widget to be made focus """ if position != 1: raise IndexError("Overlay widget focus_position currently " "must always be set to 1, not %s" % (position,)) focus_position = property(_get_focus_position, _set_focus_position, doc="index of child widget in focus, currently always 1") def _contents(self): class OverlayContents(object): def __len__(inner_self): return 2 __getitem__ = self._contents__getitem__ __setitem__ = self._contents__setitem__ return OverlayContents() def _contents__getitem__(self, index): if index == 0: return (self.bottom_w, self._DEFAULT_BOTTOM_OPTIONS) if index == 1: return (self.top_w, ( self.align_type, self.align_amount, self.width_type, self.width_amount, self.min_width, self.left, self.right, self.valign_type, self.valign_amount, self.height_type, self.height_amount, self.min_height, self.top, self.bottom)) raise IndexError("Overlay.contents has no position %r" % (index,)) def _contents__setitem__(self, index, value): try: value_w, value_options = value except (ValueError, TypeError): raise OverlayError("added content invalid: %r" % (value,)) if index == 0: if value_options != self._DEFAULT_BOTTOM_OPTIONS: raise OverlayError("bottom_options must be set to " "%r" % (self._DEFAULT_BOTTOM_OPTIONS,)) self.bottom_w = value_w elif index == 1: try: (align_type, align_amount, width_type, width_amount, min_width, left, right, valign_type, valign_amount, height_type, height_amount, min_height, top, bottom, ) = value_options except (ValueError, TypeError): raise OverlayError("top_options is invalid: %r" % (value_options,)) # normalize first, this is where errors are raised align_type, align_amount = normalize_align( simplify_align(align_type, align_amount), OverlayError) width_type, width_amount = normalize_width( simplify_width(width_type, width_amount), OverlayError) valign_type, valign_amoun = normalize_valign( simplify_valign(valign_type, valign_amount), OverlayError) height_type, height_amount = normalize_height( simplify_height(height_type, height_amount), OverlayError) self.align_type = align_type self.align_amount = align_amount self.width_type = width_type self.width_amount = width_amount self.valign_type = valign_type self.valign_amount = valign_amount self.height_type = height_type self.height_amount = height_amount self.left = left self.right = right self.top = top self.bottom = bottom self.min_width = min_width self.min_height = min_height else: raise IndexError("Overlay.contents has no position %r" % (index,)) self._invalidate() contents = property(_contents, doc=""" a list-like object similar to:: [(bottom_w, bottom_options)), (top_w, top_options)] This object may be used to read or update top and bottom widgets and top widgets's options, but no widgets may be added or removed. `top_options` takes the form `(align_type, align_amount, width_type, width_amount, min_width, left, right, valign_type, valign_amount, height_type, height_amount, min_height, top, bottom)` bottom_options is always `('left', None, 'relative', 100, None, 0, 0, 'top', None, 'relative', 100, None, 0, 0)` which means that bottom widget always covers the full area of the Overlay. writing a different value for `bottom_options` raises an :exc:`OverlayError`. """) def get_cursor_coords(self, size): """Return cursor coords from top_w, if any.""" if not hasattr(self.top_w, 'get_cursor_coords'): return None (maxcol, maxrow) = size left, right, top, bottom = self.calculate_padding_filler(size, True) x, y = self.top_w.get_cursor_coords( (maxcol-left-right, maxrow-top-bottom) ) if y >= maxrow: # required?? y = maxrow-1 return x+left, y+top def calculate_padding_filler(self, size, focus): """Return (padding left, right, filler top, bottom).""" (maxcol, maxrow) = size height = None if self.width_type == PACK: width, height = self.top_w.pack((),focus=focus) if not height: raise OverlayError("fixed widget must have a height") left, right = calculate_left_right_padding(maxcol, self.align_type, self.align_amount, CLIP, width, None, self.left, self.right) else: left, right = calculate_left_right_padding(maxcol, self.align_type, self.align_amount, self.width_type, self.width_amount, self.min_width, self.left, self.right) if height: # top_w is a fixed widget top, bottom = calculate_top_bottom_filler(maxrow, self.valign_type, self.valign_amount, GIVEN, height, None, self.top, self.bottom) if maxrow-top-bottom < height: bottom = maxrow-top-height elif self.height_type == PACK: # top_w is a flow widget height = self.top_w.rows((maxcol,),focus=focus) top, bottom = calculate_top_bottom_filler(maxrow, self.valign_type, self.valign_amount, GIVEN, height, None, self.top, self.bottom) if height > maxrow: # flow widget rendered too large bottom = maxrow - height else: top, bottom = calculate_top_bottom_filler(maxrow, self.valign_type, self.valign_amount, self.height_type, self.height_amount, self.min_height, self.top, self.bottom) return left, right, top, bottom def top_w_size(self, size, left, right, top, bottom): """Return the size to pass to top_w.""" if self.width_type == PACK: # top_w is a fixed widget return () maxcol, maxrow = size if self.width_type != PACK and self.height_type == PACK: # top_w is a flow widget return (maxcol-left-right,) return (maxcol-left-right, maxrow-top-bottom) def render(self, size, focus=False): """Render top_w overlayed on bottom_w.""" left, right, top, bottom = self.calculate_padding_filler(size, focus) bottom_c = self.bottom_w.render(size) if not bottom_c.cols() or not bottom_c.rows(): return CompositeCanvas(bottom_c) top_c = self.top_w.render( self.top_w_size(size, left, right, top, bottom), focus) top_c = CompositeCanvas(top_c) if left < 0 or right < 0: top_c.pad_trim_left_right(min(0, left), min(0, right)) if top < 0 or bottom < 0: top_c.pad_trim_top_bottom(min(0, top), min(0, bottom)) return CanvasOverlay(top_c, bottom_c, left, top) def mouse_event(self, size, event, button, col, row, focus): """Pass event to top_w, ignore if outside of top_w.""" if not hasattr(self.top_w, 'mouse_event'): return False left, right, top, bottom = self.calculate_padding_filler(size, focus) maxcol, maxrow = size if ( col=maxcol-right or row=maxrow-bottom ): return False return self.top_w.mouse_event( self.top_w_size(size, left, right, top, bottom), event, button, col-left, row-top, focus ) class FrameError(Exception): pass class Frame(Widget, WidgetContainerMixin): """ Frame widget is a box widget with optional header and footer flow widgets placed above and below the box widget. .. note:: The main difference between a Frame and a :class:`Pile` widget defined as: `Pile([('pack', header), body, ('pack', footer)])` is that the Frame will not automatically change focus up and down in response to keystrokes. """ _selectable = True _sizing = frozenset([BOX]) def __init__(self, body, header=None, footer=None, focus_part='body'): """ :param body: a box widget for the body of the frame :type body: Widget :param header: a flow widget for above the body (or None) :type header: Widget :param footer: a flow widget for below the body (or None) :type footer: Widget :param focus_part: 'header', 'footer' or 'body' :type focus_part: str """ self.__super.__init__() self._header = header self._body = body self._footer = footer self.focus_part = focus_part def get_header(self): return self._header def set_header(self, header): self._header = header if header is None and self.focus_part == 'header': self.focus_part = 'body' self._invalidate() header = property(get_header, set_header) def get_body(self): return self._body def set_body(self, body): self._body = body self._invalidate() body = property(get_body, set_body) def get_footer(self): return self._footer def set_footer(self, footer): self._footer = footer if footer is None and self.focus_part == 'footer': self.focus_part = 'body' self._invalidate() footer = property(get_footer, set_footer) def set_focus(self, part): """ Determine which part of the frame is in focus. .. note:: included for backwards compatibility. You should rather use the container property :attr:`.focus_position` to set this value. :param part: 'header', 'footer' or 'body' :type part: str """ if part not in ('header', 'footer', 'body'): raise IndexError('Invalid position for Frame: %s' % (part,)) if (part == 'header' and self._header is None) or ( part == 'footer' and self._footer is None): raise IndexError('This Frame has no %s' % (part,)) self.focus_part = part self._invalidate() def get_focus(self): """ Return an indicator which part of the frame is in focus .. note:: included for backwards compatibility. You should rather use the container property :attr:`.focus_position` to get this value. :returns: one of 'header', 'footer' or 'body'. :rtype: str """ return self.focus_part def _get_focus(self): return { 'header': self._header, 'footer': self._footer, 'body': self._body }[self.focus_part] focus = property(_get_focus, doc=""" child :class:`Widget` in focus: the body, header or footer widget. This is a read-only property.""") focus_position = property(get_focus, set_focus, doc=""" writeable property containing an indicator which part of the frame that is in focus: `'body', 'header'` or `'footer'`. """) def _contents(self): class FrameContents(object): def __len__(inner_self): return len(inner_self.keys()) def items(inner_self): return [(k, inner_self[k]) for k in inner_self.keys()] def values(inner_self): return [inner_self[k] for k in inner_self.keys()] def update(inner_self, E=None, **F): if E: keys = getattr(E, 'keys', None) if keys: for k in E: inner_self[k] = E[k] else: for k, v in E: inner_self[k] = v for k in F: inner_self[k] = F[k] keys = self._contents_keys __getitem__ = self._contents__getitem__ __setitem__ = self._contents__setitem__ __delitem__ = self._contents__delitem__ return FrameContents() def _contents_keys(self): keys = ['body'] if self._header: keys.append('header') if self._footer: keys.append('footer') return keys def _contents__getitem__(self, key): if key == 'body': return (self._body, None) if key == 'header' and self._header: return (self._header, None) if key == 'footer' and self._footer: return (self._footer, None) raise KeyError("Frame.contents has no key: %r" % (key,)) def _contents__setitem__(self, key, value): if key not in ('body', 'header', 'footer'): raise KeyError("Frame.contents has no key: %r" % (key,)) try: value_w, value_options = value if value_options is not None: raise ValueError except (ValueError, TypeError): raise FrameError("added content invalid: %r" % (value,)) if key == 'body': self.body = value_w elif key == 'footer': self.footer = value_w else: self.header = value_w def _contents__delitem__(self, key): if key not in ('header', 'footer'): raise KeyError("Frame.contents can't remove key: %r" % (key,)) if (key == 'header' and self._header is None ) or (key == 'footer' and self._footer is None): raise KeyError("Frame.contents has no key: %r" % (key,)) if key == 'header': self.header = None else: self.footer = None contents = property(_contents, doc=""" a dict-like object similar to:: { 'body': (body_widget, None), 'header': (header_widget, None), # if frame has a header 'footer': (footer_widget, None) # if frame has a footer } This object may be used to read or update the contents of the Frame. The values are similar to the list-like .contents objects used in other containers with (:class:`Widget`, options) tuples, but are constrained to keys for each of the three usual parts of a Frame. When other keys are used a :exc:`KeyError` will be raised. Currently all options are `None`, but using the :meth:`options` method to create the options value is recommended for forwards compatibility. """) def options(self): """ There are currently no options for Frame contents. Return None as a placeholder for future options. """ return None def frame_top_bottom(self, size, focus): """ Calculate the number of rows for the header and footer. :param size: See :meth:`Widget.render` for details :type size: widget size :param focus: ``True`` if this widget is in focus :type focus: bool :returns: `(head rows, foot rows),(orig head, orig foot)` orig head/foot are from rows() calls. :rtype: (int, int), (int, int) """ (maxcol, maxrow) = size frows = hrows = 0 if self.header: hrows = self.header.rows((maxcol,), self.focus_part=='header' and focus) if self.footer: frows = self.footer.rows((maxcol,), self.focus_part=='footer' and focus) remaining = maxrow if self.focus_part == 'footer': if frows >= remaining: return (0, remaining),(hrows, frows) remaining -= frows if hrows >= remaining: return (remaining, frows),(hrows, frows) elif self.focus_part == 'header': if hrows >= maxrow: return (remaining, 0),(hrows, frows) remaining -= hrows if frows >= remaining: return (hrows, remaining),(hrows, frows) elif hrows + frows >= remaining: # self.focus_part == 'body' rless1 = max(0, remaining-1) if frows >= remaining-1: return (0, rless1),(hrows, frows) remaining -= frows rless1 = max(0, remaining-1) return (rless1,frows),(hrows, frows) return (hrows, frows),(hrows, frows) def render(self, size, focus=False): (maxcol, maxrow) = size (htrim, ftrim),(hrows, frows) = self.frame_top_bottom( (maxcol, maxrow), focus) combinelist = [] depends_on = [] head = None if htrim and htrim < hrows: head = Filler(self.header, 'top').render( (maxcol, htrim), focus and self.focus_part == 'header') elif htrim: head = self.header.render((maxcol,), focus and self.focus_part == 'header') assert head.rows() == hrows, "rows, render mismatch" if head: combinelist.append((head, 'header', self.focus_part == 'header')) depends_on.append(self.header) if ftrim+htrim < maxrow: body = self.body.render((maxcol, maxrow-ftrim-htrim), focus and self.focus_part == 'body') combinelist.append((body, 'body', self.focus_part == 'body')) depends_on.append(self.body) foot = None if ftrim and ftrim < frows: foot = Filler(self.footer, 'bottom').render( (maxcol, ftrim), focus and self.focus_part == 'footer') elif ftrim: foot = self.footer.render((maxcol,), focus and self.focus_part == 'footer') assert foot.rows() == frows, "rows, render mismatch" if foot: combinelist.append((foot, 'footer', self.focus_part == 'footer')) depends_on.append(self.footer) return CanvasCombine(combinelist) def keypress(self, size, key): """Pass keypress to widget in focus.""" (maxcol, maxrow) = size if self.focus_part == 'header' and self.header is not None: if not self.header.selectable(): return key return self.header.keypress((maxcol,),key) if self.focus_part == 'footer' and self.footer is not None: if not self.footer.selectable(): return key return self.footer.keypress((maxcol,),key) if self.focus_part != 'body': return key remaining = maxrow if self.header is not None: remaining -= self.header.rows((maxcol,)) if self.footer is not None: remaining -= self.footer.rows((maxcol,)) if remaining <= 0: return key if not self.body.selectable(): return key return self.body.keypress( (maxcol, remaining), key ) def mouse_event(self, size, event, button, col, row, focus): """ Pass mouse event to appropriate part of frame. Focus may be changed on button 1 press. """ (maxcol, maxrow) = size (htrim, ftrim),(hrows, frows) = self.frame_top_bottom( (maxcol, maxrow), focus) if row < htrim: # within header focus = focus and self.focus_part == 'header' if is_mouse_press(event) and button==1: if self.header.selectable(): self.set_focus('header') if not hasattr(self.header, 'mouse_event'): return False return self.header.mouse_event( (maxcol,), event, button, col, row, focus ) if row >= maxrow-ftrim: # within footer focus = focus and self.focus_part == 'footer' if is_mouse_press(event) and button==1: if self.footer.selectable(): self.set_focus('footer') if not hasattr(self.footer, 'mouse_event'): return False return self.footer.mouse_event( (maxcol,), event, button, col, row-maxrow+frows, focus ) # within body focus = focus and self.focus_part == 'body' if is_mouse_press(event) and button==1: if self.body.selectable(): self.set_focus('body') if not hasattr(self.body, 'mouse_event'): return False return self.body.mouse_event( (maxcol, maxrow-htrim-ftrim), event, button, col, row-htrim, focus ) def __iter__(self): """ Return an iterator over the positions in this Frame top to bottom. """ if self._header: yield 'header' yield 'body' if self._footer: yield 'footer' def __reversed__(self): """ Return an iterator over the positions in this Frame bottom to top. """ if self._footer: yield 'footer' yield 'body' if self._header: yield 'header' class PileError(Exception): pass class Pile(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin): """ A pile of widgets stacked vertically from top to bottom """ _sizing = frozenset([FLOW, BOX]) def __init__(self, widget_list, focus_item=None): """ :param widget_list: child widgets :type widget_list: iterable :param focus_item: child widget that gets the focus initially. Chooses the first selectable widget if unset. :type focus_item: Widget or int *widget_list* may also contain tuples such as: (*given_height*, *widget*) always treat *widget* as a box widget and give it *given_height* rows, where given_height is an int (``'pack'``, *widget*) allow *widget* to calculate its own height by calling its :meth:`rows` method, ie. treat it as a flow widget. (``'weight'``, *weight*, *widget*) if the pile is treated as a box widget then treat widget as a box widget with a height based on its relative weight value, otherwise treat the same as (``'pack'``, *widget*). Widgets not in a tuple are the same as (``'weight'``, ``1``, *widget*)` .. note:: If the Pile is treated as a box widget there must be at least one ``'weight'`` tuple in :attr:`widget_list`. """ self.__super.__init__() self._contents = MonitoredFocusList() self._contents.set_modified_callback(self._invalidate) self._contents.set_focus_changed_callback(lambda f: self._invalidate()) self._contents.set_validate_contents_modified(self._contents_modified) focus_item = focus_item for i, original in enumerate(widget_list): w = original if not isinstance(w, tuple): self.contents.append((w, (WEIGHT, 1))) elif w[0] in (FLOW, PACK): f, w = w self.contents.append((w, (PACK, None))) elif len(w) == 2: height, w = w self.contents.append((w, (GIVEN, height))) elif w[0] == FIXED: # backwards compatibility _ignore, height, w = w self.contents.append((w, (GIVEN, height))) elif w[0] == WEIGHT: f, height, w = w self.contents.append((w, (f, height))) else: raise PileError( "initial widget list item invalid %r" % (original,)) if focus_item is None and w.selectable(): focus_item = i if self.contents and focus_item is not None: self.set_focus(focus_item) self.pref_col = 0 def _contents_modified(self, slc, new_items): for item in new_items: try: w, (t, n) = item if t not in (PACK, GIVEN, WEIGHT): raise ValueError except (TypeError, ValueError): raise PileError("added content invalid: %r" % (item,)) def _get_widget_list(self): ml = MonitoredList(w for w, t in self.contents) def user_modified(): self._set_widget_list(ml) ml.set_modified_callback(user_modified) return ml def _set_widget_list(self, widgets): focus_position = self.focus_position self.contents = [ (new, options) for (new, (w, options)) in zip(widgets, # need to grow contents list if widgets is longer chain(self.contents, repeat((None, (WEIGHT, 1)))))] if focus_position < len(widgets): self.focus_position = focus_position widget_list = property(_get_widget_list, _set_widget_list, doc=""" A list of the widgets in this Pile .. note:: only for backwards compatibility. You should use the new standard container property :attr:`contents`. """) def _get_item_types(self): ml = MonitoredList( # return the old item type names ({GIVEN: FIXED, PACK: FLOW}.get(f, f), height) for w, (f, height) in self.contents) def user_modified(): self._set_item_types(ml) ml.set_modified_callback(user_modified) return ml def _set_item_types(self, item_types): focus_position = self.focus_position self.contents = [ (w, ({FIXED: GIVEN, FLOW: PACK}.get(new_t, new_t), new_height)) for ((new_t, new_height), (w, options)) in zip(item_types, self.contents)] if focus_position < len(item_types): self.focus_position = focus_position item_types = property(_get_item_types, _set_item_types, doc=""" A list of the options values for widgets in this Pile. .. note:: only for backwards compatibility. You should use the new standard container property :attr:`contents`. """) def _get_contents(self): return self._contents def _set_contents(self, c): self._contents[:] = c contents = property(_get_contents, _set_contents, doc=""" The contents of this Pile as a list of (widget, options) tuples. options currently may be one of (``'pack'``, ``None``) allow widget to calculate its own height by calling its :meth:`rows ` method, i.e. treat it as a flow widget. (``'given'``, *n*) Always treat widget as a box widget with a given height of *n* rows. (``'weight'``, *w*) If the Pile itself is treated as a box widget then the value *w* will be used as a relative weight for assigning rows to this box widget. If the Pile is being treated as a flow widget then this is the same as (``'pack'``, ``None``) and the *w* value is ignored. If the Pile itself is treated as a box widget then at least one widget must have a (``'weight'``, *w*) options value, or the Pile will not be able to grow to fill the required number of rows. This list may be modified like a normal list and the Pile widget will updated automatically. .. seealso:: Create new options tuples with the :meth:`options` method """) def options(self, height_type=WEIGHT, height_amount=1): """ Return a new options tuple for use in a Pile's :attr:`contents` list. :param height_type: ``'pack'``, ``'given'`` or ``'weight'`` :param height_amount: ``None`` for ``'pack'``, a number of rows for ``'fixed'`` or a weight value (number) for ``'weight'`` """ if height_type == PACK: return (PACK, None) if height_type not in (GIVEN, WEIGHT): raise PileError('invalid height_type: %r' % (height_type,)) return (height_type, height_amount) def selectable(self): """Return True if the focus item is selectable.""" w = self.focus return w is not None and w.selectable() def set_focus(self, item): """ Set the item in focus, for backwards compatibility. .. note:: only for backwards compatibility. You should use the new standard container property :attr:`focus_position`. to set the position by integer index instead. :param item: element to focus :type item: Widget or int """ if isinstance(item, int): return self._set_focus_position(item) for i, (w, options) in enumerate(self.contents): if item == w: self.focus_position = i return raise ValueError("Widget not found in Pile contents: %r" % (item,)) def get_focus(self): """ Return the widget in focus, for backwards compatibility. You may also use the new standard container property .focus to get the child widget in focus. """ if not self.contents: return None return self.contents[self.focus_position][0] focus = property(get_focus, doc="the child widget in focus or None when Pile is empty") focus_item = property(get_focus, set_focus, doc=""" A property for reading and setting the widget in focus. .. note:: only for backwards compatibility. You should use the new standard container properties :attr:`focus` and :attr:`focus_position` to get the child widget in focus or modify the focus position. """) def _get_focus_position(self): """ Return the index of the widget in focus or None if this Pile is empty. """ if not self.contents: raise IndexError("No focus_position, Pile is empty") return self.contents.focus def _set_focus_position(self, position): """ Set the widget in focus. position -- index of child widget to be made focus """ try: if position < 0 or position >= len(self.contents): raise IndexError except (TypeError, IndexError): raise IndexError("No Pile child widget at position %s" % (position,)) self.contents.focus = position focus_position = property(_get_focus_position, _set_focus_position, doc=""" index of child widget in focus. Raises :exc:`IndexError` if read when Pile is empty, or when set to an invalid index. """) def get_pref_col(self, size): """Return the preferred column for the cursor, or None.""" if not self.selectable(): return None self._update_pref_col_from_focus(size) return self.pref_col def get_item_size(self, size, i, focus, item_rows=None): """ Return a size appropriate for passing to self.contents[i][0].render """ maxcol = size[0] w, (f, height) = self.contents[i] if f == GIVEN: return (maxcol, height) elif f == WEIGHT and len(size) == 2: if not item_rows: item_rows = self.get_item_rows(size, focus) return (maxcol, item_rows[i]) else: return (maxcol,) def get_item_rows(self, size, focus): """ Return a list of the number of rows used by each widget in self.contents """ remaining = None maxcol = size[0] if len(size) == 2: remaining = size[1] l = [] if remaining is None: # pile is a flow widget for w, (f, height) in self.contents: if f == GIVEN: l.append(height) else: l.append(w.rows((maxcol,), focus=focus and self.focus_item == w)) return l # pile is a box widget # do an extra pass to calculate rows for each widget wtotal = 0 for w, (f, height) in self.contents: if f == PACK: rows = w.rows((maxcol,), focus=focus and self.focus_item == w) l.append(rows) remaining -= rows elif f == GIVEN: l.append(height) remaining -= height elif height: l.append(None) wtotal += height else: l.append(0) # zero-weighted items treated as ('given', 0) if wtotal == 0: raise PileError("No weighted widgets found for Pile treated as a box widget") if remaining < 0: remaining = 0 for i, (w, (f, height)) in enumerate(self.contents): li = l[i] if li is None: rows = int(float(remaining) * height / wtotal + 0.5) l[i] = rows remaining -= rows wtotal -= height return l def render(self, size, focus=False): maxcol = size[0] item_rows = None combinelist = [] for i, (w, (f, height)) in enumerate(self.contents): item_focus = self.focus_item == w canv = None if f == GIVEN: canv = w.render((maxcol, height), focus=focus and item_focus) elif f == PACK or len(size)==1: canv = w.render((maxcol,), focus=focus and item_focus) else: if item_rows is None: item_rows = self.get_item_rows(size, focus) rows = item_rows[i] if rows>0: canv = w.render((maxcol, rows), focus=focus and item_focus) if canv: combinelist.append((canv, i, item_focus)) if not combinelist: return SolidCanvas(" ", size[0], (size[1:]+(0,))[0]) out = CanvasCombine(combinelist) if len(size) == 2 and size[1] != out.rows(): # flow/fixed widgets rendered too large/small out = CompositeCanvas(out) out.pad_trim_top_bottom(0, size[1] - out.rows()) return out def get_cursor_coords(self, size): """Return the cursor coordinates of the focus widget.""" if not self.selectable(): return None if not hasattr(self.focus_item, 'get_cursor_coords'): return None i = self.focus_position w, (f, height) = self.contents[i] item_rows = None maxcol = size[0] if f == GIVEN or (f == WEIGHT and len(size) == 2): if f == GIVEN: maxrow = height else: if item_rows is None: item_rows = self.get_item_rows(size, focus=True) maxrow = item_rows[i] coords = self.focus_item.get_cursor_coords((maxcol, maxrow)) else: coords = self.focus_item.get_cursor_coords((maxcol,)) if coords is None: return None x,y = coords if i > 0: if item_rows is None: item_rows = self.get_item_rows(size, focus=True) for r in item_rows[:i]: y += r return x, y def rows(self, size, focus=False ): return sum(self.get_item_rows(size, focus)) def keypress(self, size, key ): """Pass the keypress to the widget in focus. Unhandled 'up' and 'down' keys may cause a focus change.""" if not self.contents: return key item_rows = None if len(size) == 2: item_rows = self.get_item_rows(size, focus=True) i = self.focus_position if self.selectable(): tsize = self.get_item_size(size, i, True, item_rows) key = self.focus.keypress(tsize, key) if self._command_map[key] not in ('cursor up', 'cursor down'): return key if self._command_map[key] == 'cursor up': candidates = list(range(i-1, -1, -1)) # count backwards to 0 else: # self._command_map[key] == 'cursor down' candidates = list(range(i+1, len(self.contents))) if not item_rows: item_rows = self.get_item_rows(size, focus=True) for j in candidates: if not self.contents[j][0].selectable(): continue self._update_pref_col_from_focus(size) self.focus_position = j if not hasattr(self.focus, 'move_cursor_to_coords'): return rows = item_rows[j] if self._command_map[key] == 'cursor up': rowlist = list(range(rows-1, -1, -1)) else: # self._command_map[key] == 'cursor down' rowlist = list(range(rows)) for row in rowlist: tsize = self.get_item_size(size, j, True, item_rows) if self.focus_item.move_cursor_to_coords( tsize, self.pref_col, row): break return # nothing to select return key def _update_pref_col_from_focus(self, size): """Update self.pref_col from the focus widget.""" if not hasattr(self.focus, 'get_pref_col'): return i = self.focus_position tsize = self.get_item_size(size, i, True) pref_col = self.focus.get_pref_col(tsize) if pref_col is not None: self.pref_col = pref_col def move_cursor_to_coords(self, size, col, row): """Capture pref col and set new focus.""" self.pref_col = col #FIXME guessing focus==True focus=True wrow = 0 item_rows = self.get_item_rows(size, focus) for i, (r, w) in enumerate(zip(item_rows, (w for (w, options) in self.contents))): if wrow + r > row: break wrow += r else: return False if not w.selectable(): return False if hasattr(w, 'move_cursor_to_coords'): tsize = self.get_item_size(size, i, focus, item_rows) rval = w.move_cursor_to_coords(tsize, col, row-wrow) if rval is False: return False self.focus_position = i return True def mouse_event(self, size, event, button, col, row, focus): """ Pass the event to the contained widget. May change focus on button 1 press. """ wrow = 0 item_rows = self.get_item_rows(size, focus) for i, (r, w) in enumerate(zip(item_rows, (w for (w, options) in self.contents))): if wrow + r > row: break wrow += r else: return False focus = focus and self.focus_item == w if is_mouse_press(event) and button == 1: if w.selectable(): self.focus_position = i if not hasattr(w, 'mouse_event'): return False tsize = self.get_item_size(size, i, focus, item_rows) return w.mouse_event(tsize, event, button, col, row-wrow, focus) class ColumnsError(Exception): pass class Columns(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin): """ Widgets arranged horizontally in columns from left to right """ _sizing = frozenset([FLOW, BOX]) def __init__(self, widget_list, dividechars=0, focus_column=None, min_width=1, box_columns=None): """ :param widget_list: iterable of flow or box widgets :param dividechars: number of blank characters between columns :param focus_column: index into widget_list of column in focus, if ``None`` the first selectable widget will be chosen. :param min_width: minimum width for each column which is not calling widget.pack() in *widget_list*. :param box_columns: a list of column indexes containing box widgets whose height is set to the maximum of the rows required by columns not listed in *box_columns*. *widget_list* may also contain tuples such as: (*given_width*, *widget*) make this column *given_width* screen columns wide, where *given_width* is an int (``'pack'``, *widget*) call :meth:`pack() ` to calculate the width of this column (``'weight'``, *weight*, *widget*) give this column a relative *weight* (number) to calculate its width from the screen columns remaining Widgets not in a tuple are the same as (``'weight'``, ``1``, *widget*) If the Columns widget is treated as a box widget then all children are treated as box widgets, and *box_columns* is ignored. If the Columns widget is treated as a flow widget then the rows are calculated as the largest rows() returned from all columns except the ones listed in *box_columns*. The box widgets in *box_columns* will be displayed with this calculated number of rows, filling the full height. """ self.__super.__init__() self._contents = MonitoredFocusList() self._contents.set_modified_callback(self._invalidate) self._contents.set_focus_changed_callback(lambda f: self._invalidate()) self._contents.set_validate_contents_modified(self._contents_modified) box_columns = set(box_columns or ()) for i, original in enumerate(widget_list): w = original if not isinstance(w, tuple): self.contents.append((w, (WEIGHT, 1, i in box_columns))) elif w[0] in (FLOW, PACK): # 'pack' used to be called 'flow' f = PACK _ignored, w = w self.contents.append((w, (f, None, i in box_columns))) elif len(w) == 2: width, w = w self.contents.append((w, (GIVEN, width, i in box_columns))) elif w[0] == FIXED: # backwards compatibility f = GIVEN _ignored, width, w = w self.contents.append((w, (GIVEN, width, i in box_columns))) elif w[0] == WEIGHT: f, width, w = w self.contents.append((w, (f, width, i in box_columns))) else: raise ColumnsError( "initial widget list item invalid: %r" % (original,)) if focus_column is None and w.selectable(): focus_column = i self.dividechars = dividechars if self.contents and focus_column is not None: self.focus_position = focus_column if focus_column is None: focus_column = 0 self.pref_col = None self.min_width = min_width self._cache_maxcol = None def _contents_modified(self, slc, new_items): for item in new_items: try: w, (t, n, b) = item if t not in (PACK, GIVEN, WEIGHT): raise ValueError except (TypeError, ValueError): raise ColumnsError("added content invalid %r" % (item,)) def _get_widget_list(self): ml = MonitoredList(w for w, t in self.contents) def user_modified(): self._set_widget_list(ml) ml.set_modified_callback(user_modified) return ml def _set_widget_list(self, widgets): focus_position = self.focus_position self.contents = [ (new, options) for (new, (w, options)) in zip(widgets, # need to grow contents list if widgets is longer chain(self.contents, repeat((None, (WEIGHT, 1, False)))))] if focus_position < len(widgets): self.focus_position = focus_position widget_list = property(_get_widget_list, _set_widget_list, doc=""" A list of the widgets in this Columns .. note:: only for backwards compatibility. You should use the new standard container property :attr:`contents`. """) def _get_column_types(self): ml = MonitoredList( # return the old column type names ({GIVEN: FIXED, PACK: FLOW}.get(t, t), n) for w, (t, n, b) in self.contents) def user_modified(): self._set_column_types(ml) ml.set_modified_callback(user_modified) return ml def _set_column_types(self, column_types): focus_position = self.focus_position self.contents = [ (w, ({FIXED: GIVEN, FLOW: PACK}.get(new_t, new_t), new_n, b)) for ((new_t, new_n), (w, (t, n, b))) in zip(column_types, self.contents)] if focus_position < len(column_types): self.focus_position = focus_position column_types = property(_get_column_types, _set_column_types, doc=""" A list of the old partial options values for widgets in this Pile, for backwards compatibility only. You should use the new standard container property .contents to modify Pile contents. """) def _get_box_columns(self): ml = MonitoredList( i for i, (w, (t, n, b)) in enumerate(self.contents) if b) def user_modified(): self._set_box_columns(ml) ml.set_modified_callback(user_modified) return ml def _set_box_columns(self, box_columns): box_columns = set(box_columns) self.contents = [ (w, (t, n, i in box_columns)) for (i, (w, (t, n, b))) in enumerate(self.contents)] box_columns = property(_get_box_columns, _set_box_columns, doc=""" A list of the indexes of the columns that are to be treated as box widgets when the Columns is treated as a flow widget. .. note:: only for backwards compatibility. You should use the new standard container property :attr:`contents`. """) def _get_has_pack_type(self): import warnings warnings.warn(".has_flow_type is deprecated, " "read values from .contents instead.", DeprecationWarning) return PACK in self.column_types def _set_has_pack_type(self, value): import warnings warnings.warn(".has_flow_type is deprecated, " "read values from .contents instead.", DeprecationWarning) has_flow_type = property(_get_has_pack_type, _set_has_pack_type, doc=""" .. deprecated:: 1.0 Read values from :attr:`contents` instead. """) def _get_contents(self): return self._contents def _set_contents(self, c): self._contents[:] = c contents = property(_get_contents, _set_contents, doc=""" The contents of this Columns as a list of `(widget, options)` tuples. This list may be modified like a normal list and the Columns widget will update automatically. .. seealso:: Create new options tuples with the :meth:`options` method """) def options(self, width_type=WEIGHT, width_amount=1, box_widget=False): """ Return a new options tuple for use in a Pile's .contents list. This sets an entry's width type: one of the following: ``'pack'`` Call the widget's :meth:`Widget.pack` method to determine how wide this column should be. *width_amount* is ignored. ``'given'`` Make column exactly width_amount screen-columns wide. ``'weight'`` Allocate the remaining space to this column by using *width_amount* as a weight value. :param width_type: ``'pack'``, ``'given'`` or ``'weight'`` :param width_amount: ``None`` for ``'pack'``, a number of screen columns for ``'given'`` or a weight value (number) for ``'weight'`` :param box_widget: set to `True` if this widget is to be treated as a box widget when the Columns widget itself is treated as a flow widget. :type box_widget: bool """ if width_type == PACK: width_amount = None if width_type not in (PACK, GIVEN, WEIGHT): raise ColumnsError('invalid width_type: %r' % (width_type,)) return (width_type, width_amount, box_widget) def _invalidate(self): self._cache_maxcol = None self.__super._invalidate() def set_focus_column(self, num): """ Set the column in focus by its index in :attr:`widget_list`. :param num: index of focus-to-be entry :type num: int .. note:: only for backwards compatibility. You may also use the new standard container property :attr:`focus_position` to set the focus. """ self._set_focus_position(num) def get_focus_column(self): """ Return the focus column index. .. note:: only for backwards compatibility. You may also use the new standard container property :attr:`focus_position` to get the focus. """ return self.focus_position def set_focus(self, item): """ Set the item in focus .. note:: only for backwards compatibility. You may also use the new standard container property :attr:`focus_position` to get the focus. :param item: widget or integer index""" if isinstance(item, int): return self._set_focus_position(item) for i, (w, options) in enumerate(self.contents): if item == w: self.focus_position = i return raise ValueError("Widget not found in Columns contents: %r" % (item,)) def get_focus(self): """ Return the widget in focus, for backwards compatibility. You may also use the new standard container property .focus to get the child widget in focus. """ if not self.contents: return None return self.contents[self.focus_position][0] focus = property(get_focus, doc="the child widget in focus or None when Columns is empty") def _get_focus_position(self): """ Return the index of the widget in focus or None if this Columns is empty. """ if not self.widget_list: raise IndexError("No focus_position, Columns is empty") return self.contents.focus def _set_focus_position(self, position): """ Set the widget in focus. position -- index of child widget to be made focus """ try: if position < 0 or position >= len(self.contents): raise IndexError except (TypeError, IndexError): raise IndexError("No Columns child widget at position %s" % (position,)) self.contents.focus = position focus_position = property(_get_focus_position, _set_focus_position, doc=""" index of child widget in focus. Raises :exc:`IndexError` if read when Columns is empty, or when set to an invalid index. """) focus_col = property(_get_focus_position, _set_focus_position, doc=""" A property for reading and setting the index of the column in focus. .. note:: only for backwards compatibility. You may also use the new standard container property :attr:`focus_position` to get the focus. """) def column_widths(self, size, focus=False): """ Return a list of column widths. 0 values in the list mean hide corresponding column completely """ maxcol = size[0] # FIXME: get rid of this check and recalculate only when # a 'pack' widget has been modified. if maxcol == self._cache_maxcol and not any( t == PACK for w, (t, n, b) in self.contents): return self._cache_column_widths widths = [] weighted = [] shared = maxcol + self.dividechars for i, (w, (t, width, b)) in enumerate(self.contents): if t == GIVEN: static_w = width elif t == PACK: # FIXME: should be able to pack with a different # maxcol value static_w = w.pack((maxcol,), focus)[0] else: static_w = self.min_width if shared < static_w + self.dividechars and i > self.focus_position: break widths.append(static_w) shared -= static_w + self.dividechars if t not in (GIVEN, PACK): weighted.append((width, i)) # drop columns on the left until we fit for i, w in enumerate(widths): if shared >= 0: break shared += widths[i] + self.dividechars widths[i] = 0 if weighted and weighted[0][1] == i: del weighted[0] if shared: # divide up the remaining space between weighted cols weighted.sort() wtotal = sum(weight for weight, i in weighted) grow = shared + len(weighted) * self.min_width for weight, i in weighted: width = int(float(grow) * weight / wtotal + 0.5) width = max(self.min_width, width) widths[i] = width grow -= width wtotal -= weight self._cache_maxcol = maxcol self._cache_column_widths = widths return widths def render(self, size, focus=False): """ Render columns and return canvas. :param size: see :meth:`Widget.render` for details :param focus: ``True`` if this widget is in focus :type focus: bool """ widths = self.column_widths(size, focus) box_maxrow = None if len(size) == 1: box_maxrow = 1 # two-pass mode to determine maxrow for box columns for i, (mc, (w, (t, n, b))) in enumerate(zip(widths, self.contents)): if b: continue rows = w.rows((mc,), focus = focus and self.focus_position == i) box_maxrow = max(box_maxrow, rows) l = [] for i, (mc, (w, (t, n, b))) in enumerate(zip(widths, self.contents)): # if the widget has a width of 0, hide it if mc <= 0: continue if box_maxrow and b: sub_size = (mc, box_maxrow) else: sub_size = (mc,) + size[1:] canv = w.render(sub_size, focus = focus and self.focus_position == i) if i < len(widths) - 1: mc += self.dividechars l.append((canv, i, self.focus_position == i, mc)) if not l: return SolidCanvas(" ", size[0], (size[1:]+(1,))[0]) canv = CanvasJoin(l) if canv.cols() < size[0]: canv.pad_trim_left_right(0, size[0] - canv.cols()) return canv def get_cursor_coords(self, size): """Return the cursor coordinates from the focus widget.""" w, (t, n, b) = self.contents[self.focus_position] if not w.selectable(): return None if not hasattr(w, 'get_cursor_coords'): return None widths = self.column_widths(size) if len(widths) <= self.focus_position: return None colw = widths[self.focus_position] if len(size) == 1 and b: coords = w.get_cursor_coords((colw, self.rows(size))) else: coords = w.get_cursor_coords((colw,)+size[1:]) if coords is None: return None x, y = coords x += sum([self.dividechars + wc for wc in widths[:self.focus_position] if wc > 0]) return x, y def move_cursor_to_coords(self, size, col, row): """ Choose a selectable column to focus based on the coords. see :meth:`Widget.move_cursor_coords` for details """ widths = self.column_widths(size) best = None x = 0 for i, (width, (w, options)) in enumerate(zip(widths, self.contents)): end = x + width if w.selectable(): if col != RIGHT and (col == LEFT or x > col) and best is None: # no other choice best = i, x, end, w, options break if col != RIGHT and x > col and col-best[2] < x-col: # choose one on left break best = i, x, end, w, options if col != RIGHT and col < end: # choose this one break x = end + self.dividechars if best is None: return False i, x, end, w, (t, n, b) = best if hasattr(w, 'move_cursor_to_coords'): if isinstance(col, int): move_x = min(max(0, col - x), end - x - 1) else: move_x = col if len(size) == 1 and b: rval = w.move_cursor_to_coords((end - x, self.rows(size)), move_x, row) else: rval = w.move_cursor_to_coords((end - x,) + size[1:], move_x, row) if rval is False: return False self.focus_position = i self.pref_col = col return True def mouse_event(self, size, event, button, col, row, focus): """ Send event to appropriate column. May change focus on button 1 press. """ widths = self.column_widths(size) x = 0 for i, (width, (w, (t, n, b))) in enumerate(zip(widths, self.contents)): if col < x: return False w = self.widget_list[i] end = x + width if col >= end: x = end + self.dividechars continue focus = focus and self.focus_col == i if is_mouse_press(event) and button == 1: if w.selectable(): self.focus_position = i if not hasattr(w, 'mouse_event'): return False if len(size) == 1 and b: return w.mouse_event((end - x, self.rows(size)), event, button, col - x, row, focus) return w.mouse_event((end - x,) + size[1:], event, button, col - x, row, focus) return False def get_pref_col(self, size): """Return the pref col from the column in focus.""" widths = self.column_widths(size) w, (t, n, b) = self.contents[self.focus_position] if len(widths) <= self.focus_position: return 0 col = None cwidth = widths[self.focus_position] if hasattr(w, 'get_pref_col'): if len(size) == 1 and b: col = w.get_pref_col((cwidth, self.rows(size))) else: col = w.get_pref_col((cwidth,) + size[1:]) if isinstance(col, int): col += self.focus_col * self.dividechars col += sum(widths[:self.focus_position]) if col is None: col = self.pref_col if col is None and w.selectable(): col = cwidth // 2 col += self.focus_position * self.dividechars col += sum(widths[:self.focus_position] ) return col def rows(self, size, focus=False): """ Return the number of rows required by the columns. This only makes sense if :attr:`widget_list` contains flow widgets. see :meth:`Widget.rows` for details """ widths = self.column_widths(size, focus) rows = 1 for i, (mc, (w, (t, n, b))) in enumerate(zip(widths, self.contents)): if b: continue rows = max(rows, w.rows((mc,), focus=focus and self.focus_position == i)) return rows def keypress(self, size, key): """ Pass keypress to the focus column. :param size: `(maxcol,)` if :attr:`widget_list` contains flow widgets or `(maxcol, maxrow)` if it contains box widgets. :type size: int, int """ if self.focus_position is None: return key widths = self.column_widths(size) if self.focus_position >= len(widths): return key i = self.focus_position mc = widths[i] w, (t, n, b) = self.contents[i] if self._command_map[key] not in ('cursor up', 'cursor down', 'cursor page up', 'cursor page down'): self.pref_col = None if len(size) == 1 and b: key = w.keypress((mc, self.rows(size, True)), key) else: key = w.keypress((mc,) + size[1:], key) if self._command_map[key] not in ('cursor left', 'cursor right'): return key if self._command_map[key] == 'cursor left': candidates = list(range(i-1, -1, -1)) # count backwards to 0 else: # key == 'right' candidates = list(range(i+1, len(self.contents))) for j in candidates: if not self.contents[j][0].selectable(): continue self.focus_position = j return return key def selectable(self): """Return the selectable value of the focus column.""" w = self.focus return w is not None and w.selectable() def _test(): import doctest doctest.testmod() if __name__=='__main__': _test() urwid-2.0.1/urwid/main_loop.py0000755000175000017500000014154413231170672020007 0ustar andersonanderson00000000000000#!/usr/bin/python # # Urwid main loop code # Copyright (C) 2004-2012 Ian Ward # Copyright (C) 2008 Walter Mundt # Copyright (C) 2009 Andrew Psaltis # # 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 import time import heapq import select import os import signal from functools import wraps from itertools import count from weakref import WeakKeyDictionary try: import fcntl except ImportError: pass # windows from urwid.util import StoppingContext, is_mouse_event from urwid.compat import PYTHON3, reraise from urwid.command_map import command_map, REDRAW_SCREEN from urwid.wimp import PopUpTarget from urwid import signals from urwid.display_common import INPUT_DESCRIPTORS_CHANGED PIPE_BUFFER_READ_SIZE = 4096 # can expect this much on Linux, so try for that class ExitMainLoop(Exception): """ When this exception is raised within a main loop the main loop will exit cleanly. """ pass class CantUseExternalLoop(Exception): pass class MainLoop(object): """ This is the standard main loop implementation for a single interactive session. :param widget: the topmost widget used for painting the screen, stored as :attr:`widget` and may be modified. Must be a box widget. :type widget: widget instance :param palette: initial palette for screen :type palette: iterable of palette entries :param screen: screen to use, default is a new :class:`raw_display.Screen` instance; stored as :attr:`screen` :type screen: display module screen instance :param handle_mouse: ``True`` to ask :attr:`.screen` to process mouse events :type handle_mouse: bool :param input_filter: a function to filter input before sending it to :attr:`.widget`, called from :meth:`.input_filter` :type input_filter: callable :param unhandled_input: a function called when input is not handled by :attr:`.widget`, called from :meth:`.unhandled_input` :type unhandled_input: callable :param event_loop: if :attr:`.screen` supports external an event loop it may be given here, default is a new :class:`SelectEventLoop` instance; stored as :attr:`.event_loop` :type event_loop: event loop instance :param pop_ups: `True` to wrap :attr:`.widget` with a :class:`PopUpTarget` instance to allow any widget to open a pop-up anywhere on the screen :type pop_ups: boolean .. attribute:: screen The screen object this main loop uses for screen updates and reading input .. attribute:: event_loop The event loop object this main loop uses for waiting on alarms and IO """ def __init__(self, widget, palette=(), screen=None, handle_mouse=True, input_filter=None, unhandled_input=None, event_loop=None, pop_ups=False): self._widget = widget self.handle_mouse = handle_mouse self.pop_ups = pop_ups # triggers property setting side-effect if not screen: from urwid import raw_display screen = raw_display.Screen() if palette: screen.register_palette(palette) self.screen = screen self.screen_size = None self._unhandled_input = unhandled_input self._input_filter = input_filter if not hasattr(screen, 'hook_event_loop' ) and event_loop is not None: raise NotImplementedError("screen object passed " "%r does not support external event loops" % (screen,)) if event_loop is None: event_loop = SelectEventLoop() self.event_loop = event_loop if hasattr(self.screen, 'signal_handler_setter'): # Tell the screen what function it must use to set # signal handlers self.screen.signal_handler_setter = self.event_loop.set_signal_handler self._watch_pipes = {} def _set_widget(self, widget): self._widget = widget if self.pop_ups: self._topmost_widget.original_widget = self._widget else: self._topmost_widget = self._widget widget = property(lambda self:self._widget, _set_widget, doc= """ Property for the topmost widget used to draw the screen. This must be a box widget. """) def _set_pop_ups(self, pop_ups): self._pop_ups = pop_ups if pop_ups: self._topmost_widget = PopUpTarget(self._widget) else: self._topmost_widget = self._widget pop_ups = property(lambda self:self._pop_ups, _set_pop_ups) def set_alarm_in(self, sec, callback, user_data=None): """ Schedule an alarm in *sec* seconds that will call *callback* from the within the :meth:`run` method. :param sec: seconds until alarm :type sec: float :param callback: function to call with two parameters: this main loop object and *user_data* :type callback: callable """ def cb(): callback(self, user_data) return self.event_loop.alarm(sec, cb) def set_alarm_at(self, tm, callback, user_data=None): """ Schedule an alarm at *tm* time that will call *callback* from the within the :meth:`run` function. Returns a handle that may be passed to :meth:`remove_alarm`. :param tm: time to call callback e.g. ``time.time() + 5`` :type tm: float :param callback: function to call with two parameters: this main loop object and *user_data* :type callback: callable """ def cb(): callback(self, user_data) return self.event_loop.alarm(tm - time.time(), cb) def remove_alarm(self, handle): """ Remove an alarm. Return ``True`` if *handle* was found, ``False`` otherwise. """ return self.event_loop.remove_alarm(handle) def watch_pipe(self, callback): """ Create a pipe for use by a subprocess or thread to trigger a callback in the process/thread running the main loop. :param callback: function taking one parameter to call from within the process/thread running the main loop :type callback: callable This method returns a file descriptor attached to the write end of a pipe. The read end of the pipe is added to the list of files :attr:`event_loop` is watching. When data is written to the pipe the callback function will be called and passed a single value containing data read from the pipe. This method may be used any time you want to update widgets from another thread or subprocess. Data may be written to the returned file descriptor with ``os.write(fd, data)``. Ensure that data is less than 512 bytes (or 4K on Linux) so that the callback will be triggered just once with the complete value of data passed in. If the callback returns ``False`` then the watch will be removed from :attr:`event_loop` and the read end of the pipe will be closed. You are responsible for closing the write end of the pipe with ``os.close(fd)``. """ pipe_rd, pipe_wr = os.pipe() fcntl.fcntl(pipe_rd, fcntl.F_SETFL, os.O_NONBLOCK) watch_handle = None def cb(): data = os.read(pipe_rd, PIPE_BUFFER_READ_SIZE) rval = callback(data) if rval is False: self.event_loop.remove_watch_file(watch_handle) os.close(pipe_rd) watch_handle = self.event_loop.watch_file(pipe_rd, cb) self._watch_pipes[pipe_wr] = (watch_handle, pipe_rd) return pipe_wr def remove_watch_pipe(self, write_fd): """ Close the read end of the pipe and remove the watch created by :meth:`watch_pipe`. You are responsible for closing the write end of the pipe. Returns ``True`` if the watch pipe exists, ``False`` otherwise """ try: watch_handle, pipe_rd = self._watch_pipes.pop(write_fd) except KeyError: return False if not self.event_loop.remove_watch_file(watch_handle): return False os.close(pipe_rd) return True def watch_file(self, fd, callback): """ Call *callback* when *fd* has some data to read. No parameters are passed to callback. Returns a handle that may be passed to :meth:`remove_watch_file`. """ return self.event_loop.watch_file(fd, callback) def remove_watch_file(self, handle): """ Remove a watch file. Returns ``True`` if the watch file exists, ``False`` otherwise. """ return self.event_loop.remove_watch_file(handle) def run(self): """ Start the main loop handling input events and updating the screen. The loop will continue until an :exc:`ExitMainLoop` exception is raised. If you would prefer to manage the event loop yourself, don't use this method. Instead, call :meth:`start` before starting the event loop, and :meth:`stop` once it's finished. """ try: self._run() except ExitMainLoop: pass def _test_run(self): """ >>> w = _refl("widget") # _refl prints out function calls >>> w.render_rval = "fake canvas" # *_rval is used for return values >>> scr = _refl("screen") >>> scr.get_input_descriptors_rval = [42] >>> scr.get_cols_rows_rval = (20, 10) >>> scr.started = True >>> scr._urwid_signals = {} >>> evl = _refl("event_loop") >>> evl.enter_idle_rval = 1 >>> evl.watch_file_rval = 2 >>> ml = MainLoop(w, [], scr, event_loop=evl) >>> ml.run() # doctest:+ELLIPSIS screen.start() screen.set_mouse_tracking() screen.unhook_event_loop(...) screen.hook_event_loop(...) event_loop.enter_idle() event_loop.run() event_loop.remove_enter_idle(1) screen.unhook_event_loop(...) screen.stop() >>> ml.draw_screen() # doctest:+ELLIPSIS screen.get_cols_rows() widget.render((20, 10), focus=True) screen.draw_screen((20, 10), 'fake canvas') """ def start(self): """ Sets up the main loop, hooking into the event loop where necessary. Starts the :attr:`screen` if it hasn't already been started. If you want to control starting and stopping the event loop yourself, you should call this method before starting, and call `stop` once the loop has finished. You may also use this method as a context manager, which will stop the loop automatically at the end of the block: with main_loop.start(): ... Note that some event loop implementations don't handle exceptions specially if you manage the event loop yourself. In particular, the Twisted and asyncio loops won't stop automatically when :exc:`ExitMainLoop` (or anything else) is raised. """ self.screen.start() if self.handle_mouse: self.screen.set_mouse_tracking() if not hasattr(self.screen, 'hook_event_loop'): raise CantUseExternalLoop( "Screen {0!r} doesn't support external event loops") try: signals.connect_signal(self.screen, INPUT_DESCRIPTORS_CHANGED, self._reset_input_descriptors) except NameError: pass # watch our input descriptors self._reset_input_descriptors() self.idle_handle = self.event_loop.enter_idle(self.entering_idle) return StoppingContext(self) def stop(self): """ Cleans up any hooks added to the event loop. Only call this if you're managing the event loop yourself, after the loop stops. """ self.event_loop.remove_enter_idle(self.idle_handle) del self.idle_handle signals.disconnect_signal(self.screen, INPUT_DESCRIPTORS_CHANGED, self._reset_input_descriptors) self.screen.unhook_event_loop(self.event_loop) self.screen.stop() def _reset_input_descriptors(self): self.screen.unhook_event_loop(self.event_loop) self.screen.hook_event_loop(self.event_loop, self._update) def _run(self): try: self.start() except CantUseExternalLoop: try: return self._run_screen_event_loop() finally: self.screen.stop() try: self.event_loop.run() except: self.screen.stop() # clean up screen control raise self.stop() def _update(self, keys, raw): """ >>> w = _refl("widget") >>> w.selectable_rval = True >>> w.mouse_event_rval = True >>> scr = _refl("screen") >>> scr.get_cols_rows_rval = (15, 5) >>> evl = _refl("event_loop") >>> ml = MainLoop(w, [], scr, event_loop=evl) >>> ml._input_timeout = "old timeout" >>> ml._update(['y'], [121]) # doctest:+ELLIPSIS screen.get_cols_rows() widget.selectable() widget.keypress((15, 5), 'y') >>> ml._update([("mouse press", 1, 5, 4)], []) widget.mouse_event((15, 5), 'mouse press', 1, 5, 4, focus=True) >>> ml._update([], []) """ keys = self.input_filter(keys, raw) if keys: self.process_input(keys) if 'window resize' in keys: self.screen_size = None def _run_screen_event_loop(self): """ This method is used when the screen does not support using external event loops. The alarms stored in the SelectEventLoop in :attr:`event_loop` are modified by this method. """ next_alarm = None while True: self.draw_screen() if not next_alarm and self.event_loop._alarms: next_alarm = heapq.heappop(self.event_loop._alarms) keys = None while not keys: if next_alarm: sec = max(0, next_alarm[0] - time.time()) self.screen.set_input_timeouts(sec) else: self.screen.set_input_timeouts(None) keys, raw = self.screen.get_input(True) if not keys and next_alarm: sec = next_alarm[0] - time.time() if sec <= 0: break keys = self.input_filter(keys, raw) if keys: self.process_input(keys) while next_alarm: sec = next_alarm[0] - time.time() if sec > 0: break tm, tie_break, callback = next_alarm callback() if self.event_loop._alarms: next_alarm = heapq.heappop(self.event_loop._alarms) else: next_alarm = None if 'window resize' in keys: self.screen_size = None def _test_run_screen_event_loop(self): """ >>> w = _refl("widget") >>> scr = _refl("screen") >>> scr.get_cols_rows_rval = (10, 5) >>> scr.get_input_rval = [], [] >>> ml = MainLoop(w, screen=scr) >>> def stop_now(loop, data): ... raise ExitMainLoop() >>> handle = ml.set_alarm_in(0, stop_now) >>> try: ... ml._run_screen_event_loop() ... except ExitMainLoop: ... pass screen.get_cols_rows() widget.render((10, 5), focus=True) screen.draw_screen((10, 5), None) screen.set_input_timeouts(0) screen.get_input(True) """ def process_input(self, keys): """ This method will pass keyboard input and mouse events to :attr:`widget`. This method is called automatically from the :meth:`run` method when there is input, but may also be called to simulate input from the user. *keys* is a list of input returned from :attr:`screen`'s get_input() or get_input_nonblocking() methods. Returns ``True`` if any key was handled by a widget or the :meth:`unhandled_input` method. """ if not self.screen_size: self.screen_size = self.screen.get_cols_rows() something_handled = False for k in keys: if k == 'window resize': continue if is_mouse_event(k): event, button, col, row = k if self._topmost_widget.mouse_event(self.screen_size, event, button, col, row, focus=True ): k = None elif self._topmost_widget.selectable(): k = self._topmost_widget.keypress(self.screen_size, k) if k: if command_map[k] == REDRAW_SCREEN: self.screen.clear() something_handled = True else: something_handled |= bool(self.unhandled_input(k)) else: something_handled = True return something_handled def _test_process_input(self): """ >>> w = _refl("widget") >>> w.selectable_rval = True >>> scr = _refl("screen") >>> scr.get_cols_rows_rval = (10, 5) >>> ml = MainLoop(w, [], scr) >>> ml.process_input(['enter', ('mouse drag', 1, 14, 20)]) screen.get_cols_rows() widget.selectable() widget.keypress((10, 5), 'enter') widget.mouse_event((10, 5), 'mouse drag', 1, 14, 20, focus=True) True """ def input_filter(self, keys, raw): """ This function is passed each all the input events and raw keystroke values. These values are passed to the *input_filter* function passed to the constructor. That function must return a list of keys to be passed to the widgets to handle. If no *input_filter* was defined this implementation will return all the input events. """ if self._input_filter: return self._input_filter(keys, raw) return keys def unhandled_input(self, input): """ This function is called with any input that was not handled by the widgets, and calls the *unhandled_input* function passed to the constructor. If no *unhandled_input* was defined then the input will be ignored. *input* is the keyboard or mouse input. The *unhandled_input* function should return ``True`` if it handled the input. """ if self._unhandled_input: return self._unhandled_input(input) def entering_idle(self): """ This method is called whenever the event loop is about to enter the idle state. :meth:`draw_screen` is called here to update the screen when anything has changed. """ if self.screen.started: self.draw_screen() def draw_screen(self): """ Render the widgets and paint the screen. This method is called automatically from :meth:`entering_idle`. If you modify the widgets displayed outside of handling input or responding to an alarm you will need to call this method yourself to repaint the screen. """ if not self.screen_size: self.screen_size = self.screen.get_cols_rows() canvas = self._topmost_widget.render(self.screen_size, focus=True) self.screen.draw_screen(self.screen_size, canvas) class EventLoop(object): """ Abstract class representing an event loop to be used by :class:`MainLoop`. """ def alarm(self, seconds, callback): """ Call callback() a given time from now. No parameters are passed to callback. This method has no default implementation. Returns a handle that may be passed to remove_alarm() seconds -- floating point time to wait before calling callback callback -- function to call from event loop """ raise NotImplementedError() def enter_idle(self, callback): """ Add a callback for entering idle. This method has no default implementation. Returns a handle that may be passed to remove_idle() """ raise NotImplementedError() def remove_alarm(self, handle): """ Remove an alarm. This method has no default implementation. Returns True if the alarm exists, False otherwise """ raise NotImplementedError() def remove_enter_idle(self, handle): """ Remove an idle callback. This method has no default implementation. Returns True if the handle was removed. """ raise NotImplementedError() def remove_watch_file(self, handle): """ Remove an input file. This method has no default implementation. Returns True if the input file exists, False otherwise """ raise NotImplementedError() def run(self): """ Start the event loop. Exit the loop when any callback raises an exception. If ExitMainLoop is raised, exit cleanly. This method has no default implementation. """ raise NotImplementedError() def watch_file(self, fd, callback): """ Call callback() when fd has some data to read. No parameters are passed to callback. This method has no default implementation. Returns a handle that may be passed to remove_watch_file() fd -- file descriptor to watch for input callback -- function to call when input is available """ raise NotImplementedError() def set_signal_handler(self, signum, handler): """ Sets the signal handler for signal signum. The default implementation of :meth:`set_signal_handler` is simply a proxy function that calls :func:`signal.signal()` and returns the resulting value. signum -- signal number handler -- function (taking signum as its single argument), or `signal.SIG_IGN`, or `signal.SIG_DFL` """ return signal.signal(signum, handler) class SelectEventLoop(EventLoop): """ Event loop based on :func:`select.select` """ def __init__(self): self._alarms = [] self._watch_files = {} self._idle_handle = 0 self._idle_callbacks = {} self._tie_break = count() def alarm(self, seconds, callback): """ Call callback() a given time from now. No parameters are passed to callback. Returns a handle that may be passed to remove_alarm() seconds -- floating point time to wait before calling callback callback -- function to call from event loop """ tm = time.time() + seconds handle = (tm, next(self._tie_break), callback) heapq.heappush(self._alarms, handle) return handle def remove_alarm(self, handle): """ Remove an alarm. Returns True if the alarm exists, False otherwise """ try: self._alarms.remove(handle) heapq.heapify(self._alarms) return True except ValueError: return False def watch_file(self, fd, callback): """ Call callback() when fd has some data to read. No parameters are passed to callback. Returns a handle that may be passed to remove_watch_file() fd -- file descriptor to watch for input callback -- function to call when input is available """ self._watch_files[fd] = callback return fd def remove_watch_file(self, handle): """ Remove an input file. Returns True if the input file exists, False otherwise """ if handle in self._watch_files: del self._watch_files[handle] return True return False def enter_idle(self, callback): """ Add a callback for entering idle. Returns a handle that may be passed to remove_idle() """ self._idle_handle += 1 self._idle_callbacks[self._idle_handle] = callback return self._idle_handle def remove_enter_idle(self, handle): """ Remove an idle callback. Returns True if the handle was removed. """ try: del self._idle_callbacks[handle] except KeyError: return False return True def _entering_idle(self): """ Call all the registered idle callbacks. """ for callback in self._idle_callbacks.values(): callback() def run(self): """ Start the event loop. Exit the loop when any callback raises an exception. If ExitMainLoop is raised, exit cleanly. """ try: self._did_something = True while True: try: self._loop() except select.error as e: if e.args[0] != 4: # not just something we need to retry raise except ExitMainLoop: pass def _loop(self): """ A single iteration of the event loop """ fds = list(self._watch_files.keys()) if self._alarms or self._did_something: if self._alarms: tm = self._alarms[0][0] timeout = max(0, tm - time.time()) if self._did_something and (not self._alarms or (self._alarms and timeout > 0)): timeout = 0 tm = 'idle' ready, w, err = select.select(fds, [], fds, timeout) else: tm = None ready, w, err = select.select(fds, [], fds) if not ready: if tm == 'idle': self._entering_idle() self._did_something = False elif tm is not None: # must have been a timeout tm, tie_break, alarm_callback = heapq.heappop(self._alarms) alarm_callback() self._did_something = True for fd in ready: self._watch_files[fd]() self._did_something = True class GLibEventLoop(EventLoop): """ Event loop based on GLib.MainLoop """ def __init__(self): from gi.repository import GLib self.GLib = GLib self._alarms = [] self._watch_files = {} self._idle_handle = 0 self._glib_idle_enabled = False # have we called glib.idle_add? self._idle_callbacks = {} self._loop = GLib.MainLoop() self._exc_info = None self._enable_glib_idle() self._signal_handlers = {} def alarm(self, seconds, callback): """ Call callback() a given time from now. No parameters are passed to callback. Returns a handle that may be passed to remove_alarm() seconds -- floating point time to wait before calling callback callback -- function to call from event loop """ @self.handle_exit def ret_false(): callback() self._enable_glib_idle() return False fd = self.GLib.timeout_add(int(seconds*1000), ret_false) self._alarms.append(fd) return (fd, callback) def set_signal_handler(self, signum, handler): """ Sets the signal handler for signal signum. .. WARNING:: Because this method uses the `GLib`-specific `unix_signal_add` function, its behaviour is different than `signal.signal().` If `signum` is not `SIGHUP`, `SIGINT`, `SIGTERM`, `SIGUSR1`, `SIGUSR2` or `SIGWINCH`, this method performs no actions and immediately returns None. Returns None in all cases (unlike :func:`signal.signal()`). .. signum -- signal number handler -- function (taking signum as its single argument), or `signal.SIG_IGN`, or `signal.SIG_DFL` """ glib_signals = [ signal.SIGHUP, signal.SIGINT, signal.SIGTERM, signal.SIGUSR1, signal.SIGUSR2, signal.SIGWINCH ] if signum not in glib_signals: # The GLib event loop supports only the signals listed above return if signum in self._signal_handlers: self.GLib.source_remove(self._signal_handlers.pop(signum)) if handler == signal.SIG_IGN: handler = lambda x: None elif handler == signal.SIG_DFL: return def final_handler(signal_number): handler(signal_number) return self.GLib.SOURCE_CONTINUE source = self.GLib.unix_signal_add(self.GLib.PRIORITY_DEFAULT, signum, final_handler, signum) self._signal_handlers[signum] = source def remove_alarm(self, handle): """ Remove an alarm. Returns True if the alarm exists, False otherwise """ try: self._alarms.remove(handle[0]) self.GLib.source_remove(handle[0]) return True except ValueError: return False def watch_file(self, fd, callback): """ Call callback() when fd has some data to read. No parameters are passed to callback. Returns a handle that may be passed to remove_watch_file() fd -- file descriptor to watch for input callback -- function to call when input is available """ @self.handle_exit def io_callback(source, cb_condition): callback() self._enable_glib_idle() return True self._watch_files[fd] = \ self.GLib.io_add_watch(fd,self.GLib.IO_IN,io_callback) return fd def remove_watch_file(self, handle): """ Remove an input file. Returns True if the input file exists, False otherwise """ if handle in self._watch_files: self.GLib.source_remove(self._watch_files[handle]) del self._watch_files[handle] return True return False def enter_idle(self, callback): """ Add a callback for entering idle. Returns a handle that may be passed to remove_enter_idle() """ self._idle_handle += 1 self._idle_callbacks[self._idle_handle] = callback return self._idle_handle def _enable_glib_idle(self): if self._glib_idle_enabled: return self.GLib.idle_add(self._glib_idle_callback) self._glib_idle_enabled = True def _glib_idle_callback(self): for callback in self._idle_callbacks.values(): callback() self._glib_idle_enabled = False return False # ask glib not to call again (or we would be called def remove_enter_idle(self, handle): """ Remove an idle callback. Returns True if the handle was removed. """ try: del self._idle_callbacks[handle] except KeyError: return False return True def run(self): """ Start the event loop. Exit the loop when any callback raises an exception. If ExitMainLoop is raised, exit cleanly. """ try: self._loop.run() finally: if self._loop.is_running(): self._loop.quit() if self._exc_info: # An exception caused us to exit, raise it now exc_info = self._exc_info self._exc_info = None reraise(*exc_info) def handle_exit(self,f): """ Decorator that cleanly exits the :class:`GLibEventLoop` if :exc:`ExitMainLoop` is thrown inside of the wrapped function. Store the exception info if some other exception occurs, it will be reraised after the loop quits. *f* -- function to be wrapped """ def wrapper(*args,**kargs): try: return f(*args,**kargs) except ExitMainLoop: self._loop.quit() except: import sys self._exc_info = sys.exc_info() if self._loop.is_running(): self._loop.quit() return False return wrapper class TornadoEventLoop(EventLoop): """ This is an Urwid-specific event loop to plug into its MainLoop. It acts as an adaptor for Tornado's IOLoop which does all heavy lifting except idle-callbacks. Notice, since Tornado has no concept of idle callbacks we monkey patch ioloop._impl.poll() function to be able to detect potential idle periods. """ _ioloop_registry = WeakKeyDictionary() # { : { : }} _max_idle_handle = 0 class PollProxy(object): """ A simple proxy for a Python's poll object that wraps the .poll() method in order to detect idle periods and call Urwid callbacks """ def __init__(self, poll_obj, idle_map): self.__poll_obj = poll_obj self.__idle_map = idle_map self._idle_done = False self._prev_timeout = 0 def __getattr__(self, name): return getattr(self.__poll_obj, name) def poll(self, timeout): if timeout > self._prev_timeout: # if timeout increased we assume a timer event was handled self._idle_done = False self._prev_timeout = timeout start = time.time() # any IO pending wins events = self.__poll_obj.poll(0) if events: self._idle_done = False return events # our chance to enter idle if not self._idle_done: for callback in self.__idle_map.values(): callback() self._idle_done = True # then complete the actual request (adjusting timeout) timeout = max(0, min(timeout, timeout + start - time.time())) events = self.__poll_obj.poll(timeout) if events: self._idle_done = False return events @classmethod def _patch_poll_impl(cls, ioloop): """ Wraps original poll object in the IOLoop's poll object """ if ioloop in cls._ioloop_registry: return # we already patched this instance cls._ioloop_registry[ioloop] = idle_map = {} ioloop._impl = cls.PollProxy(ioloop._impl, idle_map) def __init__(self, ioloop=None): if not ioloop: from tornado.ioloop import IOLoop ioloop = IOLoop.instance() self._ioloop = ioloop self._patch_poll_impl(ioloop) self._pending_alarms = {} self._watch_handles = {} # { : } self._max_watch_handle = 0 self._exception = None def alarm(self, secs, callback): ioloop = self._ioloop def wrapped(): try: del self._pending_alarms[handle] except KeyError: pass self.handle_exit(callback)() handle = ioloop.add_timeout(ioloop.time() + secs, wrapped) self._pending_alarms[handle] = 1 return handle def remove_alarm(self, handle): self._ioloop.remove_timeout(handle) try: del self._pending_alarms[handle] except KeyError: return False else: return True def watch_file(self, fd, callback): from tornado.ioloop import IOLoop handler = lambda fd,events: self.handle_exit(callback)() self._ioloop.add_handler(fd, handler, IOLoop.READ) self._max_watch_handle += 1 handle = self._max_watch_handle self._watch_handles[handle] = fd return handle def remove_watch_file(self, handle): fd = self._watch_handles.pop(handle, None) if fd is None: return False else: self._ioloop.remove_handler(fd) return True def enter_idle(self, callback): self._max_idle_handle += 1 handle = self._max_idle_handle idle_map = self._ioloop_registry[self._ioloop] idle_map[handle] = callback return handle def remove_enter_idle(self, handle): idle_map = self._ioloop_registry[self._ioloop] cb = idle_map.pop(handle, None) return cb is not None def handle_exit(self, func): @wraps(func) def wrapper(*args, **kw): try: return func(*args, **kw) except ExitMainLoop: self._ioloop.stop() except Exception as exc: self._exception = exc self._ioloop.stop() return False return wrapper def run(self): self._ioloop.start() if self._exception: exc, self._exception = self._exception, None raise exc try: from twisted.internet.abstract import FileDescriptor except ImportError: FileDescriptor = object class TwistedInputDescriptor(FileDescriptor): def __init__(self, reactor, fd, cb): self._fileno = fd self.cb = cb FileDescriptor.__init__(self, reactor) def fileno(self): return self._fileno def doRead(self): return self.cb() class TwistedEventLoop(EventLoop): """ Event loop based on Twisted_ """ _idle_emulation_delay = 1.0/256 # a short time (in seconds) def __init__(self, reactor=None, manage_reactor=True): """ :param reactor: reactor to use :type reactor: :class:`twisted.internet.reactor`. :param: manage_reactor: `True` if you want this event loop to run and stop the reactor. :type manage_reactor: boolean .. WARNING:: Twisted's reactor doesn't like to be stopped and run again. If you need to stop and run your :class:`MainLoop`, consider setting ``manage_reactor=False`` and take care of running/stopping the reactor at the beginning/ending of your program yourself. You can also forego using :class:`MainLoop`'s run() entirely, and instead call start() and stop() before and after starting the reactor. .. _Twisted: http://twistedmatrix.com/trac/ """ if reactor is None: import twisted.internet.reactor reactor = twisted.internet.reactor self.reactor = reactor self._alarms = [] self._watch_files = {} self._idle_handle = 0 self._twisted_idle_enabled = False self._idle_callbacks = {} self._exc_info = None self.manage_reactor = manage_reactor self._enable_twisted_idle() def alarm(self, seconds, callback): """ Call callback() a given time from now. No parameters are passed to callback. Returns a handle that may be passed to remove_alarm() seconds -- floating point time to wait before calling callback callback -- function to call from event loop """ handle = self.reactor.callLater(seconds, self.handle_exit(callback)) return handle def remove_alarm(self, handle): """ Remove an alarm. Returns True if the alarm exists, False otherwise """ from twisted.internet.error import AlreadyCancelled, AlreadyCalled try: handle.cancel() return True except AlreadyCancelled: return False except AlreadyCalled: return False def watch_file(self, fd, callback): """ Call callback() when fd has some data to read. No parameters are passed to callback. Returns a handle that may be passed to remove_watch_file() fd -- file descriptor to watch for input callback -- function to call when input is available """ ind = TwistedInputDescriptor(self.reactor, fd, self.handle_exit(callback)) self._watch_files[fd] = ind self.reactor.addReader(ind) return fd def remove_watch_file(self, handle): """ Remove an input file. Returns True if the input file exists, False otherwise """ if handle in self._watch_files: self.reactor.removeReader(self._watch_files[handle]) del self._watch_files[handle] return True return False def enter_idle(self, callback): """ Add a callback for entering idle. Returns a handle that may be passed to remove_enter_idle() """ self._idle_handle += 1 self._idle_callbacks[self._idle_handle] = callback return self._idle_handle def _enable_twisted_idle(self): """ Twisted's reactors don't have an idle or enter-idle callback so the best we can do for now is to set a timer event in a very short time to approximate an enter-idle callback. .. WARNING:: This will perform worse than the other event loops until we can find a fix or workaround """ if self._twisted_idle_enabled: return self.reactor.callLater(self._idle_emulation_delay, self.handle_exit(self._twisted_idle_callback, enable_idle=False)) self._twisted_idle_enabled = True def _twisted_idle_callback(self): for callback in self._idle_callbacks.values(): callback() self._twisted_idle_enabled = False def remove_enter_idle(self, handle): """ Remove an idle callback. Returns True if the handle was removed. """ try: del self._idle_callbacks[handle] except KeyError: return False return True def run(self): """ Start the event loop. Exit the loop when any callback raises an exception. If ExitMainLoop is raised, exit cleanly. """ if not self.manage_reactor: return self.reactor.run() if self._exc_info: # An exception caused us to exit, raise it now exc_info = self._exc_info self._exc_info = None reraise(*exc_info) def handle_exit(self, f, enable_idle=True): """ Decorator that cleanly exits the :class:`TwistedEventLoop` if :class:`ExitMainLoop` is thrown inside of the wrapped function. Store the exception info if some other exception occurs, it will be reraised after the loop quits. *f* -- function to be wrapped """ def wrapper(*args,**kargs): rval = None try: rval = f(*args,**kargs) except ExitMainLoop: if self.manage_reactor: self.reactor.stop() except: import sys print(sys.exc_info()) self._exc_info = sys.exc_info() if self.manage_reactor: self.reactor.crash() if enable_idle: self._enable_twisted_idle() return rval return wrapper class AsyncioEventLoop(EventLoop): """ Event loop based on the standard library ``asyncio`` module. ``asyncio`` is new in Python 3.4, but also exists as a backport on PyPI for Python 3.3. The ``trollius`` package is available for older Pythons with slightly different syntax, but also works with this loop. """ _we_started_event_loop = False _idle_emulation_delay = 1.0/256 # a short time (in seconds) def __init__(self, **kwargs): if 'loop' in kwargs: self._loop = kwargs.pop('loop') else: import asyncio self._loop = asyncio.get_event_loop() def alarm(self, seconds, callback): """ Call callback() a given time from now. No parameters are passed to callback. Returns a handle that may be passed to remove_alarm() seconds -- time in seconds to wait before calling callback callback -- function to call from event loop """ return self._loop.call_later(seconds, callback) def remove_alarm(self, handle): """ Remove an alarm. Returns True if the alarm exists, False otherwise """ existed = not handle._cancelled handle.cancel() return existed def watch_file(self, fd, callback): """ Call callback() when fd has some data to read. No parameters are passed to callback. Returns a handle that may be passed to remove_watch_file() fd -- file descriptor to watch for input callback -- function to call when input is available """ self._loop.add_reader(fd, callback) return fd def remove_watch_file(self, handle): """ Remove an input file. Returns True if the input file exists, False otherwise """ return self._loop.remove_reader(handle) def enter_idle(self, callback): """ Add a callback for entering idle. Returns a handle that may be passed to remove_idle() """ # XXX there's no such thing as "idle" in most event loops; this fakes # it the same way as Twisted, by scheduling the callback to be called # repeatedly mutable_handle = [None] def faux_idle_callback(): callback() mutable_handle[0] = self._loop.call_later( self._idle_emulation_delay, faux_idle_callback) mutable_handle[0] = self._loop.call_later( self._idle_emulation_delay, faux_idle_callback) return mutable_handle def remove_enter_idle(self, handle): """ Remove an idle callback. Returns True if the handle was removed. """ # `handle` is just a list containing the current actual handle return self.remove_alarm(handle[0]) _exc_info = None def _exception_handler(self, loop, context): exc = context.get('exception') if exc: loop.stop() if not isinstance(exc, ExitMainLoop): # Store the exc_info so we can re-raise after the loop stops import sys self._exc_info = sys.exc_info() else: loop.default_exception_handler(context) def run(self): """ Start the event loop. Exit the loop when any callback raises an exception. If ExitMainLoop is raised, exit cleanly. """ self._loop.set_exception_handler(self._exception_handler) self._loop.run_forever() if self._exc_info: exc_info = self._exc_info self._exc_info = None reraise(*exc_info) def _refl(name, rval=None, exit=False): """ This function is used to test the main loop classes. >>> scr = _refl("screen") >>> scr.function("argument") screen.function('argument') >>> scr.callme(when="now") screen.callme(when='now') >>> scr.want_something_rval = 42 >>> x = scr.want_something() screen.want_something() >>> x 42 """ class Reflect(object): def __init__(self, name, rval=None): self._name = name self._rval = rval def __call__(self, *argl, **argd): args = ", ".join([repr(a) for a in argl]) if args and argd: args = args + ", " args = args + ", ".join([k+"="+repr(v) for k,v in argd.items()]) print(self._name+"("+args+")") if exit: raise ExitMainLoop() return self._rval def __getattr__(self, attr): if attr.endswith("_rval"): raise AttributeError() #print self._name+"."+attr if hasattr(self, attr+"_rval"): return Reflect(self._name+"."+attr, getattr(self, attr+"_rval")) return Reflect(self._name+"."+attr) return Reflect(name) def _test(): import doctest doctest.testmod() if __name__=='__main__': _test() urwid-2.0.1/urwid/curses_display.py0000755000175000017500000004757313231170672021072 0ustar andersonanderson00000000000000#!/usr/bin/python # # Urwid curses output wrapper.. the horror.. # 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 """ Curses-based UI implementation """ import curses import _curses from urwid import escape from urwid.display_common import BaseScreen, RealTerminal, AttrSpec, \ UNPRINTABLE_TRANS_TABLE from urwid.compat import bytes, PYTHON3, text_type, xrange KEY_RESIZE = 410 # curses.KEY_RESIZE (sometimes not defined) KEY_MOUSE = 409 # curses.KEY_MOUSE _curses_colours = { 'default': (-1, 0), 'black': (curses.COLOR_BLACK, 0), 'dark red': (curses.COLOR_RED, 0), 'dark green': (curses.COLOR_GREEN, 0), 'brown': (curses.COLOR_YELLOW, 0), 'dark blue': (curses.COLOR_BLUE, 0), 'dark magenta': (curses.COLOR_MAGENTA, 0), 'dark cyan': (curses.COLOR_CYAN, 0), 'light gray': (curses.COLOR_WHITE, 0), 'dark gray': (curses.COLOR_BLACK, 1), 'light red': (curses.COLOR_RED, 1), 'light green': (curses.COLOR_GREEN, 1), 'yellow': (curses.COLOR_YELLOW, 1), 'light blue': (curses.COLOR_BLUE, 1), 'light magenta': (curses.COLOR_MAGENTA, 1), 'light cyan': (curses.COLOR_CYAN, 1), 'white': (curses.COLOR_WHITE, 1), } class Screen(BaseScreen, RealTerminal): def __init__(self): super(Screen,self).__init__() self.curses_pairs = [ (None,None), # Can't be sure what pair 0 will default to ] self.palette = {} self.has_color = False self.s = None self.cursor_state = None self._keyqueue = [] self.prev_input_resize = 0 self.set_input_timeouts() self.last_bstate = 0 self._mouse_tracking_enabled = False self.register_palette_entry(None, 'default','default') def set_mouse_tracking(self, enable=True): """ Enable mouse tracking. After calling this function get_input will include mouse click events along with keystrokes. """ enable = bool(enable) if enable == self._mouse_tracking_enabled: return if enable: curses.mousemask(0 | curses.BUTTON1_PRESSED | curses.BUTTON1_RELEASED | curses.BUTTON2_PRESSED | curses.BUTTON2_RELEASED | curses.BUTTON3_PRESSED | curses.BUTTON3_RELEASED | curses.BUTTON4_PRESSED | curses.BUTTON4_RELEASED | curses.BUTTON1_DOUBLE_CLICKED | curses.BUTTON1_TRIPLE_CLICKED | curses.BUTTON2_DOUBLE_CLICKED | curses.BUTTON2_TRIPLE_CLICKED | curses.BUTTON3_DOUBLE_CLICKED | curses.BUTTON3_TRIPLE_CLICKED | curses.BUTTON4_DOUBLE_CLICKED | curses.BUTTON4_TRIPLE_CLICKED | curses.BUTTON_SHIFT | curses.BUTTON_ALT | curses.BUTTON_CTRL) else: raise NotImplementedError() self._mouse_tracking_enabled = enable def _start(self): """ Initialize the screen and input mode. """ self.s = curses.initscr() self.has_color = curses.has_colors() if self.has_color: curses.start_color() if curses.COLORS < 8: # not colourful enough self.has_color = False if self.has_color: try: curses.use_default_colors() self.has_default_colors=True except _curses.error: self.has_default_colors=False self._setup_colour_pairs() curses.noecho() curses.meta(1) curses.halfdelay(10) # use set_input_timeouts to adjust self.s.keypad(0) if not self._signal_keys_set: self._old_signal_keys = self.tty_signal_keys() super(Screen, self)._start() def _stop(self): """ Restore the screen. """ curses.echo() self._curs_set(1) try: curses.endwin() except _curses.error: pass # don't block original error with curses error if self._old_signal_keys: self.tty_signal_keys(*self._old_signal_keys) super(Screen, self)._stop() def _setup_colour_pairs(self): """ Initialize all 63 color pairs based on the term: bg * 8 + 7 - fg So to get a color, we just need to use that term and get the right color pair number. """ if not self.has_color: return for fg in xrange(8): for bg in xrange(8): # leave out white on black if fg == curses.COLOR_WHITE and \ bg == curses.COLOR_BLACK: continue curses.init_pair(bg * 8 + 7 - fg, fg, bg) def _curs_set(self,x): if self.cursor_state== "fixed" or x == self.cursor_state: return try: curses.curs_set(x) self.cursor_state = x except _curses.error: self.cursor_state = "fixed" def _clear(self): self.s.clear() self.s.refresh() def _getch(self, wait_tenths): if wait_tenths==0: return self._getch_nodelay() if wait_tenths is None: curses.cbreak() else: curses.halfdelay(wait_tenths) self.s.nodelay(0) return self.s.getch() def _getch_nodelay(self): self.s.nodelay(1) while 1: # this call fails sometimes, but seems to work when I try again try: curses.cbreak() break except _curses.error: pass return self.s.getch() def set_input_timeouts(self, max_wait=None, complete_wait=0.1, resize_wait=0.1): """ Set the get_input timeout values. All values have a granularity of 0.1s, ie. any value between 0.15 and 0.05 will be treated as 0.1 and any value less than 0.05 will be treated as 0. The maximum timeout value for this module is 25.5 seconds. max_wait -- amount of time in seconds to wait for input when there is no input pending, wait forever if None complete_wait -- amount of time in seconds to wait when get_input detects an incomplete escape sequence at the end of the available input resize_wait -- amount of time in seconds to wait for more input after receiving two screen resize requests in a row to stop urwid from consuming 100% cpu during a gradual window resize operation """ def convert_to_tenths( s ): if s is None: return None return int( (s+0.05)*10 ) self.max_tenths = convert_to_tenths(max_wait) self.complete_tenths = convert_to_tenths(complete_wait) self.resize_tenths = convert_to_tenths(resize_wait) def get_input(self, raw_keys=False): """Return pending input as a list. raw_keys -- return raw keycodes as well as translated versions This function will immediately return all the input since the last time it was called. If there is no input pending it will wait before returning an empty list. The wait time may be configured with the set_input_timeouts function. If raw_keys is False (default) this function will return a list of keys pressed. If raw_keys is True this function will return a ( keys pressed, raw keycodes ) tuple instead. Examples of keys returned: * ASCII printable characters: " ", "a", "0", "A", "-", "/" * ASCII control characters: "tab", "enter" * Escape sequences: "up", "page up", "home", "insert", "f1" * Key combinations: "shift f1", "meta a", "ctrl b" * Window events: "window resize" When a narrow encoding is not enabled: * "Extended ASCII" characters: "\\xa1", "\\xb2", "\\xfe" When a wide encoding is enabled: * Double-byte characters: "\\xa1\\xea", "\\xb2\\xd4" When utf8 encoding is enabled: * Unicode characters: u"\\u00a5", u'\\u253c" Examples of mouse events returned: * Mouse button press: ('mouse press', 1, 15, 13), ('meta mouse press', 2, 17, 23) * Mouse button release: ('mouse release', 0, 18, 13), ('ctrl mouse release', 0, 17, 23) """ assert self._started keys, raw = self._get_input( self.max_tenths ) # Avoid pegging CPU at 100% when slowly resizing, and work # around a bug with some braindead curses implementations that # return "no key" between "window resize" commands if keys==['window resize'] and self.prev_input_resize: while True: keys, raw2 = self._get_input(self.resize_tenths) raw += raw2 if not keys: keys, raw2 = self._get_input( self.resize_tenths) raw += raw2 if keys!=['window resize']: break if keys[-1:]!=['window resize']: keys.append('window resize') if keys==['window resize']: self.prev_input_resize = 2 elif self.prev_input_resize == 2 and not keys: self.prev_input_resize = 1 else: self.prev_input_resize = 0 if raw_keys: return keys, raw return keys def _get_input(self, wait_tenths): # this works around a strange curses bug with window resizing # not being reported correctly with repeated calls to this # function without a doupdate call in between curses.doupdate() key = self._getch(wait_tenths) resize = False raw = [] keys = [] while key >= 0: raw.append(key) if key==KEY_RESIZE: resize = True elif key==KEY_MOUSE: keys += self._encode_mouse_event() else: keys.append(key) key = self._getch_nodelay() processed = [] try: while keys: run, keys = escape.process_keyqueue(keys, True) processed += run except escape.MoreInputRequired: key = self._getch(self.complete_tenths) while key >= 0: raw.append(key) if key==KEY_RESIZE: resize = True elif key==KEY_MOUSE: keys += self._encode_mouse_event() else: keys.append(key) key = self._getch_nodelay() while keys: run, keys = escape.process_keyqueue(keys, False) processed += run if resize: processed.append('window resize') return processed, raw def _encode_mouse_event(self): # convert to escape sequence last = next = self.last_bstate (id,x,y,z,bstate) = curses.getmouse() mod = 0 if bstate & curses.BUTTON_SHIFT: mod |= 4 if bstate & curses.BUTTON_ALT: mod |= 8 if bstate & curses.BUTTON_CTRL: mod |= 16 l = [] def append_button( b ): b |= mod l.extend([ 27, ord('['), ord('M'), b+32, x+33, y+33 ]) if bstate & curses.BUTTON1_PRESSED and last & 1 == 0: append_button( 0 ) next |= 1 if bstate & curses.BUTTON2_PRESSED and last & 2 == 0: append_button( 1 ) next |= 2 if bstate & curses.BUTTON3_PRESSED and last & 4 == 0: append_button( 2 ) next |= 4 if bstate & curses.BUTTON4_PRESSED and last & 8 == 0: append_button( 64 ) next |= 8 if bstate & curses.BUTTON1_RELEASED and last & 1: append_button( 0 + escape.MOUSE_RELEASE_FLAG ) next &= ~ 1 if bstate & curses.BUTTON2_RELEASED and last & 2: append_button( 1 + escape.MOUSE_RELEASE_FLAG ) next &= ~ 2 if bstate & curses.BUTTON3_RELEASED and last & 4: append_button( 2 + escape.MOUSE_RELEASE_FLAG ) next &= ~ 4 if bstate & curses.BUTTON4_RELEASED and last & 8: append_button( 64 + escape.MOUSE_RELEASE_FLAG ) next &= ~ 8 if bstate & curses.BUTTON1_DOUBLE_CLICKED: append_button( 0 + escape.MOUSE_MULTIPLE_CLICK_FLAG ) if bstate & curses.BUTTON2_DOUBLE_CLICKED: append_button( 1 + escape.MOUSE_MULTIPLE_CLICK_FLAG ) if bstate & curses.BUTTON3_DOUBLE_CLICKED: append_button( 2 + escape.MOUSE_MULTIPLE_CLICK_FLAG ) if bstate & curses.BUTTON4_DOUBLE_CLICKED: append_button( 64 + escape.MOUSE_MULTIPLE_CLICK_FLAG ) if bstate & curses.BUTTON1_TRIPLE_CLICKED: append_button( 0 + escape.MOUSE_MULTIPLE_CLICK_FLAG*2 ) if bstate & curses.BUTTON2_TRIPLE_CLICKED: append_button( 1 + escape.MOUSE_MULTIPLE_CLICK_FLAG*2 ) if bstate & curses.BUTTON3_TRIPLE_CLICKED: append_button( 2 + escape.MOUSE_MULTIPLE_CLICK_FLAG*2 ) if bstate & curses.BUTTON4_TRIPLE_CLICKED: append_button( 64 + escape.MOUSE_MULTIPLE_CLICK_FLAG*2 ) self.last_bstate = next return l def _dbg_instr(self): # messy input string (intended for debugging) curses.echo() self.s.nodelay(0) curses.halfdelay(100) str = self.s.getstr() curses.noecho() return str def _dbg_out(self,str): # messy output function (intended for debugging) self.s.clrtoeol() self.s.addstr(str) self.s.refresh() self._curs_set(1) def _dbg_query(self,question): # messy query (intended for debugging) self._dbg_out(question) return self._dbg_instr() def _dbg_refresh(self): self.s.refresh() def get_cols_rows(self): """Return the terminal dimensions (num columns, num rows).""" rows,cols = self.s.getmaxyx() return cols,rows def _setattr(self, a): if a is None: self.s.attrset(0) return elif not isinstance(a, AttrSpec): p = self._palette.get(a, (AttrSpec('default', 'default'),)) a = p[0] if self.has_color: if a.foreground_basic: if a.foreground_number >= 8: fg = a.foreground_number - 8 else: fg = a.foreground_number else: fg = 7 if a.background_basic: bg = a.background_number else: bg = 0 attr = curses.color_pair(bg * 8 + 7 - fg) else: attr = 0 if a.bold: attr |= curses.A_BOLD if a.standout: attr |= curses.A_STANDOUT if a.underline: attr |= curses.A_UNDERLINE if a.blink: attr |= curses.A_BLINK self.s.attrset(attr) def draw_screen(self, size, r ): """Paint screen with rendered canvas.""" assert self._started cols, rows = size assert r.rows() == rows, "canvas size and passed size don't match" y = -1 for row in r.content(): y += 1 try: self.s.move( y, 0 ) except _curses.error: # terminal shrunk? # move failed so stop rendering. return first = True lasta = None nr = 0 for a, cs, seg in row: if cs != 'U': seg = seg.translate(UNPRINTABLE_TRANS_TABLE) assert isinstance(seg, bytes) if first or lasta != a: self._setattr(a) lasta = a try: if cs in ("0", "U"): for i in range(len(seg)): self.s.addch( 0x400000 + ord(seg[i]) ) else: assert cs is None if PYTHON3: assert isinstance(seg, bytes) self.s.addstr(seg.decode('utf-8')) else: self.s.addstr(seg) except _curses.error: # it's ok to get out of the # screen on the lower right if (y == rows-1 and nr == len(row)-1): pass else: # perhaps screen size changed # quietly abort. return nr += 1 if r.cursor is not None: x,y = r.cursor self._curs_set(1) try: self.s.move(y,x) except _curses.error: pass else: self._curs_set(0) self.s.move(0,0) self.s.refresh() self.keep_cache_alive_link = r def clear(self): """ Force the screen to be completely repainted on the next call to draw_screen(). """ self.s.clear() class _test: def __init__(self): self.ui = Screen() self.l = list(_curses_colours.keys()) self.l.sort() for c in self.l: self.ui.register_palette( [ (c+" on black", c, 'black', 'underline'), (c+" on dark blue",c, 'dark blue', 'bold'), (c+" on light gray",c,'light gray', 'standout'), ]) self.ui.run_wrapper(self.run) def run(self): class FakeRender: pass r = FakeRender() text = [" has_color = "+repr(self.ui.has_color),""] attr = [[],[]] r.coords = {} r.cursor = None for c in self.l: t = "" a = [] for p in c+" on black",c+" on dark blue",c+" on light gray": a.append((p,27)) t=t+ (p+27*" ")[:27] text.append( t ) attr.append( a ) text += ["","return values from get_input(): (q exits)", ""] attr += [[],[],[]] cols,rows = self.ui.get_cols_rows() keys = None while keys!=['q']: r.text=([t.ljust(cols) for t in text]+[""]*rows)[:rows] r.attr=(attr+[[]]*rows) [:rows] self.ui.draw_screen((cols,rows),r) keys, raw = self.ui.get_input( raw_keys = True ) if 'window resize' in keys: cols, rows = self.ui.get_cols_rows() if not keys: continue t = "" a = [] for k in keys: if type(k) == text_type: k = k.encode("utf-8") t += "'"+k + "' " a += [(None,1), ('yellow on dark blue',len(k)), (None,2)] text.append(t + ": "+ repr(raw)) attr.append(a) text = text[-rows:] attr = attr[-rows:] if '__main__'==__name__: _test() urwid-2.0.1/urwid/monitored_list.py0000755000175000017500000004071413231170672021062 0ustar andersonanderson00000000000000#!/usr/bin/python # # Urwid MonitoredList class # 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.compat import PYTHON3, xrange def _call_modified(fn): def call_modified_wrapper(self, *args, **kwargs): rval = fn(self, *args, **kwargs) self._modified() return rval return call_modified_wrapper class MonitoredList(list): """ This class can trigger a callback any time its contents are changed with the usual list operations append, extend, etc. """ def _modified(self): pass def set_modified_callback(self, callback): """ Assign a callback function with no parameters that is called any time the list is modified. Callback's return value is ignored. >>> import sys >>> ml = MonitoredList([1,2,3]) >>> ml.set_modified_callback(lambda: sys.stdout.write("modified\\n")) >>> ml MonitoredList([1, 2, 3]) >>> ml.append(10) modified >>> len(ml) 4 >>> ml += [11, 12, 13] modified >>> ml[:] = ml[:2] + ml[-2:] modified >>> ml MonitoredList([1, 2, 12, 13]) """ self._modified = callback def __repr__(self): return "%s(%r)" % (self.__class__.__name__, list(self)) __add__ = _call_modified(list.__add__) __delitem__ = _call_modified(list.__delitem__) if not PYTHON3: __delslice__ = _call_modified(list.__delslice__) __iadd__ = _call_modified(list.__iadd__) __imul__ = _call_modified(list.__imul__) __rmul__ = _call_modified(list.__rmul__) __setitem__ = _call_modified(list.__setitem__) if not PYTHON3: __setslice__ = _call_modified(list.__setslice__) append = _call_modified(list.append) extend = _call_modified(list.extend) insert = _call_modified(list.insert) pop = _call_modified(list.pop) remove = _call_modified(list.remove) reverse = _call_modified(list.reverse) sort = _call_modified(list.sort) if hasattr(list, 'clear'): clear = _call_modified(list.clear) class MonitoredFocusList(MonitoredList): """ This class can trigger a callback any time its contents are modified, before and/or after modification, and any time the focus index is changed. """ def __init__(self, *argl, **argd): """ This is a list that tracks one item as the focus item. If items are inserted or removed it will update the focus. >>> ml = MonitoredFocusList([10, 11, 12, 13, 14], focus=3) >>> ml MonitoredFocusList([10, 11, 12, 13, 14], focus=3) >>> del(ml[1]) >>> ml MonitoredFocusList([10, 12, 13, 14], focus=2) >>> ml[:2] = [50, 51, 52, 53] >>> ml MonitoredFocusList([50, 51, 52, 53, 13, 14], focus=4) >>> ml[4] = 99 >>> ml MonitoredFocusList([50, 51, 52, 53, 99, 14], focus=4) >>> ml[:] = [] >>> ml MonitoredFocusList([], focus=None) """ focus = argd.pop('focus', 0) super(MonitoredFocusList, self).__init__(*argl, **argd) self._focus = focus self._focus_modified = lambda ml, indices, new_items: None def __repr__(self): return "%s(%r, focus=%r)" % ( self.__class__.__name__, list(self), self.focus) def _get_focus(self): """ Return the index of the item "in focus" or None if the list is empty. >>> MonitoredFocusList([1,2,3], focus=2)._get_focus() 2 >>> MonitoredFocusList()._get_focus() """ if not self: return None return self._focus def _set_focus(self, index): """ index -- index into this list, any index out of range will raise an IndexError, except when the list is empty and the index passed is ignored. This function may call self._focus_changed when the focus is modified, passing the new focus position to the callback just before changing the old focus setting. That method may be overridden on the instance with set_focus_changed_callback(). >>> ml = MonitoredFocusList([9, 10, 11]) >>> ml._set_focus(2); ml._get_focus() 2 >>> ml._set_focus(0); ml._get_focus() 0 >>> ml._set_focus(-2) Traceback (most recent call last): ... IndexError: focus index is out of range: -2 """ if not self: self._focus = 0 return if index < 0 or index >= len(self): raise IndexError('focus index is out of range: %s' % (index,)) if index != int(index): raise IndexError('invalid focus index: %s' % (index,)) index = int(index) if index != self._focus: self._focus_changed(index) self._focus = index focus = property(_get_focus, _set_focus, doc=""" Get/set the focus index. This value is read as None when the list is empty, and may only be set to a value between 0 and len(self)-1 or an IndexError will be raised. """) def _focus_changed(self, new_focus): pass def set_focus_changed_callback(self, callback): """ Assign a callback to be called when the focus index changes for any reason. The callback is in the form: callback(new_focus) new_focus -- new focus index >>> import sys >>> ml = MonitoredFocusList([1,2,3], focus=1) >>> ml.set_focus_changed_callback(lambda f: sys.stdout.write("focus: %d\\n" % (f,))) >>> ml MonitoredFocusList([1, 2, 3], focus=1) >>> ml.append(10) >>> ml.insert(1, 11) focus: 2 >>> ml MonitoredFocusList([1, 11, 2, 3, 10], focus=2) >>> del ml[:2] focus: 0 >>> ml[:0] = [12, 13, 14] focus: 3 >>> ml.focus = 5 focus: 5 >>> ml MonitoredFocusList([12, 13, 14, 2, 3, 10], focus=5) """ self._focus_changed = callback def _validate_contents_modified(self, indices, new_items): return None def set_validate_contents_modified(self, callback): """ Assign a callback function to handle validating changes to the list. This may raise an exception if the change should not be performed. It may also return an integer position to be the new focus after the list is modified, or None to use the default behaviour. The callback is in the form: callback(indices, new_items) indices -- a (start, stop, step) tuple whose range covers the items being modified new_items -- an iterable of items replacing those at range(*indices), empty if items are being removed, if step==1 this list may contain any number of items """ self._validate_contents_modified = callback def _adjust_focus_on_contents_modified(self, slc, new_items=()): """ Default behaviour is to move the focus to the item following any removed items, unless that item was simply replaced. Failing that choose the last item in the list. returns focus position for after change is applied """ num_new_items = len(new_items) start, stop, step = indices = slc.indices(len(self)) num_removed = len(list(xrange(*indices))) focus = self._validate_contents_modified(indices, new_items) if focus is not None: return focus focus = self._focus if step == 1: if start + num_new_items <= focus < stop: focus = stop # adjust for added/removed items if stop <= focus: focus += num_new_items - (stop - start) else: if not num_new_items: # extended slice being removed if focus in xrange(start, stop, step): focus += 1 # adjust for removed items focus -= len(list(xrange(start, min(focus, stop), step))) return min(focus, len(self) + num_new_items - num_removed -1) # override all the list methods that modify the list def __delitem__(self, y): """ >>> ml = MonitoredFocusList([0,1,2,3,4], focus=2) >>> del ml[3]; ml MonitoredFocusList([0, 1, 2, 4], focus=2) >>> del ml[-1]; ml MonitoredFocusList([0, 1, 2], focus=2) >>> del ml[0]; ml MonitoredFocusList([1, 2], focus=1) >>> del ml[1]; ml MonitoredFocusList([1], focus=0) >>> del ml[0]; ml MonitoredFocusList([], focus=None) >>> ml = MonitoredFocusList([5,4,6,4,5,4,6,4,5], focus=4) >>> del ml[1::2]; ml MonitoredFocusList([5, 6, 5, 6, 5], focus=2) >>> del ml[::2]; ml MonitoredFocusList([6, 6], focus=1) >>> ml = MonitoredFocusList([0,1,2,3,4,6,7], focus=2) >>> del ml[-2:]; ml MonitoredFocusList([0, 1, 2, 3, 4], focus=2) >>> del ml[-4:-2]; ml MonitoredFocusList([0, 3, 4], focus=1) >>> del ml[:]; ml MonitoredFocusList([], focus=None) """ if isinstance(y, slice): focus = self._adjust_focus_on_contents_modified(y) else: focus = self._adjust_focus_on_contents_modified(slice(y, y+1 or None)) rval = super(MonitoredFocusList, self).__delitem__(y) self._set_focus(focus) return rval def __setitem__(self, i, y): """ >>> def modified(indices, new_items): ... print("range%r <- %r" % (indices, new_items)) >>> ml = MonitoredFocusList([0,1,2,3], focus=2) >>> ml.set_validate_contents_modified(modified) >>> ml[0] = 9 range(0, 1, 1) <- [9] >>> ml[2] = 6 range(2, 3, 1) <- [6] >>> ml.focus 2 >>> ml[-1] = 8 range(3, 4, 1) <- [8] >>> ml MonitoredFocusList([9, 1, 6, 8], focus=2) >>> ml[1::2] = [12, 13] range(1, 4, 2) <- [12, 13] >>> ml[::2] = [10, 11] range(0, 4, 2) <- [10, 11] >>> ml[-3:-1] = [21, 22, 23] range(1, 3, 1) <- [21, 22, 23] >>> ml MonitoredFocusList([10, 21, 22, 23, 13], focus=2) >>> ml[:] = [] range(0, 5, 1) <- [] >>> ml MonitoredFocusList([], focus=None) """ if isinstance(i, slice): focus = self._adjust_focus_on_contents_modified(i, y) else: focus = self._adjust_focus_on_contents_modified(slice(i, i+1 or None), [y]) rval = super(MonitoredFocusList, self).__setitem__(i, y) self._set_focus(focus) return rval if not PYTHON3: def __delslice__(self, i, j): return self.__delitem__(slice(i,j)) def __setslice__(self, i, j, y): return self.__setitem__(slice(i, j), y) def __imul__(self, n): """ >>> def modified(indices, new_items): ... print("range%r <- %r" % (indices, list(new_items))) >>> ml = MonitoredFocusList([0,1,2], focus=2) >>> ml.set_validate_contents_modified(modified) >>> ml *= 3 range(3, 3, 1) <- [0, 1, 2, 0, 1, 2] >>> ml MonitoredFocusList([0, 1, 2, 0, 1, 2, 0, 1, 2], focus=2) >>> ml *= 0 range(0, 9, 1) <- [] >>> print(ml.focus) None """ if n > 0: focus = self._adjust_focus_on_contents_modified( slice(len(self), len(self)), list(self)*(n-1)) else: # all contents are being removed focus = self._adjust_focus_on_contents_modified(slice(0, len(self))) rval = super(MonitoredFocusList, self).__imul__(n) self._set_focus(focus) return rval def append(self, item): """ >>> def modified(indices, new_items): ... print("range%r <- %r" % (indices, new_items)) >>> ml = MonitoredFocusList([0,1,2], focus=2) >>> ml.set_validate_contents_modified(modified) >>> ml.append(6) range(3, 3, 1) <- [6] """ focus = self._adjust_focus_on_contents_modified( slice(len(self), len(self)), [item]) rval = super(MonitoredFocusList, self).append(item) self._set_focus(focus) return rval def extend(self, items): """ >>> def modified(indices, new_items): ... print("range%r <- %r" % (indices, list(new_items))) >>> ml = MonitoredFocusList([0,1,2], focus=2) >>> ml.set_validate_contents_modified(modified) >>> ml.extend((6,7,8)) range(3, 3, 1) <- [6, 7, 8] """ focus = self._adjust_focus_on_contents_modified( slice(len(self), len(self)), items) rval = super(MonitoredFocusList, self).extend(items) self._set_focus(focus) return rval def insert(self, index, item): """ >>> ml = MonitoredFocusList([0,1,2,3], focus=2) >>> ml.insert(-1, -1); ml MonitoredFocusList([0, 1, 2, -1, 3], focus=2) >>> ml.insert(0, -2); ml MonitoredFocusList([-2, 0, 1, 2, -1, 3], focus=3) >>> ml.insert(3, -3); ml MonitoredFocusList([-2, 0, 1, -3, 2, -1, 3], focus=4) """ focus = self._adjust_focus_on_contents_modified(slice(index, index), [item]) rval = super(MonitoredFocusList, self).insert(index, item) self._set_focus(focus) return rval def pop(self, index=-1): """ >>> ml = MonitoredFocusList([-2,0,1,-3,2,3], focus=4) >>> ml.pop(3); ml -3 MonitoredFocusList([-2, 0, 1, 2, 3], focus=3) >>> ml.pop(0); ml -2 MonitoredFocusList([0, 1, 2, 3], focus=2) >>> ml.pop(-1); ml 3 MonitoredFocusList([0, 1, 2], focus=2) >>> ml.pop(2); ml 2 MonitoredFocusList([0, 1], focus=1) """ focus = self._adjust_focus_on_contents_modified(slice(index, index+1 or None)) rval = super(MonitoredFocusList, self).pop(index) self._set_focus(focus) return rval def remove(self, value): """ >>> ml = MonitoredFocusList([-2,0,1,-3,2,-1,3], focus=4) >>> ml.remove(-3); ml MonitoredFocusList([-2, 0, 1, 2, -1, 3], focus=3) >>> ml.remove(-2); ml MonitoredFocusList([0, 1, 2, -1, 3], focus=2) >>> ml.remove(3); ml MonitoredFocusList([0, 1, 2, -1], focus=2) """ index = self.index(value) focus = self._adjust_focus_on_contents_modified(slice(index, index+1 or None)) rval = super(MonitoredFocusList, self).remove(value) self._set_focus(focus) return rval def reverse(self): """ >>> ml = MonitoredFocusList([0,1,2,3,4], focus=1) >>> ml.reverse(); ml MonitoredFocusList([4, 3, 2, 1, 0], focus=3) """ rval = super(MonitoredFocusList, self).reverse() self._set_focus(max(0, len(self) - self._focus - 1)) return rval def sort(self, **kwargs): """ >>> ml = MonitoredFocusList([-2,0,1,-3,2,-1,3], focus=4) >>> ml.sort(); ml MonitoredFocusList([-3, -2, -1, 0, 1, 2, 3], focus=5) """ if not self: return value = self[self._focus] rval = super(MonitoredFocusList, self).sort(**kwargs) self._set_focus(self.index(value)) return rval if hasattr(list, 'clear'): def clear(self): focus = self._adjust_focus_on_contents_modified(slice(0, 0)) rval = super(MonitoredFocusList, self).clear() self._set_focus(focus) return rval def _test(): import doctest doctest.testmod() if __name__=='__main__': _test() urwid-2.0.1/urwid/command_map.py0000644000175000017500000000641413231170672020276 0ustar andersonanderson00000000000000#!/usr/bin/python # # Urwid CommandMap class # 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 REDRAW_SCREEN = 'redraw screen' CURSOR_UP = 'cursor up' CURSOR_DOWN = 'cursor down' CURSOR_LEFT = 'cursor left' CURSOR_RIGHT = 'cursor right' CURSOR_PAGE_UP = 'cursor page up' CURSOR_PAGE_DOWN = 'cursor page down' CURSOR_MAX_LEFT = 'cursor max left' CURSOR_MAX_RIGHT = 'cursor max right' ACTIVATE = 'activate' class CommandMap(object): """ dict-like object for looking up commands from keystrokes Default values (key: command):: 'tab': 'next selectable', 'ctrl n': 'next selectable', 'shift tab': 'prev selectable', 'ctrl p': 'prev selectable', 'ctrl l': 'redraw screen', 'esc': 'menu', 'up': 'cursor up', 'down': 'cursor down', 'left': 'cursor left', 'right': 'cursor right', 'page up': 'cursor page up', 'page down': 'cursor page down', 'home': 'cursor max left', 'end': 'cursor max right', ' ': 'activate', 'enter': 'activate', """ _command_defaults = { 'tab': 'next selectable', 'ctrl n': 'next selectable', 'shift tab': 'prev selectable', 'ctrl p': 'prev selectable', 'ctrl l': REDRAW_SCREEN, 'esc': 'menu', 'up': CURSOR_UP, 'down': CURSOR_DOWN, 'left': CURSOR_LEFT, 'right': CURSOR_RIGHT, 'page up': CURSOR_PAGE_UP, 'page down': CURSOR_PAGE_DOWN, 'home': CURSOR_MAX_LEFT, 'end': CURSOR_MAX_RIGHT, ' ': ACTIVATE, 'enter': ACTIVATE, } def __init__(self): self.restore_defaults() def restore_defaults(self): self._command = dict(self._command_defaults) def __getitem__(self, key): return self._command.get(key, None) def __setitem__(self, key, command): self._command[key] = command def __delitem__(self, key): del self._command[key] def clear_command(self, command): dk = [k for k, v in self._command.items() if v == command] for k in dk: del self._command[k] def copy(self): """ Return a new copy of this CommandMap, likely so we can modify it separate from a shared one. """ c = CommandMap() c._command = dict(self._command) return c command_map = CommandMap() # shared command mappings urwid-2.0.1/urwid/escape.py0000644000175000017500000003275713231170672017274 0ustar andersonanderson00000000000000#!/usr/bin/python # -*- coding: utf-8 -*- # # Urwid escape sequences common to curses_display and raw_display # 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 """ Terminal Escape Sequences for input and display """ import re try: from urwid import str_util except ImportError: from urwid import old_str_util as str_util from urwid.compat import bytes, bytes3 within_double_byte = str_util.within_double_byte SO = "\x0e" SI = "\x0f" IBMPC_ON = "\x1b[11m" IBMPC_OFF = "\x1b[10m" DEC_TAG = "0" DEC_SPECIAL_CHARS = u'▮◆▒â‰âŒââŠÂ°Â±â¤â‹â”˜â”┌└┼⎺⎻─⎼⎽├┤┴┬│≤≥π≠£·' ALT_DEC_SPECIAL_CHARS = u"_`abcdefghijklmnopqrstuvwxyz{|}~" DEC_SPECIAL_CHARMAP = {} assert len(DEC_SPECIAL_CHARS) == len(ALT_DEC_SPECIAL_CHARS), repr((DEC_SPECIAL_CHARS, ALT_DEC_SPECIAL_CHARS)) for c, alt in zip(DEC_SPECIAL_CHARS, ALT_DEC_SPECIAL_CHARS): DEC_SPECIAL_CHARMAP[ord(c)] = SO + alt + SI SAFE_ASCII_DEC_SPECIAL_RE = re.compile(u"^[ -~%s]*$" % DEC_SPECIAL_CHARS) DEC_SPECIAL_RE = re.compile(u"[%s]" % DEC_SPECIAL_CHARS) ################### ## Input sequences ################### class MoreInputRequired(Exception): pass def escape_modifier( digit ): mode = ord(digit) - ord("1") return "shift "*(mode&1) + "meta "*((mode&2)//2) + "ctrl "*((mode&4)//4) input_sequences = [ ('[A','up'),('[B','down'),('[C','right'),('[D','left'), ('[E','5'),('[F','end'),('[G','5'),('[H','home'), ('[1~','home'),('[2~','insert'),('[3~','delete'),('[4~','end'), ('[5~','page up'),('[6~','page down'), ('[7~','home'),('[8~','end'), ('[[A','f1'),('[[B','f2'),('[[C','f3'),('[[D','f4'),('[[E','f5'), ('[11~','f1'),('[12~','f2'),('[13~','f3'),('[14~','f4'), ('[15~','f5'),('[17~','f6'),('[18~','f7'),('[19~','f8'), ('[20~','f9'),('[21~','f10'),('[23~','f11'),('[24~','f12'), ('[25~','f13'),('[26~','f14'),('[28~','f15'),('[29~','f16'), ('[31~','f17'),('[32~','f18'),('[33~','f19'),('[34~','f20'), ('OA','up'),('OB','down'),('OC','right'),('OD','left'), ('OH','home'),('OF','end'), ('OP','f1'),('OQ','f2'),('OR','f3'),('OS','f4'), ('Oo','/'),('Oj','*'),('Om','-'),('Ok','+'), ('[Z','shift tab'), ('On', '.'), ('[200~', 'begin paste'), ('[201~', 'end paste'), ] + [ (prefix + letter, modifier + key) for prefix, modifier in zip('O[', ('meta ', 'shift ')) for letter, key in zip('abcd', ('up', 'down', 'right', 'left')) ] + [ ("[" + digit + symbol, modifier + key) for modifier, symbol in zip(('shift ', 'meta '), '$^') for digit, key in zip('235678', ('insert', 'delete', 'page up', 'page down', 'home', 'end')) ] + [ ('O' + chr(ord('p')+n), str(n)) for n in range(10) ] + [ # modified cursor keys + home, end, 5 -- [#X and [1;#X forms (prefix+digit+letter, escape_modifier(digit) + key) for prefix in ("[", "[1;") for digit in "12345678" for letter,key in zip("ABCDEFGH", ('up','down','right','left','5','end','5','home')) ] + [ # modified F1-F4 keys -- O#X form ("O"+digit+letter, escape_modifier(digit) + key) for digit in "12345678" for letter,key in zip("PQRS",('f1','f2','f3','f4')) ] + [ # modified F1-F13 keys -- [XX;#~ form ("["+str(num)+";"+digit+"~", escape_modifier(digit) + key) for digit in "12345678" for num,key in zip( (3,5,6,11,12,13,14,15,17,18,19,20,21,23,24,25,26,28,29,31,32,33,34), ('delete', 'page up', 'page down', 'f1','f2','f3','f4','f5','f6','f7','f8','f9','f10','f11', 'f12','f13','f14','f15','f16','f17','f18','f19','f20')) ] + [ # mouse reporting (special handling done in KeyqueueTrie) ('[M', 'mouse'), # report status response ('[0n', 'status ok') ] class KeyqueueTrie(object): def __init__( self, sequences ): self.data = {} for s, result in sequences: assert type(result) != dict self.add(self.data, s, result) def add(self, root, s, result): assert type(root) == dict, "trie conflict detected" assert len(s) > 0, "trie conflict detected" if ord(s[0]) in root: return self.add(root[ord(s[0])], s[1:], result) if len(s)>1: d = {} root[ord(s[0])] = d return self.add(d, s[1:], result) root[ord(s)] = result def get(self, keys, more_available): result = self.get_recurse(self.data, keys, more_available) if not result: result = self.read_cursor_position(keys, more_available) return result def get_recurse(self, root, keys, more_available): if type(root) != dict: if root == "mouse": return self.read_mouse_info(keys, more_available) return (root, keys) if not keys: # get more keys if more_available: raise MoreInputRequired() return None if keys[0] not in root: return None return self.get_recurse(root[keys[0]], keys[1:], more_available) def read_mouse_info(self, keys, more_available): if len(keys) < 3: if more_available: raise MoreInputRequired() return None b = keys[0] - 32 x, y = (keys[1] - 33)%256, (keys[2] - 33)%256 # supports 0-255 prefix = "" if b & 4: prefix = prefix + "shift " if b & 8: prefix = prefix + "meta " if b & 16: prefix = prefix + "ctrl " if (b & MOUSE_MULTIPLE_CLICK_MASK)>>9 == 1: prefix = prefix + "double " if (b & MOUSE_MULTIPLE_CLICK_MASK)>>9 == 2: prefix = prefix + "triple " # 0->1, 1->2, 2->3, 64->4, 65->5 button = ((b&64)/64*3) + (b & 3) + 1 if b & 3 == 3: action = "release" button = 0 elif b & MOUSE_RELEASE_FLAG: action = "release" elif b & MOUSE_DRAG_FLAG: action = "drag" elif b & MOUSE_MULTIPLE_CLICK_MASK: action = "click" else: action = "press" return ( (prefix + "mouse " + action, button, x, y), keys[3:] ) def read_cursor_position(self, keys, more_available): """ Interpret cursor position information being sent by the user's terminal. Returned as ('cursor position', x, y) where (x, y) == (0, 0) is the top left of the screen. """ if not keys: if more_available: raise MoreInputRequired() return None if keys[0] != ord('['): return None # read y value y = 0 i = 1 for k in keys[i:]: i += 1 if k == ord(';'): if not y: return None break if k < ord('0') or k > ord('9'): return None if not y and k == ord('0'): return None y = y * 10 + k - ord('0') if not keys[i:]: if more_available: raise MoreInputRequired() return None # read x value x = 0 for k in keys[i:]: i += 1 if k == ord('R'): if not x: return None return (("cursor position", x-1, y-1), keys[i:]) if k < ord('0') or k > ord('9'): return None if not x and k == ord('0'): return None x = x * 10 + k - ord('0') if not keys[i:]: if more_available: raise MoreInputRequired() return None # This is added to button value to signal mouse release by curses_display # and raw_display when we know which button was released. NON-STANDARD MOUSE_RELEASE_FLAG = 2048 # This 2-bit mask is used to check if the mouse release from curses or gpm # is a double or triple release. 00 means single click, 01 double, # 10 triple. NON-STANDARD MOUSE_MULTIPLE_CLICK_MASK = 1536 # This is added to button value at mouse release to differentiate between # single, double and triple press. Double release adds this times one, # triple release adds this times two. NON-STANDARD MOUSE_MULTIPLE_CLICK_FLAG = 512 # xterm adds this to the button value to signal a mouse drag event MOUSE_DRAG_FLAG = 32 ################################################# # Build the input trie from input_sequences list input_trie = KeyqueueTrie(input_sequences) ################################################# _keyconv = { -1:None, 8:'backspace', 9:'tab', 10:'enter', 13:'enter', 127:'backspace', # curses-only keycodes follow.. (XXX: are these used anymore?) 258:'down', 259:'up', 260:'left', 261:'right', 262:'home', 263:'backspace', 265:'f1', 266:'f2', 267:'f3', 268:'f4', 269:'f5', 270:'f6', 271:'f7', 272:'f8', 273:'f9', 274:'f10', 275:'f11', 276:'f12', 277:'shift f1', 278:'shift f2', 279:'shift f3', 280:'shift f4', 281:'shift f5', 282:'shift f6', 283:'shift f7', 284:'shift f8', 285:'shift f9', 286:'shift f10', 287:'shift f11', 288:'shift f12', 330:'delete', 331:'insert', 338:'page down', 339:'page up', 343:'enter', # on numpad 350:'5', # on numpad 360:'end', } def process_keyqueue(codes, more_available): """ codes -- list of key codes more_available -- if True then raise MoreInputRequired when in the middle of a character sequence (escape/utf8/wide) and caller will attempt to send more key codes on the next call. returns (list of input, list of remaining key codes). """ code = codes[0] if code >= 32 and code <= 126: key = chr(code) return [key], codes[1:] if code in _keyconv: return [_keyconv[code]], codes[1:] if code >0 and code <27: return ["ctrl %s" % chr(ord('a')+code-1)], codes[1:] if code >27 and code <32: return ["ctrl %s" % chr(ord('A')+code-1)], codes[1:] em = str_util.get_byte_encoding() if (em == 'wide' and code < 256 and within_double_byte(chr(code),0,0)): if not codes[1:]: if more_available: raise MoreInputRequired() if codes[1:] and codes[1] < 256: db = chr(code)+chr(codes[1]) if within_double_byte(db, 0, 1): return [db], codes[2:] if em == 'utf8' and code>127 and code<256: if code & 0xe0 == 0xc0: # 2-byte form need_more = 1 elif code & 0xf0 == 0xe0: # 3-byte form need_more = 2 elif code & 0xf8 == 0xf0: # 4-byte form need_more = 3 else: return ["<%d>"%code], codes[1:] for i in range(need_more): if len(codes)-1 <= i: if more_available: raise MoreInputRequired() else: return ["<%d>"%code], codes[1:] k = codes[i+1] if k>256 or k&0xc0 != 0x80: return ["<%d>"%code], codes[1:] s = bytes3(codes[:need_more+1]) assert isinstance(s, bytes) try: return [s.decode("utf-8")], codes[need_more+1:] except UnicodeDecodeError: return ["<%d>"%code], codes[1:] if code >127 and code <256: key = chr(code) return [key], codes[1:] if code != 27: return ["<%d>"%code], codes[1:] result = input_trie.get(codes[1:], more_available) if result is not None: result, remaining_codes = result return [result], remaining_codes if codes[1:]: # Meta keys -- ESC+Key form run, remaining_codes = process_keyqueue(codes[1:], more_available) if run[0] == "esc" or run[0].find("meta ") >= 0: return ['esc']+run, remaining_codes return ['meta '+run[0]]+run[1:], remaining_codes return ['esc'], codes[1:] #################### ## Output sequences #################### ESC = "\x1b" CURSOR_HOME = ESC+"[H" CURSOR_HOME_COL = "\r" APP_KEYPAD_MODE = ESC+"=" NUM_KEYPAD_MODE = ESC+">" SWITCH_TO_ALTERNATE_BUFFER = ESC+"7"+ESC+"[?47h" RESTORE_NORMAL_BUFFER = ESC+"[?47l"+ESC+"8" #RESET_SCROLL_REGION = ESC+"[;r" #RESET = ESC+"c" REPORT_STATUS = ESC + "[5n" REPORT_CURSOR_POSITION = ESC+"[6n" INSERT_ON = ESC + "[4h" INSERT_OFF = ESC + "[4l" def set_cursor_position( x, y ): assert type(x) == int assert type(y) == int return ESC+"[%d;%dH" %(y+1, x+1) def move_cursor_right(x): if x < 1: return "" return ESC+"[%dC" % x def move_cursor_up(x): if x < 1: return "" return ESC+"[%dA" % x def move_cursor_down(x): if x < 1: return "" return ESC+"[%dB" % x HIDE_CURSOR = ESC+"[?25l" SHOW_CURSOR = ESC+"[?25h" MOUSE_TRACKING_ON = ESC+"[?1000h"+ESC+"[?1002h" MOUSE_TRACKING_OFF = ESC+"[?1002l"+ESC+"[?1000l" DESIGNATE_G1_SPECIAL = ESC+")0" ERASE_IN_LINE_RIGHT = ESC+"[K" urwid-2.0.1/urwid/wimp.py0000755000175000017500000005246013231170672017004 0ustar andersonanderson00000000000000#!/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")