From af7d7338b0e283a6cb04bab4979ad69f332e4187 Mon Sep 17 00:00:00 2001 From: Aaron Gutierrez Date: Tue, 5 Dec 2017 22:09:52 -0800 Subject: [PATCH] Start rewrite --- .gitignore | 4 +- .gitmodules | 3 - asana | 1 - main.py | 97 ++ python-asana | 1 - requirements.txt | 10 + {urwid/tests => ui}/__init__.py | 0 ui/auth.py | 32 + ui/palette.py | 11 + urwid/__init__.py | 78 -- urwid/canvas.py | 1317 ------------------ urwid/command_map.py | 104 -- urwid/compat.py | 48 - urwid/container.py | 2303 ------------------------------- urwid/curses_display.py | 619 --------- urwid/decoration.py | 1170 ---------------- urwid/display_common.py | 894 ------------ urwid/escape.py | 441 ------ urwid/font.py | 450 ------ urwid/graphics.py | 911 ------------ urwid/html_fragment.py | 245 ---- urwid/lcd_display.py | 485 ------- urwid/listbox.py | 1668 ---------------------- urwid/main_loop.py | 1375 ------------------ urwid/monitored_list.py | 496 ------- urwid/old_str_util.py | 368 ----- urwid/raw_display.py | 1030 -------------- urwid/signals.py | 302 ---- urwid/split_repr.py | 149 -- urwid/tests/test_canvas.py | 391 ------ urwid/tests/test_container.py | 638 --------- urwid/tests/test_decoration.py | 149 -- urwid/tests/test_doctests.py | 22 - urwid/tests/test_event_loops.py | 147 -- urwid/tests/test_graphics.py | 97 -- urwid/tests/test_listbox.py | 804 ----------- urwid/tests/test_str_util.py | 37 - urwid/tests/test_text_layout.py | 342 ----- urwid/tests/test_util.py | 178 --- urwid/tests/test_vterm.py | 334 ----- urwid/tests/test_widget.py | 153 -- urwid/tests/util.py | 8 - urwid/text_layout.py | 506 ------- urwid/treetools.py | 486 ------- urwid/util.py | 474 ------- urwid/version.py | 5 - urwid/vterm.py | 1626 ---------------------- urwid/web_display.py | 1092 --------------- urwid/widget.py | 1825 ------------------------ urwid/wimp.py | 664 --------- 50 files changed, 152 insertions(+), 24438 deletions(-) delete mode 100644 .gitmodules delete mode 120000 asana create mode 100755 main.py delete mode 160000 python-asana create mode 100644 requirements.txt rename {urwid/tests => ui}/__init__.py (100%) create mode 100644 ui/auth.py create mode 100644 ui/palette.py delete mode 100644 urwid/__init__.py delete mode 100644 urwid/canvas.py delete mode 100644 urwid/command_map.py delete mode 100644 urwid/compat.py delete mode 100755 urwid/container.py delete mode 100755 urwid/curses_display.py delete mode 100755 urwid/decoration.py delete mode 100755 urwid/display_common.py delete mode 100644 urwid/escape.py delete mode 100755 urwid/font.py delete mode 100755 urwid/graphics.py delete mode 100755 urwid/html_fragment.py delete mode 100644 urwid/lcd_display.py delete mode 100644 urwid/listbox.py delete mode 100755 urwid/main_loop.py delete mode 100755 urwid/monitored_list.py delete mode 100755 urwid/old_str_util.py delete mode 100644 urwid/raw_display.py delete mode 100644 urwid/signals.py delete mode 100755 urwid/split_repr.py delete mode 100644 urwid/tests/test_canvas.py delete mode 100644 urwid/tests/test_container.py delete mode 100644 urwid/tests/test_decoration.py delete mode 100644 urwid/tests/test_doctests.py delete mode 100644 urwid/tests/test_event_loops.py delete mode 100644 urwid/tests/test_graphics.py delete mode 100644 urwid/tests/test_listbox.py delete mode 100644 urwid/tests/test_str_util.py delete mode 100644 urwid/tests/test_text_layout.py delete mode 100644 urwid/tests/test_util.py delete mode 100644 urwid/tests/test_vterm.py delete mode 100644 urwid/tests/test_widget.py delete mode 100644 urwid/tests/util.py delete mode 100644 urwid/text_layout.py delete mode 100644 urwid/treetools.py delete mode 100644 urwid/util.py delete mode 100644 urwid/version.py delete mode 100644 urwid/vterm.py delete mode 100755 urwid/web_display.py delete mode 100644 urwid/widget.py delete mode 100755 urwid/wimp.py diff --git a/.gitignore b/.gitignore index 1a12c41..6640103 100644 --- a/.gitignore +++ b/.gitignore @@ -3,8 +3,8 @@ tags +venv + .state .oauth secrets.py - -urwid-src diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 6bc49c6..0000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "python-asana"] - path = python-asana - url = git@github.com:Asana/python-asana.git diff --git a/asana b/asana deleted file mode 120000 index ee37b13..0000000 --- a/asana +++ /dev/null @@ -1 +0,0 @@ -python-asana/asana \ No newline at end of file diff --git a/main.py b/main.py new file mode 100755 index 0000000..87e9b4c --- /dev/null +++ b/main.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python3 + +import json +import os + +import asana +from asana.session import AsanaOAuth2Session +import urwid + +import secrets +from ui.auth import AuthPrompt +from ui.palette import palette + +class NotAuthedException(Exception): + def __init__(self): + super(NotAuthedException, self) + +class CmdAsana(object): + """The main urwid loop + """ + loop = None + + """Try to get an Asana client using stored tokens + + Raises: + NotAuthedException: the user has not authorized the app + """ + def get_client(self): + try: + f = open('.oauth', 'r') + auth_json = f.read() + f.close() + token = json.loads(auth_json) + self.client = asana.Client.oauth( + client_id=secrets.CLIENT_ID, + client_secret=secrets.CLIENT_SECRET, + token=token, + token_updater=self.save_token, + auto_refresh_url=AsanaOAuth2Session.token_url, + ) + except IOError: + raise NotAuthedException() + + def authorize(self): + self.client = asana.Client.oauth( + client_id=secrets.CLIENT_ID, + client_secret=secrets.CLIENT_SECRET, + redirect_uri='urn:ietf:wg:oauth:2.0:oob', + token_updater=self.save_token, + auto_refresh_url=AsanaOAuth2Session.token_url, + ) + (url, _) = self.client.session.authorization_url() + auth = AuthPrompt(url, self.auth_callback) + + try: + import webbrowser + webbrowser.open(url) + except Exception: + pass + + self.loop = urwid.MainLoop( + auth.component(), + unhandled_input=self.exit_handler, + palette=palette + ) + self.loop.run() + + def auth_callback(self, code): + self.save_token( + self.client.session.fetch_token(code=code)) + raise urwid.ExitMainLoop() + + + def exit_handler(self, key): + if key in ('q', 'Q', 'esc'): + raise urwid.ExitMainLoop() + + def run(self): + print("Running...") + + def save_token(self, token): + f = open('.oauth', 'w') + f.write(json.dumps(token)) + f.close() + os.chmod('.oauth', 0o600) + +def main(): + cmd_asana = CmdAsana() + try: + cmd_asana.get_client() + except NotAuthedException: + cmd_asana.authorize() + + cmd_asana.run() + +if __name__ == "__main__": + main() diff --git a/python-asana b/python-asana deleted file mode 160000 index 66c31af..0000000 --- a/python-asana +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 66c31afc60bd0bc1c6e727ce580539ca9d0e938d diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..8ad6fd3 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,10 @@ +asana==0.6.5 +certifi==2017.11.5 +chardet==3.0.4 +idna==2.6 +oauthlib==2.0.6 +requests==2.14.2 +requests-oauthlib==0.6.2 +six==1.10.0 +urllib3==1.22 +urwid==1.3.1 diff --git a/urwid/tests/__init__.py b/ui/__init__.py similarity index 100% rename from urwid/tests/__init__.py rename to ui/__init__.py diff --git a/ui/auth.py b/ui/auth.py new file mode 100644 index 0000000..fbe03cb --- /dev/null +++ b/ui/auth.py @@ -0,0 +1,32 @@ +import urwid + +class TokenEdit(urwid.Edit): + def __init__(self): + urwid.register_signal(TokenEdit, 'TokenEdit-changed') + prompt = ('seondary', u'Auth Token: ') + super(TokenEdit, self).__init__(prompt, '') + + def keypress(self, size, key): + if key == 'enter': + urwid.emit_signal(self, 'TokenEdit-changed', self.edit_text) + else: + return super(TokenEdit, self).keypress(size, key) + +class AuthPrompt(object): + def __init__(self, auth_url, callback): + self.callback = callback + token_input = TokenEdit() + urwid.connect_signal(token_input, 'TokenEdit-changed', self.callback) + + self.frame = urwid.Filler( + urwid.Pile([ + urwid.Text('Visit %s and paste the token below.\n' % auth_url), + token_input, + ]) + ) + + def callback(self, token): + self.callback(token) + + def component(self): + return self.frame diff --git a/ui/palette.py b/ui/palette.py new file mode 100644 index 0000000..1bf68f3 --- /dev/null +++ b/ui/palette.py @@ -0,0 +1,11 @@ +palette = [ + ('selected', 'standout', ''), + ('selected workspace', 'standout,bold', ''), + ('header', 'bold,light green', ''), + ('secondary', 'light gray', ''), + ('task', 'light green', ''), + ('project', 'yellow', ''), + ('section', 'white', 'dark green'), + ('workspace', 'white', 'dark blue'), + ('pager', 'standout', ''), +] diff --git a/urwid/__init__.py b/urwid/__init__.py deleted file mode 100644 index bc5170e..0000000 --- a/urwid/__init__.py +++ /dev/null @@ -1,78 +0,0 @@ -#!/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 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 diff --git a/urwid/canvas.py b/urwid/canvas.py deleted file mode 100644 index 4a51d3e..0000000 --- a/urwid/canvas.py +++ /dev/null @@ -1,1317 +0,0 @@ -#!/usr/bin/python -# -# Urwid canvas class and 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/ - -import weakref - -from urwid.util import rle_len, rle_append_modify, rle_join_modify, rle_product, \ - calc_width, calc_text_pos, apply_target_encoding, trim_text_attr_cs -from urwid.text_layout import trim_line, LayoutSegment -from urwid.compat import bytes - - -class CanvasCache(object): - """ - Cache for rendered canvases. Automatically populated and - accessed by Widget render() MetaClass magic, cleared by - Widget._invalidate(). - - Stores weakrefs to the canvas objects, so an external class - must maintain a reference for this cache to be effective. - At present the Screen classes store the last topmost canvas - after redrawing the screen, keeping the canvases from being - garbage collected. - - _widgets[widget] = {(wcls, size, focus): weakref.ref(canvas), ...} - _refs[weakref.ref(canvas)] = (widget, wcls, size, focus) - _deps[widget} = [dependent_widget, ...] - """ - _widgets = {} - _refs = {} - _deps = {} - hits = 0 - fetches = 0 - cleanups = 0 - - def store(cls, wcls, canvas): - """ - Store a weakref to canvas in the cache. - - wcls -- widget class that contains render() function - canvas -- rendered canvas with widget_info (widget, size, focus) - """ - if not canvas.cacheable: - return - - assert canvas.widget_info, "Can't store canvas without widget_info" - widget, size, focus = canvas.widget_info - def walk_depends(canv): - """ - Collect all child widgets for determining who we - depend on. - """ - # FIXME: is this recursion necessary? The cache - # invalidating might work with only one level. - depends = [] - for x, y, c, pos in canv.children: - if c.widget_info: - depends.append(c.widget_info[0]) - elif hasattr(c, 'children'): - depends.extend(walk_depends(c)) - return depends - - # use explicit depends_on if available from the canvas - depends_on = getattr(canvas, 'depends_on', None) - if depends_on is None and hasattr(canvas, 'children'): - depends_on = walk_depends(canvas) - if depends_on: - for w in depends_on: - if w not in cls._widgets: - return - for w in depends_on: - cls._deps.setdefault(w,[]).append(widget) - - ref = weakref.ref(canvas, cls.cleanup) - cls._refs[ref] = (widget, wcls, size, focus) - cls._widgets.setdefault(widget, {})[(wcls, size, focus)] = ref - store = classmethod(store) - - def fetch(cls, widget, wcls, size, focus): - """ - Return the cached canvas or None. - - widget -- widget object requested - wcls -- widget class that contains render() function - size, focus -- render() parameters - """ - cls.fetches += 1 # collect stats - - sizes = cls._widgets.get(widget, None) - if not sizes: - return None - ref = sizes.get((wcls, size, focus), None) - if not ref: - return None - canv = ref() - if canv: - cls.hits += 1 # more stats - return canv - fetch = classmethod(fetch) - - def invalidate(cls, widget): - """ - Remove all canvases cached for widget. - """ - try: - for ref in cls._widgets[widget].values(): - try: - del cls._refs[ref] - except KeyError: - pass - del cls._widgets[widget] - except KeyError: - pass - if widget not in cls._deps: - return - dependants = cls._deps.get(widget, []) - try: - del cls._deps[widget] - except KeyError: - pass - for w in dependants: - cls.invalidate(w) - invalidate = classmethod(invalidate) - - def cleanup(cls, ref): - cls.cleanups += 1 # collect stats - - w = cls._refs.get(ref, None) - del cls._refs[ref] - if not w: - return - widget, wcls, size, focus = w - sizes = cls._widgets.get(widget, None) - if not sizes: - return - try: - del sizes[(wcls, size, focus)] - except KeyError: - pass - if not sizes: - try: - del cls._widgets[widget] - del cls._deps[widget] - except KeyError: - pass - cleanup = classmethod(cleanup) - - def clear(cls): - """ - Empty the cache. - """ - cls._widgets = {} - cls._refs = {} - cls._deps = {} - clear = classmethod(clear) - - - -class CanvasError(Exception): - pass - -class Canvas(object): - """ - base class for canvases - """ - cacheable = True - - _finalized_error = CanvasError("This canvas has been finalized. " - "Use CompositeCanvas to wrap this canvas if " - "you need to make changes.") - _renamed_error = CanvasError("The old Canvas class is now called " - "TextCanvas. Canvas is now the base class for all canvas " - "classes.") - _old_repr_error = CanvasError("The internal representation of " - "canvases is no longer stored as .text, .attr, and .cs " - "lists, please see the TextCanvas class for the new " - "representation of canvas content.") - - def __init__(self, value1=None, value2=None, value3=None): - """ - value1, value2, value3 -- if not None, raise a helpful error: - the old Canvas class is now called TextCanvas. - """ - if value1 is not None: - raise self._renamed_error - self._widget_info = None - self.coords = {} - self.shortcuts = {} - - def finalize(self, widget, size, focus): - """ - Mark this canvas as finalized (should not be any future - changes to its content). This is required before caching - the canvas. This happens automatically after a widget's - 'render call returns the canvas thanks to some metaclass - magic. - - widget -- widget that rendered this canvas - size -- size parameter passed to widget's render method - focus -- focus parameter passed to widget's render method - """ - if self.widget_info: - raise self._finalized_error - self._widget_info = widget, size, focus - - def _get_widget_info(self): - return self._widget_info - widget_info = property(_get_widget_info) - - def _raise_old_repr_error(self, val=None): - raise self._old_repr_error - - def _text_content(self): - """ - Return the text content of the canvas as a list of strings, - one for each row. - """ - return [bytes().join([text for (attr, cs, text) in row]) - for row in self.content()] - - text = property(_text_content, _raise_old_repr_error) - attr = property(_raise_old_repr_error, _raise_old_repr_error) - cs = property(_raise_old_repr_error, _raise_old_repr_error) - - def content(self, trim_left=0, trim_top=0, cols=None, rows=None, - attr=None): - raise NotImplementedError() - - def cols(self): - raise NotImplementedError() - - def rows(self): - raise NotImplementedError() - - def content_delta(self): - raise NotImplementedError() - - def get_cursor(self): - c = self.coords.get("cursor", None) - if not c: - return - return c[:2] # trim off data part - def set_cursor(self, c): - if self.widget_info and self.cacheable: - raise self._finalized_error - if c is None: - try: - del self.coords["cursor"] - except KeyError: - pass - return - self.coords["cursor"] = c + (None,) # data part - cursor = property(get_cursor, set_cursor) - - def get_pop_up(self): - c = self.coords.get("pop up", None) - if not c: - return - return c - def set_pop_up(self, w, left, top, overlay_width, overlay_height): - """ - This method adds pop-up information to the canvas. This information - is intercepted by a PopUpTarget widget higher in the chain to - display a pop-up at the given (left, top) position relative to the - current canvas. - - :param w: widget to use for the pop-up - :type w: widget - :param left: x position for left edge of pop-up >= 0 - :type left: int - :param top: y position for top edge of pop-up >= 0 - :type top: int - :param overlay_width: width of overlay in screen columns > 0 - :type overlay_width: int - :param overlay_height: height of overlay in screen rows > 0 - :type overlay_height: int - """ - if self.widget_info and self.cacheable: - raise self._finalized_error - - self.coords["pop up"] = (left, top, ( - w, overlay_width, overlay_height)) - - def translate_coords(self, dx, dy): - """ - Return coords shifted by (dx, dy). - """ - d = {} - for name, (x, y, data) in self.coords.items(): - d[name] = (x+dx, y+dy, data) - return d - - - -class TextCanvas(Canvas): - """ - class for storing rendered text and attributes - """ - def __init__(self, text=None, attr=None, cs=None, - cursor=None, maxcol=None, check_width=True): - """ - text -- list of strings, one for each line - attr -- list of run length encoded attributes for text - cs -- list of run length encoded character set for text - cursor -- (x,y) of cursor or None - maxcol -- screen columns taken by this canvas - check_width -- check and fix width of all lines in text - """ - Canvas.__init__(self) - if text == None: - text = [] - - if check_width: - widths = [] - for t in text: - if type(t) != bytes: - raise CanvasError("Canvas text must be plain strings encoded in the screen's encoding", repr(text)) - widths.append( calc_width( t, 0, len(t)) ) - else: - assert type(maxcol) == int - widths = [maxcol] * len(text) - - if maxcol is None: - if widths: - # find maxcol ourselves - maxcol = max(widths) - else: - maxcol = 0 - - if attr == None: - attr = [[] for x in range(len(text))] - if cs == None: - cs = [[] for x in range(len(text))] - - # pad text and attr to maxcol - for i in range(len(text)): - w = widths[i] - if w > maxcol: - raise CanvasError("Canvas text is wider than the maxcol specified \n%r\n%r\n%r"%(maxcol,widths,text)) - if w < maxcol: - text[i] = text[i] + bytes().rjust(maxcol-w) - a_gap = len(text[i]) - rle_len( attr[i] ) - if a_gap < 0: - raise CanvasError("Attribute extends beyond text \n%r\n%r" % (text[i],attr[i]) ) - if a_gap: - rle_append_modify( attr[i], (None, a_gap)) - - cs_gap = len(text[i]) - rle_len( cs[i] ) - if cs_gap < 0: - raise CanvasError("Character Set extends beyond text \n%r\n%r" % (text[i],cs[i]) ) - if cs_gap: - rle_append_modify( cs[i], (None, cs_gap)) - - self._attr = attr - self._cs = cs - self.cursor = cursor - self._text = text - self._maxcol = maxcol - - def rows(self): - """Return the number of rows in this canvas.""" - rows = len(self._text) - assert isinstance(rows, int) - return rows - - def cols(self): - """Return the screen column width of this canvas.""" - return self._maxcol - - def translated_coords(self,dx,dy): - """ - Return cursor coords shifted by (dx, dy), or None if there - is no cursor. - """ - if self.cursor: - x, y = self.cursor - return x+dx, y+dy - return None - - def content(self, trim_left=0, trim_top=0, cols=None, rows=None, - attr_map=None): - """ - Return the canvas content as a list of rows where each row - is a list of (attr, cs, text) tuples. - - trim_left, trim_top, cols, rows may be set by - CompositeCanvas when rendering a partially obscured - canvas. - """ - maxcol, maxrow = self.cols(), self.rows() - if not cols: - cols = maxcol - trim_left - if not rows: - rows = maxrow - trim_top - - assert trim_left >= 0 and trim_left < maxcol - assert cols > 0 and trim_left + cols <= maxcol - assert trim_top >=0 and trim_top < maxrow - assert rows > 0 and trim_top + rows <= maxrow - - if trim_top or rows < maxrow: - text_attr_cs = zip( - self._text[trim_top:trim_top+rows], - self._attr[trim_top:trim_top+rows], - self._cs[trim_top:trim_top+rows]) - else: - text_attr_cs = zip(self._text, self._attr, self._cs) - - for text, a_row, cs_row in text_attr_cs: - if trim_left or cols < self._maxcol: - text, a_row, cs_row = trim_text_attr_cs( - text, a_row, cs_row, trim_left, - trim_left + cols) - attr_cs = rle_product(a_row, cs_row) - i = 0 - row = [] - for (a, cs), run in attr_cs: - if attr_map and a in attr_map: - a = attr_map[a] - row.append((a, cs, text[i:i+run])) - i += run - yield row - - - def content_delta(self, other): - """ - Return the differences between other and this canvas. - - If other is the same object as self this will return no - differences, otherwise this is the same as calling - content(). - """ - if other is self: - return [self.cols()]*self.rows() - return self.content() - - - -class BlankCanvas(Canvas): - """ - a canvas with nothing on it, only works as part of a composite canvas - since it doesn't know its own size - """ - def __init__(self): - Canvas.__init__(self, None) - - def content(self, trim_left, trim_top, cols, rows, attr): - """ - return (cols, rows) of spaces with default attributes. - """ - def_attr = None - if attr and None in attr: - def_attr = attr[None] - line = [(def_attr, None, bytes().rjust(cols))] - for i in range(rows): - yield line - - def cols(self): - raise NotImplementedError("BlankCanvas doesn't know its own size!") - - def rows(self): - raise NotImplementedError("BlankCanvas doesn't know its own size!") - - def content_delta(self): - raise NotImplementedError("BlankCanvas doesn't know its own size!") - -blank_canvas = BlankCanvas() - - -class SolidCanvas(Canvas): - """ - A canvas filled completely with a single character. - """ - def __init__(self, fill_char, cols, rows): - Canvas.__init__(self) - end, col = calc_text_pos(fill_char, 0, len(fill_char), 1) - assert col == 1, "Invalid fill_char: %r" % fill_char - self._text, cs = apply_target_encoding(fill_char[:end]) - self._cs = cs[0][0] - self.size = cols, rows - self.cursor = None - - def cols(self): - return self.size[0] - - def rows(self): - return self.size[1] - - def content(self, trim_left=0, trim_top=0, cols=None, rows=None, - attr=None): - if cols is None: - cols = self.size[0] - if rows is None: - rows = self.size[1] - def_attr = None - if attr and None in attr: - def_attr = attr[None] - - line = [(def_attr, self._cs, self._text*cols)] - for i in range(rows): - yield line - - def content_delta(self, other): - """ - Return the differences between other and this canvas. - """ - if other is self: - return [self.cols()]*self.rows() - return self.content() - - - - -class CompositeCanvas(Canvas): - """ - class for storing a combination of canvases - """ - def __init__(self, canv=None): - """ - canv -- a Canvas object to wrap this CompositeCanvas around. - - if canv is a CompositeCanvas, make a copy of its contents - """ - # a "shard" is a (num_rows, list of cviews) tuple, one for - # each cview starting in this shard - - # a "cview" is a tuple that defines a view of a canvas: - # (trim_left, trim_top, cols, rows, attr_map, canv) - - # a "shard tail" is a list of tuples: - # (col_gap, done_rows, content_iter, cview) - - # tuples that define the unfinished cviews that are part of - # shards following the first shard. - Canvas.__init__(self) - - if canv is None: - self.shards = [] - self.children = [] - else: - if hasattr(canv, "shards"): - self.shards = canv.shards - else: - self.shards = [(canv.rows(), [ - (0, 0, canv.cols(), canv.rows(), - None, canv)])] - self.children = [(0, 0, canv, None)] - self.coords.update(canv.coords) - for shortcut in canv.shortcuts: - self.shortcuts[shortcut] = "wrap" - - def rows(self): - for r,cv in self.shards: - try: - assert isinstance(r, int) - except AssertionError: - raise AssertionError(r, cv) - rows = sum([r for r,cv in self.shards]) - assert isinstance(rows, int) - return rows - - def cols(self): - if not self.shards: - return 0 - cols = sum([cv[2] for cv in self.shards[0][1]]) - assert isinstance(cols, int) - return cols - - - def content(self): - """ - Return the canvas content as a list of rows where each row - is a list of (attr, cs, text) tuples. - """ - shard_tail = [] - for num_rows, cviews in self.shards: - # combine shard and shard tail - sbody = shard_body(cviews, shard_tail) - - # output rows - for i in range(num_rows): - yield shard_body_row(sbody) - - # prepare next shard tail - shard_tail = shard_body_tail(num_rows, sbody) - - - - def content_delta(self, other): - """ - Return the differences between other and this canvas. - """ - if not hasattr(other, 'shards'): - for row in self.content(): - yield row - return - - shard_tail = [] - for num_rows, cviews in shards_delta( - self.shards, other.shards): - # combine shard and shard tail - sbody = shard_body(cviews, shard_tail) - - # output rows - row = [] - for i in range(num_rows): - # if whole shard is unchanged, don't keep - # calling shard_body_row - if len(row) != 1 or type(row[0]) != int: - row = shard_body_row(sbody) - yield row - - # prepare next shard tail - shard_tail = shard_body_tail(num_rows, sbody) - - - def trim(self, top, count=None): - """Trim lines from the top and/or bottom of canvas. - - top -- number of lines to remove from top - count -- number of lines to keep, or None for all the rest - """ - assert top >= 0, "invalid trim amount %d!"%top - assert top < self.rows(), "cannot trim %d lines from %d!"%( - top, self.rows()) - if self.widget_info: - raise self._finalized_error - - if top: - self.shards = shards_trim_top(self.shards, top) - - if count == 0: - self.shards = [] - elif count is not None: - self.shards = shards_trim_rows(self.shards, count) - - self.coords = self.translate_coords(0, -top) - - - def trim_end(self, end): - """Trim lines from the bottom of the canvas. - - end -- number of lines to remove from the end - """ - assert end > 0, "invalid trim amount %d!"%end - assert end <= self.rows(), "cannot trim %d lines from %d!"%( - end, self.rows()) - if self.widget_info: - raise self._finalized_error - - self.shards = shards_trim_rows(self.shards, self.rows() - end) - - - def pad_trim_left_right(self, left, right): - """ - Pad or trim this canvas on the left and right - - values > 0 indicate screen columns to pad - values < 0 indicate screen columns to trim - """ - if self.widget_info: - raise self._finalized_error - shards = self.shards - if left < 0 or right < 0: - trim_left = max(0, -left) - cols = self.cols() - trim_left - max(0, -right) - shards = shards_trim_sides(shards, trim_left, cols) - - rows = self.rows() - if left > 0 or right > 0: - top_rows, top_cviews = shards[0] - if left > 0: - new_top_cviews = ( - [(0,0,left,rows,None,blank_canvas)] + - top_cviews) - else: - new_top_cviews = top_cviews[:] #copy - - if right > 0: - new_top_cviews.append( - (0,0,right,rows,None,blank_canvas)) - shards = [(top_rows, new_top_cviews)] + shards[1:] - - self.coords = self.translate_coords(left, 0) - self.shards = shards - - - def pad_trim_top_bottom(self, top, bottom): - """ - Pad or trim this canvas on the top and bottom. - """ - if self.widget_info: - raise self._finalized_error - orig_shards = self.shards - - if top < 0 or bottom < 0: - trim_top = max(0, -top) - rows = self.rows() - trim_top - max(0, -bottom) - self.trim(trim_top, rows) - - cols = self.cols() - if top > 0: - self.shards = [(top, - [(0,0,cols,top,None,blank_canvas)])] + \ - self.shards - self.coords = self.translate_coords(0, top) - - if bottom > 0: - if orig_shards is self.shards: - self.shards = self.shards[:] - self.shards.append((bottom, - [(0,0,cols,bottom,None,blank_canvas)])) - - - def overlay(self, other, left, top ): - """Overlay other onto this canvas.""" - if self.widget_info: - raise self._finalized_error - - width = other.cols() - height = other.rows() - right = self.cols() - left - width - bottom = self.rows() - top - height - - assert right >= 0, "top canvas of overlay not the size expected!" + repr((other.cols(),left,right,width)) - assert bottom >= 0, "top canvas of overlay not the size expected!" + repr((other.rows(),top,bottom,height)) - - shards = self.shards - top_shards = [] - side_shards = self.shards - bottom_shards = [] - if top: - side_shards = shards_trim_top(shards, top) - top_shards = shards_trim_rows(shards, top) - if bottom: - bottom_shards = shards_trim_top(side_shards, height) - side_shards = shards_trim_rows(side_shards, height) - - left_shards = [] - right_shards = [] - if left > 0: - left_shards = [shards_trim_sides(side_shards, 0, left)] - if right > 0: - right_shards = [shards_trim_sides(side_shards, - max(0, left + width), right)] - - if not self.rows(): - middle_shards = [] - elif left or right: - middle_shards = shards_join(left_shards + - [other.shards] + right_shards) - else: - middle_shards = other.shards - - self.shards = top_shards + middle_shards + bottom_shards - - self.coords.update(other.translate_coords(left, top)) - - - def fill_attr(self, a): - """ - Apply attribute a to all areas of this canvas with default - attribute currently set to None, leaving other attributes - intact.""" - self.fill_attr_apply({None:a}) - - def fill_attr_apply(self, mapping): - """ - Apply an attribute-mapping dictionary to the canvas. - - mapping -- dictionary of original-attribute:new-attribute items - """ - if self.widget_info: - raise self._finalized_error - - shards = [] - for num_rows, original_cviews in self.shards: - new_cviews = [] - for cv in original_cviews: - # cv[4] == attr_map - if cv[4] is None: - new_cviews.append(cv[:4] + - (mapping,) + cv[5:]) - else: - combined = dict(mapping) - combined.update([ - (k, mapping.get(v, v)) for k,v in cv[4].items()]) - new_cviews.append(cv[:4] + - (combined,) + cv[5:]) - shards.append((num_rows, new_cviews)) - self.shards = shards - - def set_depends(self, widget_list): - """ - Explicitly specify the list of widgets that this canvas - depends on. If any of these widgets change this canvas - will have to be updated. - """ - if self.widget_info: - raise self._finalized_error - - self.depends_on = widget_list - - -def shard_body_row(sbody): - """ - Return one row, advancing the iterators in sbody. - - ** MODIFIES sbody by calling next() on its iterators ** - """ - row = [] - for done_rows, content_iter, cview in sbody: - if content_iter: - row.extend(content_iter.next()) - else: - # need to skip this unchanged canvas - if row and type(row[-1]) == int: - row[-1] = row[-1] + cview[2] - else: - row.append(cview[2]) - - return row - - -def shard_body_tail(num_rows, sbody): - """ - Return a new shard tail that follows this shard body. - """ - shard_tail = [] - col_gap = 0 - done_rows = 0 - for done_rows, content_iter, cview in sbody: - cols, rows = cview[2:4] - done_rows += num_rows - if done_rows == rows: - col_gap += cols - continue - shard_tail.append((col_gap, done_rows, content_iter, cview)) - col_gap = 0 - return shard_tail - - -def shards_delta(shards, other_shards): - """ - Yield shards1 with cviews that are the same as shards2 - having canv = None. - """ - other_shards_iter = iter(other_shards) - other_num_rows = other_cviews = None - done = other_done = 0 - for num_rows, cviews in shards: - if other_num_rows is None: - other_num_rows, other_cviews = other_shards_iter.next() - while other_done < done: - other_done += other_num_rows - other_num_rows, other_cviews = other_shards_iter.next() - if other_done > done: - yield (num_rows, cviews) - done += num_rows - continue - # top-aligned shards, compare each cview - yield (num_rows, shard_cviews_delta(cviews, other_cviews)) - other_done += other_num_rows - other_num_rows = None - done += num_rows - -def shard_cviews_delta(cviews, other_cviews): - """ - """ - other_cviews_iter = iter(other_cviews) - other_cv = None - cols = other_cols = 0 - for cv in cviews: - if other_cv is None: - other_cv = other_cviews_iter.next() - while other_cols < cols: - other_cols += other_cv[2] - other_cv = other_cviews_iter.next() - if other_cols > cols: - yield cv - cols += cv[2] - continue - # top-left-aligned cviews, compare them - if cv[5] is other_cv[5] and cv[:5] == other_cv[:5]: - yield cv[:5]+(None,)+cv[6:] - else: - yield cv - other_cols += other_cv[2] - other_cv = None - cols += cv[2] - - - -def shard_body(cviews, shard_tail, create_iter=True, iter_default=None): - """ - Return a list of (done_rows, content_iter, cview) tuples for - this shard and shard tail. - - If a canvas in cviews is None (eg. when unchanged from - shard_cviews_delta()) or if create_iter is False then no - iterator is created for content_iter. - - iter_default is the value used for content_iter when no iterator - is created. - """ - col = 0 - body = [] # build the next shard tail - cviews_iter = iter(cviews) - for col_gap, done_rows, content_iter, tail_cview in shard_tail: - while col_gap: - try: - cview = cviews_iter.next() - except StopIteration: - raise CanvasError("cviews do not fill gaps in" - " shard_tail!") - (trim_left, trim_top, cols, rows, attr_map, canv) = \ - cview[:6] - col += cols - col_gap -= cols - if col_gap < 0: - raise CanvasError("cviews overflow gaps in" - " shard_tail!") - if create_iter and canv: - new_iter = canv.content(trim_left, trim_top, - cols, rows, attr_map) - else: - new_iter = iter_default - body.append((0, new_iter, cview)) - body.append((done_rows, content_iter, tail_cview)) - for cview in cviews_iter: - (trim_left, trim_top, cols, rows, attr_map, canv) = \ - cview[:6] - if create_iter and canv: - new_iter = canv.content(trim_left, trim_top, cols, rows, - attr_map) - else: - new_iter = iter_default - body.append((0, new_iter, cview)) - return body - - -def shards_trim_top(shards, top): - """ - Return shards with top rows removed. - """ - assert top > 0 - - shard_iter = iter(shards) - shard_tail = [] - # skip over shards that are completely removed - for num_rows, cviews in shard_iter: - if top < num_rows: - break - sbody = shard_body(cviews, shard_tail, False) - shard_tail = shard_body_tail(num_rows, sbody) - top -= num_rows - else: - raise CanvasError("tried to trim shards out of existence") - - sbody = shard_body(cviews, shard_tail, False) - shard_tail = shard_body_tail(num_rows, sbody) - # trim the top of this shard - new_sbody = [] - for done_rows, content_iter, cv in sbody: - new_sbody.append((0, content_iter, - cview_trim_top(cv, done_rows+top))) - sbody = new_sbody - - new_shards = [(num_rows-top, - [cv for done_rows, content_iter, cv in sbody])] - - # write out the rest of the shards - new_shards.extend(shard_iter) - - return new_shards - -def shards_trim_rows(shards, keep_rows): - """ - Return the topmost keep_rows rows from shards. - """ - assert keep_rows >= 0, keep_rows - - new_shards = [] - done_rows = 0 - for num_rows, cviews in shards: - if done_rows >= keep_rows: - break - new_cviews = [] - for cv in cviews: - if cv[3] + done_rows > keep_rows: - new_cviews.append(cview_trim_rows(cv, - keep_rows - done_rows)) - else: - new_cviews.append(cv) - - if num_rows + done_rows > keep_rows: - new_shards.append((keep_rows - done_rows, new_cviews)) - else: - new_shards.append((num_rows, new_cviews)) - done_rows += num_rows - - return new_shards - -def shards_trim_sides(shards, left, cols): - """ - Return shards with starting from column left and cols total width. - """ - assert left >= 0 and cols > 0, (left, cols) - shard_tail = [] - new_shards = [] - right = left + cols - for num_rows, cviews in shards: - sbody = shard_body(cviews, shard_tail, False) - shard_tail = shard_body_tail(num_rows, sbody) - new_cviews = [] - col = 0 - for done_rows, content_iter, cv in sbody: - cv_cols = cv[2] - next_col = col + cv_cols - if done_rows or next_col <= left or col >= right: - col = next_col - continue - if col < left: - cv = cview_trim_left(cv, left - col) - col = left - if next_col > right: - cv = cview_trim_cols(cv, right - col) - new_cviews.append(cv) - col = next_col - if not new_cviews: - prev_num_rows, prev_cviews = new_shards[-1] - new_shards[-1] = (prev_num_rows+num_rows, prev_cviews) - else: - new_shards.append((num_rows, new_cviews)) - return new_shards - -def shards_join(shard_lists): - """ - Return the result of joining shard lists horizontally. - All shards lists must have the same number of rows. - """ - shards_iters = [iter(sl) for sl in shard_lists] - shards_current = [i.next() for i in shards_iters] - - new_shards = [] - while True: - new_cviews = [] - num_rows = min([r for r,cv in shards_current]) - - shards_next = [] - for rows, cviews in shards_current: - if cviews: - new_cviews.extend(cviews) - shards_next.append((rows - num_rows, None)) - - shards_current = shards_next - new_shards.append((num_rows, new_cviews)) - - # advance to next shards - try: - for i in range(len(shards_current)): - if shards_current[i][0] > 0: - continue - shards_current[i] = shards_iters[i].next() - except StopIteration: - break - return new_shards - - -def cview_trim_rows(cv, rows): - return cv[:3] + (rows,) + cv[4:] - -def cview_trim_top(cv, trim): - return (cv[0], trim + cv[1], cv[2], cv[3] - trim) + cv[4:] - -def cview_trim_left(cv, trim): - return (cv[0] + trim, cv[1], cv[2] - trim,) + cv[3:] - -def cview_trim_cols(cv, cols): - return cv[:2] + (cols,) + cv[3:] - - - - -def CanvasCombine(l): - """Stack canvases in l vertically and return resulting canvas. - - :param l: list of (canvas, position, focus) tuples: - - position - a value that widget.set_focus will accept or None if not - allowed - focus - True if this canvas is the one that would be in focus - if the whole widget is in focus - """ - clist = [(CompositeCanvas(c),p,f) for c,p,f in l] - - combined_canvas = CompositeCanvas() - shards = [] - children = [] - row = 0 - focus_index = 0 - n = 0 - for canv, pos, focus in clist: - if focus: - focus_index = n - children.append((0, row, canv, pos)) - shards.extend(canv.shards) - combined_canvas.coords.update(canv.translate_coords(0, row)) - for shortcut in canv.shortcuts.keys(): - combined_canvas.shortcuts[shortcut] = pos - row += canv.rows() - n += 1 - - if focus_index: - children = [children[focus_index]] + children[:focus_index] + \ - children[focus_index+1:] - - combined_canvas.shards = shards - combined_canvas.children = children - return combined_canvas - - -def CanvasOverlay(top_c, bottom_c, left, top): - """ - Overlay canvas top_c onto bottom_c at position (left, top). - """ - overlayed_canvas = CompositeCanvas(bottom_c) - overlayed_canvas.overlay(top_c, left, top) - overlayed_canvas.children = [(left, top, top_c, None), - (0, 0, bottom_c, None)] - overlayed_canvas.shortcuts = {} # disable background shortcuts - for shortcut in top_c.shortcuts.keys(): - overlayed_canvas.shortcuts[shortcut]="fg" - return overlayed_canvas - - -def CanvasJoin(l): - """ - Join canvases in l horizontally. Return result. - - :param l: list of (canvas, position, focus, cols) tuples: - - position - value that widget.set_focus will accept or None if not allowed - focus - True if this canvas is the one that would be in focus if - the whole widget is in focus - cols - is the number of screen columns that this widget will require, - if larger than the actual canvas.cols() value then this widget - will be padded on the right. - """ - - l2 = [] - focus_item = 0 - maxrow = 0 - n = 0 - for canv, pos, focus, cols in l: - rows = canv.rows() - pad_right = cols - canv.cols() - if focus: - focus_item = n - if rows > maxrow: - maxrow = rows - l2.append((canv, pos, pad_right, rows)) - n += 1 - - shard_lists = [] - children = [] - joined_canvas = CompositeCanvas() - col = 0 - for canv, pos, pad_right, rows in l2: - canv = CompositeCanvas(canv) - if pad_right: - canv.pad_trim_left_right(0, pad_right) - if rows < maxrow: - canv.pad_trim_top_bottom(0, maxrow - rows) - joined_canvas.coords.update(canv.translate_coords(col, 0)) - for shortcut in canv.shortcuts.keys(): - joined_canvas.shortcuts[shortcut] = pos - shard_lists.append(canv.shards) - children.append((col, 0, canv, pos)) - col += canv.cols() - - if focus_item: - children = [children[focus_item]] + children[:focus_item] + \ - children[focus_item+1:] - - joined_canvas.shards = shards_join(shard_lists) - joined_canvas.children = children - return joined_canvas - - -def apply_text_layout(text, attr, ls, maxcol): - t = [] - a = [] - c = [] - - class AttrWalk: - pass - aw = AttrWalk - aw.k = 0 # counter for moving through elements of a - aw.off = 0 # current offset into text of attr[ak] - - def arange( start_offs, end_offs ): - """Return an attribute list for the range of text specified.""" - if start_offs < aw.off: - aw.k = 0 - aw.off = 0 - o = [] - while aw.off < end_offs: - if len(attr)<=aw.k: - # run out of attributes - o.append((None,end_offs-max(start_offs,aw.off))) - break - at,run = attr[aw.k] - if aw.off+run <= start_offs: - # move forward through attr to find start_offs - aw.k += 1 - aw.off += run - continue - if end_offs <= aw.off+run: - o.append((at, end_offs-max(start_offs,aw.off))) - break - o.append((at, aw.off+run-max(start_offs, aw.off))) - aw.k += 1 - aw.off += run - return o - - - for line_layout in ls: - # trim the line to fit within maxcol - line_layout = trim_line( line_layout, text, 0, maxcol ) - - line = [] - linea = [] - linec = [] - - def attrrange( start_offs, end_offs, destw ): - """ - Add attributes based on attributes between - start_offs and end_offs. - """ - if start_offs == end_offs: - [(at,run)] = arange(start_offs,end_offs) - rle_append_modify( linea, ( at, destw )) - return - if destw == end_offs-start_offs: - for at, run in arange(start_offs,end_offs): - rle_append_modify( linea, ( at, run )) - return - # encoded version has different width - o = start_offs - for at, run in arange(start_offs, end_offs): - if o+run == end_offs: - rle_append_modify( linea, ( at, destw )) - return - tseg = text[o:o+run] - tseg, cs = apply_target_encoding( tseg ) - segw = rle_len(cs) - - rle_append_modify( linea, ( at, segw )) - o += run - destw -= segw - - - for seg in line_layout: - #if seg is None: assert 0, ls - s = LayoutSegment(seg) - if s.end: - tseg, cs = apply_target_encoding( - text[s.offs:s.end]) - line.append(tseg) - attrrange(s.offs, s.end, rle_len(cs)) - rle_join_modify( linec, cs ) - elif s.text: - tseg, cs = apply_target_encoding( s.text ) - line.append(tseg) - attrrange( s.offs, s.offs, len(tseg) ) - rle_join_modify( linec, cs ) - elif s.offs: - if s.sc: - line.append(bytes().rjust(s.sc)) - attrrange( s.offs, s.offs, s.sc ) - else: - line.append(bytes().rjust(s.sc)) - linea.append((None, s.sc)) - linec.append((None, s.sc)) - - t.append(bytes().join(line)) - a.append(linea) - c.append(linec) - - return TextCanvas(t, a, c, maxcol=maxcol) - - - - diff --git a/urwid/command_map.py b/urwid/command_map.py deleted file mode 100644 index 15633f8..0000000 --- a/urwid/command_map.py +++ /dev/null @@ -1,104 +0,0 @@ -#!/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/ - -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 diff --git a/urwid/compat.py b/urwid/compat.py deleted file mode 100644 index 686d703..0000000 --- a/urwid/compat.py +++ /dev/null @@ -1,48 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# -# Urwid python compatibility definitions -# Copyright (C) 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/ - -import sys - -try: # python 2.4 and 2.5 compat - bytes = bytes -except NameError: - bytes = str - -PYTHON3 = sys.version_info > (3, 0) - -# for iterating over byte strings: -# ord2 calls ord in python2 only -# chr2 converts an ordinal value to a length-1 byte string -# B returns a byte string in all supported python versions -# bytes3 creates a byte string from a list of ordinal values -if PYTHON3: - ord2 = lambda x: x - chr2 = lambda x: bytes([x]) - B = lambda x: x.encode('iso8859-1') - bytes3 = bytes -else: - ord2 = ord - chr2 = chr - B = lambda x: x - bytes3 = lambda x: bytes().join([chr(c) for c in x]) - - diff --git a/urwid/container.py b/urwid/container.py deleted file mode 100755 index 3999f28..0000000 --- a/urwid/container.py +++ /dev/null @@ -1,2303 +0,0 @@ -#!/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 itertools import chain, repeat - -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 xrange(len(self.contents)) - - def __reversed__(self): - """ - Return an iterable of positions for this container from last - to first. - """ - return 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 = range(i-1, -1, -1) # count backwards to 0 - else: # self._command_map[key] == 'cursor down' - candidates = 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 = range(rows-1, -1, -1) - else: # self._command_map[key] == 'cursor down' - rowlist = 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.dividechars = dividechars - 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 = range(i-1, -1, -1) # count backwards to 0 - else: # key == 'right' - candidates = 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() diff --git a/urwid/curses_display.py b/urwid/curses_display.py deleted file mode 100755 index 441042e..0000000 --- a/urwid/curses_display.py +++ /dev/null @@ -1,619 +0,0 @@ -#!/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/ - -""" -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 - -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, (cols, rows), r ): - """Paint screen with rendered canvas.""" - assert self._started - - 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 = _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) == unicode: 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() diff --git a/urwid/decoration.py b/urwid/decoration.py deleted file mode 100755 index 731eb91..0000000 --- a/urwid/decoration.py +++ /dev/null @@ -1,1170 +0,0 @@ -#!/usr/bin/python -# -# Urwid widget decoration 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 urwid.util import int_scale -from urwid.widget import (Widget, WidgetError, - BOX, FLOW, LEFT, CENTER, RIGHT, PACK, CLIP, GIVEN, RELATIVE, RELATIVE_100, - TOP, MIDDLE, BOTTOM, delegate_to_widget_mixin) -from urwid.split_repr import remove_defaults -from urwid.canvas import CompositeCanvas, SolidCanvas -from urwid.widget import Divider, Edit, Text, SolidFill # doctests - - -class WidgetDecoration(Widget): # "decorator" was already taken - """ - original_widget -- the widget being decorated - - This is a base class for decoration widgets, widgets - that contain one or more widgets and only ever have - a single focus. This type of widget will affect the - display or behaviour of the original_widget but it is - not part of determining a chain of focus. - - Don't actually do this -- use a WidgetDecoration subclass - instead, these are not real widgets: - - >>> WidgetDecoration(Text(u"hi")) - > - """ - def __init__(self, original_widget): - self._original_widget = original_widget - def _repr_words(self): - return self.__super._repr_words() + [repr(self._original_widget)] - - def _get_original_widget(self): - return self._original_widget - def _set_original_widget(self, original_widget): - self._original_widget = original_widget - self._invalidate() - original_widget = property(_get_original_widget, _set_original_widget) - - def _get_base_widget(self): - """ - Return the widget without decorations. If there is only one - Decoration then this is the same as original_widget. - - >>> t = Text('hello') - >>> wd1 = WidgetDecoration(t) - >>> wd2 = WidgetDecoration(wd1) - >>> wd3 = WidgetDecoration(wd2) - >>> wd3.original_widget is wd2 - True - >>> wd3.base_widget is t - True - """ - w = self - while hasattr(w, '_original_widget'): - w = w._original_widget - return w - - base_widget = property(_get_base_widget) - - def selectable(self): - return self._original_widget.selectable() - - def sizing(self): - return self._original_widget.sizing() - - -class WidgetPlaceholder(delegate_to_widget_mixin('_original_widget'), - WidgetDecoration): - """ - This is a do-nothing decoration widget that can be used for swapping - between widgets without modifying the container of this widget. - - This can be useful for making an interface with a number of distinct - pages or for showing and hiding menu or status bars. - - The widget displayed is stored as the self.original_widget property and - can be changed by assigning a new widget to it. - """ - pass - - -class AttrMapError(WidgetError): - pass - -class AttrMap(delegate_to_widget_mixin('_original_widget'), WidgetDecoration): - """ - AttrMap is a decoration that maps one set of attributes to another. - This object will pass all function calls and variable references to the - wrapped widget. - """ - def __init__(self, w, attr_map, focus_map=None): - """ - :param w: widget to wrap (stored as self.original_widget) - :type w: widget - - :param attr_map: attribute to apply to *w*, or dict of old display - attribute: new display attribute mappings - :type attr_map: display attribute or dict - - :param focus_map: attribute to apply when in focus or dict of - old display attribute: new display attribute mappings; - if ``None`` use *attr* - :type focus_map: display attribute or dict - - >>> AttrMap(Divider(u"!"), 'bright') - attr_map={None: 'bright'}> - >>> AttrMap(Edit(), 'notfocus', 'focus') - attr_map={None: 'notfocus'} focus_map={None: 'focus'}> - >>> size = (5,) - >>> am = AttrMap(Text(u"hi"), 'greeting', 'fgreet') - >>> am.render(size, focus=False).content().next() # ... = b in Python 3 - [('greeting', None, ...'hi ')] - >>> am.render(size, focus=True).content().next() - [('fgreet', None, ...'hi ')] - >>> am2 = AttrMap(Text(('word', u"hi")), {'word':'greeting', None:'bg'}) - >>> am2 - attr_map={'word': 'greeting', None: 'bg'}> - >>> am2.render(size).content().next() - [('greeting', None, ...'hi'), ('bg', None, ...' ')] - """ - self.__super.__init__(w) - - if type(attr_map) != dict: - self.set_attr_map({None: attr_map}) - else: - self.set_attr_map(attr_map) - - if focus_map is not None and type(focus_map) != dict: - self.set_focus_map({None: focus_map}) - else: - self.set_focus_map(focus_map) - - def _repr_attrs(self): - # only include the focus_attr when it takes effect (not None) - d = dict(self.__super._repr_attrs(), attr_map=self._attr_map) - if self._focus_map is not None: - d['focus_map'] = self._focus_map - return d - - def get_attr_map(self): - # make a copy so ours is not accidentally modified - # FIXME: a dictionary that detects modifications would be better - return dict(self._attr_map) - def set_attr_map(self, attr_map): - """ - Set the attribute mapping dictionary {from_attr: to_attr, ...} - - Note this function does not accept a single attribute the way the - constructor does. You must specify {None: attribute} instead. - - >>> w = AttrMap(Text(u"hi"), None) - >>> w.set_attr_map({'a':'b'}) - >>> w - attr_map={'a': 'b'}> - """ - for from_attr, to_attr in attr_map.items(): - if not from_attr.__hash__ or not to_attr.__hash__: - raise AttrMapError("%r:%r attribute mapping is invalid. " - "Attributes must be hashable" % (from_attr, to_attr)) - self._attr_map = attr_map - self._invalidate() - attr_map = property(get_attr_map, set_attr_map) - - def get_focus_map(self): - # make a copy so ours is not accidentally modified - # FIXME: a dictionary that detects modifications would be better - if self._focus_map: - return dict(self._focus_map) - def set_focus_map(self, focus_map): - """ - Set the focus attribute mapping dictionary - {from_attr: to_attr, ...} - - If None this widget will use the attr mapping instead (no change - when in focus). - - Note this function does not accept a single attribute the way the - constructor does. You must specify {None: attribute} instead. - - >>> w = AttrMap(Text(u"hi"), {}) - >>> w.set_focus_map({'a':'b'}) - >>> w - attr_map={} focus_map={'a': 'b'}> - >>> w.set_focus_map(None) - >>> w - attr_map={}> - """ - if focus_map is not None: - for from_attr, to_attr in focus_map.items(): - if not from_attr.__hash__ or not to_attr.__hash__: - raise AttrMapError("%r:%r attribute mapping is invalid. " - "Attributes must be hashable" % (from_attr, to_attr)) - self._focus_map = focus_map - self._invalidate() - focus_map = property(get_focus_map, set_focus_map) - - def render(self, size, focus=False): - """ - Render wrapped widget and apply attribute. Return canvas. - """ - attr_map = self._attr_map - if focus and self._focus_map is not None: - attr_map = self._focus_map - canv = self._original_widget.render(size, focus=focus) - canv = CompositeCanvas(canv) - canv.fill_attr_apply(attr_map) - return canv - - - -class AttrWrap(AttrMap): - def __init__(self, w, attr, focus_attr=None): - """ - w -- widget to wrap (stored as self.original_widget) - attr -- attribute to apply to w - focus_attr -- attribute to apply when in focus, if None use attr - - This widget is a special case of the new AttrMap widget, and it - will pass all function calls and variable references to the wrapped - widget. This class is maintained for backwards compatibility only, - new code should use AttrMap instead. - - >>> AttrWrap(Divider(u"!"), 'bright') - attr='bright'> - >>> AttrWrap(Edit(), 'notfocus', 'focus') - attr='notfocus' focus_attr='focus'> - >>> size = (5,) - >>> aw = AttrWrap(Text(u"hi"), 'greeting', 'fgreet') - >>> aw.render(size, focus=False).content().next() - [('greeting', None, ...'hi ')] - >>> aw.render(size, focus=True).content().next() - [('fgreet', None, ...'hi ')] - """ - self.__super.__init__(w, attr, focus_attr) - - def _repr_attrs(self): - # only include the focus_attr when it takes effect (not None) - d = dict(self.__super._repr_attrs(), attr=self.attr) - del d['attr_map'] - if 'focus_map' in d: - del d['focus_map'] - if self.focus_attr is not None: - d['focus_attr'] = self.focus_attr - return d - - # backwards compatibility, widget used to be stored as w - get_w = WidgetDecoration._get_original_widget - set_w = WidgetDecoration._set_original_widget - w = property(get_w, set_w) - - def get_attr(self): - return self.attr_map[None] - def set_attr(self, attr): - """ - Set the attribute to apply to the wrapped widget - - >> w = AttrWrap(Divider("-"), None) - >> w.set_attr('new_attr') - >> w - attr='new_attr'> - """ - self.set_attr_map({None: attr}) - attr = property(get_attr, set_attr) - - def get_focus_attr(self): - focus_map = self.focus_map - if focus_map: - return focus_map[None] - def set_focus_attr(self, focus_attr): - """ - Set the attribute to apply to the wapped widget when it is in - focus - - If None this widget will use the attr instead (no change when in - focus). - - >> w = AttrWrap(Divider("-"), 'old') - >> w.set_focus_attr('new_attr') - >> w - attr='old' focus_attr='new_attr'> - >> w.set_focus_attr(None) - >> w - attr='old'> - """ - self.set_focus_map({None: focus_attr}) - focus_attr = property(get_focus_attr, set_focus_attr) - - def __getattr__(self,name): - """ - Call getattr on wrapped widget. This has been the longstanding - behaviour of AttrWrap, but is discouraged. New code should be - using AttrMap and .base_widget or .original_widget instead. - """ - return getattr(self._original_widget, name) - - - def sizing(self): - return self._original_widget.sizing() - - -class BoxAdapterError(Exception): - pass - -class BoxAdapter(WidgetDecoration): - """ - Adapter for using a box widget where a flow widget would usually go - """ - no_cache = ["rows"] - - def __init__(self, box_widget, height): - """ - Create a flow widget that contains a box widget - - :param box_widget: box widget to wrap - :type box_widget: Widget - :param height: number of rows for box widget - :type height: int - - >>> BoxAdapter(SolidFill(u"x"), 5) # 5-rows of x's - height=5> - """ - if hasattr(box_widget, 'sizing') and BOX not in box_widget.sizing(): - raise BoxAdapterError("%r is not a box widget" % - box_widget) - WidgetDecoration.__init__(self,box_widget) - - self.height = height - - def _repr_attrs(self): - return dict(self.__super._repr_attrs(), height=self.height) - - # originally stored as box_widget, keep for compatibility - box_widget = property(WidgetDecoration._get_original_widget, - WidgetDecoration._set_original_widget) - - def sizing(self): - return set([FLOW]) - - def rows(self, size, focus=False): - """ - Return the predetermined height (behave like a flow widget) - - >>> BoxAdapter(SolidFill(u"x"), 5).rows((20,)) - 5 - """ - return self.height - - # The next few functions simply tack-on our height and pass through - # to self._original_widget - def get_cursor_coords(self, size): - (maxcol,) = size - if not hasattr(self._original_widget,'get_cursor_coords'): - return None - return self._original_widget.get_cursor_coords((maxcol, self.height)) - - def get_pref_col(self, size): - (maxcol,) = size - if not hasattr(self._original_widget,'get_pref_col'): - return None - return self._original_widget.get_pref_col((maxcol, self.height)) - - def keypress(self, size, key): - (maxcol,) = size - return self._original_widget.keypress((maxcol, self.height), key) - - def move_cursor_to_coords(self, size, col, row): - (maxcol,) = size - if not hasattr(self._original_widget,'move_cursor_to_coords'): - return True - return self._original_widget.move_cursor_to_coords((maxcol, - self.height), col, row ) - - def mouse_event(self, size, event, button, col, row, focus): - (maxcol,) = size - if not hasattr(self._original_widget,'mouse_event'): - return False - return self._original_widget.mouse_event((maxcol, self.height), - event, button, col, row, focus) - - def render(self, size, focus=False): - (maxcol,) = size - canv = self._original_widget.render((maxcol, self.height), focus) - canv = CompositeCanvas(canv) - return canv - - def __getattr__(self, name): - """ - Pass calls to box widget. - """ - return getattr(self.box_widget, name) - - - -class PaddingError(Exception): - pass - -class Padding(WidgetDecoration): - def __init__(self, w, align=LEFT, width=RELATIVE_100, min_width=None, - left=0, right=0): - """ - :param w: a box, flow or fixed widget to pad on the left and/or right - this widget is stored as self.original_widget - :type w: Widget - - :param align: one of: ``'left'``, ``'center'``, ``'right'`` - (``'relative'``, *percentage* 0=left 100=right) - - :param width: one of: - - *given width* - integer number of columns for self.original_widget - - ``'pack'`` - try to pack self.original_widget to its ideal size - - (``'relative'``, *percentage of total width*) - make width depend on the container's width - - ``'clip'`` - to enable clipping mode for a fixed widget - - :param min_width: the minimum number of columns for - self.original_widget or ``None`` - :type min_width: int - - :param left: a fixed number of columns to pad on the left - :type left: int - - :param right: a fixed number of columns to pad on the right - :type right: int - - Clipping Mode: (width= ``'clip'``) - In clipping mode this padding widget will behave as a flow - widget and self.original_widget will be treated as a fixed - widget. self.original_widget will will be clipped to fit - the available number of columns. For example if align is - ``'left'`` then self.original_widget may be clipped on the right. - - >>> size = (7,) - >>> def pr(w): - ... for t in w.render(size).text: - ... print "|%s|" % (t.decode('ascii'),) - >>> pr(Padding(Text(u"Head"), ('relative', 20), 'pack')) - | Head | - >>> pr(Padding(Divider(u"-"), left=2, right=1)) - | ---- | - >>> pr(Padding(Divider(u"*"), 'center', 3)) - | *** | - >>> p=Padding(Text(u"1234"), 'left', 2, None, 1, 1) - >>> p - left=1 right=1 width=2> - >>> pr(p) # align against left - | 12 | - | 34 | - >>> p.align = 'right' - >>> pr(p) # align against right - | 12 | - | 34 | - >>> pr(Padding(Text(u"hi\\nthere"), 'right', 'pack')) # pack text first - | hi | - | there| - """ - self.__super.__init__(w) - - # convert obsolete parameters 'fixed left' and 'fixed right': - if type(align) == tuple and align[0] in ('fixed left', - 'fixed right'): - if align[0]=='fixed left': - left = align[1] - align = LEFT - else: - right = align[1] - align = RIGHT - if type(width) == tuple and width[0] in ('fixed left', - 'fixed right'): - if width[0]=='fixed left': - left = width[1] - else: - right = width[1] - width = RELATIVE_100 - - # convert old clipping mode width=None to width='clip' - if width is None: - width = CLIP - - self.left = left - self.right = right - self._align_type, self._align_amount = normalize_align(align, - PaddingError) - self._width_type, self._width_amount = normalize_width(width, - PaddingError) - self.min_width = min_width - - def sizing(self): - if self._width_type == CLIP: - return set([FLOW]) - return self.original_widget.sizing() - - def _repr_attrs(self): - attrs = dict(self.__super._repr_attrs(), - align=self.align, - width=self.width, - left=self.left, - right=self.right, - min_width=self.min_width) - return remove_defaults(attrs, Padding.__init__) - - def _get_align(self): - """ - Return the padding alignment setting. - """ - return simplify_align(self._align_type, self._align_amount) - def _set_align(self, align): - """ - Set the padding alignment. - """ - self._align_type, self._align_amount = normalize_align(align, - PaddingError) - self._invalidate() - align = property(_get_align, _set_align) - - def _get_width(self): - """ - Return the padding width. - """ - return simplify_width(self._width_type, self._width_amount) - def _set_width(self, width): - """ - Set the padding width. - """ - self._width_type, self._width_amount = normalize_width(width, - PaddingError) - self._invalidate() - width = property(_get_width, _set_width) - - def render(self, size, focus=False): - left, right = self.padding_values(size, focus) - - maxcol = size[0] - maxcol -= left+right - - if self._width_type == CLIP: - canv = self._original_widget.render((), focus) - else: - canv = self._original_widget.render((maxcol,)+size[1:], focus) - if canv.cols() == 0: - canv = SolidCanvas(' ', size[0], canv.rows()) - canv = CompositeCanvas(canv) - canv.set_depends([self._original_widget]) - return canv - canv = CompositeCanvas(canv) - canv.set_depends([self._original_widget]) - if left != 0 or right != 0: - canv.pad_trim_left_right(left, right) - - return canv - - def padding_values(self, size, focus): - """Return the number of columns to pad on the left and right. - - Override this method to define custom padding behaviour.""" - maxcol = size[0] - if self._width_type == CLIP: - width, ignore = self._original_widget.pack((), focus=focus) - return calculate_left_right_padding(maxcol, - self._align_type, self._align_amount, - CLIP, width, None, self.left, self.right) - if self._width_type == PACK: - maxwidth = max(maxcol - self.left - self.right, - self.min_width or 0) - (width, ignore) = self._original_widget.pack((maxwidth,), - focus=focus) - return calculate_left_right_padding(maxcol, - self._align_type, self._align_amount, - GIVEN, width, self.min_width, - self.left, self.right) - return calculate_left_right_padding(maxcol, - self._align_type, self._align_amount, - self._width_type, self._width_amount, - self.min_width, self.left, self.right) - - def rows(self, size, focus=False): - """Return the rows needed for self.original_widget.""" - (maxcol,) = size - left, right = self.padding_values(size, focus) - if self._width_type == PACK: - pcols, prows = self._original_widget.pack((maxcol-left-right,), - focus) - return prows - if self._width_type == CLIP: - fcols, frows = self._original_widget.pack((), focus) - return frows - return self._original_widget.rows((maxcol-left-right,), focus=focus) - - def keypress(self, size, key): - """Pass keypress to self._original_widget.""" - maxcol = size[0] - left, right = self.padding_values(size, True) - maxvals = (maxcol-left-right,)+size[1:] - return self._original_widget.keypress(maxvals, key) - - def get_cursor_coords(self,size): - """Return the (x,y) coordinates of cursor within self._original_widget.""" - if not hasattr(self._original_widget,'get_cursor_coords'): - return None - left, right = self.padding_values(size, True) - maxcol = size[0] - maxvals = (maxcol-left-right,)+size[1:] - if maxvals[0] == 0: - return None - coords = self._original_widget.get_cursor_coords(maxvals) - if coords is None: - return None - x, y = coords - return x+left, y - - def move_cursor_to_coords(self, size, x, y): - """Set the cursor position with (x,y) coordinates of self._original_widget. - - Returns True if move succeeded, False otherwise. - """ - if not hasattr(self._original_widget,'move_cursor_to_coords'): - return True - left, right = self.padding_values(size, True) - maxcol = size[0] - maxvals = (maxcol-left-right,)+size[1:] - if type(x)==int: - if x < left: - x = left - elif x >= maxcol-right: - x = maxcol-right-1 - x -= left - return self._original_widget.move_cursor_to_coords(maxvals, x, y) - - def mouse_event(self, size, event, button, x, y, focus): - """Send mouse event if position is within self._original_widget.""" - if not hasattr(self._original_widget,'mouse_event'): - return False - left, right = self.padding_values(size, focus) - maxcol = size[0] - if x < left or x >= maxcol-right: - return False - maxvals = (maxcol-left-right,)+size[1:] - return self._original_widget.mouse_event(maxvals, event, button, x-left, y, - focus) - - - def get_pref_col(self, size): - """Return the preferred column from self._original_widget, or None.""" - if not hasattr(self._original_widget,'get_pref_col'): - return None - left, right = self.padding_values(size, True) - maxcol = size[0] - maxvals = (maxcol-left-right,)+size[1:] - x = self._original_widget.get_pref_col(maxvals) - if type(x) == int: - return x+left - return x - - -class FillerError(Exception): - pass - -class Filler(WidgetDecoration): - def __init__(self, body, valign=MIDDLE, height=PACK, min_height=None, - top=0, bottom=0): - """ - :param body: a flow widget or box widget to be filled around (stored - as self.original_widget) - :type body: Widget - - :param valign: one of: - ``'top'``, ``'middle'``, ``'bottom'``, - (``'relative'``, *percentage* 0=top 100=bottom) - - :param height: one of: - - ``'pack'`` - if body is a flow widget - - *given height* - integer number of rows for self.original_widget - - (``'relative'``, *percentage of total height*) - make height depend on container's height - - :param min_height: one of: - - ``None`` - if no minimum or if body is a flow widget - - *minimum height* - integer number of rows for the widget when height not fixed - - :param top: a fixed number of rows to fill at the top - :type top: int - :param bottom: a fixed number of rows to fill at the bottom - :type bottom: int - - If body is a flow widget then height must be ``'flow'`` and - *min_height* will be ignored. - - Filler widgets will try to satisfy height argument first by - reducing the valign amount when necessary. If height still - cannot be satisfied it will also be reduced. - """ - self.__super.__init__(body) - - # convert old parameters to the new top/bottom values - if isinstance(height, tuple): - if height[0] == 'fixed top': - if not isinstance(valign, tuple) or valign[0] != 'fixed bottom': - raise FillerError("fixed bottom height may only be used " - "with fixed top valign") - top = height[1] - height = RELATIVE_100 - elif height[0] == 'fixed bottom': - if not isinstance(valign, tuple) or valign[0] != 'fixed top': - raise FillerError("fixed top height may only be used " - "with fixed bottom valign") - bottom = height[1] - height = 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 - - # convert old flow mode parameter height=None to height='flow' - if height is None or height == FLOW: - height = PACK - - self.top = top - self.bottom = bottom - self.valign_type, self.valign_amount = normalize_valign(valign, - FillerError) - self.height_type, self.height_amount = normalize_height(height, - FillerError) - - if self.height_type not in (GIVEN, PACK): - self.min_height = min_height - else: - self.min_height = None - - def sizing(self): - return set([BOX]) # always a box widget - - def _repr_attrs(self): - attrs = dict(self.__super._repr_attrs(), - valign=simplify_valign(self.valign_type, self.valign_amount), - height=simplify_height(self.height_type, self.height_amount), - top=self.top, - bottom=self.bottom, - min_height=self.min_height) - return remove_defaults(attrs, Filler.__init__) - - # backwards compatibility, widget used to be stored as body - get_body = WidgetDecoration._get_original_widget - set_body = WidgetDecoration._set_original_widget - body = property(get_body, set_body) - - def selectable(self): - """Return selectable from body.""" - return self._original_widget.selectable() - - def filler_values(self, size, focus): - """ - Return the number of rows to pad on the top and bottom. - - Override this method to define custom padding behaviour. - """ - (maxcol, maxrow) = size - - if self.height_type == PACK: - height = self._original_widget.rows((maxcol,),focus=focus) - return calculate_top_bottom_filler(maxrow, - self.valign_type, self.valign_amount, - GIVEN, height, - None, self.top, self.bottom) - - return calculate_top_bottom_filler(maxrow, - self.valign_type, self.valign_amount, - self.height_type, self.height_amount, - self.min_height, self.top, self.bottom) - - - def render(self, size, focus=False): - """Render self.original_widget with space above and/or below.""" - (maxcol, maxrow) = size - top, bottom = self.filler_values(size, focus) - - if self.height_type == PACK: - canv = self._original_widget.render((maxcol,), focus) - else: - canv = self._original_widget.render((maxcol,maxrow-top-bottom),focus) - canv = CompositeCanvas(canv) - - if maxrow and canv.rows() > maxrow and canv.cursor is not None: - cx, cy = canv.cursor - if cy >= maxrow: - canv.trim(cy-maxrow+1,maxrow-top-bottom) - if canv.rows() > maxrow: - canv.trim(0, maxrow) - return canv - canv.pad_trim_top_bottom(top, bottom) - return canv - - - def keypress(self, size, key): - """Pass keypress to self.original_widget.""" - (maxcol, maxrow) = size - if self.height_type == PACK: - return self._original_widget.keypress((maxcol,), key) - - top, bottom = self.filler_values((maxcol,maxrow), True) - return self._original_widget.keypress((maxcol,maxrow-top-bottom), key) - - def get_cursor_coords(self, size): - """Return cursor coords from self.original_widget if any.""" - (maxcol, maxrow) = size - if not hasattr(self._original_widget, 'get_cursor_coords'): - return None - - top, bottom = self.filler_values(size, True) - if self.height_type == PACK: - coords = self._original_widget.get_cursor_coords((maxcol,)) - else: - coords = self._original_widget.get_cursor_coords( - (maxcol,maxrow-top-bottom)) - if not coords: - return None - x, y = coords - if y >= maxrow: - y = maxrow-1 - return x, y+top - - def get_pref_col(self, size): - """Return pref_col from self.original_widget if any.""" - (maxcol, maxrow) = size - if not hasattr(self._original_widget, 'get_pref_col'): - return None - - if self.height_type == PACK: - x = self._original_widget.get_pref_col((maxcol,)) - else: - top, bottom = self.filler_values(size, True) - x = self._original_widget.get_pref_col( - (maxcol, maxrow-top-bottom)) - - return x - - def move_cursor_to_coords(self, size, col, row): - """Pass to self.original_widget.""" - (maxcol, maxrow) = size - if not hasattr(self._original_widget, 'move_cursor_to_coords'): - return True - - top, bottom = self.filler_values(size, True) - if row < top or row >= maxcol-bottom: - return False - - if self.height_type == PACK: - return self._original_widget.move_cursor_to_coords((maxcol,), - col, row-top) - return self._original_widget.move_cursor_to_coords( - (maxcol, maxrow-top-bottom), col, row-top) - - def mouse_event(self, size, event, button, col, row, focus): - """Pass to self.original_widget.""" - (maxcol, maxrow) = size - if not hasattr(self._original_widget, 'mouse_event'): - return False - - top, bottom = self.filler_values(size, True) - if row < top or row >= maxrow-bottom: - return False - - if self.height_type == PACK: - return self._original_widget.mouse_event((maxcol,), - event, button, col, row-top, focus) - return self._original_widget.mouse_event((maxcol, maxrow-top-bottom), - event, button,col, row-top, focus) - -class WidgetDisable(WidgetDecoration): - """ - A decoration widget that disables interaction with the widget it - wraps. This widget always passes focus=False to the wrapped widget, - even if it somehow does become the focus. - """ - no_cache = ["rows"] - ignore_focus = True - - def selectable(self): - return False - def rows(self, size, focus=False): - return self._original_widget.rows(size, False) - def sizing(self): - return self._original_widget.sizing() - def pack(self, size, focus=False): - return self._original_widget.pack(size, False) - def render(self, size, focus=False): - canv = self._original_widget.render(size, False) - return CompositeCanvas(canv) - -def normalize_align(align, err): - """ - Split align into (align_type, align_amount). Raise exception err - if align doesn't match a valid alignment. - """ - if align in (LEFT, CENTER, RIGHT): - return (align, None) - elif type(align) == tuple and len(align) == 2 and align[0] == RELATIVE: - return align - raise err("align value %r is not one of 'left', 'center', " - "'right', ('relative', percentage 0=left 100=right)" - % (align,)) - -def simplify_align(align_type, align_amount): - """ - Recombine (align_type, align_amount) into an align value. - Inverse of normalize_align. - """ - if align_type == RELATIVE: - return (align_type, align_amount) - return align_type - -def normalize_width(width, err): - """ - Split width into (width_type, width_amount). Raise exception err - if width doesn't match a valid alignment. - """ - if width in (CLIP, PACK): - return (width, None) - elif type(width) == int: - return (GIVEN, width) - elif type(width) == tuple and len(width) == 2 and width[0] == RELATIVE: - return width - raise err("width value %r is not one of fixed number of columns, " - "'pack', ('relative', percentage of total width), 'clip'" - % (width,)) - -def simplify_width(width_type, width_amount): - """ - Recombine (width_type, width_amount) into an width value. - Inverse of normalize_width. - """ - if width_type in (CLIP, PACK): - return width_type - elif width_type == GIVEN: - return width_amount - return (width_type, width_amount) - -def normalize_valign(valign, err): - """ - Split align into (valign_type, valign_amount). Raise exception err - if align doesn't match a valid alignment. - """ - if valign in (TOP, MIDDLE, BOTTOM): - return (valign, None) - elif (isinstance(valign, tuple) and len(valign) == 2 and - valign[0] == RELATIVE): - return valign - raise err("valign value %r is not one of 'top', 'middle', " - "'bottom', ('relative', percentage 0=left 100=right)" - % (valign,)) - -def simplify_valign(valign_type, valign_amount): - """ - Recombine (valign_type, valign_amount) into an valign value. - Inverse of normalize_valign. - """ - if valign_type == RELATIVE: - return (valign_type, valign_amount) - return valign_type - -def normalize_height(height, err): - """ - Split height into (height_type, height_amount). Raise exception err - if height isn't valid. - """ - if height in (FLOW, PACK): - return (height, None) - elif (isinstance(height, tuple) and len(height) == 2 and - height[0] == RELATIVE): - return height - elif isinstance(height, int): - return (GIVEN, height) - raise err("height value %r is not one of fixed number of columns, " - "'pack', ('relative', percentage of total height)" - % (height,)) - -def simplify_height(height_type, height_amount): - """ - Recombine (height_type, height_amount) into an height value. - Inverse of normalize_height. - """ - if height_type in (FLOW, PACK): - return height_type - elif height_type == GIVEN: - return height_amount - return (height_type, height_amount) - - -def calculate_top_bottom_filler(maxrow, valign_type, valign_amount, height_type, - height_amount, min_height, top, bottom): - """ - Return the amount of filler (or clipping) on the top and - bottom part of maxrow rows to satisfy the following: - - valign_type -- 'top', 'middle', 'bottom', 'relative' - valign_amount -- a percentage when align_type=='relative' - height_type -- 'given', 'relative', 'clip' - height_amount -- a percentage when width_type=='relative' - otherwise equal to the height of the widget - min_height -- a desired minimum width for the widget or None - top -- a fixed number of rows to fill on the top - bottom -- a fixed number of rows to fill on the bottom - - >>> ctbf = calculate_top_bottom_filler - >>> ctbf(15, 'top', 0, 'given', 10, None, 2, 0) - (2, 3) - >>> ctbf(15, 'relative', 0, 'given', 10, None, 2, 0) - (2, 3) - >>> ctbf(15, 'relative', 100, 'given', 10, None, 2, 0) - (5, 0) - >>> ctbf(15, 'middle', 0, 'given', 4, None, 2, 0) - (6, 5) - >>> ctbf(15, 'middle', 0, 'given', 18, None, 2, 0) - (0, 0) - >>> ctbf(20, 'top', 0, 'relative', 60, None, 0, 0) - (0, 8) - >>> ctbf(20, 'relative', 30, 'relative', 60, None, 0, 0) - (2, 6) - >>> ctbf(20, 'relative', 30, 'relative', 60, 14, 0, 0) - (2, 4) - """ - if height_type == RELATIVE: - maxheight = max(maxrow - top - bottom, 0) - height = int_scale(height_amount, 101, maxheight + 1) - if min_height is not None: - height = max(height, min_height) - else: - height = height_amount - - standard_alignments = {TOP:0, MIDDLE:50, BOTTOM:100} - valign = standard_alignments.get(valign_type, valign_amount) - - # add the remainder of top/bottom to the filler - filler = maxrow - height - top - bottom - bottom += int_scale(100 - valign, 101, filler + 1) - top = maxrow - height - bottom - - # reduce filler if we are clipping an edge - if bottom < 0 < top: - shift = min(top, -bottom) - top -= shift - bottom += shift - elif top < 0 < bottom: - shift = min(bottom, -top) - bottom -= shift - top += shift - - # no negative values for filler at the moment - top = max(top, 0) - bottom = max(bottom, 0) - - return top, bottom - - -def calculate_left_right_padding(maxcol, align_type, align_amount, - width_type, width_amount, min_width, left, right): - """ - Return the amount of padding (or clipping) on the left and - right part of maxcol columns to satisfy the following: - - align_type -- 'left', 'center', 'right', 'relative' - align_amount -- a percentage when align_type=='relative' - width_type -- 'fixed', 'relative', 'clip' - width_amount -- a percentage when width_type=='relative' - otherwise equal to the width of the widget - min_width -- a desired minimum width for the widget or None - left -- a fixed number of columns to pad on the left - right -- a fixed number of columns to pad on the right - - >>> clrp = calculate_left_right_padding - >>> clrp(15, 'left', 0, 'given', 10, None, 2, 0) - (2, 3) - >>> clrp(15, 'relative', 0, 'given', 10, None, 2, 0) - (2, 3) - >>> clrp(15, 'relative', 100, 'given', 10, None, 2, 0) - (5, 0) - >>> clrp(15, 'center', 0, 'given', 4, None, 2, 0) - (6, 5) - >>> clrp(15, 'left', 0, 'clip', 18, None, 0, 0) - (0, -3) - >>> clrp(15, 'right', 0, 'clip', 18, None, 0, -1) - (-2, -1) - >>> clrp(15, 'center', 0, 'given', 18, None, 2, 0) - (0, 0) - >>> clrp(20, 'left', 0, 'relative', 60, None, 0, 0) - (0, 8) - >>> clrp(20, 'relative', 30, 'relative', 60, None, 0, 0) - (2, 6) - >>> clrp(20, 'relative', 30, 'relative', 60, 14, 0, 0) - (2, 4) - """ - if width_type == RELATIVE: - maxwidth = max(maxcol - left - right, 0) - width = int_scale(width_amount, 101, maxwidth + 1) - if min_width is not None: - width = max(width, min_width) - else: - width = width_amount - - standard_alignments = {LEFT:0, CENTER:50, RIGHT:100} - align = standard_alignments.get(align_type, align_amount) - - # add the remainder of left/right the padding - padding = maxcol - width - left - right - right += int_scale(100 - align, 101, padding + 1) - left = maxcol - width - right - - # reduce padding if we are clipping an edge - if right < 0 and left > 0: - shift = min(left, -right) - left -= shift - right += shift - elif left < 0 and right > 0: - shift = min(right, -left) - right -= shift - left += shift - - # only clip if width_type == 'clip' - if width_type != CLIP and (left < 0 or right < 0): - left = max(left, 0) - right = max(right, 0) - - return left, right - - - -def _test(): - import doctest - doctest.testmod() - -if __name__=='__main__': - _test() diff --git a/urwid/display_common.py b/urwid/display_common.py deleted file mode 100755 index 7ff4eef..0000000 --- a/urwid/display_common.py +++ /dev/null @@ -1,894 +0,0 @@ -#!/usr/bin/python -# Urwid common display code -# 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/ - -import os -import sys - -try: - import termios -except ImportError: - pass # windows - -from urwid.util import StoppingContext, int_scale -from urwid import signals -from urwid.compat import B, bytes3 - -# for replacing unprintable bytes with '?' -UNPRINTABLE_TRANS_TABLE = B("?") * 32 + bytes3(range(32,256)) - - -# signals sent by BaseScreen -UPDATE_PALETTE_ENTRY = "update palette entry" -INPUT_DESCRIPTORS_CHANGED = "input descriptors changed" - - -# AttrSpec internal values -_BASIC_START = 0 # first index of basic color aliases -_CUBE_START = 16 # first index of color cube -_CUBE_SIZE_256 = 6 # one side of the color cube -_GRAY_SIZE_256 = 24 -_GRAY_START_256 = _CUBE_SIZE_256 ** 3 + _CUBE_START -_CUBE_WHITE_256 = _GRAY_START_256 -1 -_CUBE_SIZE_88 = 4 -_GRAY_SIZE_88 = 8 -_GRAY_START_88 = _CUBE_SIZE_88 ** 3 + _CUBE_START -_CUBE_WHITE_88 = _GRAY_START_88 -1 -_CUBE_BLACK = _CUBE_START - -# values copied from xterm 256colres.h: -_CUBE_STEPS_256 = [0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff] -_GRAY_STEPS_256 = [0x08, 0x12, 0x1c, 0x26, 0x30, 0x3a, 0x44, 0x4e, 0x58, 0x62, - 0x6c, 0x76, 0x80, 0x84, 0x94, 0x9e, 0xa8, 0xb2, 0xbc, 0xc6, 0xd0, - 0xda, 0xe4, 0xee] -# values copied from xterm 88colres.h: -_CUBE_STEPS_88 = [0x00, 0x8b, 0xcd, 0xff] -_GRAY_STEPS_88 = [0x2e, 0x5c, 0x73, 0x8b, 0xa2, 0xb9, 0xd0, 0xe7] -# values copied from X11/rgb.txt and XTerm-col.ad: -_BASIC_COLOR_VALUES = [(0,0,0), (205, 0, 0), (0, 205, 0), (205, 205, 0), - (0, 0, 238), (205, 0, 205), (0, 205, 205), (229, 229, 229), - (127, 127, 127), (255, 0, 0), (0, 255, 0), (255, 255, 0), - (0x5c, 0x5c, 0xff), (255, 0, 255), (0, 255, 255), (255, 255, 255)] - -_COLOR_VALUES_256 = (_BASIC_COLOR_VALUES + - [(r, g, b) for r in _CUBE_STEPS_256 for g in _CUBE_STEPS_256 - for b in _CUBE_STEPS_256] + - [(gr, gr, gr) for gr in _GRAY_STEPS_256]) -_COLOR_VALUES_88 = (_BASIC_COLOR_VALUES + - [(r, g, b) for r in _CUBE_STEPS_88 for g in _CUBE_STEPS_88 - for b in _CUBE_STEPS_88] + - [(gr, gr, gr) for gr in _GRAY_STEPS_88]) - -assert len(_COLOR_VALUES_256) == 256 -assert len(_COLOR_VALUES_88) == 88 - -_FG_COLOR_MASK = 0x000000ff -_BG_COLOR_MASK = 0x0000ff00 -_FG_BASIC_COLOR = 0x00010000 -_FG_HIGH_COLOR = 0x00020000 -_BG_BASIC_COLOR = 0x00040000 -_BG_HIGH_COLOR = 0x00080000 -_BG_SHIFT = 8 -_HIGH_88_COLOR = 0x00100000 -_STANDOUT = 0x02000000 -_UNDERLINE = 0x04000000 -_BOLD = 0x08000000 -_BLINK = 0x10000000 -_FG_MASK = (_FG_COLOR_MASK | _FG_BASIC_COLOR | _FG_HIGH_COLOR | - _STANDOUT | _UNDERLINE | _BLINK | _BOLD) -_BG_MASK = _BG_COLOR_MASK | _BG_BASIC_COLOR | _BG_HIGH_COLOR - -DEFAULT = 'default' -BLACK = 'black' -DARK_RED = 'dark red' -DARK_GREEN = 'dark green' -BROWN = 'brown' -DARK_BLUE = 'dark blue' -DARK_MAGENTA = 'dark magenta' -DARK_CYAN = 'dark cyan' -LIGHT_GRAY = 'light gray' -DARK_GRAY = 'dark gray' -LIGHT_RED = 'light red' -LIGHT_GREEN = 'light green' -YELLOW = 'yellow' -LIGHT_BLUE = 'light blue' -LIGHT_MAGENTA = 'light magenta' -LIGHT_CYAN = 'light cyan' -WHITE = 'white' - -_BASIC_COLORS = [ - 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, -] - -_ATTRIBUTES = { - 'bold': _BOLD, - 'underline': _UNDERLINE, - 'blink': _BLINK, - 'standout': _STANDOUT, -} - -def _value_lookup_table(values, size): - """ - Generate a lookup table for finding the closest item in values. - Lookup returns (index into values)+1 - - values -- list of values in ascending order, all < size - size -- size of lookup table and maximum value - - >>> _value_lookup_table([0, 7, 9], 10) - [0, 0, 0, 0, 1, 1, 1, 1, 2, 2] - """ - - middle_values = [0] + [(values[i] + values[i + 1] + 1) // 2 - for i in range(len(values) - 1)] + [size] - lookup_table = [] - for i in range(len(middle_values)-1): - count = middle_values[i + 1] - middle_values[i] - lookup_table.extend([i] * count) - return lookup_table - -_CUBE_256_LOOKUP = _value_lookup_table(_CUBE_STEPS_256, 256) -_GRAY_256_LOOKUP = _value_lookup_table([0] + _GRAY_STEPS_256 + [0xff], 256) -_CUBE_88_LOOKUP = _value_lookup_table(_CUBE_STEPS_88, 256) -_GRAY_88_LOOKUP = _value_lookup_table([0] + _GRAY_STEPS_88 + [0xff], 256) - -# convert steps to values that will be used by string versions of the colors -# 1 hex digit for rgb and 0..100 for grayscale -_CUBE_STEPS_256_16 = [int_scale(n, 0x100, 0x10) for n in _CUBE_STEPS_256] -_GRAY_STEPS_256_101 = [int_scale(n, 0x100, 101) for n in _GRAY_STEPS_256] -_CUBE_STEPS_88_16 = [int_scale(n, 0x100, 0x10) for n in _CUBE_STEPS_88] -_GRAY_STEPS_88_101 = [int_scale(n, 0x100, 101) for n in _GRAY_STEPS_88] - -# create lookup tables for 1 hex digit rgb and 0..100 for grayscale values -_CUBE_256_LOOKUP_16 = [_CUBE_256_LOOKUP[int_scale(n, 16, 0x100)] - for n in range(16)] -_GRAY_256_LOOKUP_101 = [_GRAY_256_LOOKUP[int_scale(n, 101, 0x100)] - for n in range(101)] -_CUBE_88_LOOKUP_16 = [_CUBE_88_LOOKUP[int_scale(n, 16, 0x100)] - for n in range(16)] -_GRAY_88_LOOKUP_101 = [_GRAY_88_LOOKUP[int_scale(n, 101, 0x100)] - for n in range(101)] - - -# The functions _gray_num_256() and _gray_num_88() do not include the gray -# values from the color cube so that the gray steps are an even width. -# The color cube grays are available by using the rgb functions. Pure -# white and black are taken from the color cube, since the gray range does -# not include them, and the basic colors are more likely to have been -# customized by an end-user. - - -def _gray_num_256(gnum): - """Return ths color number for gray number gnum. - - Color cube black and white are returned for 0 and 25 respectively - since those values aren't included in the gray scale. - - """ - # grays start from index 1 - gnum -= 1 - - if gnum < 0: - return _CUBE_BLACK - if gnum >= _GRAY_SIZE_256: - return _CUBE_WHITE_256 - return _GRAY_START_256 + gnum - - -def _gray_num_88(gnum): - """Return ths color number for gray number gnum. - - Color cube black and white are returned for 0 and 9 respectively - since those values aren't included in the gray scale. - - """ - # gnums start from index 1 - gnum -= 1 - - if gnum < 0: - return _CUBE_BLACK - if gnum >= _GRAY_SIZE_88: - return _CUBE_WHITE_88 - return _GRAY_START_88 + gnum - - -def _color_desc_256(num): - """ - Return a string description of color number num. - 0..15 -> 'h0'..'h15' basic colors (as high-colors) - 16..231 -> '#000'..'#fff' color cube colors - 232..255 -> 'g3'..'g93' grays - - >>> _color_desc_256(15) - 'h15' - >>> _color_desc_256(16) - '#000' - >>> _color_desc_256(17) - '#006' - >>> _color_desc_256(230) - '#ffd' - >>> _color_desc_256(233) - 'g7' - >>> _color_desc_256(234) - 'g11' - - """ - assert num >= 0 and num < 256, num - if num < _CUBE_START: - return 'h%d' % num - if num < _GRAY_START_256: - num -= _CUBE_START - b, num = num % _CUBE_SIZE_256, num // _CUBE_SIZE_256 - g, num = num % _CUBE_SIZE_256, num // _CUBE_SIZE_256 - r = num % _CUBE_SIZE_256 - return '#%x%x%x' % (_CUBE_STEPS_256_16[r], _CUBE_STEPS_256_16[g], - _CUBE_STEPS_256_16[b]) - return 'g%d' % _GRAY_STEPS_256_101[num - _GRAY_START_256] - -def _color_desc_88(num): - """ - Return a string description of color number num. - 0..15 -> 'h0'..'h15' basic colors (as high-colors) - 16..79 -> '#000'..'#fff' color cube colors - 80..87 -> 'g18'..'g90' grays - - >>> _color_desc_88(15) - 'h15' - >>> _color_desc_88(16) - '#000' - >>> _color_desc_88(17) - '#008' - >>> _color_desc_88(78) - '#ffc' - >>> _color_desc_88(81) - 'g36' - >>> _color_desc_88(82) - 'g45' - - """ - assert num > 0 and num < 88 - if num < _CUBE_START: - return 'h%d' % num - if num < _GRAY_START_88: - num -= _CUBE_START - b, num = num % _CUBE_SIZE_88, num // _CUBE_SIZE_88 - g, r= num % _CUBE_SIZE_88, num // _CUBE_SIZE_88 - return '#%x%x%x' % (_CUBE_STEPS_88_16[r], _CUBE_STEPS_88_16[g], - _CUBE_STEPS_88_16[b]) - return 'g%d' % _GRAY_STEPS_88_101[num - _GRAY_START_88] - -def _parse_color_256(desc): - """ - Return a color number for the description desc. - 'h0'..'h255' -> 0..255 actual color number - '#000'..'#fff' -> 16..231 color cube colors - 'g0'..'g100' -> 16, 232..255, 231 grays and color cube black/white - 'g#00'..'g#ff' -> 16, 232...255, 231 gray and color cube black/white - - Returns None if desc is invalid. - - >>> _parse_color_256('h142') - 142 - >>> _parse_color_256('#f00') - 196 - >>> _parse_color_256('g100') - 231 - >>> _parse_color_256('g#80') - 244 - """ - if len(desc) > 4: - # keep the length within reason before parsing - return None - try: - if desc.startswith('h'): - # high-color number - num = int(desc[1:], 10) - if num < 0 or num > 255: - return None - return num - - if desc.startswith('#') and len(desc) == 4: - # color-cube coordinates - rgb = int(desc[1:], 16) - if rgb < 0: - return None - b, rgb = rgb % 16, rgb // 16 - g, r = rgb % 16, rgb // 16 - # find the closest rgb values - r = _CUBE_256_LOOKUP_16[r] - g = _CUBE_256_LOOKUP_16[g] - b = _CUBE_256_LOOKUP_16[b] - return _CUBE_START + (r * _CUBE_SIZE_256 + g) * _CUBE_SIZE_256 + b - - # Only remaining possibility is gray value - if desc.startswith('g#'): - # hex value 00..ff - gray = int(desc[2:], 16) - if gray < 0 or gray > 255: - return None - gray = _GRAY_256_LOOKUP[gray] - elif desc.startswith('g'): - # decimal value 0..100 - gray = int(desc[1:], 10) - if gray < 0 or gray > 100: - return None - gray = _GRAY_256_LOOKUP_101[gray] - else: - return None - if gray == 0: - return _CUBE_BLACK - gray -= 1 - if gray == _GRAY_SIZE_256: - return _CUBE_WHITE_256 - return _GRAY_START_256 + gray - - except ValueError: - return None - -def _parse_color_88(desc): - """ - Return a color number for the description desc. - 'h0'..'h87' -> 0..87 actual color number - '#000'..'#fff' -> 16..79 color cube colors - 'g0'..'g100' -> 16, 80..87, 79 grays and color cube black/white - 'g#00'..'g#ff' -> 16, 80...87, 79 gray and color cube black/white - - Returns None if desc is invalid. - - >>> _parse_color_88('h142') - >>> _parse_color_88('h42') - 42 - >>> _parse_color_88('#f00') - 64 - >>> _parse_color_88('g100') - 79 - >>> _parse_color_88('g#80') - 83 - """ - if len(desc) > 4: - # keep the length within reason before parsing - return None - try: - if desc.startswith('h'): - # high-color number - num = int(desc[1:], 10) - if num < 0 or num > 87: - return None - return num - - if desc.startswith('#') and len(desc) == 4: - # color-cube coordinates - rgb = int(desc[1:], 16) - if rgb < 0: - return None - b, rgb = rgb % 16, rgb // 16 - g, r = rgb % 16, rgb // 16 - # find the closest rgb values - r = _CUBE_88_LOOKUP_16[r] - g = _CUBE_88_LOOKUP_16[g] - b = _CUBE_88_LOOKUP_16[b] - return _CUBE_START + (r * _CUBE_SIZE_88 + g) * _CUBE_SIZE_88 + b - - # Only remaining possibility is gray value - if desc.startswith('g#'): - # hex value 00..ff - gray = int(desc[2:], 16) - if gray < 0 or gray > 255: - return None - gray = _GRAY_88_LOOKUP[gray] - elif desc.startswith('g'): - # decimal value 0..100 - gray = int(desc[1:], 10) - if gray < 0 or gray > 100: - return None - gray = _GRAY_88_LOOKUP_101[gray] - else: - return None - if gray == 0: - return _CUBE_BLACK - gray -= 1 - if gray == _GRAY_SIZE_88: - return _CUBE_WHITE_88 - return _GRAY_START_88 + gray - - except ValueError: - return None - -class AttrSpecError(Exception): - pass - -class AttrSpec(object): - def __init__(self, fg, bg, colors=256): - """ - fg -- a string containing a comma-separated foreground color - and settings - - Color values: - 'default' (use the terminal's default foreground), - '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' - - High-color example values: - '#009' (0% red, 0% green, 60% red, like HTML colors) - '#fcc' (100% red, 80% green, 80% blue) - 'g40' (40% gray, decimal), 'g#cc' (80% gray, hex), - '#000', 'g0', 'g#00' (black), - '#fff', 'g100', 'g#ff' (white) - 'h8' (color number 8), 'h255' (color number 255) - - Setting: - 'bold', 'underline', 'blink', 'standout' - - Some terminals use 'bold' for bright colors. Most terminals - ignore the 'blink' setting. If the color is not given then - 'default' will be assumed. - - bg -- a string containing the background color - - Color values: - 'default' (use the terminal's default background), - 'black', 'dark red', 'dark green', 'brown', 'dark blue', - 'dark magenta', 'dark cyan', 'light gray' - - High-color exaples: - see fg examples above - - An empty string will be treated the same as 'default'. - - colors -- the maximum colors available for the specification - - Valid values include: 1, 16, 88 and 256. High-color - values are only usable with 88 or 256 colors. With - 1 color only the foreground settings may be used. - - >>> AttrSpec('dark red', 'light gray', 16) - AttrSpec('dark red', 'light gray') - >>> AttrSpec('yellow, underline, bold', 'dark blue') - AttrSpec('yellow,bold,underline', 'dark blue') - >>> AttrSpec('#ddb', '#004', 256) # closest colors will be found - AttrSpec('#dda', '#006') - >>> AttrSpec('#ddb', '#004', 88) - AttrSpec('#ccc', '#000', colors=88) - """ - if colors not in (1, 16, 88, 256): - raise AttrSpecError('invalid number of colors (%d).' % colors) - self._value = 0 | _HIGH_88_COLOR * (colors == 88) - self.foreground = fg - self.background = bg - if self.colors > colors: - raise AttrSpecError(('foreground/background (%s/%s) require ' + - 'more colors than have been specified (%d).') % - (repr(fg), repr(bg), colors)) - - foreground_basic = property(lambda s: s._value & _FG_BASIC_COLOR != 0) - foreground_high = property(lambda s: s._value & _FG_HIGH_COLOR != 0) - foreground_number = property(lambda s: s._value & _FG_COLOR_MASK) - background_basic = property(lambda s: s._value & _BG_BASIC_COLOR != 0) - background_high = property(lambda s: s._value & _BG_HIGH_COLOR != 0) - background_number = property(lambda s: (s._value & _BG_COLOR_MASK) - >> _BG_SHIFT) - bold = property(lambda s: s._value & _BOLD != 0) - underline = property(lambda s: s._value & _UNDERLINE != 0) - blink = property(lambda s: s._value & _BLINK != 0) - standout = property(lambda s: s._value & _STANDOUT != 0) - - def _colors(self): - """ - Return the maximum colors required for this object. - - Returns 256, 88, 16 or 1. - """ - if self._value & _HIGH_88_COLOR: - return 88 - if self._value & (_BG_HIGH_COLOR | _FG_HIGH_COLOR): - return 256 - if self._value & (_BG_BASIC_COLOR | _BG_BASIC_COLOR): - return 16 - return 1 - colors = property(_colors) - - def __repr__(self): - """ - Return an executable python representation of the AttrSpec - object. - """ - args = "%r, %r" % (self.foreground, self.background) - if self.colors == 88: - # 88-color mode is the only one that is handled differently - args = args + ", colors=88" - return "%s(%s)" % (self.__class__.__name__, args) - - def _foreground_color(self): - """Return only the color component of the foreground.""" - if not (self.foreground_basic or self.foreground_high): - return 'default' - if self.foreground_basic: - return _BASIC_COLORS[self.foreground_number] - if self.colors == 88: - return _color_desc_88(self.foreground_number) - return _color_desc_256(self.foreground_number) - - def _foreground(self): - return (self._foreground_color() + - ',bold' * self.bold + ',standout' * self.standout + - ',blink' * self.blink + ',underline' * self.underline) - - def _set_foreground(self, foreground): - color = None - flags = 0 - # handle comma-separated foreground - for part in foreground.split(','): - part = part.strip() - if part in _ATTRIBUTES: - # parse and store "settings"/attributes in flags - if flags & _ATTRIBUTES[part]: - raise AttrSpecError(("Setting %s specified more than" + - "once in foreground (%s)") % (repr(part), - repr(foreground))) - flags |= _ATTRIBUTES[part] - continue - # past this point we must be specifying a color - if part in ('', 'default'): - scolor = 0 - elif part in _BASIC_COLORS: - scolor = _BASIC_COLORS.index(part) - flags |= _FG_BASIC_COLOR - elif self._value & _HIGH_88_COLOR: - scolor = _parse_color_88(part) - flags |= _FG_HIGH_COLOR - else: - scolor = _parse_color_256(part) - flags |= _FG_HIGH_COLOR - # _parse_color_*() return None for unrecognised colors - if scolor is None: - raise AttrSpecError(("Unrecognised color specification %s " + - "in foreground (%s)") % (repr(part), repr(foreground))) - if color is not None: - raise AttrSpecError(("More than one color given for " + - "foreground (%s)") % (repr(foreground),)) - color = scolor - if color is None: - color = 0 - self._value = (self._value & ~_FG_MASK) | color | flags - - foreground = property(_foreground, _set_foreground) - - def _background(self): - """Return the background color.""" - if not (self.background_basic or self.background_high): - return 'default' - if self.background_basic: - return _BASIC_COLORS[self.background_number] - if self._value & _HIGH_88_COLOR: - return _color_desc_88(self.background_number) - return _color_desc_256(self.background_number) - - def _set_background(self, background): - flags = 0 - if background in ('', 'default'): - color = 0 - elif background in _BASIC_COLORS: - color = _BASIC_COLORS.index(background) - flags |= _BG_BASIC_COLOR - elif self._value & _HIGH_88_COLOR: - color = _parse_color_88(background) - flags |= _BG_HIGH_COLOR - else: - color = _parse_color_256(background) - flags |= _BG_HIGH_COLOR - if color is None: - raise AttrSpecError(("Unrecognised color specification " + - "in background (%s)") % (repr(background),)) - self._value = (self._value & ~_BG_MASK) | (color << _BG_SHIFT) | flags - - background = property(_background, _set_background) - - def get_rgb_values(self): - """ - Return (fg_red, fg_green, fg_blue, bg_red, bg_green, bg_blue) color - components. Each component is in the range 0-255. Values are taken - from the XTerm defaults and may not exactly match the user's terminal. - - If the foreground or background is 'default' then all their compenents - will be returned as None. - - >>> AttrSpec('yellow', '#ccf', colors=88).get_rgb_values() - (255, 255, 0, 205, 205, 255) - >>> AttrSpec('default', 'g92').get_rgb_values() - (None, None, None, 238, 238, 238) - """ - if not (self.foreground_basic or self.foreground_high): - vals = (None, None, None) - elif self.colors == 88: - assert self.foreground_number < 88, "Invalid AttrSpec _value" - vals = _COLOR_VALUES_88[self.foreground_number] - else: - vals = _COLOR_VALUES_256[self.foreground_number] - - if not (self.background_basic or self.background_high): - return vals + (None, None, None) - elif self.colors == 88: - assert self.background_number < 88, "Invalid AttrSpec _value" - return vals + _COLOR_VALUES_88[self.background_number] - else: - return vals + _COLOR_VALUES_256[self.background_number] - - def __eq__(self, other): - return isinstance(other, AttrSpec) and self._value == other._value - - def __ne__(self, other): - return not self == other - - __hash__ = object.__hash__ - - -class RealTerminal(object): - def __init__(self): - super(RealTerminal,self).__init__() - self._signal_keys_set = False - self._old_signal_keys = None - - def tty_signal_keys(self, intr=None, quit=None, start=None, - stop=None, susp=None, fileno=None): - """ - Read and/or set the tty's signal character settings. - This function returns the current settings as a tuple. - - Use the string 'undefined' to unmap keys from their signals. - The value None is used when no change is being made. - Setting signal keys is done using the integer ascii - code for the key, eg. 3 for CTRL+C. - - If this function is called after start() has been called - then the original settings will be restored when stop() - is called. - """ - if fileno is None: - fileno = sys.stdin.fileno() - if not os.isatty(fileno): - return - - tattr = termios.tcgetattr(fileno) - sattr = tattr[6] - skeys = (sattr[termios.VINTR], sattr[termios.VQUIT], - sattr[termios.VSTART], sattr[termios.VSTOP], - sattr[termios.VSUSP]) - - if intr == 'undefined': intr = 0 - if quit == 'undefined': quit = 0 - if start == 'undefined': start = 0 - if stop == 'undefined': stop = 0 - if susp == 'undefined': susp = 0 - - if intr is not None: tattr[6][termios.VINTR] = intr - if quit is not None: tattr[6][termios.VQUIT] = quit - if start is not None: tattr[6][termios.VSTART] = start - if stop is not None: tattr[6][termios.VSTOP] = stop - if susp is not None: tattr[6][termios.VSUSP] = susp - - if intr is not None or quit is not None or \ - start is not None or stop is not None or \ - susp is not None: - termios.tcsetattr(fileno, termios.TCSADRAIN, tattr) - self._signal_keys_set = True - - return skeys - - -class ScreenError(Exception): - pass - -class BaseScreen(object): - """ - Base class for Screen classes (raw_display.Screen, .. etc) - """ - __metaclass__ = signals.MetaSignals - signals = [UPDATE_PALETTE_ENTRY, INPUT_DESCRIPTORS_CHANGED] - - def __init__(self): - super(BaseScreen,self).__init__() - self._palette = {} - self._started = False - - started = property(lambda self: self._started) - - def start(self, *args, **kwargs): - """Set up the screen. If the screen has already been started, does - nothing. - - May be used as a context manager, in which case :meth:`stop` will - automatically be called at the end of the block: - - with screen.start(): - ... - - You shouldn't override this method in a subclass; instead, override - :meth:`_start`. - """ - if not self._started: - self._start(*args, **kwargs) - self._started = True - return StoppingContext(self) - - def _start(self): - pass - - def stop(self): - if self._started: - self._stop() - self._started = False - - def _stop(self): - pass - - def run_wrapper(self, fn, *args, **kwargs): - """Start the screen, call a function, then stop the screen. Extra - arguments are passed to `start`. - - Deprecated in favor of calling `start` as a context manager. - """ - with self.start(*args, **kwargs): - return fn() - - - def register_palette(self, palette): - """Register a set of palette entries. - - palette -- a list of (name, like_other_name) or - (name, foreground, background, mono, foreground_high, - background_high) tuples - - The (name, like_other_name) format will copy the settings - from the palette entry like_other_name, which must appear - before this tuple in the list. - - The mono and foreground/background_high values are - optional ie. the second tuple format may have 3, 4 or 6 - values. See register_palette_entry() for a description - of the tuple values. - """ - - for item in palette: - if len(item) in (3,4,6): - self.register_palette_entry(*item) - continue - if len(item) != 2: - raise ScreenError("Invalid register_palette entry: %s" % - repr(item)) - name, like_name = item - if like_name not in self._palette: - raise ScreenError("palette entry '%s' doesn't exist"%like_name) - self._palette[name] = self._palette[like_name] - - def register_palette_entry(self, name, foreground, background, - mono=None, foreground_high=None, background_high=None): - """Register a single palette entry. - - name -- new entry/attribute name - - foreground -- a string containing a comma-separated foreground - color and settings - - Color values: - 'default' (use the terminal's default foreground), - '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' - - Settings: - 'bold', 'underline', 'blink', 'standout' - - Some terminals use 'bold' for bright colors. Most terminals - ignore the 'blink' setting. If the color is not given then - 'default' will be assumed. - - background -- a string containing the background color - - Background color values: - 'default' (use the terminal's default background), - 'black', 'dark red', 'dark green', 'brown', 'dark blue', - 'dark magenta', 'dark cyan', 'light gray' - - mono -- a comma-separated string containing monochrome terminal - settings (see "Settings" above.) - - None = no terminal settings (same as 'default') - - foreground_high -- a string containing a comma-separated - foreground color and settings, standard foreground - colors (see "Color values" above) or high-colors may - be used - - High-color example values: - '#009' (0% red, 0% green, 60% red, like HTML colors) - '#fcc' (100% red, 80% green, 80% blue) - 'g40' (40% gray, decimal), 'g#cc' (80% gray, hex), - '#000', 'g0', 'g#00' (black), - '#fff', 'g100', 'g#ff' (white) - 'h8' (color number 8), 'h255' (color number 255) - - None = use foreground parameter value - - background_high -- a string containing the background color, - standard background colors (see "Background colors" above) - or high-colors (see "High-color example values" above) - may be used - - None = use background parameter value - """ - basic = AttrSpec(foreground, background, 16) - - if type(mono) == tuple: - # old style of specifying mono attributes was to put them - # in a tuple. convert to comma-separated string - mono = ",".join(mono) - if mono is None: - mono = DEFAULT - mono = AttrSpec(mono, DEFAULT, 1) - - if foreground_high is None: - foreground_high = foreground - if background_high is None: - background_high = background - high_256 = AttrSpec(foreground_high, background_high, 256) - - # 'hX' where X > 15 are different in 88/256 color, use - # basic colors for 88-color mode if high colors are specified - # in this way (also avoids crash when X > 87) - def large_h(desc): - if not desc.startswith('h'): - return False - if ',' in desc: - desc = desc.split(',',1)[0] - num = int(desc[1:], 10) - return num > 15 - if large_h(foreground_high) or large_h(background_high): - high_88 = basic - else: - high_88 = AttrSpec(foreground_high, background_high, 88) - - signals.emit_signal(self, UPDATE_PALETTE_ENTRY, - name, basic, mono, high_88, high_256) - self._palette[name] = (basic, mono, high_88, high_256) - - -def _test(): - import doctest - doctest.testmod() - -if __name__=='__main__': - _test() diff --git a/urwid/escape.py b/urwid/escape.py deleted file mode 100644 index 683466c..0000000 --- a/urwid/escape.py +++ /dev/null @@ -1,441 +0,0 @@ -#!/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/ - -""" -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" diff --git a/urwid/font.py b/urwid/font.py deleted file mode 100755 index bf0c2b1..0000000 --- a/urwid/font.py +++ /dev/null @@ -1,450 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# -# Urwid BigText fonts -# Copyright (C) 2004-2006 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 urwid.escape import SAFE_ASCII_DEC_SPECIAL_RE -from urwid.util import apply_target_encoding, str_util -from urwid.canvas import TextCanvas - - -def separate_glyphs(gdata, height): - """return (dictionary of glyphs, utf8 required)""" - gl = gdata.split("\n") - del gl[0] - del gl[-1] - for g in gl: - assert "\t" not in g - assert len(gl) == height+1, repr(gdata) - key_line = gl[0] - del gl[0] - c = None # current character - key_index = 0 # index into character key line - end_col = 0 # column position at end of glyph - start_col = 0 # column position at start of glyph - jl = [0]*height # indexes into lines of gdata (gl) - dout = {} - utf8_required = False - while True: - if c is None: - if key_index >= len(key_line): - break - c = key_line[key_index] - if key_index < len(key_line) and key_line[key_index] == c: - end_col += str_util.get_width(ord(c)) - key_index += 1 - continue - out = [] - for k in range(height): - l = gl[k] - j = jl[k] - y = 0 - fill = 0 - while y < end_col - start_col: - if j >= len(l): - fill = end_col - start_col - y - break - y += str_util.get_width(ord(l[j])) - j += 1 - assert y + fill == end_col - start_col, \ - repr((y, fill, end_col)) - - segment = l[jl[k]:j] - if not SAFE_ASCII_DEC_SPECIAL_RE.match(segment): - utf8_required = True - - out.append(segment + " " * fill) - jl[k] = j - - start_col = end_col - dout[c] = (y + fill, out) - c = None - return dout, utf8_required - -_all_fonts = [] -def get_all_fonts(): - """ - Return a list of (font name, font class) tuples. - """ - return _all_fonts[:] - -def add_font(name, cls): - _all_fonts.append((name, cls)) - - -class Font(object): - def __init__(self): - assert self.height - assert self.data - self.char = {} - self.canvas = {} - self.utf8_required = False - for gdata in self.data: - self.add_glyphs(gdata) - - - def add_glyphs(self, gdata): - d, utf8_required = separate_glyphs(gdata, self.height) - self.char.update(d) - self.utf8_required |= utf8_required - - def characters(self): - l = self.char.keys() - l.sort() - return "".join(l) - - def char_width(self, c): - if c in self.char: - return self.char[c][0] - return 0 - - def char_data(self, c): - return self.char[c][1] - - def render(self, c): - if c in self.canvas: - return self.canvas[c] - width, l = self.char[c] - tl = [] - csl = [] - for d in l: - t, cs = apply_target_encoding(d) - tl.append(t) - csl.append(cs) - canv = TextCanvas(tl, None, csl, maxcol=width, - check_width=False) - self.canvas[c] = canv - return canv - - - -#safe_palette = u"┘┐┌└┼─├┤┴┬│" -#more_palette = u"═║╒╓╔╕╖╗╘╙╚╛╜╝╞╟╠╡╢╣╤╥╦╧╨╩╪╫╬○" -#block_palette = u"▄#█#▀#▌#▐#▖#▗#▘#▙#▚#▛#▜#▝#▞#▟" - - -class Thin3x3Font(Font): - height = 3 - data = [u""" -000111222333444555666777888999 ! -┌─┐ ┐ ┌─┐┌─┐ ┐┌─ ┌─ ┌─┐┌─┐┌─┐ │ -│ │ │ ┌─┘ ─┤└─┼└─┐├─┐ ┼├─┤└─┤ │ -└─┘ ┴ └─ └─┘ ┴ ─┘└─┘ ┴└─┘ ─┘ . -""", ur""" -"###$$$%%%'*++,--.///:;==???[[\\\]]^__` -" ┼┼┌┼┐O /' /.. _┌─┐┌ \ ┐^ ` - ┼┼└┼┐ / * ┼ ─ / ., _ ┌┘│ \ │ - └┼┘/ O , ./ . └ \ ┘ ── -"""] -add_font("Thin 3x3",Thin3x3Font) - -class Thin4x3Font(Font): - height = 3 - data = Thin3x3Font.data + [u""" -0000111122223333444455556666777788889999 ####$$$$ -┌──┐ ┐ ┌──┐┌──┐ ┐┌── ┌── ┌──┐┌──┐┌──┐ ┼─┼┌┼┼┐ -│ │ │ ┌──┘ ─┤└──┼└──┐├──┐ ┼├──┤└──┤ ┼─┼└┼┼┐ -└──┘ ┴ └── └──┘ ┴ ──┘└──┘ ┴└──┘ ──┘ └┼┼┘ -"""] -add_font("Thin 4x3",Thin4x3Font) - -class HalfBlock5x4Font(Font): - height = 4 - data = [u""" -00000111112222233333444445555566666777778888899999 !! -▄▀▀▄ ▄█ ▄▀▀▄ ▄▀▀▄ ▄ █ █▀▀▀ ▄▀▀ ▀▀▀█ ▄▀▀▄ ▄▀▀▄ █ -█ █ █ ▄▀ ▄▀ █▄▄█ █▄▄ █▄▄ ▐▌ ▀▄▄▀ ▀▄▄█ █ -█ █ █ ▄▀ ▄ █ █ █ █ █ █ █ █ █ ▀ - ▀▀ ▀▀▀ ▀▀▀▀ ▀▀ ▀ ▀▀▀ ▀▀ ▀ ▀▀ ▀▀ ▀ -""", u''' -"""######$$$$$$%%%%%&&&&&((()))******++++++,,,-----..////:::;; -█▐▌ █ █ ▄▀█▀▄ ▐▌▐▌ ▄▀▄ █ █ ▄ ▄ ▄ ▐▌ - ▀█▀█▀ ▀▄█▄ █ ▀▄▀ ▐▌ ▐▌ ▄▄█▄▄ ▄▄█▄▄ ▄▄▄▄ █ ▀ ▀ - ▀█▀█▀ ▄ █ █ ▐▌▄ █ ▀▄▌▐▌ ▐▌ ▄▀▄ █ ▐▌ ▀ ▄▀ - ▀ ▀ ▀▀▀ ▀ ▀ ▀▀ ▀ ▀ ▄▀ ▀ ▀ -''', ur""" -<<<<<=====>>>>>?????@@@@@@[[[[\\\\]]]]^^^^____```{{{{||}}}}~~~~''´´´ - ▄▀ ▀▄ ▄▀▀▄ ▄▀▀▀▄ █▀▀ ▐▌ ▀▀█ ▄▀▄ ▀▄ ▄▀ █ ▀▄ ▄ █ ▄▀ -▄▀ ▀▀▀▀ ▀▄ ▄▀ █ █▀█ █ █ █ ▄▀ █ ▀▄ ▐▐▌▌ - ▀▄ ▀▀▀▀ ▄▀ ▀ █ ▀▀▀ █ ▐▌ █ █ █ █ ▀ - ▀ ▀ ▀ ▀▀▀ ▀▀▀ ▀ ▀▀▀ ▀▀▀▀ ▀ ▀ ▀ -""", u''' -AAAAABBBBBCCCCCDDDDDEEEEEFFFFFGGGGGHHHHHIIJJJJJKKKKK -▄▀▀▄ █▀▀▄ ▄▀▀▄ █▀▀▄ █▀▀▀ █▀▀▀ ▄▀▀▄ █ █ █ █ █ █ -█▄▄█ █▄▄▀ █ █ █ █▄▄ █▄▄ █ █▄▄█ █ █ █▄▀ -█ █ █ █ █ ▄ █ █ █ █ █ ▀█ █ █ █ ▄ █ █ ▀▄ -▀ ▀ ▀▀▀ ▀▀ ▀▀▀ ▀▀▀▀ ▀ ▀▀ ▀ ▀ ▀ ▀▀ ▀ ▀ -''', u''' -LLLLLMMMMMMNNNNNOOOOOPPPPPQQQQQRRRRRSSSSSTTTTT -█ █▄ ▄█ ██ █ ▄▀▀▄ █▀▀▄ ▄▀▀▄ █▀▀▄ ▄▀▀▄ ▀▀█▀▀ -█ █ ▀ █ █▐▌█ █ █ █▄▄▀ █ █ █▄▄▀ ▀▄▄ █ -█ █ █ █ ██ █ █ █ █ ▌█ █ █ ▄ █ █ -▀▀▀▀ ▀ ▀ ▀ ▀ ▀▀ ▀ ▀▀▌ ▀ ▀ ▀▀ ▀ -''', u''' -UUUUUVVVVVVWWWWWWXXXXXXYYYYYYZZZZZ -█ █ █ █ █ █ █ █ █ █ ▀▀▀█ -█ █ ▐▌ ▐▌ █ ▄ █ ▀▄▀ ▀▄▀ ▄▀ -█ █ █ █ ▐▌█▐▌ ▄▀ ▀▄ █ █ - ▀▀ ▀ ▀ ▀ ▀ ▀ ▀ ▀▀▀▀ -''', u''' -aaaaabbbbbcccccdddddeeeeeffffggggghhhhhiijjjjkkkkk - █ █ ▄▀▀ █ ▄ ▄ █ - ▀▀▄ █▀▀▄ ▄▀▀▄ ▄▀▀█ ▄▀▀▄ ▀█▀ ▄▀▀▄ █▀▀▄ ▄ ▄ █ ▄▀ -▄▀▀█ █ █ █ ▄ █ █ █▀▀ █ ▀▄▄█ █ █ █ █ █▀▄ - ▀▀▀ ▀▀▀ ▀▀ ▀▀▀ ▀▀ ▀ ▄▄▀ ▀ ▀ ▀ ▄▄▀ ▀ ▀ -''', u''' -llmmmmmmnnnnnooooopppppqqqqqrrrrssssstttt -█ █ -█ █▀▄▀▄ █▀▀▄ ▄▀▀▄ █▀▀▄ ▄▀▀█ █▀▀ ▄▀▀▀ ▀█▀ -█ █ █ █ █ █ █ █ █ █ █ █ █ ▀▀▄ █ -▀ ▀ ▀ ▀ ▀ ▀▀ █▀▀ ▀▀█ ▀ ▀▀▀ ▀ -''', u''' -uuuuuvvvvvwwwwwwxxxxxxyyyyyzzzzz - -█ █ █ █ █ ▄ █ ▀▄ ▄▀ █ █ ▀▀█▀ -█ █ ▐▌▐▌ ▐▌█▐▌ ▄▀▄ ▀▄▄█ ▄▀ - ▀▀ ▀▀ ▀ ▀ ▀ ▀ ▄▄▀ ▀▀▀▀ -'''] -add_font("Half Block 5x4",HalfBlock5x4Font) - -class HalfBlock6x5Font(Font): - height = 5 - data = [u""" -000000111111222222333333444444555555666666777777888888999999 ..:://// -▄▀▀▀▄ ▄█ ▄▀▀▀▄ ▄▀▀▀▄ ▄ █ █▀▀▀▀ ▄▀▀▀ ▀▀▀▀█ ▄▀▀▀▄ ▄▀▀▀▄ █ -█ █ █ █ █ █ █ █ █ ▐▌ █ █ █ █ ▀ ▐▌ -█ █ █ ▄▀ ▀▀▄ ▀▀▀█▀ ▀▀▀▀▄ █▀▀▀▄ █ ▄▀▀▀▄ ▀▀▀█ ▄ █ -█ █ █ ▄▀ ▄ █ █ █ █ █ ▐▌ █ █ █ ▐▌ - ▀▀▀ ▀▀▀ ▀▀▀▀▀ ▀▀▀ ▀ ▀▀▀▀ ▀▀▀ ▀ ▀▀▀ ▀▀▀ ▀ ▀ -"""] -add_font("Half Block 6x5",HalfBlock6x5Font) - -class HalfBlockHeavy6x5Font(Font): - height = 5 - data = [u""" -000000111111222222333333444444555555666666777777888888999999 ..:://// -▄███▄ ▐█▌ ▄███▄ ▄███▄ █▌ █████ ▄███▄ █████ ▄███▄ ▄███▄ █▌ -█▌ ▐█ ▀█▌ ▀ ▐█ ▀ ▐█ █▌ █▌ █▌ █▌ █▌ █▌ ▐█ █▌ ▐█ █▌ ▐█ -█▌ ▐█ █▌ ▄█▀ ██▌ █████ ████▄ ████▄ ▐█ ▐███▌ ▀████ █▌ -█▌ ▐█ █▌ ▄█▀ ▄ ▐█ █▌ ▐█ █▌ ▐█ █▌ █▌ ▐█ ▐█ █▌▐█ -▀███▀ ███▌ █████ ▀███▀ █▌ ████▀ ▀███▀ ▐█ ▀███▀ ▀███▀ █▌ █▌ -"""] -add_font("Half Block Heavy 6x5",HalfBlockHeavy6x5Font) - -class Thin6x6Font(Font): - height = 6 - data = [u""" -000000111111222222333333444444555555666666777777888888999999'' -┌───┐ ┐ ┌───┐ ┌───┐ ┐ ┌─── ┌─── ┌───┐ ┌───┐ ┌───┐ │ -│ │ │ │ │ ┌ │ │ │ │ │ │ │ │ -│ / │ │ ┌───┘ ─┤ └──┼─ └───┐ ├───┐ ┼ ├───┤ └───┤ -│ │ │ │ │ │ │ │ │ │ │ │ │ -└───┘ ┴ └─── └───┘ ┴ ───┘ └───┘ ┴ └───┘ ───┘ - -""", ur''' -!! """######$$$$$$%%%%%%&&&&&&((()))******++++++ -│ ││ ┌ ┌ ┌─┼─┐ ┌┐ / ┌─┐ / \ -│ ─┼─┼─ │ │ └┘ / │ │ │ │ \ / │ -│ │ │ └─┼─┐ / ┌─\┘ │ │ ──X── ──┼── -│ ─┼─┼─ │ │ / ┌┐ │ \, │ │ / \ │ -. ┘ ┘ └─┼─┘ / └┘ └───\ \ / - -''', ur""" -,,-----..//////::;;<<<<=====>>>>??????@@@@@@ - / ┌───┐ ┌───┐ - / . . / ──── \ │ │┌──┤ - ──── / / \ ┌─┘ ││ │ - / . , \ ──── / │ │└──┘ -, . / \ / . └───┘ - -""", ur""" -[[\\\\\\]]^^^____``{{||}}~~~~~~ -┌ \ ┐ /\ \ ┌ │ ┐ -│ \ │ │ │ │ ┌─┐ -│ \ │ ┤ │ ├ └─┘ -│ \ │ │ │ │ -└ \ ┘ ──── └ │ ┘ - -""", u""" -AAAAAABBBBBBCCCCCCDDDDDDEEEEEEFFFFFFGGGGGGHHHHHHIIJJJJJJ -┌───┐ ┬───┐ ┌───┐ ┬───┐ ┬───┐ ┬───┐ ┌───┐ ┬ ┬ ┬ ┬ -│ │ │ │ │ │ │ │ │ │ │ │ │ │ -├───┤ ├───┤ │ │ │ ├── ├── │ ──┬ ├───┤ │ │ -│ │ │ │ │ │ │ │ │ │ │ │ │ │ ┬ │ -┴ ┴ ┴───┘ └───┘ ┴───┘ ┴───┘ ┴ └───┘ ┴ ┴ ┴ └───┘ - -""", u""" -KKKKKKLLLLLLMMMMMMNNNNNNOOOOOOPPPPPPQQQQQQRRRRRRSSSSSS -┬ ┬ ┬ ┌─┬─┐ ┬─┐ ┬ ┌───┐ ┬───┐ ┌───┐ ┬───┐ ┌───┐ -│ ┌─┘ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ -├─┴┐ │ │ │ │ │ │ │ │ │ ├───┘ │ │ ├─┬─┘ └───┐ -│ └┐ │ │ │ │ │ │ │ │ │ │ ┐│ │ └─┐ │ -┴ ┴ ┴───┘ ┴ ┴ ┴ └─┴ └───┘ ┴ └──┼┘ ┴ ┴ └───┘ - └ -""", u""" -TTTTTTUUUUUUVVVVVVWWWWWWXXXXXXYYYYYYZZZZZZ -┌─┬─┐ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┌───┐ - │ │ │ │ │ │ │ └┐ ┌┘ │ │ ┌─┘ - │ │ │ │ │ │ │ │ ├─┤ └─┬─┘ ┌┘ - │ │ │ └┐ ┌┘ │ │ │ ┌┘ └┐ │ ┌┘ - ┴ └───┘ └─┘ └─┴─┘ ┴ ┴ ┴ └───┘ - -""", u""" -aaaaaabbbbbbccccccddddddeeeeeefffgggggghhhhhhiijjj - ┌─┐ - │ │ │ │ . . -┌───┐ ├───┐ ┌───┐ ┌───┤ ┌───┐ ┼ ┌───┐ ├───┐ ┐ ┐ -┌───┤ │ │ │ │ │ ├───┘ │ │ │ │ │ │ │ -└───┴ └───┘ └───┘ └───┘ └───┘ ┴ └───┤ ┴ ┴ ┴ │ - └───┘ ─┘ -""", u""" -kkkkkkllmmmmmmnnnnnnooooooppppppqqqqqqrrrrrssssss - -│ │ -│ ┌─ │ ┬─┬─┐ ┬───┐ ┌───┐ ┌───┐ ┌───┐ ┬──┐ ┌───┐ -├─┴┐ │ │ │ │ │ │ │ │ │ │ │ │ │ └───┐ -┴ └─ └ ┴ ┴ ┴ ┴ └───┘ ├───┘ └───┤ ┴ └───┘ - │ │ -""", u""" -ttttuuuuuuvvvvvvwwwwwwxxxxxxyyyyyyzzzzzz - - │ -─┼─ ┬ ┬ ┬ ┬ ┬ ┬ ─┐ ┌─ ┬ ┬ ────┬ - │ │ │ └┐ ┌┘ │ │ │ ├─┤ │ │ ┌───┘ - └─ └───┴ └─┘ └─┴─┘ ─┘ └─ └───┤ ┴──── - └───┘ -"""] -add_font("Thin 6x6",Thin6x6Font) - - -class HalfBlock7x7Font(Font): - height = 7 - data = [u""" -0000000111111122222223333333444444455555556666666777777788888889999999''' - ▄███▄ ▐█▌ ▄███▄ ▄███▄ █▌ ▐█████▌ ▄███▄ ▐█████▌ ▄███▄ ▄███▄ ▐█ -▐█ █▌ ▀█▌ ▐█ █▌▐█ █▌▐█ █▌ ▐█ ▐█ ▐█ ▐█ █▌▐█ █▌▐█ -▐█ ▐ █▌ █▌ █▌ ▐██ ▐█████▌▐████▄ ▐████▄ █▌ █████ ▀████▌ -▐█ ▌ █▌ █▌ ▄█▀ █▌ █▌ █▌▐█ █▌ ▐█ ▐█ █▌ █▌ -▐█ █▌ █▌ ▄█▀ ▐█ █▌ █▌ █▌▐█ █▌ █▌ ▐█ █▌ █▌ - ▀███▀ ███▌ ▐█████▌ ▀███▀ █▌ ▐████▀ ▀███▀ ▐█ ▀███▀ ▀███▀ - -""", u''' -!!! """""#######$$$$$$$%%%%%%%&&&&&&&(((())))*******++++++ -▐█ ▐█ █▌ ▐█ █▌ █ ▄ █▌ ▄█▄ █▌▐█ ▄▄ ▄▄ -▐█ ▐█ █▌▐█████▌ ▄███▄ ▐█▌▐█ ▐█ █▌ ▐█ █▌ ▀█▄█▀ ▐█ -▐█ ▐█ █▌ ▐█▄█▄▄ ▀ █▌ ███ █▌ ▐█ ▐█████▌ ████▌ -▐█ ▐█████▌ ▀▀█▀█▌ ▐█ ▄ ███▌▄ █▌ ▐█ ▄█▀█▄ ▐█ - ▐█ █▌ ▀███▀ █▌▐█▌▐█ █▌ ▐█ █▌ ▀▀ ▀▀ -▐█ █ ▐█ ▀ ▀██▀█▌ █▌▐█ - -''', u""" -,,,------.../////:::;;;<<<<<<<======>>>>>>>???????@@@@@@@ - █▌ ▄█▌ ▐█▄ ▄███▄ ▄███▄ - ▐█ ▐█ ▐█ ▄█▀ ▐████▌ ▀█▄ ▐█ █▌▐█ ▄▄█▌ - ▐████▌ █▌ ▐██ ██▌ █▌ ▐█▐█▀█▌ - ▐█ ▐█ ▐█ ▀█▄ ▐████▌ ▄█▀ █▌ ▐█▐█▄█▌ - █▌ ▀ ▀█▌ ▐█▀ ▐█ ▀▀▀ -▐█ ▐█ ▐█ █▌ ▀███▀ -▀ -""", ur""" -[[[[\\\\\]]]]^^^^^^^_____```{{{{{|||}}}}}~~~~~~~´´´ -▐██▌▐█ ▐██▌ ▐█▌ ▐█ █▌▐█ ▐█ █▌ -▐█ █▌ █▌ ▐█ █▌ █▌ █▌ ▐█ ▐█ ▄▄ ▐█ -▐█ ▐█ █▌▐█ █▌ ▄█▌ ▐█ ▐█▄ ▐▀▀█▄▄▌ -▐█ █▌ █▌ ▀█▌ ▐█ ▐█▀ ▀▀ -▐█ ▐█ █▌ █▌ ▐█ ▐█ -▐██▌ █▌▐██▌ █████ █▌▐█ ▐█ - -""", u""" -AAAAAAABBBBBBBCCCCCCCDDDDDDDEEEEEEEFFFFFFFGGGGGGGHHHHHHHIIIIJJJJJJJ - ▄███▄ ▐████▄ ▄███▄ ▐████▄ ▐█████▌▐█████▌ ▄███▄ ▐█ █▌ ██▌ █▌ -▐█ █▌▐█ █▌▐█ ▐█ █▌▐█ ▐█ ▐█ ▐█ █▌ ▐█ █▌ -▐█████▌▐█████ ▐█ ▐█ █▌▐████ ▐████ ▐█ ▐█████▌ ▐█ █▌ -▐█ █▌▐█ █▌▐█ ▐█ █▌▐█ ▐█ ▐█ ██▌▐█ █▌ ▐█ █▌ -▐█ █▌▐█ █▌▐█ ▐█ █▌▐█ ▐█ ▐█ █▌▐█ █▌ ▐█ ▐█ █▌ -▐█ █▌▐████▀ ▀███▀ ▐████▀ ▐█████▌▐█ ▀███▀ ▐█ █▌ ██▌ ▀███▀ - -""", u""" -KKKKKKKLLLLLLLMMMMMMMMNNNNNNNOOOOOOOPPPPPPPQQQQQQQRRRRRRRSSSSSSS -▐█ █▌▐█ ▄█▌▐█▄ ▐██ █▌ ▄███▄ ▐████▄ ▄███▄ ▐████▄ ▄███▄ -▐█ █▌ ▐█ ▐█ ▐▌ █▌▐██▌ █▌▐█ █▌▐█ █▌▐█ █▌▐█ █▌▐█ -▐█▄█▌ ▐█ ▐█ ▐▌ █▌▐█▐█ █▌▐█ █▌▐████▀ ▐█ █▌▐█████ ▀███▄ -▐█▀█▌ ▐█ ▐█ █▌▐█ █▌█▌▐█ █▌▐█ ▐█ █▌▐█ █▌ █▌ -▐█ █▌ ▐█ ▐█ █▌▐█ ▐██▌▐█ █▌▐█ ▐█ █▌█▌▐█ █▌ █▌ -▐█ █▌▐█████▌▐█ █▌▐█ ██▌ ▀███▀ ▐█ ▀███▀ ▐█ █▌ ▀███▀ - ▀▀ -""", u""" -TTTTTTTUUUUUUUVVVVVVVWWWWWWWWXXXXXXXYYYYYYYZZZZZZZ - █████▌▐█ █▌▐█ █▌▐█ █▌▐█ █▌ █▌ █▌▐█████▌ - █▌ ▐█ █▌ █▌ ▐█ ▐█ █▌ ▐█ █▌ ▐█ ▐█ █▌ - █▌ ▐█ █▌ ▐█ █▌ ▐█ █▌ ▐█▌ ▐██ █▌ - █▌ ▐█ █▌ ███ ▐█ ▐▌ █▌ ███ █▌ █▌ - █▌ ▐█ █▌ ▐█▌ ▐█ ▐▌ █▌ █▌ ▐█ █▌ █▌ - █▌ ▀███▀ █ ▀█▌▐█▀ ▐█ █▌ █▌ ▐█████▌ - -""", u""" -aaaaaaabbbbbbbcccccccdddddddeeeeeeefffffggggggghhhhhhhiiijjjj - ▐█ █▌ ▄█▌ ▐█ █▌ █▌ - ▐█ █▌ ▐█ ▐█ - ▄███▄ ▐████▄ ▄███▄ ▄████▌ ▄███▄ ▐███ ▄███▄ ▐████▄ ▐█▌ ▐█▌ - ▄▄▄█▌▐█ █▌▐█ ▐█ █▌▐█▄▄▄█▌ ▐█ ▐█ █▌▐█ █▌ █▌ █▌ -▐█▀▀▀█▌▐█ █▌▐█ ▐█ █▌▐█▀▀▀ ▐█ ▐█▄▄▄█▌▐█ █▌ █▌ █▌ - ▀████▌▐████▀ ▀███▀ ▀████▌ ▀███▀ ▐█ ▀▀▀█▌▐█ █▌ █▌ █▌ - ▀███▀ ▐██ -""", u""" -kkkkkkkllllmmmmmmmmnnnnnnnooooooopppppppqqqqqqqrrrrrrsssssss -▐█ ██ -▐█ ▐█ -▐█ ▄█▌ ▐█ ▄█▌▐█▄ ▐████▄ ▄███▄ ▐████▄ ▄████▌ ▄███▌ ▄███▄ -▐█▄█▀ ▐█ ▐█ ▐▌ █▌▐█ █▌▐█ █▌▐█ █▌▐█ █▌▐█ ▐█▄▄▄ -▐█▀▀█▄ ▐█ ▐█ ▐▌ █▌▐█ █▌▐█ █▌▐█ █▌▐█ █▌▐█ ▀▀▀█▌ -▐█ █▌ ▐█▌▐█ █▌▐█ █▌ ▀███▀ ▐████▀ ▀████▌▐█ ▀███▀ - ▐█ █▌ -""", u""" -tttttuuuuuuuvvvvvvvwwwwwwwwxxxxxxxyyyyyyyzzzzzzz - █▌ - █▌ - ███▌▐█ █▌▐█ █▌▐█ █▌▐█ █▌▐█ █▌▐█████▌ - █▌ ▐█ █▌ █▌ ▐█ ▐█ █▌ ▀█▄█▀ ▐█ █▌ ▄█▀ - █▌ ▐█ █▌ ███ ▐█ ▐▌ █▌ ▄█▀█▄ ▐█▄▄▄█▌ ▄█▀ - █▌ ▀███▀ ▐█▌ ▀█▌▐█▀ ▐█ █▌ ▀▀▀█▌▐█████▌ - ▀███▀ -"""] -add_font("Half Block 7x7",HalfBlock7x7Font) - - -if __name__ == "__main__": - l = get_all_fonts() - all_ascii = "".join([chr(x) for x in range(32, 127)]) - print "Available Fonts: (U) = UTF-8 required" - print "----------------" - for n,cls in l: - f = cls() - u = "" - if f.utf8_required: - u = "(U)" - print ("%-20s %3s " % (n,u)), - c = f.characters() - if c == all_ascii: - print "Full ASCII" - elif c.startswith(all_ascii): - print "Full ASCII + " + c[len(all_ascii):] - else: - print "Characters: " + c diff --git a/urwid/graphics.py b/urwid/graphics.py deleted file mode 100755 index cd03d33..0000000 --- a/urwid/graphics.py +++ /dev/null @@ -1,911 +0,0 @@ -#!/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 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="", - 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. - - 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 - - """ - - tline, bline = Divider(tline), Divider(bline) - lline, rline = SolidFill(lline), SolidFill(rline) - tlcorner, trcorner = Text(tlcorner), Text(trcorner) - blcorner, brcorner = Text(blcorner), Text(brcorner) - - self.title_widget = Text(self.format_title(title)) - self.tline_widget = Columns([ - tline, - ('flow', self.title_widget), - tline, - ]) - - top = Columns([ - ('fixed', 1, tlcorner), - self.tline_widget, - ('fixed', 1, trcorner) - ]) - - middle = Columns([ - ('fixed', 1, lline), - original_widget, - ('fixed', 1, rline), - ], box_columns=[0, 2], focus_column=1) - - bottom = Columns([ - ('fixed', 1, blcorner), bline, ('fixed', 1, brcorner) - ]) - - pile = Pile([('flow', top), middle, ('flow', bottom)], focus_item=1) - - 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): - 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(Widget): - __metaclass__ = BarGraphMeta - - _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((bt1, w1), (bt2, w2)): - 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. - """ - 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. - """ - 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 = int(cf) - 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 diff --git a/urwid/html_fragment.py b/urwid/html_fragment.py deleted file mode 100755 index 380d1d3..0000000 --- a/urwid/html_fragment.py +++ /dev/null @@ -1,245 +0,0 @@ -#!/usr/bin/python -# -# Urwid html fragment output wrapper for "screen shots" -# Copyright (C) 2004-2007 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/ - -""" -HTML PRE-based UI implementation -""" - -from urwid import util -from urwid.main_loop import ExitMainLoop -from urwid.display_common import AttrSpec, BaseScreen - - -# replace control characters with ?'s -_trans_table = "?" * 32 + "".join([chr(x) for x in range(32, 256)]) - -_default_foreground = 'black' -_default_background = 'light gray' - -class HtmlGeneratorSimulationError(Exception): - pass - -class HtmlGenerator(BaseScreen): - # class variables - fragments = [] - sizes = [] - keys = [] - started = True - - def __init__(self): - super(HtmlGenerator, self).__init__() - self.colors = 16 - self.bright_is_bold = False # ignored - self.has_underline = True # ignored - self.register_palette_entry(None, - _default_foreground, _default_background) - - def set_terminal_properties(self, colors=None, bright_is_bold=None, - has_underline=None): - - if colors is None: - colors = self.colors - if bright_is_bold is None: - bright_is_bold = self.bright_is_bold - if has_underline is None: - has_underline = self.has_underline - - self.colors = colors - self.bright_is_bold = bright_is_bold - self.has_underline = has_underline - - def set_mouse_tracking(self, enable=True): - """Not yet implemented""" - pass - - def set_input_timeouts(self, *args): - pass - - def reset_default_terminal_palette(self, *args): - pass - - def draw_screen(self, (cols, rows), r ): - """Create an html fragment from the render object. - Append it to HtmlGenerator.fragments list. - """ - # collect output in l - l = [] - - assert r.rows() == rows - - if r.cursor is not None: - cx, cy = r.cursor - else: - cx = cy = None - - y = -1 - for row in r.content(): - y += 1 - col = 0 - - for a, cs, run in row: - run = run.translate(_trans_table) - if isinstance(a, AttrSpec): - aspec = a - else: - aspec = self._palette[a][ - {1: 1, 16: 0, 88:2, 256:3}[self.colors]] - - if y == cy and col <= cx: - run_width = util.calc_width(run, 0, - len(run)) - if col+run_width > cx: - l.append(html_span(run, - aspec, cx-col)) - else: - l.append(html_span(run, aspec)) - col += run_width - else: - l.append(html_span(run, aspec)) - - l.append("\n") - - # add the fragment to the list - self.fragments.append( "
%s
" % "".join(l) ) - - def clear(self): - """ - Force the screen to be completely repainted on the next - call to draw_screen(). - - (does nothing for html_fragment) - """ - pass - - def get_cols_rows(self): - """Return the next screen size in HtmlGenerator.sizes.""" - if not self.sizes: - raise HtmlGeneratorSimulationError, "Ran out of screen sizes to return!" - return self.sizes.pop(0) - - def get_input(self, raw_keys=False): - """Return the next list of keypresses in HtmlGenerator.keys.""" - if not self.keys: - raise ExitMainLoop() - if raw_keys: - return (self.keys.pop(0), []) - return self.keys.pop(0) - -_default_aspec = AttrSpec(_default_foreground, _default_background) -(_d_fg_r, _d_fg_g, _d_fg_b, _d_bg_r, _d_bg_g, _d_bg_b) = ( - _default_aspec.get_rgb_values()) - -def html_span(s, aspec, cursor = -1): - fg_r, fg_g, fg_b, bg_r, bg_g, bg_b = aspec.get_rgb_values() - # use real colours instead of default fg/bg - if fg_r is None: - fg_r, fg_g, fg_b = _d_fg_r, _d_fg_g, _d_fg_b - if bg_r is None: - bg_r, bg_g, bg_b = _d_bg_r, _d_bg_g, _d_bg_b - html_fg = "#%02x%02x%02x" % (fg_r, fg_g, fg_b) - html_bg = "#%02x%02x%02x" % (bg_r, bg_g, bg_b) - if aspec.standout: - html_fg, html_bg = html_bg, html_fg - extra = (";text-decoration:underline" * aspec.underline + - ";font-weight:bold" * aspec.bold) - def html_span(fg, bg, s): - if not s: return "" - return ('%s' % - (fg, bg, extra, html_escape(s))) - - if cursor >= 0: - c_off, _ign = util.calc_text_pos(s, 0, len(s), cursor) - c2_off = util.move_next_char(s, c_off, len(s)) - return (html_span(html_fg, html_bg, s[:c_off]) + - html_span(html_bg, html_fg, s[c_off:c2_off]) + - html_span(html_fg, html_bg, s[c2_off:])) - else: - return html_span(html_fg, html_bg, s) - - -def html_escape(text): - """Escape text so that it will be displayed safely within HTML""" - text = text.replace('&','&') - text = text.replace('<','<') - text = text.replace('>','>') - return text - -def screenshot_init( sizes, keys ): - """ - Replace curses_display.Screen and raw_display.Screen class with - HtmlGenerator. - - Call this function before executing an application that uses - curses_display.Screen to have that code use HtmlGenerator instead. - - sizes -- list of ( columns, rows ) tuples to be returned by each call - to HtmlGenerator.get_cols_rows() - keys -- list of lists of keys to be returned by each call to - HtmlGenerator.get_input() - - Lists of keys may include "window resize" to force the application to - call get_cols_rows and read a new screen size. - - For example, the following call will prepare an application to: - 1. start in 80x25 with its first call to get_cols_rows() - 2. take a screenshot when it calls draw_screen(..) - 3. simulate 5 "down" keys from get_input() - 4. take a screenshot when it calls draw_screen(..) - 5. simulate keys "a", "b", "c" and a "window resize" - 6. resize to 20x10 on its second call to get_cols_rows() - 7. take a screenshot when it calls draw_screen(..) - 8. simulate a "Q" keypress to quit the application - - screenshot_init( [ (80,25), (20,10) ], - [ ["down"]*5, ["a","b","c","window resize"], ["Q"] ] ) - """ - try: - for (row,col) in sizes: - assert type(row) == int - assert row>0 and col>0 - except (AssertionError, ValueError): - raise Exception, "sizes must be in the form [ (col1,row1), (col2,row2), ...]" - - try: - for l in keys: - assert type(l) == list - for k in l: - assert type(k) == str - except (AssertionError, ValueError): - raise Exception, "keys must be in the form [ [keyA1, keyA2, ..], [keyB1, ..], ...]" - - import curses_display - curses_display.Screen = HtmlGenerator - import raw_display - raw_display.Screen = HtmlGenerator - - HtmlGenerator.sizes = sizes - HtmlGenerator.keys = keys - - -def screenshot_collect(): - """Return screenshots as a list of HTML fragments.""" - l = HtmlGenerator.fragments - HtmlGenerator.fragments = [] - return l - - diff --git a/urwid/lcd_display.py b/urwid/lcd_display.py deleted file mode 100644 index 4f62173..0000000 --- a/urwid/lcd_display.py +++ /dev/null @@ -1,485 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# -# Urwid LCD display module -# Copyright (C) 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 display_common import BaseScreen - -import time - -class LCDScreen(BaseScreen): - def set_terminal_properties(self, colors=None, bright_is_bold=None, - has_underline=None): - pass - - def set_mouse_tracking(self, enable=True): - pass - - def set_input_timeouts(self, *args): - pass - - def reset_default_terminal_palette(self, *args): - pass - - def draw_screen(self, (cols, rows), r ): - pass - - def clear(self): - pass - - def get_cols_rows(self): - return self.DISPLAY_SIZE - - - -class CFLCDScreen(LCDScreen): - """ - Common methods for Crystal Fontz LCD displays - """ - KEYS = [None, # no key with code 0 - 'up_press', 'down_press', 'left_press', - 'right_press', 'enter_press', 'exit_press', - 'up_release', 'down_release', 'left_release', - 'right_release', 'enter_release', 'exit_release', - 'ul_press', 'ur_press', 'll_press', 'lr_press', - 'ul_release', 'ur_release', 'll_release', 'lr_release'] - CMD_PING = 0 - CMD_VERSION = 1 - CMD_CLEAR = 6 - CMD_CGRAM = 9 - CMD_CURSOR_POSITION = 11 # data = [col, row] - CMD_CURSOR_STYLE = 12 # data = [style (0-4)] - CMD_LCD_CONTRAST = 13 # data = [contrast (0-255)] - CMD_BACKLIGHT = 14 # data = [power (0-100)] - CMD_LCD_DATA = 31 # data = [col, row] + text - CMD_GPO = 34 # data = [pin(0-12), value(0-100)] - - # sent from device - CMD_KEY_ACTIVITY = 0x80 - CMD_ACK = 0x40 # in high two bits ie. & 0xc0 - - CURSOR_NONE = 0 - CURSOR_BLINKING_BLOCK = 1 - CURSOR_UNDERSCORE = 2 - CURSOR_BLINKING_BLOCK_UNDERSCORE = 3 - CURSOR_INVERTING_BLINKING_BLOCK = 4 - - MAX_PACKET_DATA_LENGTH = 22 - - colors = 1 - has_underline = False - - def __init__(self, device_path, baud): - """ - device_path -- eg. '/dev/ttyUSB0' - baud -- baud rate - """ - super(CFLCDScreen, self).__init__() - self.device_path = device_path - from serial import Serial - self._device = Serial(device_path, baud, timeout=0) - self._unprocessed = "" - - - @classmethod - def get_crc(cls, buf): - # This seed makes the output of this shift based algorithm match - # the table based algorithm. The center 16 bits of the 32-bit - # "newCRC" are used for the CRC. The MSB of the lower byte is used - # to see what bit was shifted out of the center 16 bit CRC - # accumulator ("carry flag analog"); - newCRC = 0x00F32100 - for byte in buf: - # Push this byte’s bits through a software - # implementation of a hardware shift & xor. - for bit_count in range(8): - # Shift the CRC accumulator - newCRC >>= 1 - # The new MSB of the CRC accumulator comes - # from the LSB of the current data byte. - if ord(byte) & (0x01 << bit_count): - newCRC |= 0x00800000 - # If the low bit of the current CRC accumulator was set - # before the shift, then we need to XOR the accumulator - # with the polynomial (center 16 bits of 0x00840800) - if newCRC & 0x00000080: - newCRC ^= 0x00840800 - # All the data has been done. Do 16 more bits of 0 data. - for bit_count in range(16): - # Shift the CRC accumulator - newCRC >>= 1 - # If the low bit of the current CRC accumulator was set - # before the shift we need to XOR the accumulator with - # 0x00840800. - if newCRC & 0x00000080: - newCRC ^= 0x00840800 - # Return the center 16 bits, making this CRC match the one’s - # complement that is sent in the packet. - return ((~newCRC)>>8) & 0xffff - - def _send_packet(self, command, data): - """ - low-level packet sending. - Following the protocol requires waiting for ack packet between - sending each packet to the device. - """ - buf = chr(command) + chr(len(data)) + data - crc = self.get_crc(buf) - buf = buf + chr(crc & 0xff) + chr(crc >> 8) - self._device.write(buf) - - def _read_packet(self): - """ - low-level packet reading. - returns (command/report code, data) or None - - This method stored data read and tries to resync when bad data - is received. - """ - # pull in any new data available - self._unprocessed = self._unprocessed + self._device.read() - while True: - try: - command, data, unprocessed = self._parse_data(self._unprocessed) - self._unprocessed = unprocessed - return command, data - except self.MoreDataRequired: - return - except self.InvalidPacket: - # throw out a byte and try to parse again - self._unprocessed = self._unprocessed[1:] - - class InvalidPacket(Exception): - pass - class MoreDataRequired(Exception): - pass - - @classmethod - def _parse_data(cls, data): - """ - Try to read a packet from the start of data, returning - (command/report code, packet_data, remaining_data) - or raising InvalidPacket or MoreDataRequired - """ - if len(data) < 2: - raise cls.MoreDataRequired - command = ord(data[0]) - plen = ord(data[1]) - if plen > cls.MAX_PACKET_DATA_LENGTH: - raise cls.InvalidPacket("length value too large") - if len(data) < plen + 4: - raise cls.MoreDataRequired - crc = cls.get_crc(data[:2 + plen]) - pcrc = ord(data[2 + plen]) + (ord(data[3 + plen]) << 8 ) - if crc != pcrc: - raise cls.InvalidPacket("CRC doesn't match") - return (command, data[2:2 + plen], data[4 + plen:]) - - - -class KeyRepeatSimulator(object): - """ - Provide simulated repeat key events when given press and - release events. - - If two or more keys are pressed disable repeating until all - keys are released. - """ - def __init__(self, repeat_delay, repeat_next): - """ - repeat_delay -- seconds to wait before starting to repeat keys - repeat_next -- time between each repeated key - """ - self.repeat_delay = repeat_delay - self.repeat_next = repeat_next - self.pressed = {} - self.multiple_pressed = False - - def press(self, key): - if self.pressed: - self.multiple_pressed = True - self.pressed[key] = time.time() - - def release(self, key): - if key not in self.pressed: - return # ignore extra release events - del self.pressed[key] - if not self.pressed: - self.multiple_pressed = False - - def next_event(self): - """ - Return (remaining, key) where remaining is the number of seconds - (float) until the key repeat event should be sent, or None if no - events are pending. - """ - if len(self.pressed) != 1 or self.multiple_pressed: - return - for key in self.pressed: - return max(0, self.pressed[key] + self.repeat_delay - - time.time()), key - - def sent_event(self): - """ - Cakk this method when you have sent a key repeat event so the - timer will be reset for the next event - """ - if len(self.pressed) != 1: - return # ignore event that shouldn't have been sent - for key in self.pressed: - self.pressed[key] = ( - time.time() - self.repeat_delay + self.repeat_next) - return - - -class CF635Screen(CFLCDScreen): - u""" - Crystal Fontz 635 display - - 20x4 character display + cursor - no foreground/background colors or settings supported - - see CGROM for list of close unicode matches to characters available - - 6 button input - up, down, left, right, enter (check mark), exit (cross) - """ - DISPLAY_SIZE = (20, 4) - - # ① through ⑧ are programmable CGRAM (chars 0-7, repeated at 8-15) - # double arrows (⇑⇓) appear as double arrowheads (chars 18, 19) - # ⑴ resembles a bell - # ⑵ resembles a filled-in "Y" - # ⑶ is the letters "Pt" together - # partial blocks (▇▆▄▃▁) are actually shorter versions of (▉▋▌▍▏) - # both groups are intended to draw horizontal bars with pixel - # precision, use ▇*[▆▄▃▁]? for a thin bar or ▉*[▋▌▍▏]? for a thick bar - CGROM = ( - u"①②③④⑤⑥⑦⑧①②③④⑤⑥⑦⑧" - u"►◄⇑⇓«»↖↗↙↘▲▼↲^ˇ█" - u" !\"#¤%&'()*+,-./" - u"0123456789:;<=>?" - u"¡ABCDEFGHIJKLMNO" - u"PQRSTUVWXYZÄÖÑܧ" - u"¿abcdefghijklmno" - u"pqrstuvwxyzäöñüà" - u"⁰¹²³⁴⁵⁶⁷⁸⁹½¼±≥≤μ" - u"♪♫⑴♥♦⑵⌜⌟“”()αɛδ∞" - u"@£$¥èéùìòÇᴾØøʳÅå" - u"⌂¢ΦτλΩπΨΣθΞ♈ÆæßÉ" - u"ΓΛΠϒ_ÈÊêçğŞşİι~◊" - u"▇▆▄▃▁ƒ▉▋▌▍▏⑶◽▪↑→" - u"↓←ÁÍÓÚÝáíóúýÔôŮů" - u"ČĔŘŠŽčĕřšž[\]{|}") - - cursor_style = CFLCDScreen.CURSOR_INVERTING_BLINKING_BLOCK - - def __init__(self, device_path, baud=115200, - repeat_delay=0.5, repeat_next=0.125, - key_map=['up', 'down', 'left', 'right', 'enter', 'esc']): - """ - device_path -- eg. '/dev/ttyUSB0' - baud -- baud rate - repeat_delay -- seconds to wait before starting to repeat keys - repeat_next -- time between each repeated key - key_map -- the keys to send for this device's buttons - """ - super(CF635Screen, self).__init__(device_path, baud) - - self.repeat_delay = repeat_delay - self.repeat_next = repeat_next - self.key_repeat = KeyRepeatSimulator(repeat_delay, repeat_next) - self.key_map = key_map - - self._last_command = None - self._last_command_time = 0 - self._command_queue = [] - self._screen_buf = None - self._previous_canvas = None - self._update_cursor = False - - - def get_input_descriptors(self): - """ - return the fd from our serial device so we get called - on input and responses - """ - return [self._device.fd] - - def get_input_nonblocking(self): - """ - Return a (next_input_timeout, keys_pressed, raw_keycodes) - tuple. - - The protocol for our device requires waiting for acks between - each command, so this method responds to those as well as key - press and release events. - - Key repeat events are simulated here as the device doesn't send - any for us. - - raw_keycodes are the bytes of messages we received, which might - not seem to have any correspondence to keys_pressed. - """ - input = [] - raw_input = [] - timeout = None - - while True: - packet = self._read_packet() - if not packet: - break - command, data = packet - - if command == self.CMD_KEY_ACTIVITY and data: - d0 = ord(data[0]) - if 1 <= d0 <= 12: - release = d0 > 6 - keycode = d0 - (release * 6) - 1 - key = self.key_map[keycode] - if release: - self.key_repeat.release(key) - else: - input.append(key) - self.key_repeat.press(key) - raw_input.append(d0) - - elif command & 0xc0 == 0x40: # "ACK" - if command & 0x3f == self._last_command: - self._send_next_command() - - next_repeat = self.key_repeat.next_event() - if next_repeat: - timeout, key = next_repeat - if not timeout: - input.append(key) - self.key_repeat.sent_event() - timeout = None - - return timeout, input, [] - - - def _send_next_command(self): - """ - send out the next command in the queue - """ - if not self._command_queue: - self._last_command = None - return - command, data = self._command_queue.pop(0) - self._send_packet(command, data) - self._last_command = command # record command for ACK - self._last_command_time = time.time() - - def queue_command(self, command, data): - self._command_queue.append((command, data)) - # not waiting? send away! - if self._last_command is None: - self._send_next_command() - - def draw_screen(self, size, canvas): - assert size == self.DISPLAY_SIZE - - if self._screen_buf: - osb = self._screen_buf - else: - osb = [] - sb = [] - - y = 0 - for row in canvas.content(): - text = [] - for a, cs, run in row: - text.append(run) - if not osb or osb[y] != text: - self.queue_command(self.CMD_LCD_DATA, chr(0) + chr(y) + - "".join(text)) - sb.append(text) - y += 1 - - if (self._previous_canvas and - self._previous_canvas.cursor == canvas.cursor and - (not self._update_cursor or not canvas.cursor)): - pass - elif canvas.cursor is None: - self.queue_command(self.CMD_CURSOR_STYLE, chr(self.CURSOR_NONE)) - else: - x, y = canvas.cursor - self.queue_command(self.CMD_CURSOR_POSITION, chr(x) + chr(y)) - self.queue_command(self.CMD_CURSOR_STYLE, chr(self.cursor_style)) - - self._update_cursor = False - self._screen_buf = sb - self._previous_canvas = canvas - - def program_cgram(self, index, data): - """ - Program character data. Characters available as chr(0) through - chr(7), and repeated as chr(8) through chr(15). - - index -- 0 to 7 index of character to program - - data -- list of 8, 6-bit integer values top to bottom with MSB - on the left side of the character. - """ - assert 0 <= index <= 7 - assert len(data) == 8 - self.queue_command(self.CMD_CGRAM, chr(index) + - "".join([chr(x) for x in data])) - - def set_cursor_style(self, style): - """ - style -- CURSOR_BLINKING_BLOCK, CURSOR_UNDERSCORE, - CURSOR_BLINKING_BLOCK_UNDERSCORE or - CURSOR_INVERTING_BLINKING_BLOCK - """ - assert 1 <= style <= 4 - self.cursor_style = style - self._update_cursor = True - - def set_backlight(self, value): - """ - Set backlight brightness - - value -- 0 to 100 - """ - assert 0 <= value <= 100 - self.queue_command(self.CMD_BACKLIGHT, chr(value)) - - def set_lcd_contrast(self, value): - """ - value -- 0 to 255 - """ - assert 0 <= value <= 255 - self.queue_command(self.CMD_LCD_CONTRAST, chr(value)) - - def set_led_pin(self, led, rg, value): - """ - led -- 0 to 3 - rg -- 0 for red, 1 for green - value -- 0 to 100 - """ - assert 0 <= led <= 3 - assert rg in (0, 1) - assert 0 <= value <= 100 - self.queue_command(self.CMD_GPO, chr(12 - 2 * led - rg) + - chr(value)) - diff --git a/urwid/listbox.py b/urwid/listbox.py deleted file mode 100644 index 5370e23..0000000 --- a/urwid/listbox.py +++ /dev/null @@ -1,1668 +0,0 @@ -#!/usr/bin/python -# -# Urwid listbox class -# 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 urwid.util import is_mouse_press -from urwid.canvas import SolidCanvas, CanvasCombine -from urwid.widget import Widget, nocache_widget_render_instance, BOX, GIVEN -from urwid.decoration import calculate_top_bottom_filler, normalize_valign -from urwid import signals -from urwid.signals import connect_signal -from urwid.monitored_list import MonitoredList, MonitoredFocusList -from urwid.container import WidgetContainerMixin -from urwid.command_map import (CURSOR_UP, CURSOR_DOWN, - CURSOR_PAGE_UP, CURSOR_PAGE_DOWN) - -class ListWalkerError(Exception): - pass - -class ListWalker(object): - __metaclass__ = signals.MetaSignals - - signals = ["modified"] - - def _modified(self): - signals.emit_signal(self, "modified") - - def get_focus(self): - """ - This default implementation relies on a focus attribute and a - __getitem__() method defined in a subclass. - - Override and don't call this method if these are not defined. - """ - try: - focus = self.focus - return self[focus], focus - except (IndexError, KeyError, TypeError): - return None, None - - def get_next(self, position): - """ - This default implementation relies on a next_position() method and a - __getitem__() method defined in a subclass. - - Override and don't call this method if these are not defined. - """ - try: - position = self.next_position(position) - return self[position], position - except (IndexError, KeyError): - return None, None - - def get_prev(self, position): - """ - This default implementation relies on a prev_position() method and a - __getitem__() method defined in a subclass. - - Override and don't call this method if these are not defined. - """ - try: - position = self.prev_position(position) - return self[position], position - except (IndexError, KeyError): - return None, None - - -class PollingListWalker(object): # NOT ListWalker subclass - def __init__(self, contents): - """ - contents -- list to poll for changes - - This class is deprecated. Use SimpleFocusListWalker instead. - """ - import warnings - warnings.warn("PollingListWalker is deprecated, " - "use SimpleFocusListWalker instead.", DeprecationWarning) - - self.contents = contents - if not getattr(contents, '__getitem__', None): - raise ListWalkerError("PollingListWalker expecting list like " - "object, got: %r" % (contents,)) - self.focus = 0 - - def _clamp_focus(self): - if self.focus >= len(self.contents): - self.focus = len(self.contents)-1 - - def get_focus(self): - """Return (focus widget, focus position).""" - if len(self.contents) == 0: return None, None - self._clamp_focus() - return self.contents[self.focus], self.focus - - def set_focus(self, position): - """Set focus position.""" - # this class is deprecated, otherwise I might have fixed this: - assert type(position) == int - self.focus = position - - def get_next(self, start_from): - """ - Return (widget after start_from, position after start_from). - """ - pos = start_from + 1 - if len(self.contents) <= pos: return None, None - return self.contents[pos],pos - - def get_prev(self, start_from): - """ - Return (widget before start_from, position before start_from). - """ - pos = start_from - 1 - if pos < 0: return None, None - return self.contents[pos],pos - - -class SimpleListWalker(MonitoredList, ListWalker): - def __init__(self, contents): - """ - contents -- list to copy into this object - - Changes made to this object (when it is treated as a list) are - detected automatically and will cause ListBox objects using - this list walker to be updated. - """ - if not getattr(contents, '__getitem__', None): - raise ListWalkerError, "SimpleListWalker expecting list like object, got: %r"%(contents,) - MonitoredList.__init__(self, contents) - self.focus = 0 - - def _get_contents(self): - """ - Return self. - - Provides compatibility with old SimpleListWalker class. - """ - return self - contents = property(_get_contents) - - def _modified(self): - if self.focus >= len(self): - self.focus = max(0, len(self)-1) - ListWalker._modified(self) - - def set_modified_callback(self, callback): - """ - This function inherited from MonitoredList is not - implemented in SimpleListWalker. - - Use connect_signal(list_walker, "modified", ...) instead. - """ - raise NotImplementedError('Use connect_signal(' - 'list_walker, "modified", ...) instead.') - - def set_focus(self, position): - """Set focus position.""" - try: - if position < 0 or position >= len(self): - raise ValueError - except (TypeError, ValueError): - raise IndexError, "No widget at position %s" % (position,) - self.focus = position - self._modified() - - def next_position(self, position): - """ - Return position after start_from. - """ - if len(self) - 1 <= position: - raise IndexError - return position + 1 - - def prev_position(self, position): - """ - Return position before start_from. - """ - if position <= 0: - raise IndexError - return position - 1 - - def positions(self, reverse=False): - """ - Optional method for returning an iterable of positions. - """ - if reverse: - return xrange(len(self) - 1, -1, -1) - return xrange(len(self)) - - -class SimpleFocusListWalker(ListWalker, MonitoredFocusList): - def __init__(self, contents): - """ - contents -- list to copy into this object - - Changes made to this object (when it is treated as a list) are - detected automatically and will cause ListBox objects using - this list walker to be updated. - - Also, items added or removed before the widget in focus with - normal list methods will cause the focus to be updated - intelligently. - """ - if not getattr(contents, '__getitem__', None): - raise ListWalkerError("SimpleFocusListWalker expecting list like " - "object, got: %r"%(contents,)) - MonitoredFocusList.__init__(self, contents) - - def set_modified_callback(self, callback): - """ - This function inherited from MonitoredList is not - implemented in SimpleFocusListWalker. - - Use connect_signal(list_walker, "modified", ...) instead. - """ - raise NotImplementedError('Use connect_signal(' - 'list_walker, "modified", ...) instead.') - - def set_focus(self, position): - """Set focus position.""" - self.focus = position - - def next_position(self, position): - """ - Return position after start_from. - """ - if len(self) - 1 <= position: - raise IndexError - return position + 1 - - def prev_position(self, position): - """ - Return position before start_from. - """ - if position <= 0: - raise IndexError - return position - 1 - - def positions(self, reverse=False): - """ - Optional method for returning an iterable of positions. - """ - if reverse: - return xrange(len(self) - 1, -1, -1) - return xrange(len(self)) - - -class ListBoxError(Exception): - pass - -class ListBox(Widget, WidgetContainerMixin): - """ - a horizontally stacked list of widgets - """ - _selectable = True - _sizing = frozenset([BOX]) - - def __init__(self, body): - """ - :param body: a ListWalker subclass such as - :class:`SimpleFocusListWalker` that contains - widgets to be displayed inside the list box - :type body: ListWalker - """ - if getattr(body, 'get_focus', None): - self.body = body - else: - self.body = PollingListWalker(body) - - try: - connect_signal(self.body, "modified", self._invalidate) - except NameError: - # our list walker has no modified signal so we must not - # cache our canvases because we don't know when our - # content has changed - self.render = nocache_widget_render_instance(self) - - # offset_rows is the number of rows between the top of the view - # and the top of the focused item - self.offset_rows = 0 - # inset_fraction is used when the focused widget is off the - # top of the view. it is the fraction of the widget cut off - # at the top. (numerator, denominator) - self.inset_fraction = (0,1) - - # pref_col is the preferred column for the cursor when moving - # between widgets that use the cursor (edit boxes etc.) - self.pref_col = 'left' - - # variable for delayed focus change used by set_focus - self.set_focus_pending = 'first selectable' - - # variable for delayed valign change used by set_focus_valign - self.set_focus_valign_pending = None - - - def calculate_visible(self, size, focus=False ): - """ - Returns the widgets that would be displayed in - the ListBox given the current *size* and *focus*. - - see :meth:`Widget.render` for parameter details - - :returns: (*middle*, *top*, *bottom*) or (``None``, ``None``, ``None``) - - *middle* - (*row offset*(when +ve) or *inset*(when -ve), - *focus widget*, *focus position*, *focus rows*, - *cursor coords* or ``None``) - *top* - (*# lines to trim off top*, - list of (*widget*, *position*, *rows*) tuples above focus - in order from bottom to top) - *bottom* - (*# lines to trim off bottom*, - list of (*widget*, *position*, *rows*) tuples below focus - in order from top to bottom) - """ - (maxcol, maxrow) = size - - # 0. set the focus if a change is pending - if self.set_focus_pending or self.set_focus_valign_pending: - self._set_focus_complete( (maxcol, maxrow), focus ) - - # 1. start with the focus widget - focus_widget, focus_pos = self.body.get_focus() - if focus_widget is None: #list box is empty? - return None,None,None - top_pos = focus_pos - - offset_rows, inset_rows = self.get_focus_offset_inset( - (maxcol,maxrow)) - # force at least one line of focus to be visible - if maxrow and offset_rows >= maxrow: - offset_rows = maxrow -1 - - # adjust position so cursor remains visible - cursor = None - if maxrow and focus_widget.selectable() and focus: - if hasattr(focus_widget,'get_cursor_coords'): - cursor=focus_widget.get_cursor_coords((maxcol,)) - - if cursor is not None: - cx, cy = cursor - effective_cy = cy + offset_rows - inset_rows - - if effective_cy < 0: # cursor above top? - inset_rows = cy - elif effective_cy >= maxrow: # cursor below bottom? - offset_rows = maxrow - cy -1 - if offset_rows < 0: # need to trim the top - inset_rows, offset_rows = -offset_rows, 0 - - # set trim_top by focus trimmimg - trim_top = inset_rows - focus_rows = focus_widget.rows((maxcol,),True) - - # 2. collect the widgets above the focus - pos = focus_pos - fill_lines = offset_rows - fill_above = [] - top_pos = pos - while fill_lines > 0: - prev, pos = self.body.get_prev( pos ) - if prev is None: # run out of widgets above? - offset_rows -= fill_lines - break - top_pos = pos - - p_rows = prev.rows( (maxcol,) ) - if p_rows: # filter out 0-height widgets - fill_above.append( (prev, pos, p_rows) ) - if p_rows > fill_lines: # crosses top edge? - trim_top = p_rows-fill_lines - break - fill_lines -= p_rows - - trim_bottom = focus_rows + offset_rows - inset_rows - maxrow - if trim_bottom < 0: trim_bottom = 0 - - # 3. collect the widgets below the focus - pos = focus_pos - fill_lines = maxrow - focus_rows - offset_rows + inset_rows - fill_below = [] - while fill_lines > 0: - next, pos = self.body.get_next( pos ) - if next is None: # run out of widgets below? - break - - n_rows = next.rows( (maxcol,) ) - if n_rows: # filter out 0-height widgets - fill_below.append( (next, pos, n_rows) ) - if n_rows > fill_lines: # crosses bottom edge? - trim_bottom = n_rows-fill_lines - fill_lines -= n_rows - break - fill_lines -= n_rows - - # 4. fill from top again if necessary & possible - fill_lines = max(0, fill_lines) - - if fill_lines >0 and trim_top >0: - if fill_lines <= trim_top: - trim_top -= fill_lines - offset_rows += fill_lines - fill_lines = 0 - else: - fill_lines -= trim_top - offset_rows += trim_top - trim_top = 0 - pos = top_pos - while fill_lines > 0: - prev, pos = self.body.get_prev( pos ) - if prev is None: - break - - p_rows = prev.rows( (maxcol,) ) - fill_above.append( (prev, pos, p_rows) ) - if p_rows > fill_lines: # more than required - trim_top = p_rows-fill_lines - offset_rows += fill_lines - break - fill_lines -= p_rows - offset_rows += p_rows - - # 5. return the interesting bits - return ((offset_rows - inset_rows, focus_widget, - focus_pos, focus_rows, cursor ), - (trim_top, fill_above), (trim_bottom, fill_below)) - - - def render(self, size, focus=False ): - """ - Render ListBox and return canvas. - - see :meth:`Widget.render` for details - """ - (maxcol, maxrow) = size - - middle, top, bottom = self.calculate_visible( - (maxcol, maxrow), focus=focus) - if middle is None: - return SolidCanvas(" ", maxcol, maxrow) - - _ignore, focus_widget, focus_pos, focus_rows, cursor = middle - trim_top, fill_above = top - trim_bottom, fill_below = bottom - - combinelist = [] - rows = 0 - fill_above.reverse() # fill_above is in bottom-up order - for widget,w_pos,w_rows in fill_above: - canvas = widget.render((maxcol,)) - if w_rows != canvas.rows(): - raise ListBoxError, "Widget %r at position %r within listbox calculated %d rows but rendered %d!"% (widget,w_pos,w_rows, canvas.rows()) - rows += w_rows - combinelist.append((canvas, w_pos, False)) - - focus_canvas = focus_widget.render((maxcol,), focus=focus) - - if focus_canvas.rows() != focus_rows: - raise ListBoxError, "Focus Widget %r at position %r within listbox calculated %d rows but rendered %d!"% (focus_widget,focus_pos,focus_rows, focus_canvas.rows()) - c_cursor = focus_canvas.cursor - if cursor != c_cursor: - raise ListBoxError, "Focus Widget %r at position %r within listbox calculated cursor coords %r but rendered cursor coords %r!" %(focus_widget,focus_pos,cursor,c_cursor) - - rows += focus_rows - combinelist.append((focus_canvas, focus_pos, True)) - - for widget,w_pos,w_rows in fill_below: - canvas = widget.render((maxcol,)) - if w_rows != canvas.rows(): - raise ListBoxError, "Widget %r at position %r within listbox calculated %d rows but rendered %d!"% (widget,w_pos,w_rows, canvas.rows()) - rows += w_rows - combinelist.append((canvas, w_pos, False)) - - final_canvas = CanvasCombine(combinelist) - - if trim_top: - final_canvas.trim(trim_top) - rows -= trim_top - if trim_bottom: - final_canvas.trim_end(trim_bottom) - rows -= trim_bottom - - if rows > maxrow: - raise ListBoxError, "Listbox contents too long! Probably urwid's fault (please report): %r" % ((top,middle,bottom),) - - if rows < maxrow: - bottom_pos = focus_pos - if fill_below: bottom_pos = fill_below[-1][1] - if trim_bottom != 0 or self.body.get_next(bottom_pos) != (None,None): - raise ListBoxError, "Listbox contents too short! Probably urwid's fault (please report): %r" % ((top,middle,bottom),) - final_canvas.pad_trim_top_bottom(0, maxrow - rows) - - return final_canvas - - - def get_cursor_coords(self, size): - """ - See :meth:`Widget.get_cursor_coords` for details - """ - (maxcol, maxrow) = size - - middle, top, bottom = self.calculate_visible( - (maxcol, maxrow), True) - if middle is None: - return None - - offset_inset, _ignore1, _ignore2, _ignore3, cursor = middle - if not cursor: - return None - - x, y = cursor - y += offset_inset - if y < 0 or y >= maxrow: - return None - return (x, y) - - - def set_focus_valign(self, valign): - """Set the focus widget's display offset and inset. - - :param valign: one of: - 'top', 'middle', 'bottom' - ('fixed top', rows) - ('fixed bottom', rows) - ('relative', percentage 0=top 100=bottom) - """ - vt, va = normalize_valign(valign,ListBoxError) - self.set_focus_valign_pending = vt, va - - - def set_focus(self, position, coming_from=None): - """ - Set the focus position and try to keep the old focus in view. - - :param position: a position compatible with :meth:`self.body.set_focus` - :param coming_from: set to 'above' or 'below' if you know that - old position is above or below the new position. - :type coming_from: str - """ - if coming_from not in ('above', 'below', None): - raise ListBoxError("coming_from value invalid: %r" % - (coming_from,)) - focus_widget, focus_pos = self.body.get_focus() - if focus_widget is None: - raise IndexError("Can't set focus, ListBox is empty") - - self.set_focus_pending = coming_from, focus_widget, focus_pos - self.body.set_focus(position) - - def get_focus(self): - """ - Return a `(focus widget, focus position)` tuple, for backwards - compatibility. You may also use the new standard container - properties :attr:`focus` and :attr:`focus_position` to read these values. - """ - return self.body.get_focus() - - def _get_focus(self): - """ - Return the widget in focus according to our :obj:`list walker `. - """ - return self.body.get_focus()[0] - focus = property(_get_focus, - doc="the child widget in focus or None when ListBox is empty") - - def _get_focus_position(self): - """ - Return the list walker position of the widget in focus. The type - of value returned depends on the :obj:`list walker `. - - """ - w, pos = self.body.get_focus() - if w is None: - raise IndexError, "No focus_position, ListBox is empty" - return pos - focus_position = property(_get_focus_position, set_focus, doc=""" - the position of child widget in focus. The valid values for this - position depend on the list walker in use. - :exc:`IndexError` will be raised by reading this property when the - ListBox is empty or setting this property to an invalid position. - """) - - def _contents(self): - class ListBoxContents(object): - __getitem__ = self._contents__getitem__ - return ListBoxContents() - def _contents__getitem__(self, key): - # try list walker protocol v2 first - getitem = getattr(self.body, '__getitem__', None) - if getitem: - try: - return (getitem(key), None) - except (IndexError, KeyError): - raise KeyError("ListBox.contents key not found: %r" % (key,)) - # fall back to v1 - w, old_focus = self.body.get_focus() - try: - try: - self.body.set_focus(key) - return self.body.get_focus()[0] - except (IndexError, KeyError): - raise KeyError("ListBox.contents key not found: %r" % (key,)) - finally: - self.body.set_focus(old_focus) - contents = property(lambda self: self._contents, doc=""" - An object that allows reading widgets from the ListBox's list - walker as a `(widget, options)` tuple. `None` is currently the only - value for options. - - .. warning:: - - This object may not be used to set or iterate over contents. - - You must use the list walker stored as - :attr:`.body` to perform manipulation and iteration, if supported. - """) - - def options(self): - """ - There are currently no options for ListBox contents. - - Return None as a placeholder for future options. - """ - return None - - def _set_focus_valign_complete(self, size, focus): - """ - Finish setting the offset and inset now that we have have a - maxcol & maxrow. - """ - (maxcol, maxrow) = size - vt,va = self.set_focus_valign_pending - self.set_focus_valign_pending = None - self.set_focus_pending = None - - focus_widget, focus_pos = self.body.get_focus() - if focus_widget is None: - return - - rows = focus_widget.rows((maxcol,), focus) - rtop, rbot = calculate_top_bottom_filler(maxrow, - vt, va, GIVEN, rows, None, 0, 0) - - self.shift_focus((maxcol, maxrow), rtop) - - def _set_focus_first_selectable(self, size, focus): - """ - Choose the first visible, selectable widget below the - current focus as the focus widget. - """ - (maxcol, maxrow) = size - self.set_focus_valign_pending = None - self.set_focus_pending = None - middle, top, bottom = self.calculate_visible( - (maxcol, maxrow), focus=focus) - if middle is None: - return - - row_offset, focus_widget, focus_pos, focus_rows, cursor = middle - trim_top, fill_above = top - trim_bottom, fill_below = bottom - - if focus_widget.selectable(): - return - - if trim_bottom: - fill_below = fill_below[:-1] - new_row_offset = row_offset + focus_rows - for widget, pos, rows in fill_below: - if widget.selectable(): - self.body.set_focus(pos) - self.shift_focus((maxcol, maxrow), - new_row_offset) - return - new_row_offset += rows - - def _set_focus_complete(self, size, focus): - """ - Finish setting the position now that we have maxcol & maxrow. - """ - (maxcol, maxrow) = size - self._invalidate() - if self.set_focus_pending == "first selectable": - return self._set_focus_first_selectable( - (maxcol,maxrow), focus) - if self.set_focus_valign_pending is not None: - return self._set_focus_valign_complete( - (maxcol,maxrow), focus) - coming_from, focus_widget, focus_pos = self.set_focus_pending - self.set_focus_pending = None - - # new position - new_focus_widget, position = self.body.get_focus() - if focus_pos == position: - # do nothing - return - - # restore old focus temporarily - self.body.set_focus(focus_pos) - - middle,top,bottom=self.calculate_visible((maxcol,maxrow),focus) - focus_offset, focus_widget, focus_pos, focus_rows, cursor=middle - trim_top, fill_above = top - trim_bottom, fill_below = bottom - - offset = focus_offset - for widget, pos, rows in fill_above: - offset -= rows - if pos == position: - self.change_focus((maxcol, maxrow), pos, - offset, 'below' ) - return - - offset = focus_offset + focus_rows - for widget, pos, rows in fill_below: - if pos == position: - self.change_focus((maxcol, maxrow), pos, - offset, 'above' ) - return - offset += rows - - # failed to find widget among visible widgets - self.body.set_focus( position ) - widget, position = self.body.get_focus() - rows = widget.rows((maxcol,), focus) - - if coming_from=='below': - offset = 0 - elif coming_from=='above': - offset = maxrow-rows - else: - offset = (maxrow-rows) // 2 - self.shift_focus((maxcol, maxrow), offset) - - - def shift_focus(self, size, offset_inset): - """ - Move the location of the current focus relative to the top. - This is used internally by methods that know the widget's *size*. - - See also :meth:`.set_focus_valign`. - - :param size: see :meth:`Widget.render` for details - :param offset_inset: either the number of rows between the - top of the listbox and the start of the focus widget (+ve - value) or the number of lines of the focus widget hidden off - the top edge of the listbox (-ve value) or ``0`` if the top edge - of the focus widget is aligned with the top edge of the - listbox. - :type offset_inset: int - """ - (maxcol, maxrow) = size - - if offset_inset >= 0: - if offset_inset >= maxrow: - raise ListBoxError, "Invalid offset_inset: %r, only %r rows in list box"% (offset_inset, maxrow) - self.offset_rows = offset_inset - self.inset_fraction = (0,1) - else: - target, _ignore = self.body.get_focus() - tgt_rows = target.rows( (maxcol,), True ) - if offset_inset + tgt_rows <= 0: - raise ListBoxError, "Invalid offset_inset: %r, only %r rows in target!" %(offset_inset, tgt_rows) - self.offset_rows = 0 - self.inset_fraction = (-offset_inset,tgt_rows) - self._invalidate() - - def update_pref_col_from_focus(self, size): - """Update self.pref_col from the focus widget.""" - # TODO: should this not be private? - (maxcol, maxrow) = size - - widget, old_pos = self.body.get_focus() - if widget is None: return - - pref_col = None - if hasattr(widget,'get_pref_col'): - pref_col = widget.get_pref_col((maxcol,)) - if pref_col is None and hasattr(widget,'get_cursor_coords'): - coords = widget.get_cursor_coords((maxcol,)) - if type(coords) == tuple: - pref_col,y = coords - if pref_col is not None: - self.pref_col = pref_col - - - def change_focus(self, size, position, - offset_inset = 0, coming_from = None, - cursor_coords = None, snap_rows = None): - """ - Change the current focus widget. - This is used internally by methods that know the widget's *size*. - - See also :meth:`.set_focus`. - - :param size: see :meth:`Widget.render` for details - :param position: a position compatible with :meth:`self.body.set_focus` - :param offset_inset: either the number of rows between the - top of the listbox and the start of the focus widget (+ve - value) or the number of lines of the focus widget hidden off - the top edge of the listbox (-ve value) or 0 if the top edge - of the focus widget is aligned with the top edge of the - listbox (default if unspecified) - :type offset_inset: int - :param coming_from: either 'above', 'below' or unspecified `None` - :type coming_from: str - :param cursor_coords: (x, y) tuple indicating the desired - column and row for the cursor, a (x,) tuple indicating only - the column for the cursor, or unspecified - :type cursor_coords: (int, int) - :param snap_rows: the maximum number of extra rows to scroll - when trying to "snap" a selectable focus into the view - :type snap_rows: int - """ - (maxcol, maxrow) = size - - # update pref_col before change - if cursor_coords: - self.pref_col = cursor_coords[0] - else: - self.update_pref_col_from_focus((maxcol,maxrow)) - - self._invalidate() - self.body.set_focus(position) - target, _ignore = self.body.get_focus() - tgt_rows = target.rows( (maxcol,), True) - if snap_rows is None: - snap_rows = maxrow - 1 - - # "snap" to selectable widgets - align_top = 0 - align_bottom = maxrow - tgt_rows - - if ( coming_from == 'above' - and target.selectable() - and offset_inset > align_bottom ): - if snap_rows >= offset_inset - align_bottom: - offset_inset = align_bottom - elif snap_rows >= offset_inset - align_top: - offset_inset = align_top - else: - offset_inset -= snap_rows - - if ( coming_from == 'below' - and target.selectable() - and offset_inset < align_top ): - if snap_rows >= align_top - offset_inset: - offset_inset = align_top - elif snap_rows >= align_bottom - offset_inset: - offset_inset = align_bottom - else: - offset_inset += snap_rows - - # convert offset_inset to offset_rows or inset_fraction - if offset_inset >= 0: - self.offset_rows = offset_inset - self.inset_fraction = (0,1) - else: - if offset_inset + tgt_rows <= 0: - raise ListBoxError, "Invalid offset_inset: %s, only %s rows in target!" %(offset_inset, tgt_rows) - self.offset_rows = 0 - self.inset_fraction = (-offset_inset,tgt_rows) - - if cursor_coords is None: - if coming_from is None: - return # must either know row or coming_from - cursor_coords = (self.pref_col,) - - if not hasattr(target,'move_cursor_to_coords'): - return - - attempt_rows = [] - - if len(cursor_coords) == 1: - # only column (not row) specified - # start from closest edge and move inwards - (pref_col,) = cursor_coords - if coming_from=='above': - attempt_rows = range( 0, tgt_rows ) - else: - assert coming_from == 'below', "must specify coming_from ('above' or 'below') if cursor row is not specified" - attempt_rows = range( tgt_rows, -1, -1) - else: - # both column and row specified - # start from preferred row and move back to closest edge - (pref_col, pref_row) = cursor_coords - if pref_row < 0 or pref_row >= tgt_rows: - raise ListBoxError, "cursor_coords row outside valid range for target. pref_row:%r target_rows:%r"%(pref_row,tgt_rows) - - if coming_from=='above': - attempt_rows = range( pref_row, -1, -1 ) - elif coming_from=='below': - attempt_rows = range( pref_row, tgt_rows ) - else: - attempt_rows = [pref_row] - - for row in attempt_rows: - if target.move_cursor_to_coords((maxcol,),pref_col,row): - break - - def get_focus_offset_inset(self, size): - """Return (offset rows, inset rows) for focus widget.""" - (maxcol, maxrow) = size - focus_widget, pos = self.body.get_focus() - focus_rows = focus_widget.rows((maxcol,), True) - offset_rows = self.offset_rows - inset_rows = 0 - if offset_rows == 0: - inum, iden = self.inset_fraction - if inum < 0 or iden < 0 or inum >= iden: - raise ListBoxError, "Invalid inset_fraction: %r"%(self.inset_fraction,) - inset_rows = focus_rows * inum // iden - if inset_rows and inset_rows >= focus_rows: - raise ListBoxError, "urwid inset_fraction error (please report)" - return offset_rows, inset_rows - - - def make_cursor_visible(self, size): - """Shift the focus widget so that its cursor is visible.""" - (maxcol, maxrow) = size - - focus_widget, pos = self.body.get_focus() - if focus_widget is None: - return - if not focus_widget.selectable(): - return - if not hasattr(focus_widget,'get_cursor_coords'): - return - cursor = focus_widget.get_cursor_coords((maxcol,)) - if cursor is None: - return - cx, cy = cursor - offset_rows, inset_rows = self.get_focus_offset_inset( - (maxcol, maxrow)) - - if cy < inset_rows: - self.shift_focus( (maxcol,maxrow), - (cy) ) - return - - if offset_rows - inset_rows + cy >= maxrow: - self.shift_focus( (maxcol,maxrow), maxrow-cy-1 ) - return - - - def keypress(self, size, key): - """Move selection through the list elements scrolling when - necessary. 'up' and 'down' are first passed to widget in focus - in case that widget can handle them. 'page up' and 'page down' - are always handled by the ListBox. - - Keystrokes handled by this widget are: - 'up' up one line (or widget) - 'down' down one line (or widget) - 'page up' move cursor up one listbox length - 'page down' move cursor down one listbox length - """ - (maxcol, maxrow) = size - - if self.set_focus_pending or self.set_focus_valign_pending: - self._set_focus_complete( (maxcol,maxrow), focus=True ) - - focus_widget, pos = self.body.get_focus() - if focus_widget is None: # empty listbox, can't do anything - return key - - if self._command_map[key] not in [CURSOR_PAGE_UP, CURSOR_PAGE_DOWN]: - if focus_widget.selectable(): - key = focus_widget.keypress((maxcol,),key) - if key is None: - self.make_cursor_visible((maxcol,maxrow)) - return - - def actual_key(unhandled): - if unhandled: - return key - - # pass off the heavy lifting - if self._command_map[key] == CURSOR_UP: - return actual_key(self._keypress_up((maxcol, maxrow))) - - if self._command_map[key] == CURSOR_DOWN: - return actual_key(self._keypress_down((maxcol, maxrow))) - - if self._command_map[key] == CURSOR_PAGE_UP: - return actual_key(self._keypress_page_up((maxcol, maxrow))) - - if self._command_map[key] == CURSOR_PAGE_DOWN: - return actual_key(self._keypress_page_down((maxcol, maxrow))) - - return key - - - def _keypress_up(self, size): - (maxcol, maxrow) = size - - middle, top, bottom = self.calculate_visible( - (maxcol,maxrow), True) - if middle is None: return True - - focus_row_offset,focus_widget,focus_pos,_ignore,cursor = middle - trim_top, fill_above = top - - row_offset = focus_row_offset - - # look for selectable widget above - pos = focus_pos - widget = None - for widget, pos, rows in fill_above: - row_offset -= rows - if rows and widget.selectable(): - # this one will do - self.change_focus((maxcol,maxrow), pos, - row_offset, 'below') - return - - # at this point we must scroll - row_offset += 1 - self._invalidate() - - while row_offset > 0: - # need to scroll in another candidate widget - widget, pos = self.body.get_prev(pos) - if widget is None: - # cannot scroll any further - return True # keypress not handled - rows = widget.rows((maxcol,), True) - row_offset -= rows - if rows and widget.selectable(): - # this one will do - self.change_focus((maxcol,maxrow), pos, - row_offset, 'below') - return - - if not focus_widget.selectable() or focus_row_offset+1>=maxrow: - # just take top one if focus is not selectable - # or if focus has moved out of view - if widget is None: - self.shift_focus((maxcol,maxrow), row_offset) - return - self.change_focus((maxcol,maxrow), pos, - row_offset, 'below') - return - - # check if cursor will stop scroll from taking effect - if cursor is not None: - x,y = cursor - if y+focus_row_offset+1 >= maxrow: - # cursor position is a problem, - # choose another focus - if widget is None: - # try harder to get prev widget - widget, pos = self.body.get_prev(pos) - if widget is None: - return # can't do anything - rows = widget.rows((maxcol,), True) - row_offset -= rows - - if -row_offset >= rows: - # must scroll further than 1 line - row_offset = - (rows-1) - - self.change_focus((maxcol,maxrow),pos, - row_offset, 'below') - return - - # if all else fails, just shift the current focus. - self.shift_focus((maxcol,maxrow), focus_row_offset+1) - - - def _keypress_down(self, size): - (maxcol, maxrow) = size - - middle, top, bottom = self.calculate_visible( - (maxcol,maxrow), True) - if middle is None: return True - - focus_row_offset,focus_widget,focus_pos,focus_rows,cursor=middle - trim_bottom, fill_below = bottom - - row_offset = focus_row_offset + focus_rows - rows = focus_rows - - # look for selectable widget below - pos = focus_pos - widget = None - for widget, pos, rows in fill_below: - if rows and widget.selectable(): - # this one will do - self.change_focus((maxcol,maxrow), pos, - row_offset, 'above') - return - row_offset += rows - - # at this point we must scroll - row_offset -= 1 - self._invalidate() - - while row_offset < maxrow: - # need to scroll in another candidate widget - widget, pos = self.body.get_next(pos) - if widget is None: - # cannot scroll any further - return True # keypress not handled - rows = widget.rows((maxcol,)) - if rows and widget.selectable(): - # this one will do - self.change_focus((maxcol,maxrow), pos, - row_offset, 'above') - return - row_offset += rows - - if not focus_widget.selectable() or focus_row_offset+focus_rows-1 <= 0: - # just take bottom one if current is not selectable - # or if focus has moved out of view - if widget is None: - self.shift_focus((maxcol,maxrow), - row_offset-rows) - return - # FIXME: catch this bug in testcase - #self.change_focus((maxcol,maxrow), pos, - # row_offset+rows, 'above') - self.change_focus((maxcol,maxrow), pos, - row_offset-rows, 'above') - return - - # check if cursor will stop scroll from taking effect - if cursor is not None: - x,y = cursor - if y+focus_row_offset-1 < 0: - # cursor position is a problem, - # choose another focus - if widget is None: - # try harder to get next widget - widget, pos = self.body.get_next(pos) - if widget is None: - return # can't do anything - else: - row_offset -= rows - - if row_offset >= maxrow: - # must scroll further than 1 line - row_offset = maxrow-1 - - self.change_focus((maxcol,maxrow),pos, - row_offset, 'above', ) - return - - # if all else fails, keep the current focus. - self.shift_focus((maxcol,maxrow), focus_row_offset-1) - - - def _keypress_page_up(self, size): - (maxcol, maxrow) = size - - middle, top, bottom = self.calculate_visible( - (maxcol,maxrow), True) - if middle is None: return True - - row_offset, focus_widget, focus_pos, focus_rows, cursor = middle - trim_top, fill_above = top - - # topmost_visible is row_offset rows above top row of - # focus (+ve) or -row_offset rows below top row of focus (-ve) - topmost_visible = row_offset - - # scroll_from_row is (first match) - # 1. topmost visible row if focus is not selectable - # 2. row containing cursor if focus has a cursor - # 3. top row of focus widget if it is visible - # 4. topmost visible row otherwise - if not focus_widget.selectable(): - scroll_from_row = topmost_visible - elif cursor is not None: - x,y = cursor - scroll_from_row = -y - elif row_offset >= 0: - scroll_from_row = 0 - else: - scroll_from_row = topmost_visible - - # snap_rows is maximum extra rows to scroll when - # snapping to new a focus - snap_rows = topmost_visible - scroll_from_row - - # move row_offset to the new desired value (1 "page" up) - row_offset = scroll_from_row + maxrow - - # not used below: - scroll_from_row = topmost_visible = None - - - # gather potential target widgets - t = [] - # add current focus - t.append((row_offset,focus_widget,focus_pos,focus_rows)) - pos = focus_pos - # include widgets from calculate_visible(..) - for widget, pos, rows in fill_above: - row_offset -= rows - t.append( (row_offset, widget, pos, rows) ) - # add newly visible ones, including within snap_rows - snap_region_start = len(t) - while row_offset > -snap_rows: - widget, pos = self.body.get_prev(pos) - if widget is None: break - rows = widget.rows((maxcol,)) - row_offset -= rows - # determine if one below puts current one into snap rgn - if row_offset > 0: - snap_region_start += 1 - t.append( (row_offset, widget, pos, rows) ) - - # if we can't fill the top we need to adjust the row offsets - row_offset, w, p, r = t[-1] - if row_offset > 0: - adjust = - row_offset - t = [(ro+adjust, w, p, r) for (ro,w,p,r) in t] - - # if focus_widget (first in t) is off edge, remove it - row_offset, w, p, r = t[0] - if row_offset >= maxrow: - del t[0] - snap_region_start -= 1 - - # we'll need this soon - self.update_pref_col_from_focus((maxcol,maxrow)) - - # choose the topmost selectable and (newly) visible widget - # search within snap_rows then visible region - search_order = ( range( snap_region_start, len(t)) - + range( snap_region_start-1, -1, -1 ) ) - #assert 0, repr((t, search_order)) - bad_choices = [] - cut_off_selectable_chosen = 0 - for i in search_order: - row_offset, widget, pos, rows = t[i] - if not widget.selectable(): - continue - - if not rows: - continue - - # try selecting this widget - pref_row = max(0, -row_offset) - - # if completely within snap region, adjust row_offset - if rows + row_offset <= 0: - self.change_focus( (maxcol,maxrow), pos, - -(rows-1), 'below', - (self.pref_col, rows-1), - snap_rows-((-row_offset)-(rows-1))) - else: - self.change_focus( (maxcol,maxrow), pos, - row_offset, 'below', - (self.pref_col, pref_row), snap_rows ) - - # if we're as far up as we can scroll, take this one - if (fill_above and self.body.get_prev(fill_above[-1][1]) - == (None,None) ): - pass #return - - # find out where that actually puts us - middle, top, bottom = self.calculate_visible( - (maxcol,maxrow), True) - act_row_offset, _ign1, _ign2, _ign3, _ign4 = middle - - # discard chosen widget if it will reduce scroll amount - # because of a fixed cursor (absolute last resort) - if act_row_offset > row_offset+snap_rows: - bad_choices.append(i) - continue - if act_row_offset < row_offset: - bad_choices.append(i) - continue - - # also discard if off top edge (second last resort) - if act_row_offset < 0: - bad_choices.append(i) - cut_off_selectable_chosen = 1 - continue - - return - - # anything selectable is better than what follows: - if cut_off_selectable_chosen: - return - - if fill_above and focus_widget.selectable(): - # if we're at the top and have a selectable, return - if self.body.get_prev(fill_above[-1][1]) == (None,None): - pass #return - - # if still none found choose the topmost widget - good_choices = [j for j in search_order if j not in bad_choices] - for i in good_choices + search_order: - row_offset, widget, pos, rows = t[i] - if pos == focus_pos: continue - - if not rows: # never focus a 0-height widget - continue - - # if completely within snap region, adjust row_offset - if rows + row_offset <= 0: - snap_rows -= (-row_offset) - (rows-1) - row_offset = -(rows-1) - - self.change_focus( (maxcol,maxrow), pos, - row_offset, 'below', None, - snap_rows ) - return - - # no choices available, just shift current one - self.shift_focus((maxcol, maxrow), min(maxrow-1,row_offset)) - - # final check for pathological case where we may fall short - middle, top, bottom = self.calculate_visible( - (maxcol,maxrow), True) - act_row_offset, _ign1, pos, _ign2, _ign3 = middle - if act_row_offset >= row_offset: - # no problem - return - - # fell short, try to select anything else above - if not t: - return - _ign1, _ign2, pos, _ign3 = t[-1] - widget, pos = self.body.get_prev(pos) - if widget is None: - # no dice, we're stuck here - return - # bring in only one row if possible - rows = widget.rows((maxcol,), True) - self.change_focus((maxcol,maxrow), pos, -(rows-1), - 'below', (self.pref_col, rows-1), 0 ) - - - def _keypress_page_down(self, size): - (maxcol, maxrow) = size - - middle, top, bottom = self.calculate_visible( - (maxcol,maxrow), True) - if middle is None: return True - - row_offset, focus_widget, focus_pos, focus_rows, cursor = middle - trim_bottom, fill_below = bottom - - # bottom_edge is maxrow-focus_pos rows below top row of focus - bottom_edge = maxrow - row_offset - - # scroll_from_row is (first match) - # 1. bottom edge if focus is not selectable - # 2. row containing cursor + 1 if focus has a cursor - # 3. bottom edge of focus widget if it is visible - # 4. bottom edge otherwise - if not focus_widget.selectable(): - scroll_from_row = bottom_edge - elif cursor is not None: - x,y = cursor - scroll_from_row = y + 1 - elif bottom_edge >= focus_rows: - scroll_from_row = focus_rows - else: - scroll_from_row = bottom_edge - - # snap_rows is maximum extra rows to scroll when - # snapping to new a focus - snap_rows = bottom_edge - scroll_from_row - - # move row_offset to the new desired value (1 "page" down) - row_offset = -scroll_from_row - - # not used below: - scroll_from_row = bottom_edge = None - - - # gather potential target widgets - t = [] - # add current focus - t.append((row_offset,focus_widget,focus_pos,focus_rows)) - pos = focus_pos - row_offset += focus_rows - # include widgets from calculate_visible(..) - for widget, pos, rows in fill_below: - t.append( (row_offset, widget, pos, rows) ) - row_offset += rows - # add newly visible ones, including within snap_rows - snap_region_start = len(t) - while row_offset < maxrow+snap_rows: - widget, pos = self.body.get_next(pos) - if widget is None: break - rows = widget.rows((maxcol,)) - t.append( (row_offset, widget, pos, rows) ) - row_offset += rows - # determine if one above puts current one into snap rgn - if row_offset < maxrow: - snap_region_start += 1 - - # if we can't fill the bottom we need to adjust the row offsets - row_offset, w, p, rows = t[-1] - if row_offset + rows < maxrow: - adjust = maxrow - (row_offset + rows) - t = [(ro+adjust, w, p, r) for (ro,w,p,r) in t] - - # if focus_widget (first in t) is off edge, remove it - row_offset, w, p, rows = t[0] - if row_offset+rows <= 0: - del t[0] - snap_region_start -= 1 - - # we'll need this soon - self.update_pref_col_from_focus((maxcol,maxrow)) - - # choose the bottommost selectable and (newly) visible widget - # search within snap_rows then visible region - search_order = ( range( snap_region_start, len(t)) - + range( snap_region_start-1, -1, -1 ) ) - #assert 0, repr((t, search_order)) - bad_choices = [] - cut_off_selectable_chosen = 0 - for i in search_order: - row_offset, widget, pos, rows = t[i] - if not widget.selectable(): - continue - - if not rows: - continue - - # try selecting this widget - pref_row = min(maxrow-row_offset-1, rows-1) - - # if completely within snap region, adjust row_offset - if row_offset >= maxrow: - self.change_focus( (maxcol,maxrow), pos, - maxrow-1, 'above', - (self.pref_col, 0), - snap_rows+maxrow-row_offset-1 ) - else: - self.change_focus( (maxcol,maxrow), pos, - row_offset, 'above', - (self.pref_col, pref_row), snap_rows ) - - # find out where that actually puts us - middle, top, bottom = self.calculate_visible( - (maxcol,maxrow), True) - act_row_offset, _ign1, _ign2, _ign3, _ign4 = middle - - # discard chosen widget if it will reduce scroll amount - # because of a fixed cursor (absolute last resort) - if act_row_offset < row_offset-snap_rows: - bad_choices.append(i) - continue - if act_row_offset > row_offset: - bad_choices.append(i) - continue - - # also discard if off top edge (second last resort) - if act_row_offset+rows > maxrow: - bad_choices.append(i) - cut_off_selectable_chosen = 1 - continue - - return - - # anything selectable is better than what follows: - if cut_off_selectable_chosen: - return - - # if still none found choose the bottommost widget - good_choices = [j for j in search_order if j not in bad_choices] - for i in good_choices + search_order: - row_offset, widget, pos, rows = t[i] - if pos == focus_pos: continue - - if not rows: # never focus a 0-height widget - continue - - # if completely within snap region, adjust row_offset - if row_offset >= maxrow: - snap_rows -= snap_rows+maxrow-row_offset-1 - row_offset = maxrow-1 - - self.change_focus( (maxcol,maxrow), pos, - row_offset, 'above', None, - snap_rows ) - return - - - # no choices available, just shift current one - self.shift_focus((maxcol, maxrow), max(1-focus_rows,row_offset)) - - # final check for pathological case where we may fall short - middle, top, bottom = self.calculate_visible( - (maxcol,maxrow), True) - act_row_offset, _ign1, pos, _ign2, _ign3 = middle - if act_row_offset <= row_offset: - # no problem - return - - # fell short, try to select anything else below - if not t: - return - _ign1, _ign2, pos, _ign3 = t[-1] - widget, pos = self.body.get_next(pos) - if widget is None: - # no dice, we're stuck here - return - # bring in only one row if possible - rows = widget.rows((maxcol,), True) - self.change_focus((maxcol,maxrow), pos, maxrow-1, - 'above', (self.pref_col, 0), 0 ) - - def mouse_event(self, size, event, button, col, row, focus): - """ - Pass the event to the contained widgets. - May change focus on button 1 press. - """ - (maxcol, maxrow) = size - middle, top, bottom = self.calculate_visible((maxcol, maxrow), - focus=True) - if middle is None: - return False - - _ignore, focus_widget, focus_pos, focus_rows, cursor = middle - trim_top, fill_above = top - _ignore, fill_below = bottom - - fill_above.reverse() # fill_above is in bottom-up order - w_list = ( fill_above + - [ (focus_widget, focus_pos, focus_rows) ] + - fill_below ) - - wrow = -trim_top - for w, w_pos, w_rows in w_list: - if wrow + w_rows > row: - break - wrow += w_rows - else: - return False - - focus = focus and w == focus_widget - if is_mouse_press(event) and button==1: - if w.selectable(): - self.change_focus((maxcol,maxrow), w_pos, wrow) - - if not hasattr(w,'mouse_event'): - return False - - return w.mouse_event((maxcol,), event, button, col, row-wrow, - focus) - - - def ends_visible(self, size, focus=False): - """ - Return a list that may contain ``'top'`` and/or ``'bottom'``. - - i.e. this function will return one of: [], [``'top'``], - [``'bottom'``] or [``'top'``, ``'bottom'``]. - - convenience function for checking whether the top and bottom - of the list are visible - """ - (maxcol, maxrow) = size - l = [] - middle,top,bottom = self.calculate_visible( (maxcol,maxrow), - focus=focus ) - if middle is None: # empty listbox - return ['top','bottom'] - trim_top, above = top - trim_bottom, below = bottom - - if trim_bottom == 0: - row_offset, w, pos, rows, c = middle - row_offset += rows - for w, pos, rows in below: - row_offset += rows - if row_offset < maxrow: - l.append('bottom') - elif self.body.get_next(pos) == (None,None): - l.append('bottom') - - if trim_top == 0: - row_offset, w, pos, rows, c = middle - for w, pos, rows in above: - row_offset -= rows - if self.body.get_prev(pos) == (None,None): - l.insert(0, 'top') - - return l - - def __iter__(self): - """ - Return an iterator over the positions in this ListBox. - - If self.body does not implement positions() then iterate - from the focus widget down to the bottom, then from above - the focus up to the top. This is the best we can do with - a minimal list walker implementation. - """ - positions_fn = getattr(self.body, 'positions', None) - if positions_fn: - for pos in positions_fn(): - yield pos - return - - focus_widget, focus_pos = self.body.get_focus() - if focus_widget is None: - return - pos = focus_pos - while True: - yield pos - w, pos = self.body.get_next(pos) - if not w: break - pos = focus_pos - while True: - w, pos = self.body.get_prev(pos) - if not w: break - yield pos - - def __reversed__(self): - """ - Return a reversed iterator over the positions in this ListBox. - - If :attr:`body` does not implement :meth:`positions` then iterate - from above the focus widget up to the top, then from the focus - widget down to the bottom. Note that this is not actually the - reverse of what `__iter__()` produces, but this is the best we can - do with a minimal list walker implementation. - """ - positions_fn = getattr(self.body, 'positions', None) - if positions_fn: - for pos in positions_fn(reverse=True): - yield pos - return - - focus_widget, focus_pos = self.body.get_focus() - if focus_widget is None: - return - pos = focus_pos - while True: - w, pos = self.body.get_prev(pos) - if not w: break - yield pos - pos = focus_pos - while True: - yield pos - w, pos = self.body.get_next(pos) - if not w: break - - diff --git a/urwid/main_loop.py b/urwid/main_loop.py deleted file mode 100755 index 6ada032..0000000 --- a/urwid/main_loop.py +++ /dev/null @@ -1,1375 +0,0 @@ -#!/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/ - - -import time -import heapq -import select -import os -from functools import wraps -from weakref import WeakKeyDictionary - -try: - import fcntl -except ImportError: - pass # windows - -from urwid.util import StoppingContext, is_mouse_event -from urwid.compat import PYTHON3 -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 - - 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 Exception as e: - self.screen.stop() # clean up screen control - raise e - 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, 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 SelectEventLoop(object): - """ - Event loop based on :func:`select.select` - """ - - def __init__(self): - self._alarms = [] - self._watch_files = {} - self._idle_handle = 0 - self._idle_callbacks = {} - - 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 - heapq.heappush(self._alarms, (tm, callback)) - return (tm, callback) - - 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 = 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, alarm_callback = self._alarms.pop(0) - alarm_callback() - self._did_something = True - - for fd in ready: - self._watch_files[fd]() - self._did_something = True - - -class GLibEventLoop(object): - """ - 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() - - 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 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 - raise exc_info[0], exc_info[1], exc_info[2] - - 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(object): - """ 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(object): - """ - 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 - raise exc_info[0], exc_info[1], exc_info[2] - - 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(object): - """ - 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: - raise self._exc_info[0], self._exc_info[1], self._exc_info[2] - self._exc_info = None - - -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() diff --git a/urwid/monitored_list.py b/urwid/monitored_list.py deleted file mode 100755 index dc67c84..0000000 --- a/urwid/monitored_list.py +++ /dev/null @@ -1,496 +0,0 @@ -#!/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 urwid.compat import PYTHON3 - - -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(range(*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 range(start, stop, step): - focus += 1 - - # adjust for removed items - focus -= len(range(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() - diff --git a/urwid/old_str_util.py b/urwid/old_str_util.py deleted file mode 100755 index 83190f5..0000000 --- a/urwid/old_str_util.py +++ /dev/null @@ -1,368 +0,0 @@ -#!/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 print_function - -import re - -from urwid.compat import bytes, B, ord2 - -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, unicode): - 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, unicode): - 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, unicode): - 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() - diff --git a/urwid/raw_display.py b/urwid/raw_display.py deleted file mode 100644 index 0e76c3f..0000000 --- a/urwid/raw_display.py +++ /dev/null @@ -1,1030 +0,0 @@ -#!/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/ - -""" -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.register_palette_entry( None, 'default','default') - 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 - term = os.environ.get('TERM', '') - self.fg_bright_is_bold = not term.startswith("xterm") - self.bg_bright_is_blink = (term == "linux") - self.back_color_erase = not term.startswith("screen") - self._next_timeout = None - - # 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): - 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): - 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. - """ - signal.signal(signal.SIGWINCH, self._sigwinch_handler) - signal.signal(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. - """ - signal.signal(signal.SIGCONT, signal.SIG_DFL) - signal.signal(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 = [] - for fd in fds: - event_loop.watch_file(fd, wrapper) - 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 = 80, 24 - 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, (maxcol, maxrow), r ): - """Paint screen with rendered canvas.""" - 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(a): - a = self._pal_attrspec.get(a, a) - return isinstance(a, AttrSpec) and a.standout - - ins = None - o.append(set_cursor_home()) - cy = 0 - for row in r.content(): - y += 1 - if 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(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') - 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 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 + "4;" * a.underline + - "5;" * a.blink + "7;" * a.standout) - 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 - """ - - 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() diff --git a/urwid/signals.py b/urwid/signals.py deleted file mode 100644 index b716939..0000000 --- a/urwid/signals.py +++ /dev/null @@ -1,302 +0,0 @@ -#!/usr/bin/python -# -# Urwid signal dispatching -# 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/ - - -import itertools -import weakref - - -class MetaSignals(type): - """ - register the list of signals in the class varable signals, - including signals in superclasses. - """ - def __init__(cls, name, bases, d): - signals = d.get("signals", []) - for superclass in cls.__bases__: - signals.extend(getattr(superclass, 'signals', [])) - signals = dict([(x,None) for x in signals]).keys() - d["signals"] = signals - register_signal(cls, signals) - super(MetaSignals, cls).__init__(name, bases, d) - -def setdefaultattr(obj, name, value): - # like dict.setdefault() for object attributes - if hasattr(obj, name): - return getattr(obj, name) - setattr(obj, name, value) - return value - -class Key(object): - """ - Minimal class, whose only purpose is to produce objects with a - unique hash - """ - __slots__ = [] - -class Signals(object): - _signal_attr = '_urwid_signals' # attribute to attach to signal senders - - def __init__(self): - self._supported = {} - - def register(self, sig_cls, signals): - """ - :param sig_class: the class of an object that will be sending signals - :type sig_class: class - :param signals: a list of signals that may be sent, typically each - signal is represented by a string - :type signals: signal names - - This function must be called for a class before connecting any - signal callbacks or emiting any signals from that class' objects - """ - self._supported[sig_cls] = signals - - def connect(self, obj, name, callback, user_arg=None, weak_args=None, user_args=None): - """ - :param obj: the object sending a signal - :type obj: object - :param name: the signal to listen for, typically a string - :type name: signal name - :param callback: the function to call when that signal is sent - :type callback: function - :param user_arg: deprecated additional argument to callback (appended - after the arguments passed when the signal is - emitted). If None no arguments will be added. - Don't use this argument, use user_args instead. - :param weak_args: additional arguments passed to the callback - (before any arguments passed when the signal - is emitted and before any user_args). - - These arguments are stored as weak references - (but converted back into their original value - before passing them to callback) to prevent - any objects referenced (indirectly) from - weak_args from being kept alive just because - they are referenced by this signal handler. - - Use this argument only as a keyword argument, - since user_arg might be removed in the future. - :type weak_args: iterable - :param user_args: additional arguments to pass to the callback, - (before any arguments passed when the signal - is emitted but after any weak_args). - - Use this argument only as a keyword argument, - since user_arg might be removed in the future. - :type user_args: iterable - - When a matching signal is sent, callback will be called. The - arguments it receives will be the user_args passed at connect - time (as individual arguments) followed by all the positional - parameters sent with the signal. - - As an example of using weak_args, consider the following snippet: - - >>> import urwid - >>> debug = urwid.Text('') - >>> def handler(widget, newtext): - ... debug.set_text("Edit widget changed to %s" % newtext) - >>> edit = urwid.Edit('') - >>> key = urwid.connect_signal(edit, 'change', handler) - - If you now build some interface using "edit" and "debug", the - "debug" widget will show whatever you type in the "edit" widget. - However, if you remove all references to the "debug" widget, it - will still be kept alive by the signal handler. This because the - signal handler is a closure that (implicitly) references the - "edit" widget. If you want to allow the "debug" widget to be - garbage collected, you can create a "fake" or "weak" closure - (it's not really a closure, since it doesn't reference any - outside variables, so it's just a dynamic function): - - >>> debug = urwid.Text('') - >>> def handler(weak_debug, widget, newtext): - ... weak_debug.set_text("Edit widget changed to %s" % newtext) - >>> edit = urwid.Edit('') - >>> key = urwid.connect_signal(edit, 'change', handler, weak_args=[debug]) - - Here the weak_debug parameter in print_debug is the value passed - in the weak_args list to connect_signal. Note that the - weak_debug value passed is not a weak reference anymore, the - signals code transparently dereferences the weakref parameter - before passing it to print_debug. - - Returns a key associated by this signal handler, which can be - used to disconnect the signal later on using - urwid.disconnect_signal_by_key. Alternatively, the signal - handler can also be disconnected by calling - urwid.disconnect_signal, which doesn't need this key. - """ - - sig_cls = obj.__class__ - if not name in self._supported.get(sig_cls, []): - raise NameError("No such signal %r for object %r" % - (name, obj)) - - # Just generate an arbitrary (but unique) key - key = Key() - - signals = setdefaultattr(obj, self._signal_attr, {}) - handlers = signals.setdefault(name, []) - - # Remove the signal handler when any of the weakref'd arguments - # are garbage collected. Note that this means that the handlers - # dictionary can be modified _at any time_, so it should never - # be iterated directly (e.g. iterate only over .keys() and - # .items(), never over .iterkeys(), .iteritems() or the object - # itself). - # We let the callback keep a weakref to the object as well, to - # prevent a circular reference between the handler and the - # object (via the weakrefs, which keep strong references to - # their callbacks) from existing. - obj_weak = weakref.ref(obj) - def weakref_callback(weakref): - o = obj_weak() - if o: - try: - del getattr(o, self._signal_attr, {})[name][key] - except KeyError: - pass - - user_args = self._prepare_user_args(weak_args, user_args, weakref_callback) - handlers.append((key, callback, user_arg, user_args)) - - return key - - def _prepare_user_args(self, weak_args, user_args, callback = None): - # Turn weak_args into weakrefs and prepend them to user_args - return [weakref.ref(a, callback) for a in (weak_args or [])] + (user_args or []) - - - def disconnect(self, obj, name, callback, user_arg=None, weak_args=None, user_args=None): - """ - :param obj: the object to disconnect the signal from - :type obj: object - :param name: the signal to disconnect, typically a string - :type name: signal name - :param callback: the callback function passed to connect_signal - :type callback: function - :param user_arg: the user_arg parameter passed to connect_signal - :param weak_args: the weak_args parameter passed to connect_signal - :param user_args: the weak_args parameter passed to connect_signal - - This function will remove a callback from the list connected - to a signal with connect_signal(). The arguments passed should - be exactly the same as those passed to connect_signal(). - - If the callback is not connected or already disconnected, this - function will simply do nothing. - """ - signals = setdefaultattr(obj, self._signal_attr, {}) - if name not in signals: - return - - handlers = signals[name] - - # Do the same processing as in connect, so we can compare the - # resulting tuple. - user_args = self._prepare_user_args(weak_args, user_args) - - # Remove the given handler - for h in handlers: - if h[1:] == (callback, user_arg, user_args): - return self.disconnect_by_key(obj, name, h[0]) - - def disconnect_by_key(self, obj, name, key): - """ - :param obj: the object to disconnect the signal from - :type obj: object - :param name: the signal to disconnect, typically a string - :type name: signal name - :param key: the key for this signal handler, as returned by - connect_signal(). - :type key: Key - - This function will remove a callback from the list connected - to a signal with connect_signal(). The key passed should be the - value returned by connect_signal(). - - If the callback is not connected or already disconnected, this - function will simply do nothing. - """ - signals = setdefaultattr(obj, self._signal_attr, {}) - handlers = signals.get(name, []) - handlers[:] = [h for h in handlers if h[0] is not key] - - def emit(self, obj, name, *args): - """ - :param obj: the object sending a signal - :type obj: object - :param name: the signal to send, typically a string - :type name: signal name - :param \*args: zero or more positional arguments to pass to the signal - callback functions - - This function calls each of the callbacks connected to this signal - with the args arguments as positional parameters. - - This function returns True if any of the callbacks returned True. - """ - result = False - signals = getattr(obj, self._signal_attr, {}) - handlers = signals.get(name, []) - for key, callback, user_arg, user_args in handlers: - result |= self._call_callback(callback, user_arg, user_args, args) - return result - - def _call_callback(self, callback, user_arg, user_args, emit_args): - if user_args: - args_to_pass = [] - for arg in user_args: - if isinstance(arg, weakref.ReferenceType): - arg = arg() - if arg is None: - # If the weakref is None, the referenced object - # was cleaned up. We just skip the entire - # callback in this case. The weakref cleanup - # handler will have removed the callback when - # this happens, so no need to actually remove - # the callback here. - return False - args_to_pass.append(arg) - - args_to_pass.extend(emit_args) - else: - # Optimization: Don't create a new list when there are - # no user_args - args_to_pass = emit_args - - # The deprecated user_arg argument was added to the end - # instead of the beginning. - if user_arg is not None: - args_to_pass = itertools.chain(args_to_pass, (user_arg,)) - - return bool(callback(*args_to_pass)) - -_signals = Signals() -emit_signal = _signals.emit -register_signal = _signals.register -connect_signal = _signals.connect -disconnect_signal = _signals.disconnect -disconnect_signal_by_key = _signals.disconnect_by_key - diff --git a/urwid/split_repr.py b/urwid/split_repr.py deleted file mode 100755 index fb108b5..0000000 --- a/urwid/split_repr.py +++ /dev/null @@ -1,149 +0,0 @@ -#!/usr/bin/python -# -# Urwid split_repr helper 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 inspect import getargspec -from urwid.compat import PYTHON3, bytes - -def split_repr(self): - """ - Return a helpful description of the object using - self._repr_words() and self._repr_attrs() to add - to the description. This function may be used by - adding code to your class like this: - - >>> class Foo(object): - ... __repr__ = split_repr - ... def _repr_words(self): - ... return ["words", "here"] - ... def _repr_attrs(self): - ... return {'attrs': "appear too"} - >>> Foo() - - >>> class Bar(Foo): - ... def _repr_words(self): - ... return Foo._repr_words(self) + ["too"] - ... def _repr_attrs(self): - ... return dict(Foo._repr_attrs(self), barttr=42) - >>> Bar() - - """ - alist = [(str(k), normalize_repr(v)) - for k, v in self._repr_attrs().items()] - alist.sort() - words = self._repr_words() - if not words and not alist: - # if we're just going to print the classname fall back - # to the previous __repr__ implementation instead - return super(self.__class__, self).__repr__() - if words and alist: words.append("") - return "<%s %s>" % (self.__class__.__name__, - " ".join(words) + - " ".join(["%s=%s" % itm for itm in alist])) - -def normalize_repr(v): - """ - Return dictionary repr sorted by keys, leave others unchanged - - >>> normalize_repr({1:2,3:4,5:6,7:8}) - '{1: 2, 3: 4, 5: 6, 7: 8}' - >>> normalize_repr('foo') - "'foo'" - """ - if isinstance(v, dict): - items = [(repr(k), repr(v)) for k, v in v.items()] - items.sort() - return "{" + ", ".join([ - "%s: %s" % itm for itm in items]) + "}" - - return repr(v) - -def python3_repr(v): - """ - Return strings and byte strings as they appear in Python 3 - - >>> python3_repr(u"text") - "'text'" - >>> python3_repr(bytes()) - "b''" - """ - r = repr(v) - if not PYTHON3: - if isinstance(v, bytes): - return 'b' + r - if r.startswith('u'): - return r[1:] - return r - - - -def remove_defaults(d, fn): - """ - Remove keys in d that are set to the default values from - fn. This method is used to unclutter the _repr_attrs() - return value. - - d will be modified by this function. - - Returns d. - - >>> class Foo(object): - ... def __init__(self, a=1, b=2): - ... self.values = a, b - ... __repr__ = split_repr - ... def _repr_words(self): - ... return ["object"] - ... def _repr_attrs(self): - ... d = dict(a=self.values[0], b=self.values[1]) - ... return remove_defaults(d, Foo.__init__) - >>> Foo(42, 100) - - >>> Foo(10, 2) - - >>> Foo() - - """ - args, varargs, varkw, defaults = getargspec(fn) - - # ignore *varargs and **kwargs - if varkw: - del args[-1] - if varargs: - del args[-1] - - # create a dictionary of args with default values - ddict = dict(zip(args[len(args) - len(defaults):], defaults)) - - for k, v in d.items(): - if k in ddict: - # remove values that match their defaults - if ddict[k] == v: - del d[k] - - return d - - - -def _test(): - import doctest - doctest.testmod() - -if __name__=='__main__': - _test() diff --git a/urwid/tests/test_canvas.py b/urwid/tests/test_canvas.py deleted file mode 100644 index 40a303b..0000000 --- a/urwid/tests/test_canvas.py +++ /dev/null @@ -1,391 +0,0 @@ -import unittest - -from urwid import canvas -from urwid.compat import B -import urwid - - -class CanvasCacheTest(unittest.TestCase): - def setUp(self): - # purge the cache - urwid.CanvasCache._widgets.clear() - - def cct(self, widget, size, focus, expected): - got = urwid.CanvasCache.fetch(widget, urwid.Widget, size, focus) - assert expected==got, "got: %s expected: %s"%(got, expected) - - def test1(self): - a = urwid.Text("") - b = urwid.Text("") - blah = urwid.TextCanvas() - blah.finalize(a, (10,1), False) - blah2 = urwid.TextCanvas() - blah2.finalize(a, (15,1), False) - bloo = urwid.TextCanvas() - bloo.finalize(b, (20,2), True) - - urwid.CanvasCache.store(urwid.Widget, blah) - urwid.CanvasCache.store(urwid.Widget, blah2) - urwid.CanvasCache.store(urwid.Widget, bloo) - - self.cct(a, (10,1), False, blah) - self.cct(a, (15,1), False, blah2) - self.cct(a, (15,1), True, None) - self.cct(a, (10,2), False, None) - self.cct(b, (20,2), True, bloo) - self.cct(b, (21,2), True, None) - urwid.CanvasCache.invalidate(a) - self.cct(a, (10,1), False, None) - self.cct(a, (15,1), False, None) - self.cct(b, (20,2), True, bloo) - - -class CanvasTest(unittest.TestCase): - def ct(self, text, attr, exp_content): - c = urwid.TextCanvas([B(t) for t in text], attr) - content = list(c.content()) - assert content == exp_content, "got: %r expected: %r" % (content, - exp_content) - - def ct2(self, text, attr, left, top, cols, rows, def_attr, exp_content): - c = urwid.TextCanvas([B(t) for t in text], attr) - content = list(c.content(left, top, cols, rows, def_attr)) - assert content == exp_content, "got: %r expected: %r" % (content, - exp_content) - - def test1(self): - self.ct(["Hello world"], None, [[(None, None, B("Hello world"))]]) - self.ct(["Hello world"], [[("a",5)]], - [[("a", None, B("Hello")), (None, None, B(" world"))]]) - self.ct(["Hi","There"], None, - [[(None, None, B("Hi "))], [(None, None, B("There"))]]) - - def test2(self): - self.ct2(["Hello"], None, 0, 0, 5, 1, None, - [[(None, None, B("Hello"))]]) - self.ct2(["Hello"], None, 1, 0, 4, 1, None, - [[(None, None, B("ello"))]]) - self.ct2(["Hello"], None, 0, 0, 4, 1, None, - [[(None, None, B("Hell"))]]) - self.ct2(["Hi","There"], None, 1, 0, 3, 2, None, - [[(None, None, B("i "))], [(None, None, B("her"))]]) - self.ct2(["Hi","There"], None, 0, 0, 5, 1, None, - [[(None, None, B("Hi "))]]) - self.ct2(["Hi","There"], None, 0, 1, 5, 1, None, - [[(None, None, B("There"))]]) - - -class ShardBodyTest(unittest.TestCase): - def sbt(self, shards, shard_tail, expected): - result = canvas.shard_body(shards, shard_tail, False) - assert result == expected, "got: %r expected: %r" % (result, expected) - - def sbttail(self, num_rows, sbody, expected): - result = canvas.shard_body_tail(num_rows, sbody) - assert result == expected, "got: %r expected: %r" % (result, expected) - - def sbtrow(self, sbody, expected): - result = list(canvas.shard_body_row(sbody)) - assert result == expected, "got: %r expected: %r" % (result, expected) - - - def test1(self): - cviews = [(0,0,10,5,None,"foo"),(0,0,5,5,None,"bar")] - self.sbt(cviews, [], - [(0, None, (0,0,10,5,None,"foo")), - (0, None, (0,0,5,5,None,"bar"))]) - self.sbt(cviews, [(0, 3, None, (0,0,5,8,None,"baz"))], - [(3, None, (0,0,5,8,None,"baz")), - (0, None, (0,0,10,5,None,"foo")), - (0, None, (0,0,5,5,None,"bar"))]) - self.sbt(cviews, [(10, 3, None, (0,0,5,8,None,"baz"))], - [(0, None, (0,0,10,5,None,"foo")), - (3, None, (0,0,5,8,None,"baz")), - (0, None, (0,0,5,5,None,"bar"))]) - self.sbt(cviews, [(15, 3, None, (0,0,5,8,None,"baz"))], - [(0, None, (0,0,10,5,None,"foo")), - (0, None, (0,0,5,5,None,"bar")), - (3, None, (0,0,5,8,None,"baz"))]) - - def test2(self): - sbody = [(0, None, (0,0,10,5,None,"foo")), - (0, None, (0,0,5,5,None,"bar")), - (3, None, (0,0,5,8,None,"baz"))] - self.sbttail(5, sbody, []) - self.sbttail(3, sbody, - [(0, 3, None, (0,0,10,5,None,"foo")), - (0, 3, None, (0,0,5,5,None,"bar")), - (0, 6, None, (0,0,5,8,None,"baz"))]) - - sbody = [(0, None, (0,0,10,3,None,"foo")), - (0, None, (0,0,5,5,None,"bar")), - (3, None, (0,0,5,9,None,"baz"))] - self.sbttail(3, sbody, - [(10, 3, None, (0,0,5,5,None,"bar")), - (0, 6, None, (0,0,5,9,None,"baz"))]) - - def test3(self): - self.sbtrow([(0, None, (0,0,10,5,None,"foo")), - (0, None, (0,0,5,5,None,"bar")), - (3, None, (0,0,5,8,None,"baz"))], - [20]) - self.sbtrow([(0, iter("foo"), (0,0,10,5,None,"foo")), - (0, iter("bar"), (0,0,5,5,None,"bar")), - (3, iter("zzz"), (0,0,5,8,None,"baz"))], - ["f","b","z"]) - - -class ShardsTrimTest(unittest.TestCase): - def sttop(self, shards, top, expected): - result = canvas.shards_trim_top(shards, top) - assert result == expected, "got: %r expected: %r" (result, expected) - - def strows(self, shards, rows, expected): - result = canvas.shards_trim_rows(shards, rows) - assert result == expected, "got: %r expected: %r" (result, expected) - - def stsides(self, shards, left, cols, expected): - result = canvas.shards_trim_sides(shards, left, cols) - assert result == expected, "got: %r expected: %r" (result, expected) - - - def test1(self): - shards = [(5, [(0,0,10,5,None,"foo"),(0,0,5,5,None,"bar")])] - self.sttop(shards, 2, - [(3, [(0,2,10,3,None,"foo"),(0,2,5,3,None,"bar")])]) - self.strows(shards, 2, - [(2, [(0,0,10,2,None,"foo"),(0,0,5,2,None,"bar")])]) - - shards = [(5, [(0,0,10,5,None,"foo")]),(3,[(0,0,10,3,None,"bar")])] - self.sttop(shards, 2, - [(3, [(0,2,10,3,None,"foo")]),(3,[(0,0,10,3,None,"bar")])]) - self.sttop(shards, 5, - [(3, [(0,0,10,3,None,"bar")])]) - self.sttop(shards, 7, - [(1, [(0,2,10,1,None,"bar")])]) - self.strows(shards, 7, - [(5, [(0,0,10,5,None,"foo")]),(2, [(0,0,10,2,None,"bar")])]) - self.strows(shards, 5, - [(5, [(0,0,10,5,None,"foo")])]) - self.strows(shards, 4, - [(4, [(0,0,10,4,None,"foo")])]) - - shards = [(5, [(0,0,10,5,None,"foo"), (0,0,5,8,None,"baz")]), - (3,[(0,0,10,3,None,"bar")])] - self.sttop(shards, 2, - [(3, [(0,2,10,3,None,"foo"), (0,2,5,6,None,"baz")]), - (3,[(0,0,10,3,None,"bar")])]) - self.sttop(shards, 5, - [(3, [(0,0,10,3,None,"bar"), (0,5,5,3,None,"baz")])]) - self.sttop(shards, 7, - [(1, [(0,2,10,1,None,"bar"), (0,7,5,1,None,"baz")])]) - self.strows(shards, 7, - [(5, [(0,0,10,5,None,"foo"), (0,0,5,7,None,"baz")]), - (2, [(0,0,10,2,None,"bar")])]) - self.strows(shards, 5, - [(5, [(0,0,10,5,None,"foo"), (0,0,5,5,None,"baz")])]) - self.strows(shards, 4, - [(4, [(0,0,10,4,None,"foo"), (0,0,5,4,None,"baz")])]) - - - def test2(self): - shards = [(5, [(0,0,10,5,None,"foo"),(0,0,5,5,None,"bar")])] - self.stsides(shards, 0, 15, - [(5, [(0,0,10,5,None,"foo"),(0,0,5,5,None,"bar")])]) - self.stsides(shards, 6, 9, - [(5, [(6,0,4,5,None,"foo"),(0,0,5,5,None,"bar")])]) - self.stsides(shards, 6, 6, - [(5, [(6,0,4,5,None,"foo"),(0,0,2,5,None,"bar")])]) - self.stsides(shards, 0, 10, - [(5, [(0,0,10,5,None,"foo")])]) - self.stsides(shards, 10, 5, - [(5, [(0,0,5,5,None,"bar")])]) - self.stsides(shards, 1, 7, - [(5, [(1,0,7,5,None,"foo")])]) - - shards = [(5, [(0,0,10,5,None,"foo"), (0,0,5,8,None,"baz")]), - (3,[(0,0,10,3,None,"bar")])] - self.stsides(shards, 0, 15, - [(5, [(0,0,10,5,None,"foo"), (0,0,5,8,None,"baz")]), - (3,[(0,0,10,3,None,"bar")])]) - self.stsides(shards, 2, 13, - [(5, [(2,0,8,5,None,"foo"), (0,0,5,8,None,"baz")]), - (3,[(2,0,8,3,None,"bar")])]) - self.stsides(shards, 2, 10, - [(5, [(2,0,8,5,None,"foo"), (0,0,2,8,None,"baz")]), - (3,[(2,0,8,3,None,"bar")])]) - self.stsides(shards, 2, 8, - [(5, [(2,0,8,5,None,"foo")]), - (3,[(2,0,8,3,None,"bar")])]) - self.stsides(shards, 2, 6, - [(5, [(2,0,6,5,None,"foo")]), - (3,[(2,0,6,3,None,"bar")])]) - self.stsides(shards, 10, 5, - [(8, [(0,0,5,8,None,"baz")])]) - self.stsides(shards, 11, 3, - [(8, [(1,0,3,8,None,"baz")])]) - - -class ShardsJoinTest(unittest.TestCase): - def sjt(self, shard_lists, expected): - result = canvas.shards_join(shard_lists) - assert result == expected, "got: %r expected: %r" (result, expected) - - def test(self): - shards1 = [(5, [(0,0,10,5,None,"foo"), (0,0,5,8,None,"baz")]), - (3,[(0,0,10,3,None,"bar")])] - shards2 = [(3, [(0,0,10,3,None,"aaa")]), - (5,[(0,0,10,5,None,"bbb")])] - shards3 = [(3, [(0,0,10,3,None,"111")]), - (2,[(0,0,10,3,None,"222")]), - (3,[(0,0,10,3,None,"333")])] - - self.sjt([shards1], shards1) - self.sjt([shards1, shards2], - [(3, [(0,0,10,5,None,"foo"), (0,0,5,8,None,"baz"), - (0,0,10,3,None,"aaa")]), - (2, [(0,0,10,5,None,"bbb")]), - (3, [(0,0,10,3,None,"bar")])]) - self.sjt([shards1, shards3], - [(3, [(0,0,10,5,None,"foo"), (0,0,5,8,None,"baz"), - (0,0,10,3,None,"111")]), - (2, [(0,0,10,3,None,"222")]), - (3, [(0,0,10,3,None,"bar"), (0,0,10,3,None,"333")])]) - self.sjt([shards1, shards2, shards3], - [(3, [(0,0,10,5,None,"foo"), (0,0,5,8,None,"baz"), - (0,0,10,3,None,"aaa"), (0,0,10,3,None,"111")]), - (2, [(0,0,10,5,None,"bbb"), (0,0,10,3,None,"222")]), - (3, [(0,0,10,3,None,"bar"), (0,0,10,3,None,"333")])]) - - -class CanvasJoinTest(unittest.TestCase): - def cjtest(self, desc, l, expected): - l = [(c, None, False, n) for c, n in l] - result = list(urwid.CanvasJoin(l).content()) - - assert result == expected, "%s expected %r, got %r"%( - desc, expected, result) - - def test(self): - C = urwid.TextCanvas - hello = C([B("hello")]) - there = C([B("there")], [[("a",5)]]) - a = C([B("a")]) - hi = C([B("hi")]) - how = C([B("how")], [[("a",1)]]) - dy = C([B("dy")]) - how_you = C([B("how"), B("you")]) - - self.cjtest("one", [(hello, 5)], - [[(None, None, B("hello"))]]) - self.cjtest("two", [(hello, 5), (there, 5)], - [[(None, None, B("hello")), ("a", None, B("there"))]]) - self.cjtest("two space", [(hello, 7), (there, 5)], - [[(None, None, B("hello")),(None,None,B(" ")), - ("a", None, B("there"))]]) - self.cjtest("three space", [(hi, 4), (how, 3), (dy, 2)], - [[(None, None, B("hi")),(None,None,B(" ")),("a",None, B("h")), - (None,None,B("ow")),(None,None,B("dy"))]]) - self.cjtest("four space", [(a, 2), (hi, 3), (dy, 3), (a, 1)], - [[(None, None, B("a")),(None,None,B(" ")), - (None, None, B("hi")),(None,None,B(" ")), - (None, None, B("dy")),(None,None,B(" ")), - (None, None, B("a"))]]) - self.cjtest("pile 2", [(how_you, 4), (hi, 2)], - [[(None, None, B('how')), (None, None, B(' ')), - (None, None, B('hi'))], - [(None, None, B('you')), (None, None, B(' ')), - (None, None, B(' '))]]) - self.cjtest("pile 2r", [(hi, 4), (how_you, 3)], - [[(None, None, B('hi')), (None, None, B(' ')), - (None, None, B('how'))], - [(None, None, B(' ')), - (None, None, B('you'))]]) - - -class CanvasOverlayTest(unittest.TestCase): - def cotest(self, desc, bgt, bga, fgt, fga, l, r, et): - bgt = B(bgt) - fgt = B(fgt) - bg = urwid.CompositeCanvas( - urwid.TextCanvas([bgt],[bga])) - fg = urwid.CompositeCanvas( - urwid.TextCanvas([fgt],[fga])) - bg.overlay(fg, l, 0) - result = list(bg.content()) - assert result == et, "%s expected %r, got %r"%( - desc, et, result) - - def test1(self): - self.cotest("left", "qxqxqxqx", [], "HI", [], 0, 6, - [[(None, None, B("HI")),(None,None,B("qxqxqx"))]]) - self.cotest("right", "qxqxqxqx", [], "HI", [], 6, 0, - [[(None, None, B("qxqxqx")),(None,None,B("HI"))]]) - self.cotest("center", "qxqxqxqx", [], "HI", [], 3, 3, - [[(None, None, B("qxq")),(None,None,B("HI")), - (None,None,B("xqx"))]]) - self.cotest("center2", "qxqxqxqx", [], "HI ", [], 2, 2, - [[(None, None, B("qx")),(None,None,B("HI ")), - (None,None,B("qx"))]]) - self.cotest("full", "rz", [], "HI", [], 0, 0, - [[(None, None, B("HI"))]]) - - def test2(self): - self.cotest("same","asdfghjkl",[('a',9)],"HI",[('a',2)],4,3, - [[('a',None,B("asdf")),('a',None,B("HI")),('a',None,B("jkl"))]]) - self.cotest("diff","asdfghjkl",[('a',9)],"HI",[('b',2)],4,3, - [[('a',None,B("asdf")),('b',None,B("HI")),('a',None,B("jkl"))]]) - self.cotest("None end","asdfghjkl",[('a',9)],"HI ",[('a',2)], - 2,3, - [[('a',None,B("as")),('a',None,B("HI")), - (None,None,B(" ")),('a',None,B("jkl"))]]) - self.cotest("float end","asdfghjkl",[('a',3)],"HI",[('a',2)], - 4,3, - [[('a',None,B("asd")),(None,None,B("f")), - ('a',None,B("HI")),(None,None,B("jkl"))]]) - self.cotest("cover 2","asdfghjkl",[('a',5),('c',4)],"HI", - [('b',2)],4,3, - [[('a',None,B("asdf")),('b',None,B("HI")),('c',None,B("jkl"))]]) - self.cotest("cover 2-2","asdfghjkl", - [('a',4),('d',1),('e',1),('c',3)], - "HI",[('b',2)], 4, 3, - [[('a',None,B("asdf")),('b',None,B("HI")),('c',None,B("jkl"))]]) - - def test3(self): - urwid.set_encoding("euc-jp") - self.cotest("db0","\xA1\xA1\xA1\xA1\xA1\xA1",[],"HI",[],2,2, - [[(None,None,B("\xA1\xA1")),(None,None,B("HI")), - (None,None,B("\xA1\xA1"))]]) - self.cotest("db1","\xA1\xA1\xA1\xA1\xA1\xA1",[],"OHI",[],1,2, - [[(None,None,B(" ")),(None,None,B("OHI")), - (None,None,B("\xA1\xA1"))]]) - self.cotest("db2","\xA1\xA1\xA1\xA1\xA1\xA1",[],"OHI",[],2,1, - [[(None,None,B("\xA1\xA1")),(None,None,B("OHI")), - (None,None,B(" "))]]) - self.cotest("db3","\xA1\xA1\xA1\xA1\xA1\xA1",[],"OHIO",[],1,1, - [[(None,None,B(" ")),(None,None,B("OHIO")),(None,None,B(" "))]]) - - -class CanvasPadTrimTest(unittest.TestCase): - def cptest(self, desc, ct, ca, l, r, et): - ct = B(ct) - c = urwid.CompositeCanvas( - urwid.TextCanvas([ct], [ca])) - c.pad_trim_left_right(l, r) - result = list(c.content()) - assert result == et, "%s expected %r, got %r"%( - desc, et, result) - - def test1(self): - self.cptest("none", "asdf", [], 0, 0, - [[(None,None,B("asdf"))]]) - self.cptest("left pad", "asdf", [], 2, 0, - [[(None,None,B(" ")),(None,None,B("asdf"))]]) - self.cptest("right pad", "asdf", [], 0, 2, - [[(None,None,B("asdf")),(None,None,B(" "))]]) - - def test2(self): - self.cptest("left trim", "asdf", [], -2, 0, - [[(None,None,B("df"))]]) - self.cptest("right trim", "asdf", [], 0, -2, - [[(None,None,B("as"))]]) diff --git a/urwid/tests/test_container.py b/urwid/tests/test_container.py deleted file mode 100644 index 6ddb909..0000000 --- a/urwid/tests/test_container.py +++ /dev/null @@ -1,638 +0,0 @@ -import unittest - -from urwid.tests.util import SelectableText -import urwid - - -class FrameTest(unittest.TestCase): - def ftbtest(self, desc, focus_part, header_rows, footer_rows, size, - focus, top, bottom): - class FakeWidget: - def __init__(self, rows, want_focus): - self.ret_rows = rows - self.want_focus = want_focus - def rows(self, size, focus=False): - assert self.want_focus == focus - return self.ret_rows - header = footer = None - if header_rows: - header = FakeWidget(header_rows, - focus and focus_part == 'header') - if footer_rows: - footer = FakeWidget(footer_rows, - focus and focus_part == 'footer') - - f = urwid.Frame(None, header, footer, focus_part) - - rval = f.frame_top_bottom(size, focus) - exp = (top, bottom), (header_rows, footer_rows) - assert exp == rval, "%s expected %r but got %r"%( - desc,exp,rval) - - def test(self): - self.ftbtest("simple", 'body', 0, 0, (9, 10), True, 0, 0) - self.ftbtest("simple h", 'body', 3, 0, (9, 10), True, 3, 0) - self.ftbtest("simple f", 'body', 0, 3, (9, 10), True, 0, 3) - self.ftbtest("simple hf", 'body', 3, 3, (9, 10), True, 3, 3) - self.ftbtest("almost full hf", 'body', 4, 5, (9, 10), - True, 4, 5) - self.ftbtest("full hf", 'body', 5, 5, (9, 10), - True, 4, 5) - self.ftbtest("x full h+1f", 'body', 6, 5, (9, 10), - False, 4, 5) - self.ftbtest("full h+1f", 'body', 6, 5, (9, 10), - True, 4, 5) - self.ftbtest("full hf+1", 'body', 5, 6, (9, 10), - True, 3, 6) - self.ftbtest("F full h+1f", 'footer', 6, 5, (9, 10), - True, 5, 5) - self.ftbtest("F full hf+1", 'footer', 5, 6, (9, 10), - True, 4, 6) - self.ftbtest("F full hf+5", 'footer', 5, 11, (9, 10), - True, 0, 10) - self.ftbtest("full hf+5", 'body', 5, 11, (9, 10), - True, 0, 9) - self.ftbtest("H full hf+1", 'header', 5, 6, (9, 10), - True, 5, 5) - self.ftbtest("H full h+1f", 'header', 6, 5, (9, 10), - True, 6, 4) - self.ftbtest("H full h+5f", 'header', 11, 5, (9, 10), - True, 10, 0) - - -class PileTest(unittest.TestCase): - def ktest(self, desc, l, focus_item, key, - rkey, rfocus, rpref_col): - p = urwid.Pile( l, focus_item ) - rval = p.keypress( (20,), key ) - assert rkey == rval, "%s key expected %r but got %r" %( - desc, rkey, rval) - new_focus = l.index(p.get_focus()) - assert new_focus == rfocus, "%s focus expected %r but got %r" %( - desc, rfocus, new_focus) - new_pref = p.get_pref_col((20,)) - assert new_pref == rpref_col, ( - "%s pref_col expected %r but got %r" % ( - desc, rpref_col, new_pref)) - - def test_select_change(self): - T,S,E = urwid.Text, SelectableText, urwid.Edit - - self.ktest("simple up", [S("")], 0, "up", "up", 0, 0) - self.ktest("simple down", [S("")], 0, "down", "down", 0, 0) - self.ktest("ignore up", [T(""),S("")], 1, "up", "up", 1, 0) - self.ktest("ignore down", [S(""),T("")], 0, "down", - "down", 0, 0) - self.ktest("step up", [S(""),S("")], 1, "up", None, 0, 0) - self.ktest("step down", [S(""),S("")], 0, "down", - None, 1, 0) - self.ktest("skip step up", [S(""),T(""),S("")], 2, "up", - None, 0, 0) - self.ktest("skip step down", [S(""),T(""),S("")], 0, "down", - None, 2, 0) - self.ktest("pad skip step up", [T(""),S(""),T(""),S("")], 3, - "up", None, 1, 0) - self.ktest("pad skip step down", [S(""),T(""),S(""),T("")], 0, - "down", None, 2, 0) - self.ktest("padi skip step up", [S(""),T(""),S(""),T(""),S("")], - 4, "up", None, 2, 0) - self.ktest("padi skip step down", [S(""),T(""),S(""),T(""), - S("")], 0, "down", None, 2, 0) - e = E("","abcd", edit_pos=1) - e.keypress((20,),"right") # set a pref_col - self.ktest("pref step up", [S(""),T(""),e], 2, "up", - None, 0, 2) - self.ktest("pref step down", [e,T(""),S("")], 0, "down", - None, 2, 2) - z = E("","1234") - self.ktest("prefx step up", [z,T(""),e], 2, "up", - None, 0, 2) - assert z.get_pref_col((20,)) == 2 - z = E("","1234") - self.ktest("prefx step down", [e,T(""),z], 0, "down", - None, 2, 2) - assert z.get_pref_col((20,)) == 2 - - def test_init_with_a_generator(self): - urwid.Pile(urwid.Text(c) for c in "ABC") - - def test_change_focus_with_mouse(self): - p = urwid.Pile([urwid.Edit(), urwid.Edit()]) - self.assertEqual(p.focus_position, 0) - p.mouse_event((10,), 'button press', 1, 1, 1, True) - self.assertEqual(p.focus_position, 1) - - def test_zero_weight(self): - p = urwid.Pile([ - urwid.SolidFill('a'), - ('weight', 0, urwid.SolidFill('d')), - ]) - p.render((5, 4)) - - def test_mouse_event_in_empty_pile(self): - p = urwid.Pile([]) - p.mouse_event((5,), 'button press', 1, 1, 1, False) - p.mouse_event((5,), 'button press', 1, 1, 1, True) - - -class ColumnsTest(unittest.TestCase): - def cwtest(self, desc, l, divide, size, exp, focus_column=0): - c = urwid.Columns(l, divide, focus_column) - rval = c.column_widths( size ) - assert rval == exp, "%s expected %s, got %s"%(desc,exp,rval) - - def test_widths(self): - x = urwid.Text("") # sample "column" - self.cwtest( "simple 1", [x], 0, (20,), [20] ) - self.cwtest( "simple 2", [x,x], 0, (20,), [10,10] ) - self.cwtest( "simple 2+1", [x,x], 1, (20,), [10,9] ) - self.cwtest( "simple 3+1", [x,x,x], 1, (20,), [6,6,6] ) - self.cwtest( "simple 3+2", [x,x,x], 2, (20,), [5,6,5] ) - self.cwtest( "simple 3+2", [x,x,x], 2, (21,), [6,6,5] ) - self.cwtest( "simple 4+1", [x,x,x,x], 1, (25,), [6,5,6,5] ) - self.cwtest( "squish 4+1", [x,x,x,x], 1, (7,), [1,1,1,1] ) - self.cwtest( "squish 4+1", [x,x,x,x], 1, (6,), [1,2,1] ) - self.cwtest( "squish 4+1", [x,x,x,x], 1, (4,), [2,1] ) - - self.cwtest( "fixed 3", [('fixed',4,x),('fixed',6,x), - ('fixed',2,x)], 1, (25,), [4,6,2] ) - self.cwtest( "fixed 3 cut", [('fixed',4,x),('fixed',6,x), - ('fixed',2,x)], 1, (13,), [4,6] ) - self.cwtest( "fixed 3 cut2", [('fixed',4,x),('fixed',6,x), - ('fixed',2,x)], 1, (10,), [4] ) - - self.cwtest( "mixed 4", [('weight',2,x),('fixed',5,x), - x, ('weight',3,x)], 1, (14,), [2,5,1,3] ) - self.cwtest( "mixed 4 a", [('weight',2,x),('fixed',5,x), - x, ('weight',3,x)], 1, (12,), [1,5,1,2] ) - self.cwtest( "mixed 4 b", [('weight',2,x),('fixed',5,x), - x, ('weight',3,x)], 1, (10,), [2,5,1] ) - self.cwtest( "mixed 4 c", [('weight',2,x),('fixed',5,x), - x, ('weight',3,x)], 1, (20,), [4,5,2,6] ) - - def test_widths_focus_end(self): - x = urwid.Text("") # sample "column" - self.cwtest("end simple 2", [x,x], 0, (20,), [10,10], 1) - self.cwtest("end simple 2+1", [x,x], 1, (20,), [10,9], 1) - self.cwtest("end simple 3+1", [x,x,x], 1, (20,), [6,6,6], 2) - self.cwtest("end simple 3+2", [x,x,x], 2, (20,), [5,6,5], 2) - self.cwtest("end simple 3+2", [x,x,x], 2, (21,), [6,6,5], 2) - self.cwtest("end simple 4+1", [x,x,x,x], 1, (25,), [6,5,6,5], 3) - self.cwtest("end squish 4+1", [x,x,x,x], 1, (7,), [1,1,1,1], 3) - self.cwtest("end squish 4+1", [x,x,x,x], 1, (6,), [0,1,2,1], 3) - self.cwtest("end squish 4+1", [x,x,x,x], 1, (4,), [0,0,2,1], 3) - - self.cwtest("end fixed 3", [('fixed',4,x),('fixed',6,x), - ('fixed',2,x)], 1, (25,), [4,6,2], 2) - self.cwtest("end fixed 3 cut", [('fixed',4,x),('fixed',6,x), - ('fixed',2,x)], 1, (13,), [0,6,2], 2) - self.cwtest("end fixed 3 cut2", [('fixed',4,x),('fixed',6,x), - ('fixed',2,x)], 1, (8,), [0,0,2], 2) - - self.cwtest("end mixed 4", [('weight',2,x),('fixed',5,x), - x, ('weight',3,x)], 1, (14,), [2,5,1,3], 3) - self.cwtest("end mixed 4 a", [('weight',2,x),('fixed',5,x), - x, ('weight',3,x)], 1, (12,), [1,5,1,2], 3) - self.cwtest("end mixed 4 b", [('weight',2,x),('fixed',5,x), - x, ('weight',3,x)], 1, (10,), [0,5,1,2], 3) - self.cwtest("end mixed 4 c", [('weight',2,x),('fixed',5,x), - x, ('weight',3,x)], 1, (20,), [4,5,2,6], 3) - - def mctest(self, desc, l, divide, size, col, row, exp, f_col, pref_col): - c = urwid.Columns( l, divide ) - rval = c.move_cursor_to_coords( size, col, row ) - assert rval == exp, "%s expected %r, got %r"%(desc,exp,rval) - assert c.focus_col == f_col, "%s expected focus_col %s got %s"%( - desc, f_col, c.focus_col) - pc = c.get_pref_col( size ) - assert pc == pref_col, "%s expected pref_col %s, got %s"%( - desc, pref_col, pc) - - def test_move_cursor(self): - e, s, x = urwid.Edit("",""),SelectableText(""), urwid.Text("") - self.mctest("nothing selectbl",[x,x,x],1,(20,),9,0,False,0,None) - self.mctest("dead on",[x,s,x],1,(20,),9,0,True,1,9) - self.mctest("l edge",[x,s,x],1,(20,),6,0,True,1,6) - self.mctest("r edge",[x,s,x],1,(20,),13,0,True,1,13) - self.mctest("l off",[x,s,x],1,(20,),2,0,True,1,2) - self.mctest("r off",[x,s,x],1,(20,),17,0,True,1,17) - self.mctest("l off 2",[x,x,s],1,(20,),2,0,True,2,2) - self.mctest("r off 2",[s,x,x],1,(20,),17,0,True,0,17) - - self.mctest("l between",[s,s,x],1,(20,),6,0,True,0,6) - self.mctest("r between",[x,s,s],1,(20,),13,0,True,1,13) - self.mctest("l between 2l",[s,s,x],2,(22,),6,0,True,0,6) - self.mctest("r between 2l",[x,s,s],2,(22,),14,0,True,1,14) - self.mctest("l between 2r",[s,s,x],2,(22,),7,0,True,1,7) - self.mctest("r between 2r",[x,s,s],2,(22,),15,0,True,2,15) - - # unfortunate pref_col shifting - self.mctest("l e edge",[x,e,x],1,(20,),6,0,True,1,7) - self.mctest("r e edge",[x,e,x],1,(20,),13,0,True,1,12) - - # 'left'/'right' special cases - self.mctest("right", [e, e, e], 0, (12,), 'right', 0, True, 2, 'right') - self.mctest("left", [e, e, e], 0, (12,), 'left', 0, True, 0, 'left') - - def test_init_with_a_generator(self): - urwid.Columns(urwid.Text(c) for c in "ABC") - - def test_old_attributes(self): - c = urwid.Columns([urwid.Text(u'a'), urwid.SolidFill(u'x')], - box_columns=[1]) - self.assertEqual(c.box_columns, [1]) - c.box_columns=[] - self.assertEqual(c.box_columns, []) - - def test_box_column(self): - c = urwid.Columns([urwid.Filler(urwid.Edit()),urwid.Text('')], - box_columns=[0]) - c.keypress((10,), 'x') - c.get_cursor_coords((10,)) - c.move_cursor_to_coords((10,), 0, 0) - c.mouse_event((10,), 'foo', 1, 0, 0, True) - c.get_pref_col((10,)) - - - -class OverlayTest(unittest.TestCase): - def test_old_params(self): - o1 = urwid.Overlay(urwid.SolidFill(u'X'), urwid.SolidFill(u'O'), - ('fixed left', 5), ('fixed right', 4), - ('fixed top', 3), ('fixed bottom', 2),) - self.assertEqual(o1.contents[1][1], ( - 'left', None, 'relative', 100, None, 5, 4, - 'top', None, 'relative', 100, None, 3, 2)) - o2 = urwid.Overlay(urwid.SolidFill(u'X'), urwid.SolidFill(u'O'), - ('fixed right', 5), ('fixed left', 4), - ('fixed bottom', 3), ('fixed top', 2),) - self.assertEqual(o2.contents[1][1], ( - 'right', None, 'relative', 100, None, 4, 5, - 'bottom', None, 'relative', 100, None, 2, 3)) - - def test_get_cursor_coords(self): - self.assertEqual(urwid.Overlay(urwid.Filler(urwid.Edit()), - urwid.SolidFill(u'B'), - 'right', 1, 'bottom', 1).get_cursor_coords((2,2)), (1,1)) - - -class GridFlowTest(unittest.TestCase): - def test_cell_width(self): - gf = urwid.GridFlow([], 5, 0, 0, 'left') - self.assertEqual(gf.cell_width, 5) - - def test_basics(self): - repr(urwid.GridFlow([], 5, 0, 0, 'left')) # should not fail - - def test_v_sep(self): - gf = urwid.GridFlow([urwid.Text("test")], 10, 3, 1, "center") - self.assertEqual(gf.rows((40,), False), 1) - - -class WidgetSquishTest(unittest.TestCase): - def wstest(self, w): - c = w.render((80,0), focus=False) - assert c.rows() == 0 - c = w.render((80,0), focus=True) - assert c.rows() == 0 - c = w.render((80,1), focus=False) - assert c.rows() == 1 - c = w.render((0, 25), focus=False) - c = w.render((1, 25), focus=False) - - def fwstest(self, w): - def t(cols, focus): - wrows = w.rows((cols,), focus) - c = w.render((cols,), focus) - assert c.rows() == wrows, (c.rows(), wrows) - if focus and hasattr(w, 'get_cursor_coords'): - gcc = w.get_cursor_coords((cols,)) - assert c.cursor == gcc, (c.cursor, gcc) - t(0, False) - t(1, False) - t(0, True) - t(1, True) - - def test_listbox(self): - self.wstest(urwid.ListBox([])) - self.wstest(urwid.ListBox([urwid.Text("hello")])) - - def test_bargraph(self): - self.wstest(urwid.BarGraph(['foo','bar'])) - - def test_graphvscale(self): - self.wstest(urwid.GraphVScale([(0,"hello")], 1)) - self.wstest(urwid.GraphVScale([(5,"hello")], 1)) - - def test_solidfill(self): - self.wstest(urwid.SolidFill()) - - def test_filler(self): - self.wstest(urwid.Filler(urwid.Text("hello"))) - - def test_overlay(self): - self.wstest(urwid.Overlay( - urwid.BigText("hello",urwid.Thin6x6Font()), - urwid.SolidFill(), - 'center', None, 'middle', None)) - self.wstest(urwid.Overlay( - urwid.Text("hello"), urwid.SolidFill(), - 'center', ('relative', 100), 'middle', None)) - - def test_frame(self): - self.wstest(urwid.Frame(urwid.SolidFill())) - self.wstest(urwid.Frame(urwid.SolidFill(), - header=urwid.Text("hello"))) - self.wstest(urwid.Frame(urwid.SolidFill(), - header=urwid.Text("hello"), - footer=urwid.Text("hello"))) - - def test_pile(self): - self.wstest(urwid.Pile([urwid.SolidFill()])) - self.wstest(urwid.Pile([('flow', urwid.Text("hello"))])) - self.wstest(urwid.Pile([])) - - def test_columns(self): - self.wstest(urwid.Columns([urwid.SolidFill()])) - self.wstest(urwid.Columns([(4, urwid.SolidFill())])) - - def test_buttons(self): - self.fwstest(urwid.Button(u"hello")) - self.fwstest(urwid.RadioButton([], u"hello")) - - -class CommonContainerTest(unittest.TestCase): - def test_pile(self): - t1 = urwid.Text(u'one') - t2 = urwid.Text(u'two') - t3 = urwid.Text(u'three') - sf = urwid.SolidFill('x') - p = urwid.Pile([]) - self.assertEqual(p.focus, None) - self.assertRaises(IndexError, lambda: getattr(p, 'focus_position')) - self.assertRaises(IndexError, lambda: setattr(p, 'focus_position', - None)) - self.assertRaises(IndexError, lambda: setattr(p, 'focus_position', 0)) - p.contents = [(t1, ('pack', None)), (t2, ('pack', None)), - (sf, ('given', 3)), (t3, ('pack', None))] - p.focus_position = 1 - del p.contents[0] - self.assertEqual(p.focus_position, 0) - p.contents[0:0] = [(t3, ('pack', None)), (t2, ('pack', None))] - p.contents.insert(3, (t1, ('pack', None))) - self.assertEqual(p.focus_position, 2) - self.assertRaises(urwid.PileError, lambda: p.contents.append(t1)) - self.assertRaises(urwid.PileError, lambda: p.contents.append((t1, None))) - self.assertRaises(urwid.PileError, lambda: p.contents.append((t1, 'given'))) - - p = urwid.Pile([t1, t2]) - self.assertEqual(p.focus, t1) - self.assertEqual(p.focus_position, 0) - p.focus_position = 1 - self.assertEqual(p.focus, t2) - self.assertEqual(p.focus_position, 1) - p.focus_position = 0 - self.assertRaises(IndexError, lambda: setattr(p, 'focus_position', -1)) - self.assertRaises(IndexError, lambda: setattr(p, 'focus_position', 2)) - # old methods: - p.set_focus(0) - self.assertRaises(IndexError, lambda: p.set_focus(-1)) - self.assertRaises(IndexError, lambda: p.set_focus(2)) - p.set_focus(t2) - self.assertEqual(p.focus_position, 1) - self.assertRaises(ValueError, lambda: p.set_focus('nonexistant')) - self.assertEqual(p.widget_list, [t1, t2]) - self.assertEqual(p.item_types, [('weight', 1), ('weight', 1)]) - p.widget_list = [t2, t1] - self.assertEqual(p.widget_list, [t2, t1]) - self.assertEqual(p.contents, [(t2, ('weight', 1)), (t1, ('weight', 1))]) - self.assertEqual(p.focus_position, 1) # focus unchanged - p.item_types = [('flow', None), ('weight', 2)] - self.assertEqual(p.item_types, [('flow', None), ('weight', 2)]) - self.assertEqual(p.contents, [(t2, ('pack', None)), (t1, ('weight', 2))]) - self.assertEqual(p.focus_position, 1) # focus unchanged - p.widget_list = [t1] - self.assertEqual(len(p.contents), 1) - self.assertEqual(p.focus_position, 0) - p.widget_list.extend([t2, t1]) - self.assertEqual(len(p.contents), 3) - self.assertEqual(p.item_types, [ - ('flow', None), ('weight', 1), ('weight', 1)]) - p.item_types[:] = [('weight', 2)] - self.assertEqual(len(p.contents), 1) - - def test_columns(self): - t1 = urwid.Text(u'one') - t2 = urwid.Text(u'two') - t3 = urwid.Text(u'three') - sf = urwid.SolidFill('x') - c = urwid.Columns([]) - self.assertEqual(c.focus, None) - self.assertRaises(IndexError, lambda: getattr(c, 'focus_position')) - self.assertRaises(IndexError, lambda: setattr(c, 'focus_position', - None)) - self.assertRaises(IndexError, lambda: setattr(c, 'focus_position', 0)) - c.contents = [ - (t1, ('pack', None, False)), - (t2, ('weight', 1, False)), - (sf, ('weight', 2, True)), - (t3, ('given', 10, False))] - c.focus_position = 1 - del c.contents[0] - self.assertEqual(c.focus_position, 0) - c.contents[0:0] = [ - (t3, ('given', 10, False)), - (t2, ('weight', 1, False))] - c.contents.insert(3, (t1, ('pack', None, False))) - self.assertEqual(c.focus_position, 2) - self.assertRaises(urwid.ColumnsError, lambda: c.contents.append(t1)) - self.assertRaises(urwid.ColumnsError, lambda: c.contents.append((t1, None))) - self.assertRaises(urwid.ColumnsError, lambda: c.contents.append((t1, 'given'))) - - c = urwid.Columns([t1, t2]) - self.assertEqual(c.focus, t1) - self.assertEqual(c.focus_position, 0) - c.focus_position = 1 - self.assertEqual(c.focus, t2) - self.assertEqual(c.focus_position, 1) - c.focus_position = 0 - self.assertRaises(IndexError, lambda: setattr(c, 'focus_position', -1)) - self.assertRaises(IndexError, lambda: setattr(c, 'focus_position', 2)) - # old methods: - c = urwid.Columns([t1, ('weight', 3, t2), sf], box_columns=[2]) - c.set_focus(0) - self.assertRaises(IndexError, lambda: c.set_focus(-1)) - self.assertRaises(IndexError, lambda: c.set_focus(3)) - c.set_focus(t2) - self.assertEqual(c.focus_position, 1) - self.assertRaises(ValueError, lambda: c.set_focus('nonexistant')) - self.assertEqual(c.widget_list, [t1, t2, sf]) - self.assertEqual(c.column_types, [ - ('weight', 1), ('weight', 3), ('weight', 1)]) - self.assertEqual(c.box_columns, [2]) - c.widget_list = [t2, t1, sf] - self.assertEqual(c.widget_list, [t2, t1, sf]) - self.assertEqual(c.box_columns, [2]) - - self.assertEqual(c.contents, [ - (t2, ('weight', 1, False)), - (t1, ('weight', 3, False)), - (sf, ('weight', 1, True))]) - self.assertEqual(c.focus_position, 1) # focus unchanged - c.column_types = [ - ('flow', None), # use the old name - ('weight', 2), - ('fixed', 5)] - self.assertEqual(c.column_types, [ - ('flow', None), - ('weight', 2), - ('fixed', 5)]) - self.assertEqual(c.contents, [ - (t2, ('pack', None, False)), - (t1, ('weight', 2, False)), - (sf, ('given', 5, True))]) - self.assertEqual(c.focus_position, 1) # focus unchanged - c.widget_list = [t1] - self.assertEqual(len(c.contents), 1) - self.assertEqual(c.focus_position, 0) - c.widget_list.extend([t2, t1]) - self.assertEqual(len(c.contents), 3) - self.assertEqual(c.column_types, [ - ('flow', None), ('weight', 1), ('weight', 1)]) - c.column_types[:] = [('weight', 2)] - self.assertEqual(len(c.contents), 1) - - def test_list_box(self): - lb = urwid.ListBox(urwid.SimpleFocusListWalker([])) - self.assertEqual(lb.focus, None) - self.assertRaises(IndexError, lambda: getattr(lb, 'focus_position')) - self.assertRaises(IndexError, lambda: setattr(lb, 'focus_position', - None)) - self.assertRaises(IndexError, lambda: setattr(lb, 'focus_position', 0)) - - t1 = urwid.Text(u'one') - t2 = urwid.Text(u'two') - lb = urwid.ListBox(urwid.SimpleListWalker([t1, t2])) - self.assertEqual(lb.focus, t1) - self.assertEqual(lb.focus_position, 0) - lb.focus_position = 1 - self.assertEqual(lb.focus, t2) - self.assertEqual(lb.focus_position, 1) - lb.focus_position = 0 - self.assertRaises(IndexError, lambda: setattr(lb, 'focus_position', -1)) - self.assertRaises(IndexError, lambda: setattr(lb, 'focus_position', 2)) - - def test_grid_flow(self): - gf = urwid.GridFlow([], 5, 1, 0, 'left') - self.assertEqual(gf.focus, None) - self.assertEqual(gf.contents, []) - self.assertRaises(IndexError, lambda: getattr(gf, 'focus_position')) - self.assertRaises(IndexError, lambda: setattr(gf, 'focus_position', - None)) - self.assertRaises(IndexError, lambda: setattr(gf, 'focus_position', 0)) - self.assertEqual(gf.options(), ('given', 5)) - self.assertEqual(gf.options(width_amount=9), ('given', 9)) - self.assertRaises(urwid.GridFlowError, lambda: gf.options( - 'pack', None)) - - t1 = urwid.Text(u'one') - t2 = urwid.Text(u'two') - gf = urwid.GridFlow([t1, t2], 5, 1, 0, 'left') - self.assertEqual(gf.focus, t1) - self.assertEqual(gf.focus_position, 0) - self.assertEqual(gf.contents, [(t1, ('given', 5)), (t2, ('given', 5))]) - gf.focus_position = 1 - self.assertEqual(gf.focus, t2) - self.assertEqual(gf.focus_position, 1) - gf.contents.insert(0, (t2, ('given', 5))) - self.assertEqual(gf.focus_position, 2) - self.assertRaises(urwid.GridFlowError, lambda: gf.contents.append(())) - self.assertRaises(urwid.GridFlowError, lambda: gf.contents.insert(1, - (t1, ('pack', None)))) - gf.focus_position = 0 - self.assertRaises(IndexError, lambda: setattr(gf, 'focus_position', -1)) - self.assertRaises(IndexError, lambda: setattr(gf, 'focus_position', 3)) - # old methods: - gf.set_focus(0) - self.assertRaises(IndexError, lambda: gf.set_focus(-1)) - self.assertRaises(IndexError, lambda: gf.set_focus(3)) - gf.set_focus(t1) - self.assertEqual(gf.focus_position, 1) - self.assertRaises(ValueError, lambda: gf.set_focus('nonexistant')) - - def test_overlay(self): - s1 = urwid.SolidFill(u'1') - s2 = urwid.SolidFill(u'2') - o = urwid.Overlay(s1, s2, - 'center', ('relative', 50), 'middle', ('relative', 50)) - self.assertEqual(o.focus, s1) - self.assertEqual(o.focus_position, 1) - self.assertRaises(IndexError, lambda: setattr(o, 'focus_position', - None)) - self.assertRaises(IndexError, lambda: setattr(o, 'focus_position', 2)) - - self.assertEqual(o.contents[0], (s2, - urwid.Overlay._DEFAULT_BOTTOM_OPTIONS)) - self.assertEqual(o.contents[1], (s1, ( - 'center', None, 'relative', 50, None, 0, 0, - 'middle', None, 'relative', 50, None, 0, 0))) - - def test_frame(self): - s1 = urwid.SolidFill(u'1') - - f = urwid.Frame(s1) - self.assertEqual(f.focus, s1) - self.assertEqual(f.focus_position, 'body') - self.assertRaises(IndexError, lambda: setattr(f, 'focus_position', - None)) - self.assertRaises(IndexError, lambda: setattr(f, 'focus_position', - 'header')) - - t1 = urwid.Text(u'one') - t2 = urwid.Text(u'two') - t3 = urwid.Text(u'three') - f = urwid.Frame(s1, t1, t2, 'header') - self.assertEqual(f.focus, t1) - self.assertEqual(f.focus_position, 'header') - f.focus_position = 'footer' - self.assertEqual(f.focus, t2) - self.assertEqual(f.focus_position, 'footer') - self.assertRaises(IndexError, lambda: setattr(f, 'focus_position', -1)) - self.assertRaises(IndexError, lambda: setattr(f, 'focus_position', 2)) - del f.contents['footer'] - self.assertEqual(f.footer, None) - self.assertEqual(f.focus_position, 'body') - f.contents.update(footer=(t3, None), header=(t2, None)) - self.assertEqual(f.header, t2) - self.assertEqual(f.footer, t3) - def set1(): - f.contents['body'] = t1 - self.assertRaises(urwid.FrameError, set1) - def set2(): - f.contents['body'] = (t1, 'given') - self.assertRaises(urwid.FrameError, set2) - - def test_focus_path(self): - # big tree of containers - t = urwid.Text(u'x') - e = urwid.Edit(u'?') - c = urwid.Columns([t, e, t, t]) - p = urwid.Pile([t, t, c, t]) - a = urwid.AttrMap(p, 'gets ignored') - s = urwid.SolidFill(u'/') - o = urwid.Overlay(e, s, 'center', 'pack', 'middle', 'pack') - lb = urwid.ListBox(urwid.SimpleFocusListWalker([t, a, o, t])) - lb.focus_position = 1 - g = urwid.GridFlow([t, t, t, t, e, t], 10, 0, 0, 'left') - g.focus_position = 4 - f = urwid.Frame(lb, header=t, footer=g) - - self.assertEqual(f.get_focus_path(), ['body', 1, 2, 1]) - f.set_focus_path(['footer']) # same as f.focus_position = 'footer' - self.assertEqual(f.get_focus_path(), ['footer', 4]) - f.set_focus_path(['body', 1, 2, 2]) - self.assertEqual(f.get_focus_path(), ['body', 1, 2, 2]) - self.assertRaises(IndexError, lambda: f.set_focus_path([0, 1, 2])) - self.assertRaises(IndexError, lambda: f.set_focus_path(['body', 2, 2])) - f.set_focus_path(['body', 2]) # focus the overlay - self.assertEqual(f.get_focus_path(), ['body', 2, 1]) diff --git a/urwid/tests/test_decoration.py b/urwid/tests/test_decoration.py deleted file mode 100644 index 8ab1acd..0000000 --- a/urwid/tests/test_decoration.py +++ /dev/null @@ -1,149 +0,0 @@ -import unittest - -import urwid - - -class PaddingTest(unittest.TestCase): - def ptest(self, desc, align, width, maxcol, left, right,min_width=None): - p = urwid.Padding(None, align, width, min_width) - l, r = p.padding_values((maxcol,),False) - assert (l,r)==(left,right), "%s expected %s but got %s"%( - desc, (left,right), (l,r)) - - def petest(self, desc, align, width): - self.assertRaises(urwid.PaddingError, lambda: - urwid.Padding(None, align, width)) - - def test_create(self): - self.petest("invalid pad",6,5) - self.petest("invalid pad type",('bad',2),5) - self.petest("invalid width",'center','42') - self.petest("invalid width type",'center',('gouranga',4)) - - def test_values(self): - self.ptest("left align 5 7",'left',5,7,0,2) - self.ptest("left align 7 7",'left',7,7,0,0) - self.ptest("left align 9 7",'left',9,7,0,0) - self.ptest("right align 5 7",'right',5,7,2,0) - self.ptest("center align 5 7",'center',5,7,1,1) - self.ptest("fixed left",('fixed left',3),5,10,3,2) - self.ptest("fixed left reduce",('fixed left',3),8,10,2,0) - self.ptest("fixed left shrink",('fixed left',3),18,10,0,0) - self.ptest("fixed left, right", - ('fixed left',3),('fixed right',4),17,3,4) - self.ptest("fixed left, right, min_width", - ('fixed left',3),('fixed right',4),10,3,2,5) - self.ptest("fixed left, right, min_width 2", - ('fixed left',3),('fixed right',4),10,2,0,8) - self.ptest("fixed right",('fixed right',3),5,10,2,3) - self.ptest("fixed right reduce",('fixed right',3),8,10,0,2) - self.ptest("fixed right shrink",('fixed right',3),18,10,0,0) - self.ptest("fixed right, left", - ('fixed right',3),('fixed left',4),17,4,3) - self.ptest("fixed right, left, min_width", - ('fixed right',3),('fixed left',4),10,2,3,5) - self.ptest("fixed right, left, min_width 2", - ('fixed right',3),('fixed left',4),10,0,2,8) - self.ptest("relative 30",('relative',30),5,10,1,4) - self.ptest("relative 50",('relative',50),5,10,2,3) - self.ptest("relative 130 edge",('relative',130),5,10,5,0) - self.ptest("relative -10 edge",('relative',-10),4,10,0,6) - self.ptest("center relative 70",'center',('relative',70), - 10,1,2) - self.ptest("center relative 70 grow 8",'center',('relative',70), - 10,1,1,8) - - def mctest(self, desc, left, right, size, cx, innercx): - class Inner: - def __init__(self, desc, innercx): - self.desc = desc - self.innercx = innercx - def move_cursor_to_coords(self,size,cx,cy): - assert cx==self.innercx, desc - i = Inner(desc,innercx) - p = urwid.Padding(i, ('fixed left',left), - ('fixed right',right)) - p.move_cursor_to_coords(size, cx, 0) - - def test_cursor(self): - self.mctest("cursor left edge",2,2,(10,2),2,0) - self.mctest("cursor left edge-1",2,2,(10,2),1,0) - self.mctest("cursor right edge",2,2,(10,2),7,5) - self.mctest("cursor right edge+1",2,2,(10,2),8,5) - - def test_reduced_padding_cursor(self): - # FIXME: This is at least consistent now, but I don't like it. - # pack() on an Edit should leave room for the cursor - # fixing this gets deep into things like Edit._shift_view_to_cursor - # though, so this might not get fixed for a while - - p = urwid.Padding(urwid.Edit(u'',u''), width='pack', left=4) - self.assertEqual(p.render((10,), True).cursor, None) - self.assertEqual(p.get_cursor_coords((10,)), None) - self.assertEqual(p.render((4,), True).cursor, None) - self.assertEqual(p.get_cursor_coords((4,)), None) - - p = urwid.Padding(urwid.Edit(u'',u''), width=('relative', 100), left=4) - self.assertEqual(p.render((10,), True).cursor, (4, 0)) - self.assertEqual(p.get_cursor_coords((10,)), (4, 0)) - self.assertEqual(p.render((4,), True).cursor, None) - self.assertEqual(p.get_cursor_coords((4,)), None) - - -class FillerTest(unittest.TestCase): - def ftest(self, desc, valign, height, maxrow, top, bottom, - min_height=None): - f = urwid.Filler(None, valign, height, min_height) - t, b = f.filler_values((20,maxrow), False) - assert (t,b)==(top,bottom), "%s expected %s but got %s"%( - desc, (top,bottom), (t,b)) - - def fetest(self, desc, valign, height): - self.assertRaises(urwid.FillerError, lambda: - urwid.Filler(None, valign, height)) - - def test_create(self): - self.fetest("invalid pad",6,5) - self.fetest("invalid pad type",('bad',2),5) - self.fetest("invalid width",'middle','42') - self.fetest("invalid width type",'middle',('gouranga',4)) - self.fetest("invalid combination",('relative',20), - ('fixed bottom',4)) - self.fetest("invalid combination 2",('relative',20), - ('fixed top',4)) - - def test_values(self): - self.ftest("top align 5 7",'top',5,7,0,2) - self.ftest("top align 7 7",'top',7,7,0,0) - self.ftest("top align 9 7",'top',9,7,0,0) - self.ftest("bottom align 5 7",'bottom',5,7,2,0) - self.ftest("middle align 5 7",'middle',5,7,1,1) - self.ftest("fixed top",('fixed top',3),5,10,3,2) - self.ftest("fixed top reduce",('fixed top',3),8,10,2,0) - self.ftest("fixed top shrink",('fixed top',3),18,10,0,0) - self.ftest("fixed top, bottom", - ('fixed top',3),('fixed bottom',4),17,3,4) - self.ftest("fixed top, bottom, min_width", - ('fixed top',3),('fixed bottom',4),10,3,2,5) - self.ftest("fixed top, bottom, min_width 2", - ('fixed top',3),('fixed bottom',4),10,2,0,8) - self.ftest("fixed bottom",('fixed bottom',3),5,10,2,3) - self.ftest("fixed bottom reduce",('fixed bottom',3),8,10,0,2) - self.ftest("fixed bottom shrink",('fixed bottom',3),18,10,0,0) - self.ftest("fixed bottom, top", - ('fixed bottom',3),('fixed top',4),17,4,3) - self.ftest("fixed bottom, top, min_height", - ('fixed bottom',3),('fixed top',4),10,2,3,5) - self.ftest("fixed bottom, top, min_height 2", - ('fixed bottom',3),('fixed top',4),10,0,2,8) - self.ftest("relative 30",('relative',30),5,10,1,4) - self.ftest("relative 50",('relative',50),5,10,2,3) - self.ftest("relative 130 edge",('relative',130),5,10,5,0) - self.ftest("relative -10 edge",('relative',-10),4,10,0,6) - self.ftest("middle relative 70",'middle',('relative',70), - 10,1,2) - self.ftest("middle relative 70 grow 8",'middle',('relative',70), - 10,1,1,8) - - def test_repr(self): - repr(urwid.Filler(urwid.Text(u'hai'))) diff --git a/urwid/tests/test_doctests.py b/urwid/tests/test_doctests.py deleted file mode 100644 index 1720a48..0000000 --- a/urwid/tests/test_doctests.py +++ /dev/null @@ -1,22 +0,0 @@ -import unittest -import doctest - -import urwid - -def load_tests(loader, tests, ignore): - module_doctests = [ - urwid.widget, - urwid.wimp, - urwid.decoration, - urwid.display_common, - urwid.main_loop, - urwid.monitored_list, - urwid.raw_display, - 'urwid.split_repr', # override function with same name - urwid.util, - urwid.signals, - ] - for m in module_doctests: - tests.addTests(doctest.DocTestSuite(m, - optionflags=doctest.ELLIPSIS | doctest.IGNORE_EXCEPTION_DETAIL)) - return tests diff --git a/urwid/tests/test_event_loops.py b/urwid/tests/test_event_loops.py deleted file mode 100644 index c85bbed..0000000 --- a/urwid/tests/test_event_loops.py +++ /dev/null @@ -1,147 +0,0 @@ -import os -import unittest -import platform - -import urwid -from urwid.compat import PYTHON3 - - -class EventLoopTestMixin(object): - def test_event_loop(self): - rd, wr = os.pipe() - evl = self.evl - out = [] - def step1(): - out.append("writing") - os.write(wr, "hi".encode('ascii')) - def step2(): - out.append(os.read(rd, 2).decode('ascii')) - raise urwid.ExitMainLoop - handle = evl.alarm(0, step1) - handle = evl.watch_file(rd, step2) - evl.run() - self.assertEqual(out, ["writing", "hi"]) - - def test_remove_alarm(self): - evl = self.evl - handle = evl.alarm(50, lambda: None) - self.assertTrue(evl.remove_alarm(handle)) - self.assertFalse(evl.remove_alarm(handle)) - - def test_remove_watch_file(self): - evl = self.evl - handle = evl.watch_file(5, lambda: None) - self.assertTrue(evl.remove_watch_file(handle)) - self.assertFalse(evl.remove_watch_file(handle)) - - _expected_idle_handle = 1 - - def test_run(self): - evl = self.evl - out = [] - rd, wr = os.pipe() - self.assertEqual(os.write(wr, "data".encode('ascii')), 4) - def say_hello(): - out.append("hello") - def say_waiting(): - out.append("waiting") - def exit_clean(): - out.append("clean exit") - raise urwid.ExitMainLoop - def exit_error(): - 1/0 - handle = evl.alarm(0.01, exit_clean) - handle = evl.alarm(0.005, say_hello) - idle_handle = evl.enter_idle(say_waiting) - if self._expected_idle_handle is not None: - self.assertEqual(idle_handle, 1) - evl.run() - self.assertTrue("hello" in out, out) - self.assertTrue("clean exit"in out, out) - handle = evl.watch_file(rd, exit_clean) - del out[:] - evl.run() - self.assertEqual(out, ["clean exit"]) - self.assertTrue(evl.remove_watch_file(handle)) - handle = evl.alarm(0, exit_error) - self.assertRaises(ZeroDivisionError, evl.run) - handle = evl.watch_file(rd, exit_error) - self.assertRaises(ZeroDivisionError, evl.run) - - -class SelectEventLoopTest(unittest.TestCase, EventLoopTestMixin): - def setUp(self): - self.evl = urwid.SelectEventLoop() - - -try: - import gi.repository -except ImportError: - pass -else: - class GLibEventLoopTest(unittest.TestCase, EventLoopTestMixin): - def setUp(self): - self.evl = urwid.GLibEventLoop() - - -try: - import tornado -except ImportError: - pass -else: - class TornadoEventLoopTest(unittest.TestCase, EventLoopTestMixin): - def setUp(self): - from tornado.ioloop import IOLoop - self.evl = urwid.TornadoEventLoop(IOLoop()) - - -try: - import twisted -except ImportError: - pass -else: - class TwistedEventLoopTest(unittest.TestCase, EventLoopTestMixin): - def setUp(self): - self.evl = urwid.TwistedEventLoop() - - # can't restart twisted reactor, so use shortened tests - def test_event_loop(self): - pass - - def test_run(self): - evl = self.evl - out = [] - rd, wr = os.pipe() - self.assertEqual(os.write(wr, "data".encode('ascii')), 4) - def step2(): - out.append(os.read(rd, 2).decode('ascii')) - def say_hello(): - out.append("hello") - def say_waiting(): - out.append("waiting") - def exit_clean(): - out.append("clean exit") - raise urwid.ExitMainLoop - def exit_error(): - 1/0 - handle = evl.watch_file(rd, step2) - handle = evl.alarm(0.01, exit_clean) - handle = evl.alarm(0.005, say_hello) - self.assertEqual(evl.enter_idle(say_waiting), 1) - evl.run() - self.assertTrue("da" in out, out) - self.assertTrue("ta" in out, out) - self.assertTrue("hello" in out, out) - self.assertTrue("clean exit" in out, out) - - -try: - import asyncio -except ImportError: - pass -else: - class AsyncioEventLoopTest(unittest.TestCase, EventLoopTestMixin): - def setUp(self): - self.evl = urwid.AsyncioEventLoop() - - _expected_idle_handle = None diff --git a/urwid/tests/test_graphics.py b/urwid/tests/test_graphics.py deleted file mode 100644 index 08e34d2..0000000 --- a/urwid/tests/test_graphics.py +++ /dev/null @@ -1,97 +0,0 @@ -import unittest - -from urwid import graphics -from urwid.compat import B -import urwid - - -class LineBoxTest(unittest.TestCase): - def border(self, tl, t, tr, l, r, bl, b, br): - return [bytes().join([tl, t, tr]), - bytes().join([l, B(" "), r]), - bytes().join([bl, b, br]),] - - def test_linebox_border(self): - urwid.set_encoding("utf-8") - t = urwid.Text("") - - l = urwid.LineBox(t).render((3,)).text - - # default - self.assertEqual(l, - self.border(B("\xe2\x94\x8c"), B("\xe2\x94\x80"), - B("\xe2\x94\x90"), B("\xe2\x94\x82"), B("\xe2\x94\x82"), - B("\xe2\x94\x94"), B("\xe2\x94\x80"), B("\xe2\x94\x98"))) - - nums = [B(str(n)) for n in range(8)] - b = dict(zip(["tlcorner", "tline", "trcorner", "lline", "rline", - "blcorner", "bline", "brcorner"], nums)) - l = urwid.LineBox(t, **b).render((3,)).text - - self.assertEqual(l, self.border(*nums)) - - -class BarGraphTest(unittest.TestCase): - def bgtest(self, desc, data, top, widths, maxrow, exp ): - rval = graphics.calculate_bargraph_display(data,top,widths,maxrow) - assert rval == exp, "%s expected %r, got %r"%(desc,exp,rval) - - def test1(self): - self.bgtest('simplest',[[0]],5,[1],1, - [(1,[(0,1)])] ) - self.bgtest('simpler',[[0],[0]],5,[1,2],5, - [(5,[(0,3)])] ) - self.bgtest('simple',[[5]],5,[1],1, - [(1,[(1,1)])] ) - self.bgtest('2col-1',[[2],[0]],5,[1,2],5, - [(3,[(0,3)]), (2,[(1,1),(0,2)]) ] ) - self.bgtest('2col-2',[[0],[2]],5,[1,2],5, - [(3,[(0,3)]), (2,[(0,1),(1,2)]) ] ) - self.bgtest('2col-3',[[2],[3]],5,[1,2],5, - [(2,[(0,3)]), (1,[(0,1),(1,2)]), (2,[(1,3)]) ] ) - self.bgtest('3col-1',[[5],[3],[0]],5,[2,1,1],5, - [(2,[(1,2),(0,2)]), (3,[(1,3),(0,1)]) ] ) - self.bgtest('3col-2',[[4],[4],[4]],5,[2,1,1],5, - [(1,[(0,4)]), (4,[(1,4)]) ] ) - self.bgtest('3col-3',[[1],[2],[3]],5,[2,1,1],5, - [(2,[(0,4)]), (1,[(0,3),(1,1)]), (1,[(0,2),(1,2)]), - (1,[(1,4)]) ] ) - self.bgtest('3col-4',[[4],[2],[4]],5,[1,2,1],5, - [(1,[(0,4)]), (2,[(1,1),(0,2),(1,1)]), (2,[(1,4)]) ] ) - - def test2(self): - self.bgtest('simple1a',[[2,0],[2,1]],2,[1,1],2, - [(1,[(1,2)]),(1,[(1,1),(2,1)]) ] ) - self.bgtest('simple1b',[[2,1],[2,0]],2,[1,1],2, - [(1,[(1,2)]),(1,[(2,1),(1,1)]) ] ) - self.bgtest('cross1a',[[2,2],[1,2]],2,[1,1],2, - [(2,[(2,2)]) ] ) - self.bgtest('cross1b',[[1,2],[2,2]],2,[1,1],2, - [(2,[(2,2)]) ] ) - self.bgtest('mix1a',[[3,2,1],[2,2,2],[1,2,3]],3,[1,1,1],3, - [(1,[(1,1),(0,1),(3,1)]),(1,[(2,1),(3,2)]), - (1,[(3,3)]) ] ) - self.bgtest('mix1b',[[1,2,3],[2,2,2],[3,2,1]],3,[1,1,1],3, - [(1,[(3,1),(0,1),(1,1)]),(1,[(3,2),(2,1)]), - (1,[(3,3)]) ] ) - -class SmoothBarGraphTest(unittest.TestCase): - def sbgtest(self, desc, data, top, exp ): - urwid.set_encoding('utf-8') - g = urwid.BarGraph( ['black','red','blue'], - None, {(1,0):'red/black', (2,1):'blue/red'}) - g.set_data( data, top ) - rval = g.calculate_display((5,3)) - assert rval == exp, "%s expected %r, got %r"%(desc,exp,rval) - - def test1(self): - self.sbgtest('simple', [[3]], 5, - [(1, [(0, 5)]), (1, [((1, 0, 6), 5)]), (1, [(1, 5)])] ) - self.sbgtest('boring', [[4,2]], 6, - [(1, [(0, 5)]), (1, [(1, 5)]), (1, [(2,5)]) ] ) - self.sbgtest('two', [[4],[2]], 6, - [(1, [(0, 5)]), (1, [(1, 3), (0, 2)]), (1, [(1, 5)]) ] ) - self.sbgtest('twos', [[3],[4]], 6, - [(1, [(0, 5)]), (1, [((1,0,4), 3), (1, 2)]), (1, [(1,5)]) ] ) - self.sbgtest('twof', [[4],[3]], 6, - [(1, [(0, 5)]), (1, [(1,3), ((1,0,4), 2)]), (1, [(1,5)]) ] ) diff --git a/urwid/tests/test_listbox.py b/urwid/tests/test_listbox.py deleted file mode 100644 index 8afeb97..0000000 --- a/urwid/tests/test_listbox.py +++ /dev/null @@ -1,804 +0,0 @@ -import unittest - -from urwid.compat import B -from urwid.tests.util import SelectableText -import urwid - - -class ListBoxCalculateVisibleTest(unittest.TestCase): - def cvtest(self, desc, body, focus, offset_rows, inset_fraction, - exp_offset_inset, exp_cur ): - - lbox = urwid.ListBox(body) - lbox.body.set_focus( focus ) - lbox.offset_rows = offset_rows - lbox.inset_fraction = inset_fraction - - middle, top, bottom = lbox.calculate_visible((4,5),focus=1) - offset_inset, focus_widget, focus_pos, _ign, cursor = middle - - if cursor is not None: - x, y = cursor - y += offset_inset - cursor = x, y - - assert offset_inset == exp_offset_inset, "%s got: %r expected: %r" %(desc,offset_inset,exp_offset_inset) - assert cursor == exp_cur, "%s (cursor) got: %r expected: %r" %(desc,cursor,exp_cur) - - def test1_simple(self): - T = urwid.Text - - l = [T(""),T(""),T("\n"),T("\n\n"),T("\n"),T(""),T("")] - - self.cvtest( "simple top position", - l, 3, 0, (0,1), 0, None ) - - self.cvtest( "simple middle position", - l, 3, 1, (0,1), 1, None ) - - self.cvtest( "simple bottom postion", - l, 3, 2, (0,1), 2, None ) - - self.cvtest( "straddle top edge", - l, 3, 0, (1,2), -1, None ) - - self.cvtest( "straddle bottom edge", - l, 3, 4, (0,1), 4, None ) - - self.cvtest( "off bottom edge", - l, 3, 5, (0,1), 4, None ) - - self.cvtest( "way off bottom edge", - l, 3, 100, (0,1), 4, None ) - - self.cvtest( "gap at top", - l, 0, 2, (0,1), 0, None ) - - self.cvtest( "gap at top and off bottom edge", - l, 2, 5, (0,1), 2, None ) - - self.cvtest( "gap at bottom", - l, 6, 1, (0,1), 4, None ) - - self.cvtest( "gap at bottom and straddling top edge", - l, 4, 0, (1,2), 1, None ) - - self.cvtest( "gap at bottom cannot completely fill", - [T(""),T(""),T("")], 1, 0, (0,1), 1, None ) - - self.cvtest( "gap at top and bottom", - [T(""),T(""),T("")], 1, 2, (0,1), 1, None ) - - - def test2_cursor(self): - T, E = urwid.Text, urwid.Edit - - l1 = [T(""),T(""),T("\n"),E("","\n\nX"),T("\n"),T(""),T("")] - l2 = [T(""),T(""),T("\n"),E("","YY\n\n"),T("\n"),T(""),T("")] - - l2[3].set_edit_pos(2) - - self.cvtest( "plain cursor in view", - l1, 3, 1, (0,1), 1, (1,3) ) - - self.cvtest( "cursor off top", - l2, 3, 0, (1,3), 0, (2, 0) ) - - self.cvtest( "cursor further off top", - l2, 3, 0, (2,3), 0, (2, 0) ) - - self.cvtest( "cursor off bottom", - l1, 3, 3, (0,1), 2, (1, 4) ) - - self.cvtest( "cursor way off bottom", - l1, 3, 100, (0,1), 2, (1, 4) ) - - -class ListBoxChangeFocusTest(unittest.TestCase): - def cftest(self, desc, body, pos, offset_inset, - coming_from, cursor, snap_rows, - exp_offset_rows, exp_inset_fraction, exp_cur ): - - lbox = urwid.ListBox(body) - - lbox.change_focus( (4,5), pos, offset_inset, coming_from, - cursor, snap_rows ) - - exp = exp_offset_rows, exp_inset_fraction - act = lbox.offset_rows, lbox.inset_fraction - - cursor = None - focus_widget, focus_pos = lbox.body.get_focus() - if focus_widget.selectable(): - if hasattr(focus_widget,'get_cursor_coords'): - cursor=focus_widget.get_cursor_coords((4,)) - - assert act == exp, "%s got: %s expected: %s" %(desc, act, exp) - assert cursor == exp_cur, "%s (cursor) got: %r expected: %r" %(desc,cursor,exp_cur) - - - def test1unselectable(self): - T = urwid.Text - l = [T("\n"),T("\n\n"),T("\n\n"),T("\n\n"),T("\n")] - - self.cftest( "simple unselectable", - l, 2, 0, None, None, None, 0, (0,1), None ) - - self.cftest( "unselectable", - l, 2, 1, None, None, None, 1, (0,1), None ) - - self.cftest( "unselectable off top", - l, 2, -2, None, None, None, 0, (2,3), None ) - - self.cftest( "unselectable off bottom", - l, 3, 2, None, None, None, 2, (0,1), None ) - - def test2selectable(self): - T, S = urwid.Text, SelectableText - l = [T("\n"),T("\n\n"),S("\n\n"),T("\n\n"),T("\n")] - - self.cftest( "simple selectable", - l, 2, 0, None, None, None, 0, (0,1), None ) - - self.cftest( "selectable", - l, 2, 1, None, None, None, 1, (0,1), None ) - - self.cftest( "selectable at top", - l, 2, 0, 'below', None, None, 0, (0,1), None ) - - self.cftest( "selectable at bottom", - l, 2, 2, 'above', None, None, 2, (0,1), None ) - - self.cftest( "selectable off top snap", - l, 2, -1, 'below', None, None, 0, (0,1), None ) - - self.cftest( "selectable off bottom snap", - l, 2, 3, 'above', None, None, 2, (0,1), None ) - - self.cftest( "selectable off top no snap", - l, 2, -1, 'above', None, None, 0, (1,3), None ) - - self.cftest( "selectable off bottom no snap", - l, 2, 3, 'below', None, None, 3, (0,1), None ) - - def test3large_selectable(self): - T, S = urwid.Text, SelectableText - l = [T("\n"),S("\n\n\n\n\n\n"),T("\n")] - self.cftest( "large selectable no snap", - l, 1, -1, None, None, None, 0, (1,7), None ) - - self.cftest( "large selectable snap up", - l, 1, -2, 'below', None, None, 0, (0,1), None ) - - self.cftest( "large selectable snap up2", - l, 1, -2, 'below', None, 2, 0, (0,1), None ) - - self.cftest( "large selectable almost snap up", - l, 1, -2, 'below', None, 1, 0, (2,7), None ) - - self.cftest( "large selectable snap down", - l, 1, 0, 'above', None, None, 0, (2,7), None ) - - self.cftest( "large selectable snap down2", - l, 1, 0, 'above', None, 2, 0, (2,7), None ) - - self.cftest( "large selectable almost snap down", - l, 1, 0, 'above', None, 1, 0, (0,1), None ) - - m = [T("\n\n\n\n"), S("\n\n\n\n\n"), T("\n\n\n\n")] - self.cftest( "large selectable outside view down", - m, 1, 4, 'above', None, None, 0, (0,1), None ) - - self.cftest( "large selectable outside view up", - m, 1, -5, 'below', None, None, 0, (1,6), None ) - - def test4cursor(self): - T,E = urwid.Text, urwid.Edit - #... - - def test5set_focus_valign(self): - T,E = urwid.Text, urwid.Edit - lbox = urwid.ListBox(urwid.SimpleFocusListWalker([ - T(''), T('')])) - lbox.set_focus_valign('middle') - # TODO: actually test the result - - -class ListBoxRenderTest(unittest.TestCase): - def ltest(self,desc,body,focus,offset_inset_rows,exp_text,exp_cur): - exp_text = [B(t) for t in exp_text] - lbox = urwid.ListBox(body) - lbox.body.set_focus( focus ) - lbox.shift_focus((4,10), offset_inset_rows ) - canvas = lbox.render( (4,5), focus=1 ) - - text = [bytes().join([t for at, cs, t in ln]) for ln in canvas.content()] - - cursor = canvas.cursor - - assert text == exp_text, "%s (text) got: %r expected: %r" %(desc,text,exp_text) - assert cursor == exp_cur, "%s (cursor) got: %r expected: %r" %(desc,cursor,exp_cur) - - - def test1_simple(self): - T = urwid.Text - - self.ltest( "simple one text item render", - [T("1\n2")], 0, 0, - ["1 ","2 "," "," "," "],None) - - self.ltest( "simple multi text item render off bottom", - [T("1"),T("2"),T("3\n4"),T("5"),T("6")], 2, 2, - ["1 ","2 ","3 ","4 ","5 "],None) - - self.ltest( "simple multi text item render off top", - [T("1"),T("2"),T("3\n4"),T("5"),T("6")], 2, 1, - ["2 ","3 ","4 ","5 ","6 "],None) - - def test2_trim(self): - T = urwid.Text - - self.ltest( "trim unfocused bottom", - [T("1\n2"),T("3\n4"),T("5\n6")], 1, 2, - ["1 ","2 ","3 ","4 ","5 "],None) - - self.ltest( "trim unfocused top", - [T("1\n2"),T("3\n4"),T("5\n6")], 1, 1, - ["2 ","3 ","4 ","5 ","6 "],None) - - self.ltest( "trim none full focus", - [T("1\n2\n3\n4\n5")], 0, 0, - ["1 ","2 ","3 ","4 ","5 "],None) - - self.ltest( "trim focus bottom", - [T("1\n2\n3\n4\n5\n6")], 0, 0, - ["1 ","2 ","3 ","4 ","5 "],None) - - self.ltest( "trim focus top", - [T("1\n2\n3\n4\n5\n6")], 0, -1, - ["2 ","3 ","4 ","5 ","6 "],None) - - self.ltest( "trim focus top and bottom", - [T("1\n2\n3\n4\n5\n6\n7")], 0, -1, - ["2 ","3 ","4 ","5 ","6 "],None) - - def test3_shift(self): - T,E = urwid.Text, urwid.Edit - - self.ltest( "shift up one fit", - [T("1\n2"),T("3"),T("4"),T("5"),T("6")], 4, 5, - ["2 ","3 ","4 ","5 ","6 "],None) - - e = E("","ab\nc",1) - e.set_edit_pos( 2 ) - self.ltest( "shift down one cursor over edge", - [e,T("3"),T("4"),T("5\n6")], 0, -1, - ["ab ","c ","3 ","4 ","5 "], (2,0)) - - self.ltest( "shift up one cursor over edge", - [T("1\n2"),T("3"),T("4"),E("","d\ne")], 3, 4, - ["2 ","3 ","4 ","d ","e "], (1,4)) - - self.ltest( "shift none cursor top focus over edge", - [E("","ab\n"),T("3"),T("4"),T("5\n6")], 0, -1, - [" ","3 ","4 ","5 ","6 "], (0,0)) - - e = E("","abc\nd") - e.set_edit_pos( 3 ) - self.ltest( "shift none cursor bottom focus over edge", - [T("1\n2"),T("3"),T("4"),e], 3, 4, - ["1 ","2 ","3 ","4 ","abc "], (3,4)) - - def test4_really_large_contents(self): - T,E = urwid.Text, urwid.Edit - self.ltest("really large edit", - [T(u"hello"*100)], 0, 0, - ["hell","ohel","lohe","lloh","ello"], None) - - self.ltest("really large edit", - [E(u"", u"hello"*100)], 0, 0, - ["hell","ohel","lohe","lloh","llo "], (3,4)) - - -class ListBoxKeypressTest(unittest.TestCase): - def ktest(self, desc, key, body, focus, offset_inset, - exp_focus, exp_offset_inset, exp_cur, lbox = None): - - if lbox is None: - lbox = urwid.ListBox(body) - lbox.body.set_focus( focus ) - lbox.shift_focus((4,10), offset_inset ) - - ret_key = lbox.keypress((4,5),key) - middle, top, bottom = lbox.calculate_visible((4,5),focus=1) - offset_inset, focus_widget, focus_pos, _ign, cursor = middle - - if cursor is not None: - x, y = cursor - y += offset_inset - cursor = x, y - - exp = exp_focus, exp_offset_inset - act = focus_pos, offset_inset - assert act == exp, "%s got: %r expected: %r" %(desc,act,exp) - assert cursor == exp_cur, "%s (cursor) got: %r expected: %r" %(desc,cursor,exp_cur) - return ret_key,lbox - - - def test1_up(self): - T,S,E = urwid.Text, SelectableText, urwid.Edit - - self.ktest( "direct selectable both visible", 'up', - [S(""),S("")], 1, 1, - 0, 0, None ) - - self.ktest( "selectable skip one all visible", 'up', - [S(""),T(""),S("")], 2, 2, - 0, 0, None ) - - key,lbox = self.ktest( "nothing above no scroll", 'up', - [S("")], 0, 0, - 0, 0, None ) - assert key == 'up' - - key, lbox = self.ktest( "unselectable above no scroll", 'up', - [T(""),T(""),S("")], 2, 2, - 2, 2, None ) - assert key == 'up' - - self.ktest( "unselectable above scroll 1", 'up', - [T(""),S(""),T("\n\n\n")], 1, 0, - 1, 1, None ) - - self.ktest( "selectable above scroll 1", 'up', - [S(""),S(""),T("\n\n\n")], 1, 0, - 0, 0, None ) - - self.ktest( "selectable above too far", 'up', - [S(""),T(""),S(""),T("\n\n\n")], 2, 0, - 2, 1, None ) - - self.ktest( "selectable above skip 1 scroll 1", 'up', - [S(""),T(""),S(""),T("\n\n\n")], 2, 1, - 0, 0, None ) - - self.ktest( "tall selectable above scroll 2", 'up', - [S(""),S("\n"),S(""),T("\n\n\n")], 2, 0, - 1, 0, None ) - - self.ktest( "very tall selectable above scroll 5", 'up', - [S(""),S("\n\n\n\n"),S(""),T("\n\n\n\n")], 2, 0, - 1, 0, None ) - - self.ktest( "very tall selected scroll within 1", 'up', - [S(""),S("\n\n\n\n\n")], 1, -1, - 1, 0, None ) - - self.ktest( "edit above pass cursor", 'up', - [E("","abc"),E("","de")], 1, 1, - 0, 0, (2, 0) ) - - key,lbox = self.ktest( "edit too far above pass cursor A", 'up', - [E("","abc"),T("\n\n\n\n"),E("","de")], 2, 4, - 1, 0, None ) - - self.ktest( "edit too far above pass cursor B", 'up', - None, None, None, - 0, 0, (2,0), lbox ) - - self.ktest( "within focus cursor made not visible", 'up', - [T("\n\n\n"),E("hi\n","ab")], 1, 3, - 0, 0, None ) - - self.ktest( "within focus cursor made not visible (2)", 'up', - [T("\n\n\n\n"),E("hi\n","ab")], 1, 3, - 0, -1, None ) - - self.ktest( "force focus unselectable" , 'up', - [T("\n\n\n\n"),S("")], 1, 4, - 0, 0, None ) - - self.ktest( "pathological cursor widget", 'up', - [T("\n"),E("\n\n\n\n\n","a")], 1, 4, - 0, -1, None ) - - self.ktest( "unselectable to unselectable", 'up', - [T(""),T(""),T(""),T(""),T(""),T(""),T("")], 2, 0, - 1, 0, None ) - - self.ktest( "unselectable over edge to same", 'up', - [T(""),T("12\n34"),T(""),T(""),T(""),T("")],1,-1, - 1, 0, None ) - - key,lbox = self.ktest( "edit short between pass cursor A", 'up', - [E("","abcd"),E("","a"),E("","def")], 2, 2, - 1, 1, (1,1) ) - - self.ktest( "edit short between pass cursor B", 'up', - None, None, None, - 0, 0, (3,0), lbox ) - - e = E("","\n\n\n\n\n") - e.set_edit_pos(1) - key,lbox = self.ktest( "edit cursor force scroll", 'up', - [e], 0, -1, - 0, 0, (0,0) ) - assert lbox.inset_fraction[0] == 0 - - def test2_down(self): - T,S,E = urwid.Text, SelectableText, urwid.Edit - - self.ktest( "direct selectable both visible", 'down', - [S(""),S("")], 0, 0, - 1, 1, None ) - - self.ktest( "selectable skip one all visible", 'down', - [S(""),T(""),S("")], 0, 0, - 2, 2, None ) - - key,lbox = self.ktest( "nothing below no scroll", 'down', - [S("")], 0, 0, - 0, 0, None ) - assert key == 'down' - - key, lbox = self.ktest( "unselectable below no scroll", 'down', - [S(""),T(""),T("")], 0, 0, - 0, 0, None ) - assert key == 'down' - - self.ktest( "unselectable below scroll 1", 'down', - [T("\n\n\n"),S(""),T("")], 1, 4, - 1, 3, None ) - - self.ktest( "selectable below scroll 1", 'down', - [T("\n\n\n"),S(""),S("")], 1, 4, - 2, 4, None ) - - self.ktest( "selectable below too far", 'down', - [T("\n\n\n"),S(""),T(""),S("")], 1, 4, - 1, 3, None ) - - self.ktest( "selectable below skip 1 scroll 1", 'down', - [T("\n\n\n"),S(""),T(""),S("")], 1, 3, - 3, 4, None ) - - self.ktest( "tall selectable below scroll 2", 'down', - [T("\n\n\n"),S(""),S("\n"),S("")], 1, 4, - 2, 3, None ) - - self.ktest( "very tall selectable below scroll 5", 'down', - [T("\n\n\n\n"),S(""),S("\n\n\n\n"),S("")], 1, 4, - 2, 0, None ) - - self.ktest( "very tall selected scroll within 1", 'down', - [S("\n\n\n\n\n"),S("")], 0, 0, - 0, -1, None ) - - self.ktest( "edit below pass cursor", 'down', - [E("","de"),E("","abc")], 0, 0, - 1, 1, (2, 1) ) - - key,lbox=self.ktest( "edit too far below pass cursor A", 'down', - [E("","de"),T("\n\n\n\n"),E("","abc")], 0, 0, - 1, 0, None ) - - self.ktest( "edit too far below pass cursor B", 'down', - None, None, None, - 2, 4, (2,4), lbox ) - - odd_e = E("","hi\nab") - odd_e.set_edit_pos( 2 ) - # disble cursor movement in odd_e object - odd_e.move_cursor_to_coords = lambda s,c,xy: 0 - self.ktest( "within focus cursor made not visible", 'down', - [odd_e,T("\n\n\n\n")], 0, 0, - 1, 1, None ) - - self.ktest( "within focus cursor made not visible (2)", 'down', - [odd_e,T("\n\n\n\n"),], 0, 0, - 1, 1, None ) - - self.ktest( "force focus unselectable" , 'down', - [S(""),T("\n\n\n\n")], 0, 0, - 1, 0, None ) - - odd_e.set_edit_text( "hi\n\n\n\n\n" ) - self.ktest( "pathological cursor widget", 'down', - [odd_e,T("\n")], 0, 0, - 1, 4, None ) - - self.ktest( "unselectable to unselectable", 'down', - [T(""),T(""),T(""),T(""),T(""),T(""),T("")], 4, 4, - 5, 4, None ) - - self.ktest( "unselectable over edge to same", 'down', - [T(""),T(""),T(""),T(""),T("12\n34"),T("")],4,4, - 4, 3, None ) - - key,lbox=self.ktest( "edit short between pass cursor A", 'down', - [E("","abc"),E("","a"),E("","defg")], 0, 0, - 1, 1, (1,1) ) - - self.ktest( "edit short between pass cursor B", 'down', - None, None, None, - 2, 2, (3,2), lbox ) - - e = E("","\n\n\n\n\n") - e.set_edit_pos(4) - key,lbox = self.ktest( "edit cursor force scroll", 'down', - [e], 0, 0, - 0, -1, (0,4) ) - assert lbox.inset_fraction[0] == 1 - - def test3_page_up(self): - T,S,E = urwid.Text, SelectableText, urwid.Edit - - self.ktest( "unselectable aligned to aligned", 'page up', - [T(""),T("\n"),T("\n\n"),T(""),T("\n"),T("\n\n")], 3, 0, - 1, 0, None ) - - self.ktest( "unselectable unaligned to aligned", 'page up', - [T(""),T("\n"),T("\n"),T("\n"),T("\n"),T("\n\n")], 3,-1, - 1, 0, None ) - - self.ktest( "selectable to unselectable", 'page up', - [T(""),T("\n"),T("\n"),T("\n"),S("\n"),T("\n\n")], 4, 1, - 1, -1, None ) - - self.ktest( "selectable to cut off selectable", 'page up', - [S("\n\n"),T("\n"),T("\n"),S("\n"),T("\n\n")], 3, 1, - 0, -1, None ) - - self.ktest( "seletable to selectable", 'page up', - [T("\n\n"),S("\n"),T("\n"),S("\n"),T("\n\n")], 3, 1, - 1, 1, None ) - - self.ktest( "within very long selectable", 'page up', - [S(""),S("\n\n\n\n\n\n\n\n"),T("\n")], 1, -6, - 1, -1, None ) - - e = E("","\n\nab\n\n\n\n\ncd\n") - e.set_edit_pos(11) - self.ktest( "within very long cursor widget", 'page up', - [S(""),e,T("\n")], 1, -6, - 1, -2, (2, 0) ) - - self.ktest( "pathological cursor widget", 'page up', - [T(""),E("\n\n\n\n\n\n\n\n","ab"),T("")], 1, -5, - 0, 0, None ) - - e = E("","\nab\n\n\n\n\ncd\n") - e.set_edit_pos(10) - self.ktest( "very long cursor widget snap", 'page up', - [T(""),e,T("\n")], 1, -5, - 1, 0, (2, 1) ) - - self.ktest( "slight scroll selectable", 'page up', - [T("\n"),S("\n"),T(""),S(""),T("\n\n\n"),S("")], 5, 4, - 3, 0, None ) - - self.ktest( "scroll into snap region", 'page up', - [T("\n"),S("\n"),T(""),T(""),T("\n\n\n"),S("")], 5, 4, - 1, 0, None ) - - self.ktest( "mid scroll short", 'page up', - [T("\n"),T(""),T(""),S(""),T(""),T("\n"),S(""),T("\n")], - 6, 2, 3, 1, None ) - - self.ktest( "mid scroll long", 'page up', - [T("\n"),S(""),T(""),S(""),T(""),T("\n"),S(""),T("\n")], - 6, 2, 1, 0, None ) - - self.ktest( "mid scroll perfect", 'page up', - [T("\n"),S(""),S(""),S(""),T(""),T("\n"),S(""),T("\n")], - 6, 2, 2, 0, None ) - - self.ktest( "cursor move up fail short", 'page up', - [T("\n"),T("\n"),E("","\nab"),T(""),T("")], 2, 1, - 2, 4, (0, 4) ) - - self.ktest( "cursor force fail short", 'page up', - [T("\n"),T("\n"),E("\n","ab"),T(""),T("")], 2, 1, - 0, 0, None ) - - odd_e = E("","hi\nab") - odd_e.set_edit_pos( 2 ) - # disble cursor movement in odd_e object - odd_e.move_cursor_to_coords = lambda s,c,xy: 0 - self.ktest( "cursor force fail long", 'page up', - [odd_e,T("\n"),T("\n"),T("\n"),S(""),T("\n")], 4, 2, - 1, -1, None ) - - self.ktest( "prefer not cut off", 'page up', - [S("\n"),T("\n"),S(""),T("\n\n"),S(""),T("\n")], 4, 2, - 2, 1, None ) - - self.ktest( "allow cut off", 'page up', - [S("\n"),T("\n"),T(""),T("\n\n"),S(""),T("\n")], 4, 2, - 0, -1, None ) - - self.ktest( "at top fail", 'page up', - [T("\n\n"),T("\n"),T("\n\n\n")], 0, 0, - 0, 0, None ) - - self.ktest( "all visible fail", 'page up', - [T("a"),T("\n")], 0, 0, - 0, 0, None ) - - self.ktest( "current ok fail", 'page up', - [T("\n\n"),S("hi")], 1, 3, - 1, 3, None ) - - self.ktest( "all visible choose top selectable", 'page up', - [T(""),S("a"),S("b"),S("c")], 3, 3, - 1, 1, None ) - - self.ktest( "bring in edge choose top", 'page up', - [S("b"),T("-"),S("-"),T("c"),S("d"),T("-")],4,3, - 0, 0, None ) - - self.ktest( "bring in edge choose top selectable", 'page up', - [T("b"),S("-"),S("-"),T("c"),S("d"),T("-")],4,3, - 1, 1, None ) - - def test4_page_down(self): - T,S,E = urwid.Text, SelectableText, urwid.Edit - - self.ktest( "unselectable aligned to aligned", 'page down', - [T("\n\n"),T("\n"),T(""),T("\n\n"),T("\n"),T("")], 2, 4, - 4, 3, None ) - - self.ktest( "unselectable unaligned to aligned", 'page down', - [T("\n\n"),T("\n"),T("\n"),T("\n"),T("\n"),T("")], 2, 4, - 4, 3, None ) - - self.ktest( "selectable to unselectable", 'page down', - [T("\n\n"),S("\n"),T("\n"),T("\n"),T("\n"),T("")], 1, 2, - 4, 4, None ) - - self.ktest( "selectable to cut off selectable", 'page down', - [T("\n\n"),S("\n"),T("\n"),T("\n"),S("\n\n")], 1, 2, - 4, 3, None ) - - self.ktest( "seletable to selectable", 'page down', - [T("\n\n"),S("\n"),T("\n"),S("\n"),T("\n\n")], 1, 1, - 3, 2, None ) - - self.ktest( "within very long selectable", 'page down', - [T("\n"),S("\n\n\n\n\n\n\n\n"),S("")], 1, 2, - 1, -3, None ) - - e = E("","\nab\n\n\n\n\ncd\n\n") - e.set_edit_pos(2) - self.ktest( "within very long cursor widget", 'page down', - [T("\n"),e,S("")], 1, 2, - 1, -2, (1, 4) ) - - odd_e = E("","ab\n\n\n\n\n\n\n\n\n") - odd_e.set_edit_pos( 1 ) - # disble cursor movement in odd_e object - odd_e.move_cursor_to_coords = lambda s,c,xy: 0 - self.ktest( "pathological cursor widget", 'page down', - [T(""),odd_e,T("")], 1, 1, - 2, 4, None ) - - e = E("","\nab\n\n\n\n\ncd\n") - e.set_edit_pos(2) - self.ktest( "very long cursor widget snap", 'page down', - [T("\n"),e,T("")], 1, 2, - 1, -3, (1, 3) ) - - self.ktest( "slight scroll selectable", 'page down', - [S(""),T("\n\n\n"),S(""),T(""),S("\n"),T("\n")], 0, 0, - 2, 4, None ) - - self.ktest( "scroll into snap region", 'page down', - [S(""),T("\n\n\n"),T(""),T(""),S("\n"),T("\n")], 0, 0, - 4, 3, None ) - - self.ktest( "mid scroll short", 'page down', - [T("\n"),S(""),T("\n"),T(""),S(""),T(""),T(""),T("\n")], - 1, 2, 4, 3, None ) - - self.ktest( "mid scroll long", 'page down', - [T("\n"),S(""),T("\n"),T(""),S(""),T(""),S(""),T("\n")], - 1, 2, 6, 4, None ) - - self.ktest( "mid scroll perfect", 'page down', - [T("\n"),S(""),T("\n"),T(""),S(""),S(""),S(""),T("\n")], - 1, 2, 5, 4, None ) - - e = E("","hi\nab") - e.set_edit_pos( 1 ) - self.ktest( "cursor move up fail short", 'page down', - [T(""),T(""),e,T("\n"),T("\n")], 2, 1, - 2, -1, (1, 0) ) - - - odd_e = E("","hi\nab") - odd_e.set_edit_pos( 1 ) - # disble cursor movement in odd_e object - odd_e.move_cursor_to_coords = lambda s,c,xy: 0 - self.ktest( "cursor force fail short", 'page down', - [T(""),T(""),odd_e,T("\n"),T("\n")], 2, 2, - 4, 3, None ) - - self.ktest( "cursor force fail long", 'page down', - [T("\n"),S(""),T("\n"),T("\n"),T("\n"),E("hi\n","ab")], - 1, 2, 4, 4, None ) - - self.ktest( "prefer not cut off", 'page down', - [T("\n"),S(""),T("\n\n"),S(""),T("\n"),S("\n")], 1, 2, - 3, 3, None ) - - self.ktest( "allow cut off", 'page down', - [T("\n"),S(""),T("\n\n"),T(""),T("\n"),S("\n")], 1, 2, - 5, 4, None ) - - self.ktest( "at bottom fail", 'page down', - [T("\n\n"),T("\n"),T("\n\n\n")], 2, 1, - 2, 1, None ) - - self.ktest( "all visible fail", 'page down', - [T("a"),T("\n")], 1, 1, - 1, 1, None ) - - self.ktest( "current ok fail", 'page down', - [S("hi"),T("\n\n")], 0, 0, - 0, 0, None ) - - self.ktest( "all visible choose last selectable", 'page down', - [S("a"),S("b"),S("c"),T("")], 0, 0, - 2, 2, None ) - - self.ktest( "bring in edge choose last", 'page down', - [T("-"),S("d"),T("c"),S("-"),T("-"),S("b")],1,1, - 5,4, None ) - - self.ktest( "bring in edge choose last selectable", 'page down', - [T("-"),S("d"),T("c"),S("-"),S("-"),T("b")],1,1, - 4,3, None ) - - -class ZeroHeightContentsTest(unittest.TestCase): - def test_listbox_pile(self): - lb = urwid.ListBox(urwid.SimpleListWalker( - [urwid.Pile([])])) - lb.render((40,10), focus=True) - - def test_listbox_text_pile_page_down(self): - lb = urwid.ListBox(urwid.SimpleListWalker( - [urwid.Text(u'above'), urwid.Pile([])])) - lb.keypress((40,10), 'page down') - self.assertEqual(lb.get_focus()[1], 0) - lb.keypress((40,10), 'page down') # second one caused ListBox failure - self.assertEqual(lb.get_focus()[1], 0) - - def test_listbox_text_pile_page_up(self): - lb = urwid.ListBox(urwid.SimpleListWalker( - [urwid.Pile([]), urwid.Text(u'below')])) - lb.set_focus(1) - lb.keypress((40,10), 'page up') - self.assertEqual(lb.get_focus()[1], 1) - lb.keypress((40,10), 'page up') # second one caused pile failure - self.assertEqual(lb.get_focus()[1], 1) - - def test_listbox_text_pile_down(self): - sp = urwid.Pile([]) - sp.selectable = lambda: True # abuse our Pile - lb = urwid.ListBox(urwid.SimpleListWalker([urwid.Text(u'above'), sp])) - lb.keypress((40,10), 'down') - self.assertEqual(lb.get_focus()[1], 0) - lb.keypress((40,10), 'down') - self.assertEqual(lb.get_focus()[1], 0) - - def test_listbox_text_pile_up(self): - sp = urwid.Pile([]) - sp.selectable = lambda: True # abuse our Pile - lb = urwid.ListBox(urwid.SimpleListWalker([sp, urwid.Text(u'below')])) - lb.set_focus(1) - lb.keypress((40,10), 'up') - self.assertEqual(lb.get_focus()[1], 1) - lb.keypress((40,10), 'up') - self.assertEqual(lb.get_focus()[1], 1) - diff --git a/urwid/tests/test_str_util.py b/urwid/tests/test_str_util.py deleted file mode 100644 index f7ad1d9..0000000 --- a/urwid/tests/test_str_util.py +++ /dev/null @@ -1,37 +0,0 @@ -import unittest - -from urwid.compat import B -from urwid.escape import str_util - - -class DecodeOneTest(unittest.TestCase): - def gwt(self, ch, exp_ord, exp_pos): - ch = B(ch) - o, pos = str_util.decode_one(ch,0) - assert o==exp_ord, " got:%r expected:%r" % (o, exp_ord) - assert pos==exp_pos, " got:%r expected:%r" % (pos, exp_pos) - - def test1byte(self): - self.gwt("ab", ord("a"), 1) - self.gwt("\xc0a", ord("?"), 1) # error - - def test2byte(self): - self.gwt("\xc2", ord("?"), 1) # error - self.gwt("\xc0\x80", ord("?"), 1) # error - self.gwt("\xc2\x80", 0x80, 2) - self.gwt("\xdf\xbf", 0x7ff, 2) - - def test3byte(self): - self.gwt("\xe0", ord("?"), 1) # error - self.gwt("\xe0\xa0", ord("?"), 1) # error - self.gwt("\xe0\x90\x80", ord("?"), 1) # error - self.gwt("\xe0\xa0\x80", 0x800, 3) - self.gwt("\xef\xbf\xbf", 0xffff, 3) - - def test4byte(self): - self.gwt("\xf0", ord("?"), 1) # error - self.gwt("\xf0\x90", ord("?"), 1) # error - self.gwt("\xf0\x90\x80", ord("?"), 1) # error - self.gwt("\xf0\x80\x80\x80", ord("?"), 1) # error - self.gwt("\xf0\x90\x80\x80", 0x10000, 4) - self.gwt("\xf3\xbf\xbf\xbf", 0xfffff, 4) diff --git a/urwid/tests/test_text_layout.py b/urwid/tests/test_text_layout.py deleted file mode 100644 index b446319..0000000 --- a/urwid/tests/test_text_layout.py +++ /dev/null @@ -1,342 +0,0 @@ -import unittest - -from urwid import text_layout -from urwid.compat import B -import urwid - - -class CalcBreaksTest(object): - def cbtest(self, width, exp): - result = text_layout.default_layout.calculate_text_segments( - B(self.text), width, self.mode ) - assert len(result) == len(exp), repr((result, exp)) - for l,e in zip(result, exp): - end = l[-1][-1] - assert end == e, repr((result,exp)) - - def test(self): - for width, exp in self.do: - self.cbtest( width, exp ) - - -class CalcBreaksCharTest(CalcBreaksTest, unittest.TestCase): - mode = 'any' - text = "abfghsdjf askhtrvs\naltjhgsdf ljahtshgf" - # tests - do = [ - ( 100, [18,38] ), - ( 6, [6, 12, 18, 25, 31, 37, 38] ), - ( 10, [10, 18, 29, 38] ), - ] - - -class CalcBreaksDBCharTest(CalcBreaksTest, unittest.TestCase): - def setUp(self): - urwid.set_encoding("euc-jp") - - mode = 'any' - text = "abfgh\xA1\xA1j\xA1\xA1xskhtrvs\naltjhgsdf\xA1\xA1jahtshgf" - # tests - do = [ - ( 10, [10, 18, 28, 38] ), - ( 6, [5, 11, 17, 18, 25, 31, 37, 38] ), - ( 100, [18, 38]), - ] - - -class CalcBreaksWordTest(CalcBreaksTest, unittest.TestCase): - mode = 'space' - text = "hello world\nout there. blah" - # tests - do = [ - ( 10, [5, 11, 22, 27] ), - ( 5, [5, 11, 17, 22, 27] ), - ( 100, [11, 27] ), - ] - - -class CalcBreaksWordTest2(CalcBreaksTest, unittest.TestCase): - mode = 'space' - text = "A simple set of words, really...." - do = [ - ( 10, [8, 15, 22, 33]), - ( 17, [15, 33]), - ( 13, [12, 22, 33]), - ] - - -class CalcBreaksDBWordTest(CalcBreaksTest, unittest.TestCase): - def setUp(self): - urwid.set_encoding("euc-jp") - - mode = 'space' - text = "hel\xA1\xA1 world\nout-\xA1\xA1tre blah" - # tests - do = [ - ( 10, [5, 11, 21, 26] ), - ( 5, [5, 11, 16, 21, 26] ), - ( 100, [11, 26] ), - ] - - -class CalcBreaksUTF8Test(CalcBreaksTest, unittest.TestCase): - def setUp(self): - urwid.set_encoding("utf-8") - - mode = 'space' - text = '\xe6\x9b\xbf\xe6\xb4\xbc\xe6\xb8\x8e\xe6\xba\x8f\xe6\xbd\xba' - do = [ - (4, [6, 12, 15] ), - (10, [15] ), - (5, [6, 12, 15] ), - ] - - -class CalcBreaksCantDisplayTest(unittest.TestCase): - def test(self): - urwid.set_encoding("euc-jp") - self.assertRaises(text_layout.CanNotDisplayText, - text_layout.default_layout.calculate_text_segments, - B('\xA1\xA1'), 1, 'space' ) - urwid.set_encoding("utf-8") - self.assertRaises(text_layout.CanNotDisplayText, - text_layout.default_layout.calculate_text_segments, - B('\xe9\xa2\x96'), 1, 'space' ) - - -class SubsegTest(unittest.TestCase): - def setUp(self): - urwid.set_encoding("euc-jp") - - def st(self, seg, text, start, end, exp): - text = B(text) - s = urwid.LayoutSegment(seg) - result = s.subseg( text, start, end ) - assert result == exp, "Expected %r, got %r"%(exp,result) - - def test1_padding(self): - self.st( (10, None), "", 0, 8, [(8, None)] ) - self.st( (10, None), "", 2, 10, [(8, None)] ) - self.st( (10, 0), "", 3, 7, [(4, 0)] ) - self.st( (10, 0), "", 0, 20, [(10, 0)] ) - - def test2_text(self): - self.st( (10, 0, B("1234567890")), "", 0, 8, [(8,0,B("12345678"))] ) - self.st( (10, 0, B("1234567890")), "", 2, 10, [(8,0,B("34567890"))] ) - self.st( (10, 0, B("12\xA1\xA156\xA1\xA190")), "", 2, 8, - [(6, 0, B("\xA1\xA156\xA1\xA1"))] ) - self.st( (10, 0, B("12\xA1\xA156\xA1\xA190")), "", 3, 8, - [(5, 0, B(" 56\xA1\xA1"))] ) - self.st( (10, 0, B("12\xA1\xA156\xA1\xA190")), "", 2, 7, - [(5, 0, B("\xA1\xA156 "))] ) - self.st( (10, 0, B("12\xA1\xA156\xA1\xA190")), "", 3, 7, - [(4, 0, B(" 56 "))] ) - self.st( (10, 0, B("12\xA1\xA156\xA1\xA190")), "", 0, 20, - [(10, 0, B("12\xA1\xA156\xA1\xA190"))] ) - - def test3_range(self): - t = "1234567890" - self.st( (10, 0, 10), t, 0, 8, [(8, 0, 8)] ) - self.st( (10, 0, 10), t, 2, 10, [(8, 2, 10)] ) - self.st( (6, 2, 8), t, 1, 6, [(5, 3, 8)] ) - self.st( (6, 2, 8), t, 0, 5, [(5, 2, 7)] ) - self.st( (6, 2, 8), t, 1, 5, [(4, 3, 7)] ) - t = "12\xA1\xA156\xA1\xA190" - self.st( (10, 0, 10), t, 0, 8, [(8, 0, 8)] ) - self.st( (10, 0, 10), t, 2, 10, [(8, 2, 10)] ) - self.st( (6, 2, 8), t, 1, 6, [(1, 3), (4, 4, 8)] ) - self.st( (6, 2, 8), t, 0, 5, [(4, 2, 6), (1, 6)] ) - self.st( (6, 2, 8), t, 1, 5, [(1, 3), (2, 4, 6), (1, 6)] ) - - -class CalcTranslateTest(object): - def setUp(self): - urwid.set_encoding("utf-8") - - def test1_left(self): - result = urwid.default_layout.layout( self.text, - self.width, 'left', self.mode) - assert result == self.result_left, result - - def test2_right(self): - result = urwid.default_layout.layout( self.text, - self.width, 'right', self.mode) - assert result == self.result_right, result - - def test3_center(self): - result = urwid.default_layout.layout( self.text, - self.width, 'center', self.mode) - assert result == self.result_center, result - - -class CalcTranslateCharTest(CalcTranslateTest, unittest.TestCase): - text = "It's out of control!\nYou've got to" - mode = 'any' - width = 15 - result_left = [ - [(15, 0, 15)], - [(5, 15, 20), (0, 20)], - [(13, 21, 34), (0, 34)]] - result_right = [ - [(15, 0, 15)], - [(10, None), (5, 15, 20), (0,20)], - [(2, None), (13, 21, 34), (0,34)]] - result_center = [ - [(15, 0, 15)], - [(5, None), (5, 15, 20), (0,20)], - [(1, None), (13, 21, 34), (0,34)]] - - -class CalcTranslateWordTest(CalcTranslateTest, unittest.TestCase): - text = "It's out of control!\nYou've got to" - mode = 'space' - width = 14 - result_left = [ - [(11, 0, 11), (0, 11)], - [(8, 12, 20), (0, 20)], - [(13, 21, 34), (0, 34)]] - result_right = [ - [(3, None), (11, 0, 11), (0, 11)], - [(6, None), (8, 12, 20), (0, 20)], - [(1, None), (13, 21, 34), (0, 34)]] - result_center = [ - [(2, None), (11, 0, 11), (0, 11)], - [(3, None), (8, 12, 20), (0, 20)], - [(1, None), (13, 21, 34), (0, 34)]] - - -class CalcTranslateWordTest2(CalcTranslateTest, unittest.TestCase): - text = "It's out of control!\nYou've got to " - mode = 'space' - width = 14 - result_left = [ - [(11, 0, 11), (0, 11)], - [(8, 12, 20), (0, 20)], - [(14, 21, 35), (0, 35)]] - result_right = [ - [(3, None), (11, 0, 11), (0, 11)], - [(6, None), (8, 12, 20), (0, 20)], - [(14, 21, 35), (0, 35)]] - result_center = [ - [(2, None), (11, 0, 11), (0, 11)], - [(3, None), (8, 12, 20), (0, 20)], - [(14, 21, 35), (0, 35)]] - - -class CalcTranslateWordTest3(CalcTranslateTest, unittest.TestCase): - def setUp(self): - urwid.set_encoding('utf-8') - - text = B('\xe6\x9b\xbf\xe6\xb4\xbc\n\xe6\xb8\x8e\xe6\xba\x8f\xe6\xbd\xba') - width = 10 - mode = 'space' - result_left = [ - [(4, 0, 6), (0, 6)], - [(6, 7, 16), (0, 16)]] - result_right = [ - [(6, None), (4, 0, 6), (0, 6)], - [(4, None), (6, 7, 16), (0, 16)]] - result_center = [ - [(3, None), (4, 0, 6), (0, 6)], - [(2, None), (6, 7, 16), (0, 16)]] - - -class CalcTranslateWordTest4(CalcTranslateTest, unittest.TestCase): - text = ' Die Gedank' - width = 3 - mode = 'space' - result_left = [ - [(0, 0)], - [(3, 1, 4), (0, 4)], - [(3, 5, 8)], - [(3, 8, 11), (0, 11)]] - result_right = [ - [(3, None), (0, 0)], - [(3, 1, 4), (0, 4)], - [(3, 5, 8)], - [(3, 8, 11), (0, 11)]] - result_center = [ - [(2, None), (0, 0)], - [(3, 1, 4), (0, 4)], - [(3, 5, 8)], - [(3, 8, 11), (0, 11)]] - - -class CalcTranslateWordTest5(CalcTranslateTest, unittest.TestCase): - text = ' Word.' - width = 3 - mode = 'space' - result_left = [[(3, 0, 3)], [(3, 3, 6), (0, 6)]] - result_right = [[(3, 0, 3)], [(3, 3, 6), (0, 6)]] - result_center = [[(3, 0, 3)], [(3, 3, 6), (0, 6)]] - - -class CalcTranslateClipTest(CalcTranslateTest, unittest.TestCase): - text = "It's out of control!\nYou've got to\n\nturn it off!!!" - mode = 'clip' - width = 14 - result_left = [ - [(20, 0, 20), (0, 20)], - [(13, 21, 34), (0, 34)], - [(0, 35)], - [(14, 36, 50), (0, 50)]] - result_right = [ - [(-6, None), (20, 0, 20), (0, 20)], - [(1, None), (13, 21, 34), (0, 34)], - [(14, None), (0, 35)], - [(14, 36, 50), (0, 50)]] - result_center = [ - [(-3, None), (20, 0, 20), (0, 20)], - [(1, None), (13, 21, 34), (0, 34)], - [(7, None), (0, 35)], - [(14, 36, 50), (0, 50)]] - -class CalcTranslateCantDisplayTest(CalcTranslateTest, unittest.TestCase): - text = B('Hello\xe9\xa2\x96') - mode = 'space' - width = 1 - result_left = [[]] - result_right = [[]] - result_center = [[]] - - -class CalcPosTest(unittest.TestCase): - def setUp(self): - self.text = "A" * 27 - self.trans = [ - [(2,None),(7,0,7),(0,7)], - [(13,8,21),(0,21)], - [(3,None),(5,22,27),(0,27)]] - self.mytests = [(1,0, 0), (2,0, 0), (11,0, 7), - (-3,1, 8), (-2,1, 8), (1,1, 9), (31,1, 21), - (1,2, 22), (11,2, 27) ] - - def tests(self): - for x,y, expected in self.mytests: - got = text_layout.calc_pos( self.text, self.trans, x, y ) - assert got == expected, "%r got:%r expected:%r" % ((x, y), got, - expected) - - -class Pos2CoordsTest(unittest.TestCase): - pos_list = [5, 9, 20, 26] - text = "1234567890" * 3 - mytests = [ - ( [[(15,0,15)], [(15,15,30),(0,30)]], - [(5,0),(9,0),(5,1),(11,1)] ), - ( [[(9,0,9)], [(12,9,21)], [(9,21,30),(0,30)]], - [(5,0),(0,1),(11,1),(5,2)] ), - ( [[(2,None), (15,0,15)], [(2,None), (15,15,30),(0,30)]], - [(7,0),(11,0),(7,1),(13,1)] ), - ( [[(3, 6, 9),(0,9)], [(5, 20, 25),(0,25)]], - [(0,0),(3,0),(0,1),(5,1)] ), - ( [[(10, 0, 10),(0,10)]], - [(5,0),(9,0),(10,0),(10,0)] ), - - ] - - def test(self): - for t, answer in self.mytests: - for pos,a in zip(self.pos_list,answer) : - r = text_layout.calc_coords( self.text, t, pos) - assert r==a, "%r got: %r expected: %r"%(t,r,a) diff --git a/urwid/tests/test_util.py b/urwid/tests/test_util.py deleted file mode 100644 index 5f0531d..0000000 --- a/urwid/tests/test_util.py +++ /dev/null @@ -1,178 +0,0 @@ -# -*- coding: utf-8 -*- -import unittest - -import urwid -from urwid import util -from urwid.compat import B - - -class CalcWidthTest(unittest.TestCase): - def wtest(self, desc, s, exp): - s = B(s) - result = util.calc_width( s, 0, len(s)) - assert result==exp, "%s got:%r expected:%r" % (desc, result, exp) - - def test1(self): - util.set_encoding("utf-8") - self.wtest("narrow", "hello", 5) - self.wtest("wide char", '\xe6\x9b\xbf', 2) - self.wtest("invalid", '\xe6', 1) - self.wtest("zero width", '\xcc\x80', 0) - self.wtest("mixed", 'hello\xe6\x9b\xbf\xe6\x9b\xbf', 9) - - def test2(self): - util.set_encoding("euc-jp") - self.wtest("narrow", "hello", 5) - self.wtest("wide", "\xA1\xA1\xA1\xA1", 4) - self.wtest("invalid", "\xA1", 1) - - -class ConvertDecSpecialTest(unittest.TestCase): - def ctest(self, desc, s, exp, expcs): - exp = B(exp) - util.set_encoding('ascii') - c = urwid.Text(s).render((5,)) - result = c._text[0] - assert result==exp, "%s got:%r expected:%r" % (desc, result, exp) - resultcs = c._cs[0] - assert resultcs==expcs, "%s got:%r expected:%r" % (desc, - resultcs, expcs) - - def test1(self): - self.ctest("no conversion", u"hello", "hello", [(None,5)]) - self.ctest("only special", u"£££££", "}}}}}", [("0",5)]) - self.ctest("mix left", u"££abc", "}}abc", [("0",2),(None,3)]) - self.ctest("mix right", u"abc££", "abc}}", [(None,3),("0",2)]) - self.ctest("mix inner", u"a££bc", "a}}bc", - [(None,1),("0",2),(None,2)] ) - self.ctest("mix well", u"£a£b£", "}a}b}", - [("0",1),(None,1),("0",1),(None,1),("0",1)] ) - - -class WithinDoubleByteTest(unittest.TestCase): - def setUp(self): - urwid.set_encoding("euc-jp") - - def wtest(self, s, ls, pos, expected, desc): - result = util.within_double_byte(B(s), ls, pos) - assert result==expected, "%s got:%r expected: %r" % (desc, - result, expected) - def test1(self): - self.wtest("mnopqr",0,2,0,'simple no high bytes') - self.wtest("mn\xA1\xA1qr",0,2,1,'simple 1st half') - self.wtest("mn\xA1\xA1qr",0,3,2,'simple 2nd half') - self.wtest("m\xA1\xA1\xA1\xA1r",0,3,1,'subsequent 1st half') - self.wtest("m\xA1\xA1\xA1\xA1r",0,4,2,'subsequent 2nd half') - self.wtest("mn\xA1@qr",0,3,2,'simple 2nd half lo') - self.wtest("mn\xA1\xA1@r",0,4,0,'subsequent not 2nd half lo') - self.wtest("m\xA1\xA1\xA1@r",0,4,2,'subsequent 2nd half lo') - - def test2(self): - self.wtest("\xA1\xA1qr",0,0,1,'begin 1st half') - self.wtest("\xA1\xA1qr",0,1,2,'begin 2nd half') - self.wtest("\xA1@qr",0,1,2,'begin 2nd half lo') - self.wtest("\xA1\xA1\xA1\xA1r",0,2,1,'begin subs. 1st half') - self.wtest("\xA1\xA1\xA1\xA1r",0,3,2,'begin subs. 2nd half') - self.wtest("\xA1\xA1\xA1@r",0,3,2,'begin subs. 2nd half lo') - self.wtest("\xA1@\xA1@r",0,3,2,'begin subs. 2nd half lo lo') - self.wtest("@\xA1\xA1@r",0,3,0,'begin subs. not 2nd half lo') - - def test3(self): - self.wtest("abc \xA1\xA1qr",4,4,1,'newline 1st half') - self.wtest("abc \xA1\xA1qr",4,5,2,'newline 2nd half') - self.wtest("abc \xA1@qr",4,5,2,'newline 2nd half lo') - self.wtest("abc \xA1\xA1\xA1\xA1r",4,6,1,'newl subs. 1st half') - self.wtest("abc \xA1\xA1\xA1\xA1r",4,7,2,'newl subs. 2nd half') - self.wtest("abc \xA1\xA1\xA1@r",4,7,2,'newl subs. 2nd half lo') - self.wtest("abc \xA1@\xA1@r",4,7,2,'newl subs. 2nd half lo lo') - self.wtest("abc @\xA1\xA1@r",4,7,0,'newl subs. not 2nd half lo') - - -class CalcTextPosTest(unittest.TestCase): - def ctptest(self, text, tests): - text = B(text) - for s,e,p, expected in tests: - got = util.calc_text_pos( text, s, e, p ) - assert got == expected, "%r got:%r expected:%r" % ((s,e,p), - got, expected) - - def test1(self): - text = "hello world out there" - tests = [ - (0,21,0, (0,0)), - (0,21,5, (5,5)), - (0,21,21, (21,21)), - (0,21,50, (21,21)), - (2,15,50, (15,13)), - (6,21,0, (6,0)), - (6,21,3, (9,3)), - ] - self.ctptest(text, tests) - - def test2_wide(self): - util.set_encoding("euc-jp") - text = "hel\xA1\xA1 world out there" - tests = [ - (0,21,0, (0,0)), - (0,21,4, (3,3)), - (2,21,2, (3,1)), - (2,21,3, (5,3)), - (6,21,0, (6,0)), - ] - self.ctptest(text, tests) - - def test3_utf8(self): - util.set_encoding("utf-8") - text = "hel\xc4\x83 world \xe2\x81\x81 there" - tests = [ - (0,21,0, (0,0)), - (0,21,4, (5,4)), - (2,21,1, (3,1)), - (2,21,2, (5,2)), - (2,21,3, (6,3)), - (6,21,7, (15,7)), - (6,21,8, (16,8)), - ] - self.ctptest(text, tests) - - def test4_utf8(self): - util.set_encoding("utf-8") - text = "he\xcc\x80llo \xe6\x9b\xbf world" - tests = [ - (0,15,0, (0,0)), - (0,15,1, (1,1)), - (0,15,2, (4,2)), - (0,15,4, (6,4)), - (8,15,0, (8,0)), - (8,15,1, (8,0)), - (8,15,2, (11,2)), - (8,15,5, (14,5)), - ] - self.ctptest(text, tests) - - -class TagMarkupTest(unittest.TestCase): - mytests = [ - ("simple one", "simple one", []), - (('blue',"john"), "john", [('blue',4)]), - (["a ","litt","le list"], "a little list", []), - (["mix",('high',[" it ",('ital',"up a")])," little"], - "mix it up a little", - [(None,3),('high',4),('ital',4)]), - ([u"££", u"x££"], u"££x££", []), - ([B("\xc2\x80"), B("\xc2\x80")], B("\xc2\x80\xc2\x80"), []), - ] - - def test(self): - for input, text, attr in self.mytests: - restext,resattr = urwid.decompose_tagmarkup( input ) - assert restext == text, "got: %r expected: %r" % (restext, text) - assert resattr == attr, "got: %r expected: %r" % (resattr, attr) - - def test_bad_tuple(self): - self.assertRaises(urwid.TagMarkupException, lambda: - urwid.decompose_tagmarkup((1,2,3))) - - def test_bad_type(self): - self.assertRaises(urwid.TagMarkupException, lambda: - urwid.decompose_tagmarkup(5)) diff --git a/urwid/tests/test_vterm.py b/urwid/tests/test_vterm.py deleted file mode 100644 index 59fe166..0000000 --- a/urwid/tests/test_vterm.py +++ /dev/null @@ -1,334 +0,0 @@ -# Urwid terminal emulation widget unit tests -# Copyright (C) 2010 aszlig -# Copyright (C) 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/ - -import os -import sys -import unittest - -from itertools import dropwhile - -from urwid import vterm -from urwid import signals -from urwid.compat import B - - -class DummyCommand(object): - QUITSTRING = B('|||quit|||') - - def __init__(self): - self.reader, self.writer = os.pipe() - - def __call__(self): - # reset - stdout = getattr(sys.stdout, 'buffer', sys.stdout) - stdout.write(B('\x1bc')) - - while True: - data = os.read(self.reader, 1024) - if self.QUITSTRING == data: - break - stdout.write(data) - stdout.flush() - - def write(self, data): - os.write(self.writer, data) - - def quit(self): - self.write(self.QUITSTRING) - - -class TermTest(unittest.TestCase): - def setUp(self): - self.command = DummyCommand() - - self.term = vterm.Terminal(self.command) - self.resize(80, 24) - - def tearDown(self): - self.command.quit() - - def connect_signal(self, signal): - self._sig_response = None - - def _set_signal_response(widget, *args, **kwargs): - self._sig_response = (args, kwargs) - self._set_signal_response = _set_signal_response - - signals.connect_signal(self.term, signal, self._set_signal_response) - - def expect_signal(self, *args, **kwargs): - self.assertEqual(self._sig_response, (args, kwargs)) - - def disconnect_signal(self, signal): - signals.disconnect_signal(self.term, signal, self._set_signal_response) - - def caught_beep(self, obj): - self.beeped = True - - def resize(self, width, height, soft=False): - self.termsize = (width, height) - if not soft: - self.term.render(self.termsize, focus=False) - - def write(self, data): - data = B(data) - self.command.write(data.replace(B('\e'), B('\x1b'))) - - def flush(self): - self.write(chr(0x7f)) - - def read(self, raw=False): - self.term.wait_and_feed() - rendered = self.term.render(self.termsize, focus=False) - if raw: - is_empty = lambda c: c == (None, None, B(' ')) - content = list(rendered.content()) - lines = [list(dropwhile(is_empty, reversed(line))) - for line in content] - return [list(reversed(line)) for line in lines if len(line)] - else: - content = rendered.text - lines = [line.rstrip() for line in content] - return B('\n').join(lines).rstrip() - - def expect(self, what, desc=None, raw=False): - if not isinstance(what, list): - what = B(what) - got = self.read(raw=raw) - if desc is None: - desc = '' - else: - desc += '\n' - desc += 'Expected:\n%r\nGot:\n%r' % (what, got) - self.assertEqual(got, what, desc) - - def test_simplestring(self): - self.write('hello world') - self.expect('hello world') - - def test_linefeed(self): - self.write('hello\x0aworld') - self.expect('hello\nworld') - - def test_linefeed2(self): - self.write('aa\b\b\eDbb') - self.expect('aa\nbb') - - def test_carriage_return(self): - self.write('hello\x0dworld') - self.expect('world') - - def test_insertlines(self): - self.write('\e[0;0flast\e[0;0f\e[10L\e[0;0ffirst\nsecond\n\e[11D') - self.expect('first\nsecond\n\n\n\n\n\n\n\n\nlast') - - def test_deletelines(self): - self.write('1\n2\n3\n4\e[2;1f\e[2M') - self.expect('1\n4') - - def test_movement(self): - self.write('\e[10;20H11\e[10;0f\e[20C\e[K') - self.expect('\n' * 9 + ' ' * 19 + '1') - self.write('\e[A\e[B\e[C\e[D\b\e[K') - self.expect('') - self.write('\e[50A2') - self.expect(' ' * 19 + '2') - self.write('\b\e[K\e[50B3') - self.expect('\n' * 23 + ' ' * 19 + '3') - self.write('\b\e[K' + '\eM' * 30 + '\e[100C4') - self.expect(' ' * 79 + '4') - self.write('\e[100D\e[K5') - self.expect('5') - - def edgewall(self): - edgewall = '1-\e[1;%(x)df-2\e[%(y)d;1f3-\e[%(y)d;%(x)df-4\x0d' - self.write(edgewall % {'x': self.termsize[0] - 1, - 'y': self.termsize[1] - 1}) - - def test_horizontal_resize(self): - self.resize(80, 24) - self.edgewall() - self.expect('1-' + ' ' * 76 + '-2' + '\n' * 22 - + '3-' + ' ' * 76 + '-4') - self.resize(78, 24, soft=True) - self.flush() - self.expect('1-' + '\n' * 22 + '3-') - self.resize(80, 24, soft=True) - self.flush() - self.expect('1-' + '\n' * 22 + '3-') - - def test_vertical_resize(self): - self.resize(80, 24) - self.edgewall() - self.expect('1-' + ' ' * 76 + '-2' + '\n' * 22 - + '3-' + ' ' * 76 + '-4') - for y in xrange(23, 1, -1): - self.resize(80, y, soft=True) - self.write('\e[%df\e[J3-\e[%d;%df-4' % (y, y, 79)) - desc = "try to rescale to 80x%d." % y - self.expect('\n' * (y - 2) + '3-' + ' ' * 76 + '-4', desc) - self.resize(80, 24, soft=True) - self.flush() - self.expect('1-' + ' ' * 76 + '-2' + '\n' * 22 - + '3-' + ' ' * 76 + '-4') - - def write_movements(self, arg): - fmt = 'XXX\n\e[faaa\e[Bccc\e[Addd\e[Bfff\e[Cbbb\e[A\e[Deee' - self.write(fmt.replace('\e[', '\e['+arg)) - - def test_defargs(self): - self.write_movements('') - self.expect('aaa ddd eee\n ccc fff bbb') - - def test_nullargs(self): - self.write_movements('0') - self.expect('aaa ddd eee\n ccc fff bbb') - - def test_erase_line(self): - self.write('1234567890\e[5D\e[K\n1234567890\e[5D\e[1K\naaaaaaaaaaaaaaa\e[2Ka') - self.expect('12345\n 7890\n a') - - def test_erase_display(self): - self.write('1234567890\e[5D\e[Ja') - self.expect('12345a') - self.write('98765\e[8D\e[1Jx') - self.expect(' x5a98765') - - def test_scrolling_region_simple(self): - self.write('\e[10;20r\e[10f1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\e[faa') - self.expect('aa' + '\n' * 9 + '2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12') - - def test_scrolling_region_reverse(self): - self.write('\e[2J\e[1;2r\e[5Baaa\r\eM\eM\eMbbb\nXXX') - self.expect('\n\nbbb\nXXX\n\naaa') - - def test_scrolling_region_move(self): - self.write('\e[10;20r\e[2J\e[10Bfoo\rbar\rblah\rmooh\r\e[10Aone\r\eM\eMtwo\r\eM\eMthree\r\eM\eMa') - self.expect('ahree\n\n\n\n\n\n\n\n\n\nmooh') - - def test_scrolling_twice(self): - self.write('\e[?6h\e[10;20r\e[2;5rtest') - self.expect('\ntest') - - def test_cursor_scrolling_region(self): - self.write('\e[?6h\e[10;20r\e[10f1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\e[faa') - self.expect('\n' * 9 + 'aa\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12') - - def test_relative_region_jump(self): - self.write('\e[21H---\e[10;20r\e[?6h\e[18Htest') - self.expect('\n' * 19 + 'test\n---') - - def test_set_multiple_modes(self): - self.write('\e[?6;5htest') - self.expect('test') - self.assertTrue(self.term.term_modes.constrain_scrolling) - self.assertTrue(self.term.term_modes.reverse_video) - self.write('\e[?6;5l') - self.expect('test') - self.assertFalse(self.term.term_modes.constrain_scrolling) - self.assertFalse(self.term.term_modes.reverse_video) - - def test_wrap_simple(self): - self.write('\e[?7h\e[1;%dHtt' % self.term.width) - self.expect(' ' * (self.term.width - 1) + 't\nt') - - def test_wrap_backspace_tab(self): - self.write('\e[?7h\e[1;%dHt\b\b\t\ta' % self.term.width) - self.expect(' ' * (self.term.width - 1) + 'a') - - def test_cursor_visibility(self): - self.write('\e[?25linvisible') - self.expect('invisible') - self.assertEqual(self.term.term.cursor, None) - self.write('\rvisible\e[?25h\e[K') - self.expect('visible') - self.assertNotEqual(self.term.term.cursor, None) - - def test_get_utf8_len(self): - length = self.term.term.get_utf8_len(int("11110000", 2)) - self.assertEqual(length, 3) - length = self.term.term.get_utf8_len(int("11000000", 2)) - self.assertEqual(length, 1) - length = self.term.term.get_utf8_len(int("11111101", 2)) - self.assertEqual(length, 5) - - def test_encoding_unicode(self): - vterm.util._target_encoding = 'utf-8' - self.write('\e%G\xe2\x80\x94') - self.expect('\xe2\x80\x94') - - def test_encoding_unicode_ascii(self): - vterm.util._target_encoding = 'ascii' - self.write('\e%G\xe2\x80\x94') - self.expect('?') - - def test_encoding_wrong_unicode(self): - vterm.util._target_encoding = 'utf-8' - self.write('\e%G\xc0\x99') - self.expect('') - - def test_encoding_vt100_graphics(self): - vterm.util._target_encoding = 'ascii' - self.write('\e)0\e(0\x0fg\x0eg\e)Bn\e)0g\e)B\e(B\x0fn') - self.expect([[ - (None, '0', B('g')), (None, '0', B('g')), - (None, None, B('n')), (None, '0', B('g')), - (None, None, B('n')) - ]], raw=True) - - def test_ibmpc_mapping(self): - vterm.util._target_encoding = 'ascii' - - self.write('\e[11m\x18\e[10m\x18') - self.expect([[(None, 'U', B('\x18'))]], raw=True) - - self.write('\ec\e)U\x0e\x18\x0f\e[3h\x18\e[3l\x18') - self.expect([[(None, None, B('\x18'))]], raw=True) - - self.write('\ec\e[11m\xdb\x18\e[10m\xdb') - self.expect([[ - (None, 'U', B('\xdb')), (None, 'U', B('\x18')), - (None, None, B('\xdb')) - ]], raw=True) - - def test_set_title(self): - self._the_title = None - - def _change_title(widget, title): - self._the_title = title - - self.connect_signal('title') - self.write('\e]666parsed right?\e\\te\e]0;test title\007st1') - self.expect('test1') - self.expect_signal(B('test title')) - self.write('\e]3;stupid title\e\\\e[0G\e[2Ktest2') - self.expect('test2') - self.expect_signal(B('stupid title')) - self.disconnect_signal('title') - - def test_set_leds(self): - self.connect_signal('leds') - self.write('\e[0qtest1') - self.expect('test1') - self.expect_signal('clear') - self.write('\e[3q\e[H\e[Ktest2') - self.expect('test2') - self.expect_signal('caps_lock') - self.disconnect_signal('leds') diff --git a/urwid/tests/test_widget.py b/urwid/tests/test_widget.py deleted file mode 100644 index 3ae9a93..0000000 --- a/urwid/tests/test_widget.py +++ /dev/null @@ -1,153 +0,0 @@ -# -*- coding: utf-8 -*- -import unittest - -from urwid.compat import B -import urwid - - -class TextTest(unittest.TestCase): - def setUp(self): - self.t = urwid.Text("I walk the\ncity in the night") - - def test1_wrap(self): - expected = [B(t) for t in "I walk the","city in ","the night "] - got = self.t.render((10,))._text - assert got == expected, "got: %r expected: %r" % (got, expected) - - def test2_left(self): - self.t.set_align_mode('left') - expected = [B(t) for t in "I walk the ","city in the night "] - got = self.t.render((18,))._text - assert got == expected, "got: %r expected: %r" % (got, expected) - - def test3_right(self): - self.t.set_align_mode('right') - expected = [B(t) for t in " I walk the"," city in the night"] - got = self.t.render((18,))._text - assert got == expected, "got: %r expected: %r" % (got, expected) - - def test4_center(self): - self.t.set_align_mode('center') - expected = [B(t) for t in " I walk the "," city in the night"] - got = self.t.render((18,))._text - assert got == expected, "got: %r expected: %r" % (got, expected) - - def test5_encode_error(self): - urwid.set_encoding("ascii") - expected = [B("? ")] - got = urwid.Text(u'û').render((3,))._text - assert got == expected, "got: %r expected: %r" % (got, expected) - - -class EditTest(unittest.TestCase): - def setUp(self): - self.t1 = urwid.Edit(B(""),"blah blah") - self.t2 = urwid.Edit(B("stuff:"), "blah blah") - self.t3 = urwid.Edit(B("junk:\n"),"blah blah\n\nbloo",1) - self.t4 = urwid.Edit(u"better:") - - def ktest(self, e, key, expected, pos, desc): - got= e.keypress((12,),key) - assert got == expected, "%s. got: %r expected:%r" % (desc, got, - expected) - assert e.edit_pos == pos, "%s. pos: %r expected pos: " % ( - desc, e.edit_pos, pos) - - def test1_left(self): - self.t1.set_edit_pos(0) - self.ktest(self.t1,'left','left',0,"left at left edge") - - self.ktest(self.t2,'left',None,8,"left within text") - - self.t3.set_edit_pos(10) - self.ktest(self.t3,'left',None,9,"left after newline") - - def test2_right(self): - self.ktest(self.t1,'right','right',9,"right at right edge") - - self.t2.set_edit_pos(8) - self.ktest(self.t2,'right',None,9,"right at right edge-1") - self.t3.set_edit_pos(0) - self.t3.keypress((12,),'right') - assert self.t3.get_pref_col((12,)) == 1 - - def test3_up(self): - self.ktest(self.t1,'up','up',9,"up at top") - self.t2.set_edit_pos(2) - self.t2.keypress((12,),"left") - assert self.t2.get_pref_col((12,)) == 7 - self.ktest(self.t2,'up','up',1,"up at top again") - assert self.t2.get_pref_col((12,)) == 7 - self.t3.set_edit_pos(10) - self.ktest(self.t3,'up',None,0,"up at top+1") - - def test4_down(self): - self.ktest(self.t1,'down','down',9,"down single line") - self.t3.set_edit_pos(5) - self.ktest(self.t3,'down',None,10,"down line 1 to 2") - self.ktest(self.t3,'down',None,15,"down line 2 to 3") - self.ktest(self.t3,'down','down',15,"down at bottom") - - def test_utf8_input(self): - urwid.set_encoding("utf-8") - self.t1.set_edit_text('') - self.t1.keypress((12,), u'û') - self.assertEqual(self.t1.edit_text, u'û'.encode('utf-8')) - self.t4.keypress((12,), u'û') - self.assertEqual(self.t4.edit_text, u'û') - - -class EditRenderTest(unittest.TestCase): - def rtest(self, w, expected_text, expected_cursor): - expected_text = [B(t) for t in expected_text] - get_cursor = w.get_cursor_coords((4,)) - assert get_cursor == expected_cursor, "got: %r expected: %r" % ( - get_cursor, expected_cursor) - r = w.render((4,), focus = 1) - text = [t for a, cs, t in [ln[0] for ln in r.content()]] - assert text == expected_text, "got: %r expected: %r" % (text, - expected_text) - assert r.cursor == expected_cursor, "got: %r expected: %r" % ( - r.cursor, expected_cursor) - - def test1_SpaceWrap(self): - w = urwid.Edit("","blah blah") - w.set_edit_pos(0) - self.rtest(w,["blah","blah"],(0,0)) - - w.set_edit_pos(4) - self.rtest(w,["lah ","blah"],(3,0)) - - w.set_edit_pos(5) - self.rtest(w,["blah","blah"],(0,1)) - - w.set_edit_pos(9) - self.rtest(w,["blah","lah "],(3,1)) - - def test2_ClipWrap(self): - w = urwid.Edit("","blah\nblargh",1) - w.set_wrap_mode('clip') - w.set_edit_pos(0) - self.rtest(w,["blah","blar"],(0,0)) - - w.set_edit_pos(10) - self.rtest(w,["blah","argh"],(3,1)) - - w.set_align_mode('right') - w.set_edit_pos(6) - self.rtest(w,["blah","larg"],(0,1)) - - def test3_AnyWrap(self): - w = urwid.Edit("","blah blah") - w.set_wrap_mode('any') - - self.rtest(w,["blah"," bla","h "],(1,2)) - - def test4_CursorNudge(self): - w = urwid.Edit("","hi",align='right') - w.keypress((4,),'end') - - self.rtest(w,[" hi "],(3,0)) - - w.keypress((4,),'left') - self.rtest(w,[" hi"],(3,0)) diff --git a/urwid/tests/util.py b/urwid/tests/util.py deleted file mode 100644 index 9808e2c..0000000 --- a/urwid/tests/util.py +++ /dev/null @@ -1,8 +0,0 @@ -import urwid - -class SelectableText(urwid.Text): - def selectable(self): - return 1 - - def keypress(self, size, key): - return key diff --git a/urwid/text_layout.py b/urwid/text_layout.py deleted file mode 100644 index f09372b..0000000 --- a/urwid/text_layout.py +++ /dev/null @@ -1,506 +0,0 @@ -#!/usr/bin/python -# -# Urwid Text Layout 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 urwid.util import calc_width, calc_text_pos, calc_trim_text, is_wide_char, \ - move_prev_char, move_next_char -from urwid.compat import bytes, PYTHON3, B - -class TextLayout: - def supports_align_mode(self, align): - """Return True if align is a supported align mode.""" - return True - def supports_wrap_mode(self, wrap): - """Return True if wrap is a supported wrap mode.""" - return True - def layout(self, text, width, align, wrap ): - """ - Return a layout structure for text. - - :param text: string in current encoding or unicode string - :param width: number of screen columns available - :param align: align mode for text - :param wrap: wrap mode for text - - Layout structure is a list of line layouts, one per output line. - Line layouts are lists than may contain the following tuples: - - * (column width of text segment, start offset, end offset) - * (number of space characters to insert, offset or None) - * (column width of insert text, offset, "insert text") - - The offset in the last two tuples is used to determine the - attribute used for the inserted spaces or text respectively. - The attribute used will be the same as the attribute at that - text offset. If the offset is None when inserting spaces - then no attribute will be used. - """ - raise NotImplementedError("This function must be overridden by a real" - " text layout class. (see StandardTextLayout)") - -class CanNotDisplayText(Exception): - pass - -class StandardTextLayout(TextLayout): - def __init__(self):#, tab_stops=(), tab_stop_every=8): - pass - #""" - #tab_stops -- list of screen column indexes for tab stops - #tab_stop_every -- repeated interval for following tab stops - #""" - #assert tab_stop_every is None or type(tab_stop_every)==int - #if not tab_stops and tab_stop_every: - # self.tab_stops = (tab_stop_every,) - #self.tab_stops = tab_stops - #self.tab_stop_every = tab_stop_every - def supports_align_mode(self, align): - """Return True if align is 'left', 'center' or 'right'.""" - return align in ('left', 'center', 'right') - def supports_wrap_mode(self, wrap): - """Return True if wrap is 'any', 'space' or 'clip'.""" - return wrap in ('any', 'space', 'clip') - def layout(self, text, width, align, wrap ): - """Return a layout structure for text.""" - try: - segs = self.calculate_text_segments( text, width, wrap ) - return self.align_layout( text, width, segs, wrap, align ) - except CanNotDisplayText: - return [[]] - - def pack(self, maxcol, layout): - """ - Return a minimal maxcol value that would result in the same - number of lines for layout. layout must be a layout structure - returned by self.layout(). - """ - maxwidth = 0 - assert layout, "huh? empty layout?: "+repr(layout) - for l in layout: - lw = line_width(l) - if lw >= maxcol: - return maxcol - maxwidth = max(maxwidth, lw) - return maxwidth - - def align_layout( self, text, width, segs, wrap, align ): - """Convert the layout segs to an aligned layout.""" - out = [] - for l in segs: - sc = line_width(l) - if sc == width or align=='left': - out.append(l) - continue - - if align == 'right': - out.append([(width-sc, None)] + l) - continue - assert align == 'center' - out.append([((width-sc+1) // 2, None)] + l) - return out - - - def calculate_text_segments(self, text, width, wrap): - """ - Calculate the segments of text to display given width screen - columns to display them. - - text - unicode text or byte string to display - width - number of available screen columns - wrap - wrapping mode used - - Returns a layout structure without alignment applied. - """ - nl, nl_o, sp_o = "\n", "\n", " " - if PYTHON3 and isinstance(text, bytes): - nl = B(nl) # can only find bytes in python3 bytestrings - nl_o = ord(nl_o) # + an item of a bytestring is the ordinal value - sp_o = ord(sp_o) - b = [] - p = 0 - if wrap == 'clip': - # no wrapping to calculate, so it's easy. - while p<=len(text): - n_cr = text.find(nl, p) - if n_cr == -1: - n_cr = len(text) - sc = calc_width(text, p, n_cr) - l = [(0,n_cr)] - if p!=n_cr: - l = [(sc, p, n_cr)] + l - b.append(l) - p = n_cr+1 - return b - - - while p<=len(text): - # look for next eligible line break - n_cr = text.find(nl, p) - if n_cr == -1: - n_cr = len(text) - sc = calc_width(text, p, n_cr) - if sc == 0: - # removed character hint - b.append([(0,n_cr)]) - p = n_cr+1 - continue - if sc <= width: - # this segment fits - b.append([(sc,p,n_cr), - # removed character hint - (0,n_cr)]) - - p = n_cr+1 - continue - pos, sc = calc_text_pos( text, p, n_cr, width ) - if pos == p: # pathological width=1 double-byte case - raise CanNotDisplayText( - "Wide character will not fit in 1-column width") - if wrap == 'any': - b.append([(sc,p,pos)]) - p = pos - continue - assert wrap == 'space' - if text[pos] == sp_o: - # perfect space wrap - b.append([(sc,p,pos), - # removed character hint - (0,pos)]) - p = pos+1 - continue - if is_wide_char(text, pos): - # perfect next wide - b.append([(sc,p,pos)]) - p = pos - continue - prev = pos - while prev > p: - prev = move_prev_char(text, p, prev) - if text[prev] == sp_o: - sc = calc_width(text,p,prev) - l = [(0,prev)] - if p!=prev: - l = [(sc,p,prev)] + l - b.append(l) - p = prev+1 - break - if is_wide_char(text,prev): - # wrap after wide char - next = move_next_char(text, prev, pos) - sc = calc_width(text,p,next) - b.append([(sc,p,next)]) - p = next - break - else: - # unwrap previous line space if possible to - # fit more text (we're breaking a word anyway) - if b and (len(b[-1]) == 2 or ( len(b[-1])==1 - and len(b[-1][0])==2 )): - # look for removed space above - if len(b[-1]) == 1: - [(h_sc, h_off)] = b[-1] - p_sc = 0 - p_off = p_end = h_off - else: - [(p_sc, p_off, p_end), - (h_sc, h_off)] = b[-1] - if (p_sc < width and h_sc==0 and - text[h_off] == sp_o): - # combine with previous line - del b[-1] - p = p_off - pos, sc = calc_text_pos( - text, p, n_cr, width ) - b.append([(sc,p,pos)]) - # check for trailing " " or "\n" - p = pos - if p < len(text) and ( - text[p] in (sp_o, nl_o)): - # removed character hint - b[-1].append((0,p)) - p += 1 - continue - - - # force any char wrap - b.append([(sc,p,pos)]) - p = pos - return b - - - -###################################### -# default layout object to use -default_layout = StandardTextLayout() -###################################### - - -class LayoutSegment: - def __init__(self, seg): - """Create object from line layout segment structure""" - - assert type(seg) == tuple, repr(seg) - assert len(seg) in (2,3), repr(seg) - - self.sc, self.offs = seg[:2] - - assert type(self.sc) == int, repr(self.sc) - - if len(seg)==3: - assert type(self.offs) == int, repr(self.offs) - assert self.sc > 0, repr(seg) - t = seg[2] - if type(t) == bytes: - self.text = t - self.end = None - else: - assert type(t) == int, repr(t) - self.text = None - self.end = t - else: - assert len(seg) == 2, repr(seg) - if self.offs is not None: - assert self.sc >= 0, repr(seg) - assert type(self.offs)==int - self.text = self.end = None - - def subseg(self, text, start, end): - """ - Return a "sub-segment" list containing segment structures - that make up a portion of this segment. - - A list is returned to handle cases where wide characters - need to be replaced with a space character at either edge - so two or three segments will be returned. - """ - if start < 0: start = 0 - if end > self.sc: end = self.sc - if start >= end: - return [] # completely gone - if self.text: - # use text stored in segment (self.text) - spos, epos, pad_left, pad_right = calc_trim_text( - self.text, 0, len(self.text), start, end ) - return [ (end-start, self.offs, bytes().ljust(pad_left) + - self.text[spos:epos] + bytes().ljust(pad_right)) ] - elif self.end: - # use text passed as parameter (text) - spos, epos, pad_left, pad_right = calc_trim_text( - text, self.offs, self.end, start, end ) - l = [] - if pad_left: - l.append((1,spos-1)) - l.append((end-start-pad_left-pad_right, spos, epos)) - if pad_right: - l.append((1,epos)) - return l - else: - # simple padding adjustment - return [(end-start,self.offs)] - - -def line_width( segs ): - """ - Return the screen column width of one line of a text layout structure. - - This function ignores any existing shift applied to the line, - represented by an (amount, None) tuple at the start of the line. - """ - sc = 0 - seglist = segs - if segs and len(segs[0])==2 and segs[0][1]==None: - seglist = segs[1:] - for s in seglist: - sc += s[0] - return sc - -def shift_line( segs, amount ): - """ - Return a shifted line from a layout structure to the left or right. - segs -- line of a layout structure - amount -- screen columns to shift right (+ve) or left (-ve) - """ - assert type(amount)==int, repr(amount) - - if segs and len(segs[0])==2 and segs[0][1]==None: - # existing shift - amount += segs[0][0] - if amount: - return [(amount,None)]+segs[1:] - return segs[1:] - - if amount: - return [(amount,None)]+segs - return segs - - -def trim_line( segs, text, start, end ): - """ - Return a trimmed line of a text layout structure. - text -- text to which this layout structure applies - start -- starting screen column - end -- ending screen column - """ - l = [] - x = 0 - for seg in segs: - sc = seg[0] - if start or sc < 0: - if start >= sc: - start -= sc - x += sc - continue - s = LayoutSegment(seg) - if x+sc >= end: - # can all be done at once - return s.subseg( text, start, end-x ) - l += s.subseg( text, start, sc ) - start = 0 - x += sc - continue - if x >= end: - break - if x+sc > end: - s = LayoutSegment(seg) - l += s.subseg( text, 0, end-x ) - break - l.append( seg ) - return l - - - -def calc_line_pos( text, line_layout, pref_col ): - """ - Calculate the closest linear position to pref_col given a - line layout structure. Returns None if no position found. - """ - closest_sc = None - closest_pos = None - current_sc = 0 - - if pref_col == 'left': - for seg in line_layout: - s = LayoutSegment(seg) - if s.offs is not None: - return s.offs - return - elif pref_col == 'right': - for seg in line_layout: - s = LayoutSegment(seg) - if s.offs is not None: - closest_pos = s - s = closest_pos - if s is None: - return - if s.end is None: - return s.offs - return calc_text_pos( text, s.offs, s.end, s.sc-1)[0] - - for seg in line_layout: - s = LayoutSegment(seg) - if s.offs is not None: - if s.end is not None: - if (current_sc <= pref_col and - pref_col < current_sc + s.sc): - # exact match within this segment - return calc_text_pos( text, - s.offs, s.end, - pref_col - current_sc )[0] - elif current_sc <= pref_col: - closest_sc = current_sc + s.sc - 1 - closest_pos = s - - if closest_sc is None or ( abs(pref_col-current_sc) - < abs(pref_col-closest_sc) ): - # this screen column is closer - closest_sc = current_sc - closest_pos = s.offs - if current_sc > closest_sc: - # we're moving past - break - current_sc += s.sc - - if closest_pos is None or type(closest_pos) == int: - return closest_pos - - # return the last positions in the segment "closest_pos" - s = closest_pos - return calc_text_pos( text, s.offs, s.end, s.sc-1)[0] - -def calc_pos( text, layout, pref_col, row ): - """ - Calculate the closest linear position to pref_col and row given a - layout structure. - """ - - if row < 0 or row >= len(layout): - raise Exception("calculate_pos: out of layout row range") - - pos = calc_line_pos( text, layout[row], pref_col ) - if pos is not None: - return pos - - rows_above = range(row-1,-1,-1) - rows_below = range(row+1,len(layout)) - while rows_above and rows_below: - if rows_above: - r = rows_above.pop(0) - pos = calc_line_pos(text, layout[r], pref_col) - if pos is not None: return pos - if rows_below: - r = rows_below.pop(0) - pos = calc_line_pos(text, layout[r], pref_col) - if pos is not None: return pos - return 0 - - -def calc_coords( text, layout, pos, clamp=1 ): - """ - Calculate the coordinates closest to position pos in text with layout. - - text -- raw string or unicode string - layout -- layout structure applied to text - pos -- integer position into text - clamp -- ignored right now - """ - closest = None - y = 0 - for line_layout in layout: - x = 0 - for seg in line_layout: - s = LayoutSegment(seg) - if s.offs is None: - x += s.sc - continue - if s.offs == pos: - return x,y - if s.end is not None and s.offs<=pos and s.end>pos: - x += calc_width( text, s.offs, pos ) - return x,y - distance = abs(s.offs - pos) - if s.end is not None and s.end 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) - diff --git a/urwid/util.py b/urwid/util.py deleted file mode 100644 index 3569f8c..0000000 --- a/urwid/util.py +++ /dev/null @@ -1,474 +0,0 @@ -#!/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 urwid import escape -from urwid.compat import bytes - -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) == unicode: - # 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) == unicode: - 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,(basestring, 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() diff --git a/urwid/version.py b/urwid/version.py deleted file mode 100644 index 74b0021..0000000 --- a/urwid/version.py +++ /dev/null @@ -1,5 +0,0 @@ - -VERSION = (1, 3, 1, 'dev') -__version__ = ''.join(['-.'[type(x) == int]+str(x) for x in VERSION])[1:] - - diff --git a/urwid/vterm.py b/urwid/vterm.py deleted file mode 100644 index cc4eb7f..0000000 --- a/urwid/vterm.py +++ /dev/null @@ -1,1626 +0,0 @@ -#!/usr/bin/python -# -# Urwid terminal emulation widget -# Copyright (C) 2010 aszlig -# Copyright (C) 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/ - -import os -import sys -import time -import copy -import errno -import select -import struct -import signal -import atexit -import traceback - -try: - import pty - import fcntl - import termios -except ImportError: - pass # windows - -from urwid import util -from urwid.escape import DEC_SPECIAL_CHARS, ALT_DEC_SPECIAL_CHARS -from urwid.canvas import Canvas -from urwid.widget import Widget, BOX -from urwid.display_common import AttrSpec, RealTerminal, _BASIC_COLORS -from urwid.compat import ord2, chr2, B, bytes, PYTHON3 - -ESC = chr(27) - -KEY_TRANSLATIONS = { - 'enter': chr(13), - 'backspace': chr(127), - 'tab': chr(9), - 'esc': ESC, - 'up': ESC + '[A', - 'down': ESC + '[B', - 'right': ESC + '[C', - 'left': ESC + '[D', - 'home': ESC + '[1~', - 'insert': ESC + '[2~', - 'delete': ESC + '[3~', - 'end': ESC + '[4~', - 'page up': ESC + '[5~', - 'page down': ESC + '[6~', - - 'f1': ESC + '[[A', - 'f2': ESC + '[[B', - 'f3': ESC + '[[C', - 'f4': ESC + '[[D', - 'f5': ESC + '[[E', - 'f6': ESC + '[17~', - 'f7': ESC + '[18~', - 'f8': ESC + '[19~', - 'f9': ESC + '[20~', - 'f10': ESC + '[21~', - 'f11': ESC + '[23~', - 'f12': ESC + '[24~', -} - -KEY_TRANSLATIONS_DECCKM = { - 'up': ESC + 'OA', - 'down': ESC + 'OB', - 'right': ESC + 'OC', - 'left': ESC + 'OD', - 'f1': ESC + 'OP', - 'f2': ESC + 'OQ', - 'f3': ESC + 'OR', - 'f4': ESC + 'OS', - 'f5': ESC + '[15~', -} - -CSI_COMMANDS = { - # possible values: - # None -> ignore sequence - # (, , callback) - # ('alias', ) - # - # while callback is executed as: - # callback(, arguments, has_question_mark) - - B('@'): (1, 1, lambda s, number, q: s.insert_chars(chars=number[0])), - B('A'): (1, 1, lambda s, rows, q: s.move_cursor(0, -rows[0], relative=True)), - B('B'): (1, 1, lambda s, rows, q: s.move_cursor(0, rows[0], relative=True)), - B('C'): (1, 1, lambda s, cols, q: s.move_cursor(cols[0], 0, relative=True)), - B('D'): (1, 1, lambda s, cols, q: s.move_cursor(-cols[0], 0, relative=True)), - B('E'): (1, 1, lambda s, rows, q: s.move_cursor(0, rows[0], relative_y=True)), - B('F'): (1, 1, lambda s, rows, q: s.move_cursor(0, -rows[0], relative_y=True)), - B('G'): (1, 1, lambda s, col, q: s.move_cursor(col[0] - 1, 0, relative_y=True)), - B('H'): (2, 1, lambda s, x_y, q: s.move_cursor(x_y[1] - 1, x_y[0] - 1)), - B('J'): (1, 0, lambda s, mode, q: s.csi_erase_display(mode[0])), - B('K'): (1, 0, lambda s, mode, q: s.csi_erase_line(mode[0])), - B('L'): (1, 1, lambda s, number, q: s.insert_lines(lines=number[0])), - B('M'): (1, 1, lambda s, number, q: s.remove_lines(lines=number[0])), - B('P'): (1, 1, lambda s, number, q: s.remove_chars(chars=number[0])), - B('X'): (1, 1, lambda s, number, q: s.erase(s.term_cursor, - (s.term_cursor[0]+number[0] - 1, - s.term_cursor[1]))), - B('a'): ('alias', B('C')), - B('c'): (0, 0, lambda s, none, q: s.csi_get_device_attributes(q)), - B('d'): (1, 1, lambda s, row, q: s.move_cursor(0, row[0] - 1, relative_x=True)), - B('e'): ('alias', B('B')), - B('f'): ('alias', B('H')), - B('g'): (1, 0, lambda s, mode, q: s.csi_clear_tabstop(mode[0])), - B('h'): (1, 0, lambda s, modes, q: s.csi_set_modes(modes, q)), - B('l'): (1, 0, lambda s, modes, q: s.csi_set_modes(modes, q, reset=True)), - B('m'): (1, 0, lambda s, attrs, q: s.csi_set_attr(attrs)), - B('n'): (1, 0, lambda s, mode, q: s.csi_status_report(mode[0])), - B('q'): (1, 0, lambda s, mode, q: s.csi_set_keyboard_leds(mode[0])), - B('r'): (2, 0, lambda s, t_b, q: s.csi_set_scroll(t_b[0], t_b[1])), - B('s'): (0, 0, lambda s, none, q: s.save_cursor()), - B('u'): (0, 0, lambda s, none, q: s.restore_cursor()), - B('`'): ('alias', B('G')), -} - -CHARSET_DEFAULT = 1 -CHARSET_UTF8 = 2 - -class TermModes(object): - def __init__(self): - self.reset() - - def reset(self): - # ECMA-48 - self.display_ctrl = False - self.insert = False - self.lfnl = False - - # DEC private modes - self.keys_decckm = False - self.reverse_video = False - self.constrain_scrolling = False - self.autowrap = True - self.visible_cursor = True - - # charset stuff - self.main_charset = CHARSET_DEFAULT - -class TermCharset(object): - MAPPING = { - 'default': None, - 'vt100': '0', - 'ibmpc': 'U', - 'user': None, - } - - def __init__(self): - self._g = [ - 'default', - 'vt100', - ] - - self._sgr_mapping = False - - self.activate(0) - - def define(self, g, charset): - """ - Redefine G'g' with new mapping. - """ - self._g[g] = charset - self.activate(g=self.active) - - def activate(self, g): - """ - Activate the given charset slot. - """ - self.active = g - self.current = self.MAPPING.get(self._g[g], None) - - def set_sgr_ibmpc(self): - """ - Set graphics rendition mapping to IBM PC CP437. - """ - self._sgr_mapping = True - - def reset_sgr_ibmpc(self): - """ - Reset graphics rendition mapping to IBM PC CP437. - """ - self._sgr_mapping = False - self.activate(g=self.active) - - def apply_mapping(self, char): - if self._sgr_mapping or self._g[self.active] == 'ibmpc': - dec_pos = DEC_SPECIAL_CHARS.find(char.decode('cp437')) - if dec_pos >= 0: - self.current = '0' - return str(ALT_DEC_SPECIAL_CHARS[dec_pos]) - else: - self.current = 'U' - return char - else: - return char - -class TermScroller(list): - """ - List subclass that handles the terminal scrollback buffer, - truncating it as necessary. - """ - SCROLLBACK_LINES = 10000 - - def trunc(self): - if len(self) >= self.SCROLLBACK_LINES: - self.pop(0) - - def append(self, obj): - self.trunc() - super(TermScroller, self).append(obj) - - def insert(self, idx, obj): - self.trunc() - super(TermScroller, self).insert(idx, obj) - - def extend(self, seq): - self.trunc() - super(TermScroller, self).extend(seq) - -class TermCanvas(Canvas): - cacheable = False - - def __init__(self, width, height, widget): - Canvas.__init__(self) - - self.width, self.height = width, height - self.widget = widget - self.modes = widget.term_modes - - self.scrollback_buffer = TermScroller() - self.scrolling_up = 0 - - self.utf8_eat_bytes = None - self.utf8_buffer = bytes() - - self.coords["cursor"] = (0, 0, None) - - self.reset() - - def set_term_cursor(self, x=None, y=None): - """ - Set terminal cursor to x/y and update canvas cursor. If one or both axes - are omitted, use the values of the current position. - """ - if x is None: - x = self.term_cursor[0] - if y is None: - y = self.term_cursor[1] - - self.term_cursor = self.constrain_coords(x, y) - - if self.modes.visible_cursor and self.scrolling_up < self.height - y: - self.cursor = (x, y + self.scrolling_up) - else: - self.cursor = None - - def reset_scroll(self): - """ - Reset scrolling region to full terminal size. - """ - self.scrollregion_start = 0 - self.scrollregion_end = self.height - 1 - - def scroll_buffer(self, up=True, reset=False, lines=None): - """ - Scroll the scrolling buffer up (up=True) or down (up=False) the given - amount of lines or half the screen height. - - If just 'reset' is True, set the scrollbuffer view to the current - terminal content. - """ - if reset: - self.scrolling_up = 0 - self.set_term_cursor() - return - - if lines is None: - lines = self.height // 2 - - if not up: - lines = -lines - - maxscroll = len(self.scrollback_buffer) - self.scrolling_up += lines - - if self.scrolling_up > maxscroll: - self.scrolling_up = maxscroll - elif self.scrolling_up < 0: - self.scrolling_up = 0 - - self.set_term_cursor() - - def reset(self): - """ - Reset the terminal. - """ - self.escbuf = bytes() - self.within_escape = False - self.parsestate = 0 - - self.attrspec = None - self.charset = TermCharset() - - self.saved_cursor = None - self.saved_attrs = None - - self.is_rotten_cursor = False - - self.reset_scroll() - - self.init_tabstops() - - # terminal modes - self.modes.reset() - - # initialize self.term - self.clear() - - def init_tabstops(self, extend=False): - tablen, mod = divmod(self.width, 8) - if mod > 0: - tablen += 1 - - if extend: - while len(self.tabstops) < tablen: - self.tabstops.append(1 << 0) - else: - self.tabstops = [1 << 0] * tablen - - def set_tabstop(self, x=None, remove=False, clear=False): - if clear: - for tab in xrange(len(self.tabstops)): - self.tabstops[tab] = 0 - return - - if x is None: - x = self.term_cursor[0] - - div, mod = divmod(x, 8) - if remove: - self.tabstops[div] &= ~(1 << mod) - else: - self.tabstops[div] |= (1 << mod) - - def is_tabstop(self, x=None): - if x is None: - x = self.term_cursor[0] - - div, mod = divmod(x, 8) - return (self.tabstops[div] & (1 << mod)) > 0 - - def empty_line(self, char=B(' ')): - return [self.empty_char(char)] * self.width - - def empty_char(self, char=B(' ')): - return (self.attrspec, self.charset.current, char) - - def addstr(self, data): - if self.width <= 0 or self.height <= 0: - # not displayable, do nothing! - return - - for byte in data: - self.addbyte(ord2(byte)) - - def resize(self, width, height): - """ - Resize the terminal to the given width and height. - """ - x, y = self.term_cursor - - if width > self.width: - # grow - for y in xrange(self.height): - self.term[y] += [self.empty_char()] * (width - self.width) - elif width < self.width: - # shrink - for y in xrange(self.height): - self.term[y] = self.term[y][:width] - - self.width = width - - if height > self.height: - # grow - for y in xrange(self.height, height): - try: - last_line = self.scrollback_buffer.pop() - except IndexError: - # nothing in scrollback buffer, append an empty line - self.term.append(self.empty_line()) - self.scrollregion_end += 1 - continue - - # adjust x axis of scrollback buffer to the current width - if len(last_line) < self.width: - last_line += [self.empty_char()] * \ - (self.width - len(last_line)) - else: - last_line = last_line[:self.width] - - y += 1 - - self.term.insert(0, last_line) - elif height < self.height: - # shrink - for y in xrange(height, self.height): - self.scrollback_buffer.append(self.term.pop(0)) - - self.height = height - - self.reset_scroll() - - x, y = self.constrain_coords(x, y) - self.set_term_cursor(x, y) - - # extend tabs - self.init_tabstops(extend=True) - - def set_g01(self, char, mod): - """ - Set G0 or G1 according to 'char' and modifier 'mod'. - """ - if self.modes.main_charset != CHARSET_DEFAULT: - return - - if mod == B('('): - g = 0 - else: - g = 1 - - if char == B('0'): - cset = 'vt100' - elif char == B('U'): - cset = 'ibmpc' - elif char == B('K'): - cset = 'user' - else: - cset = 'default' - - self.charset.define(g, cset) - - def parse_csi(self, char): - """ - Parse ECMA-48 CSI (Control Sequence Introducer) sequences. - """ - qmark = self.escbuf.startswith(B('?')) - - escbuf = [] - for arg in self.escbuf[qmark and 1 or 0:].split(B(';')): - try: - num = int(arg) - except ValueError: - num = None - - escbuf.append(num) - - if CSI_COMMANDS[char] is not None: - if CSI_COMMANDS[char][0] == 'alias': - csi_cmd = CSI_COMMANDS[(CSI_COMMANDS[char][1])] - else: - csi_cmd = CSI_COMMANDS[char] - - number_of_args, default_value, cmd = csi_cmd - while len(escbuf) < number_of_args: - escbuf.append(default_value) - for i in xrange(len(escbuf)): - if escbuf[i] is None or escbuf[i] == 0: - escbuf[i] = default_value - - try: - cmd(self, escbuf, qmark) - except ValueError: - # ignore commands that don't match the - # unpacked tuples in CSI_COMMANDS. - pass - - def parse_noncsi(self, char, mod=None): - """ - Parse escape sequences which are not CSI. - """ - if mod == B('#') and char == B('8'): - self.decaln() - elif mod == B('%'): # select main character set - if char == B('@'): - self.modes.main_charset = CHARSET_DEFAULT - elif char in B('G8'): - # 8 is obsolete and only for backwards compatibility - self.modes.main_charset = CHARSET_UTF8 - elif mod == B('(') or mod == B(')'): # define G0/G1 - self.set_g01(char, mod) - elif char == B('M'): # reverse line feed - self.linefeed(reverse=True) - elif char == B('D'): # line feed - self.linefeed() - elif char == B('c'): # reset terminal - self.reset() - elif char == B('E'): # newline - self.newline() - elif char == B('H'): # set tabstop - self.set_tabstop() - elif char == B('Z'): # DECID - self.widget.respond(ESC + '[?6c') - elif char == B('7'): # save current state - self.save_cursor(with_attrs=True) - elif char == B('8'): # restore current state - self.restore_cursor(with_attrs=True) - - def parse_osc(self, buf): - """ - Parse operating system command. - """ - if buf.startswith(B(';')): # set window title and icon - self.widget.set_title(buf[1:]) - elif buf.startswith(B('3;')): # set window title - self.widget.set_title(buf[2:]) - - def parse_escape(self, char): - if self.parsestate == 1: - # within CSI - if char in CSI_COMMANDS.keys(): - self.parse_csi(char) - self.parsestate = 0 - elif char in B('0123456789;') or (not self.escbuf and char == B('?')): - self.escbuf += char - return - elif self.parsestate == 0 and char == B(']'): - # start of OSC - self.escbuf = bytes() - self.parsestate = 2 - return - elif self.parsestate == 2 and char == B("\x07"): - # end of OSC - self.parse_osc(self.escbuf.lstrip(B('0'))) - elif self.parsestate == 2 and self.escbuf[-1:] + char == B(ESC + '\\'): - # end of OSC - self.parse_osc(self.escbuf[:-1].lstrip(B('0'))) - elif self.parsestate == 2 and self.escbuf.startswith(B('P')) and \ - len(self.escbuf) == 8: - # set palette (ESC]Pnrrggbb) - pass - elif self.parsestate == 2 and not self.escbuf and char == B('R'): - # reset palette - pass - elif self.parsestate == 2: - self.escbuf += char - return - elif self.parsestate == 0 and char == B('['): - # start of CSI - self.escbuf = bytes() - self.parsestate = 1 - return - elif self.parsestate == 0 and char in (B('%'), B('#'), B('('), B(')')): - # non-CSI sequence - self.escbuf = char - self.parsestate = 3 - return - elif self.parsestate == 3: - self.parse_noncsi(char, self.escbuf) - elif char in (B('c'), B('D'), B('E'), B('H'), B('M'), B('Z'), B('7'), B('8'), B('>'), B('=')): - self.parse_noncsi(char) - - self.leave_escape() - - def leave_escape(self): - self.within_escape = False - self.parsestate = 0 - self.escbuf = bytes() - - def get_utf8_len(self, bytenum): - """ - Process startbyte and return the number of bytes following it to get a - valid UTF-8 multibyte sequence. - - bytenum -- an integer ordinal - """ - length = 0 - - while bytenum & 0x40: - bytenum <<= 1 - length += 1 - - return length - - def addbyte(self, byte): - """ - Parse main charset and add the processed byte(s) to the terminal state - machine. - - byte -- an integer ordinal - """ - if (self.modes.main_charset == CHARSET_UTF8 or - util._target_encoding == 'utf8'): - if byte >= 0xc0: - # start multibyte sequence - self.utf8_eat_bytes = self.get_utf8_len(byte) - self.utf8_buffer = chr2(byte) - return - elif 0x80 <= byte < 0xc0 and self.utf8_eat_bytes is not None: - if self.utf8_eat_bytes > 1: - # continue multibyte sequence - self.utf8_eat_bytes -= 1 - self.utf8_buffer += chr2(byte) - return - else: - # end multibyte sequence - self.utf8_eat_bytes = None - sequence = (self.utf8_buffer+chr2(byte)).decode('utf-8', 'ignore') - if len(sequence) == 0: - # invalid multibyte sequence, stop processing - return - char = sequence.encode(util._target_encoding, 'replace') - else: - self.utf8_eat_bytes = None - char = chr2(byte) - else: - char = chr2(byte) - - self.process_char(char) - - def process_char(self, char): - """ - Process a single character (single- and multi-byte). - - char -- a byte string - """ - x, y = self.term_cursor - - if isinstance(char, int): - char = chr(char) - - dc = self.modes.display_ctrl - - if char == B("\x1b") and self.parsestate != 2: # escape - self.within_escape = True - elif not dc and char == B("\x0d"): # carriage return - self.carriage_return() - elif not dc and char == B("\x0f"): # activate G0 - self.charset.activate(0) - elif not dc and char == B("\x0e"): # activate G1 - self.charset.activate(1) - elif not dc and char in B("\x0a\x0b\x0c"): # line feed - self.linefeed() - if self.modes.lfnl: - self.carriage_return() - elif not dc and char == B("\x09"): # char tab - self.tab() - elif not dc and char == B("\x08"): # backspace - if x > 0: - self.set_term_cursor(x - 1, y) - elif not dc and char == B("\x07") and self.parsestate != 2: # beep - # we need to check if we're in parsestate 2, as an OSC can be - # terminated by the BEL character! - self.widget.beep() - elif not dc and char in B("\x18\x1a"): # CAN/SUB - self.leave_escape() - elif not dc and char == B("\x7f"): # DEL - pass # this is ignored - elif self.within_escape: - self.parse_escape(char) - elif not dc and char == B("\x9b"): # CSI (equivalent to "ESC [") - self.within_escape = True - self.escbuf = bytes() - self.parsestate = 1 - else: - self.push_cursor(char) - - def set_char(self, char, x=None, y=None): - """ - Set character of either the current cursor position - or a position given by 'x' and/or 'y' to 'char'. - """ - if x is None: - x = self.term_cursor[0] - if y is None: - y = self.term_cursor[1] - - x, y = self.constrain_coords(x, y) - self.term[y][x] = (self.attrspec, self.charset.current, char) - - def constrain_coords(self, x, y, ignore_scrolling=False): - """ - Checks if x/y are within the terminal and returns the corrected version. - If 'ignore_scrolling' is set, constrain within the full size of the - screen and not within scrolling region. - """ - if x >= self.width: - x = self.width - 1 - elif x < 0: - x = 0 - - if self.modes.constrain_scrolling and not ignore_scrolling: - if y > self.scrollregion_end: - y = self.scrollregion_end - elif y < self.scrollregion_start: - y = self.scrollregion_start - else: - if y >= self.height: - y = self.height - 1 - elif y < 0: - y = 0 - - return x, y - - def linefeed(self, reverse=False): - """ - Move the cursor down (or up if reverse is True) one line but don't reset - horizontal position. - """ - x, y = self.term_cursor - - if reverse: - if y <= 0 < self.scrollregion_start: - pass - elif y == self.scrollregion_start: - self.scroll(reverse=True) - else: - y -= 1 - else: - if y >= self.height - 1 > self.scrollregion_end: - pass - elif y == self.scrollregion_end: - self.scroll() - else: - y += 1 - - self.set_term_cursor(x, y) - - def carriage_return(self): - self.set_term_cursor(0, self.term_cursor[1]) - - def newline(self): - """ - Do a carriage return followed by a line feed. - """ - self.carriage_return() - self.linefeed() - - def move_cursor(self, x, y, relative_x=False, relative_y=False, - relative=False): - """ - Move cursor to position x/y while constraining terminal sizes. - If 'relative' is True, x/y is relative to the current cursor - position. 'relative_x' and 'relative_y' is the same but just with - the corresponding axis. - """ - if relative: - relative_y = relative_x = True - - if relative_x: - x = self.term_cursor[0] + x - - if relative_y: - y = self.term_cursor[1] + y - elif self.modes.constrain_scrolling: - y += self.scrollregion_start - - self.set_term_cursor(x, y) - - def push_char(self, char, x, y): - """ - Push one character to current position and advance cursor to x/y. - """ - if char is not None: - char = self.charset.apply_mapping(char) - if self.modes.insert: - self.insert_chars(char=char) - else: - self.set_char(char) - - self.set_term_cursor(x, y) - - def push_cursor(self, char=None): - """ - Move cursor one character forward wrapping lines as needed. - If 'char' is given, put the character into the former position. - """ - x, y = self.term_cursor - - if self.modes.autowrap: - if x + 1 >= self.width and not self.is_rotten_cursor: - # "rotten cursor" - this is when the cursor gets to the rightmost - # position of the screen, the cursor position remains the same but - # one last set_char() is allowed for that piece of sh^H^H"border". - self.is_rotten_cursor = True - self.push_char(char, x, y) - else: - x += 1 - - if x >= self.width and self.is_rotten_cursor: - if y >= self.scrollregion_end: - self.scroll() - else: - y += 1 - - x = 1 - - self.set_term_cursor(0, y) - - self.push_char(char, x, y) - - self.is_rotten_cursor = False - else: - if x + 1 < self.width: - x += 1 - - self.is_rotten_cursor = False - self.push_char(char, x, y) - - def save_cursor(self, with_attrs=False): - self.saved_cursor = tuple(self.term_cursor) - if with_attrs: - self.saved_attrs = (copy.copy(self.attrspec), - copy.copy(self.charset)) - - def restore_cursor(self, with_attrs=False): - if self.saved_cursor is None: - return - - x, y = self.saved_cursor - self.set_term_cursor(x, y) - - if with_attrs and self.saved_attrs is not None: - self.attrspec, self.charset = (copy.copy(self.saved_attrs[0]), - copy.copy(self.saved_attrs[1])) - - def tab(self, tabstop=8): - """ - Moves cursor to the next 'tabstop' filling everything in between - with spaces. - """ - x, y = self.term_cursor - - while x < self.width - 1: - self.set_char(B(" ")) - x += 1 - - if self.is_tabstop(x): - break - - self.is_rotten_cursor = False - self.set_term_cursor(x, y) - - def scroll(self, reverse=False): - """ - Append a new line at the bottom and put the topmost line into the - scrollback buffer. - - If reverse is True, do exactly the opposite, but don't save into - scrollback buffer. - """ - if reverse: - self.term.pop(self.scrollregion_end) - self.term.insert(self.scrollregion_start, self.empty_line()) - else: - killed = self.term.pop(self.scrollregion_start) - self.scrollback_buffer.append(killed) - self.term.insert(self.scrollregion_end, self.empty_line()) - - def decaln(self): - """ - DEC screen alignment test: Fill screen with E's. - """ - for row in xrange(self.height): - self.term[row] = self.empty_line('E') - - def blank_line(self, row): - """ - Blank a single line at the specified row, without modifying other lines. - """ - self.term[row] = self.empty_line() - - def insert_chars(self, position=None, chars=1, char=None): - """ - Insert 'chars' number of either empty characters - or those specified by - 'char' - before 'position' (or the current position if not specified) - pushing subsequent characters of the line to the right without wrapping. - """ - if position is None: - position = self.term_cursor - - if chars == 0: - chars = 1 - - if char is None: - char = self.empty_char() - else: - char = (self.attrspec, self.charset.current, char) - - x, y = position - - while chars > 0: - self.term[y].insert(x, char) - self.term[y].pop() - chars -= 1 - - def remove_chars(self, position=None, chars=1): - """ - Remove 'chars' number of empty characters from 'position' (or the current - position if not specified) pulling subsequent characters of the line to - the left without joining any subsequent lines. - """ - if position is None: - position = self.term_cursor - - if chars == 0: - chars = 1 - - x, y = position - - while chars > 0: - self.term[y].pop(x) - self.term[y].append(self.empty_char()) - chars -= 1 - - def insert_lines(self, row=None, lines=1): - """ - Insert 'lines' of empty lines after the specified row, pushing all - subsequent lines to the bottom. If no 'row' is specified, the current - row is used. - """ - if row is None: - row = self.term_cursor[1] - else: - row = self.scrollregion_start - - if lines == 0: - lines = 1 - - while lines > 0: - self.term.insert(row, self.empty_line()) - self.term.pop(self.scrollregion_end) - lines -= 1 - - def remove_lines(self, row=None, lines=1): - """ - Remove 'lines' number of lines at the specified row, pulling all - subsequent lines to the top. If no 'row' is specified, the current row - is used. - """ - if row is None: - row = self.term_cursor[1] - else: - row = self.scrollregion_start - - if lines == 0: - lines = 1 - - while lines > 0: - self.term.pop(row) - self.term.insert(self.scrollregion_end, self.empty_line()) - lines -= 1 - - def erase(self, start, end): - """ - Erase a region of the terminal. The 'start' tuple (x, y) defines the - starting position of the erase, while end (x, y) the last position. - - For example if the terminal size is 4x3, start=(1, 1) and end=(1, 2) - would erase the following region: - - .... - .XXX - XX.. - """ - sx, sy = self.constrain_coords(*start) - ex, ey = self.constrain_coords(*end) - - # within a single row - if sy == ey: - for x in xrange(sx, ex + 1): - self.term[sy][x] = self.empty_char() - return - - # spans multiple rows - y = sy - while y <= ey: - if y == sy: - for x in xrange(sx, self.width): - self.term[y][x] = self.empty_char() - elif y == ey: - for x in xrange(ex + 1): - self.term[y][x] = self.empty_char() - else: - self.blank_line(y) - - y += 1 - - def sgi_to_attrspec(self, attrs, fg, bg, attributes): - """ - Parse SGI sequence and return an AttrSpec representing the sequence - including all earlier sequences specified as 'fg', 'bg' and - 'attributes'. - """ - for attr in attrs: - if 30 <= attr <= 37: - fg = attr - 30 - elif 40 <= attr <= 47: - bg = attr - 40 - elif attr == 38: - # set default foreground color, set underline - attributes.add('underline') - fg = None - elif attr == 39: - # set default foreground color, remove underline - attributes.discard('underline') - fg = None - elif attr == 49: - # set default background color - bg = None - elif attr == 10: - self.charset.reset_sgr_ibmpc() - self.modes.display_ctrl = False - elif attr in (11, 12): - self.charset.set_sgr_ibmpc() - self.modes.display_ctrl = True - - # set attributes - elif attr == 1: - attributes.add('bold') - elif attr == 4: - attributes.add('underline') - elif attr == 5: - attributes.add('blink') - elif attr == 7: - attributes.add('standout') - - # unset attributes - elif attr == 24: - attributes.discard('underline') - elif attr == 25: - attributes.discard('blink') - elif attr == 27: - attributes.discard('standout') - elif attr == 0: - # clear all attributes - fg = bg = None - attributes.clear() - - if 'bold' in attributes and fg is not None: - fg += 8 - - def _defaulter(color): - if color is None: - return 'default' - else: - return _BASIC_COLORS[color] - - fg = _defaulter(fg) - bg = _defaulter(bg) - - if len(attributes) > 0: - fg = ','.join([fg] + list(attributes)) - - if fg == 'default' and bg == 'default': - return None - else: - return AttrSpec(fg, bg) - - def csi_set_attr(self, attrs): - """ - Set graphics rendition. - """ - if attrs[-1] == 0: - self.attrspec = None - - attributes = set() - if self.attrspec is None: - fg = bg = None - else: - # set default values from previous attrspec - if 'default' in self.attrspec.foreground: - fg = None - else: - fg = self.attrspec.foreground_number - if fg >= 8: fg -= 8 - - if 'default' in self.attrspec.background: - bg = None - else: - bg = self.attrspec.background_number - if bg >= 8: bg -= 8 - - for attr in ('bold', 'underline', 'blink', 'standout'): - if not getattr(self.attrspec, attr): - continue - - attributes.add(attr) - - attrspec = self.sgi_to_attrspec(attrs, fg, bg, attributes) - - if self.modes.reverse_video: - self.attrspec = self.reverse_attrspec(attrspec) - else: - self.attrspec = attrspec - - def reverse_attrspec(self, attrspec, undo=False): - """ - Put standout mode to the 'attrspec' given and remove it if 'undo' is - True. - """ - if attrspec is None: - attrspec = AttrSpec('default', 'default') - attrs = [fg.strip() for fg in attrspec.foreground.split(',')] - if 'standout' in attrs and undo: - attrs.remove('standout') - attrspec.foreground = ','.join(attrs) - elif 'standout' not in attrs and not undo: - attrs.append('standout') - attrspec.foreground = ','.join(attrs) - return attrspec - - def reverse_video(self, undo=False): - """ - Reverse video/scanmode (DECSCNM) by swapping fg and bg colors. - """ - for y in xrange(self.height): - for x in xrange(self.width): - char = self.term[y][x] - attrs = self.reverse_attrspec(char[0], undo=undo) - self.term[y][x] = (attrs,) + char[1:] - - def set_mode(self, mode, flag, qmark, reset): - """ - Helper method for csi_set_modes: set single mode. - """ - if qmark: - # DEC private mode - if mode == 1: - # cursor keys send an ESC O prefix, rather than ESC [ - self.modes.keys_decckm = flag - elif mode == 3: - # deccolm just clears the screen - self.clear() - elif mode == 5: - if self.modes.reverse_video != flag: - self.reverse_video(undo=not flag) - self.modes.reverse_video = flag - elif mode == 6: - self.modes.constrain_scrolling = flag - self.set_term_cursor(0, 0) - elif mode == 7: - self.modes.autowrap = flag - elif mode == 25: - self.modes.visible_cursor = flag - self.set_term_cursor() - else: - # ECMA-48 - if mode == 3: - self.modes.display_ctrl = flag - elif mode == 4: - self.modes.insert = flag - elif mode == 20: - self.modes.lfnl = flag - - def csi_set_modes(self, modes, qmark, reset=False): - """ - Set (DECSET/ECMA-48) or reset modes (DECRST/ECMA-48) if reset is True. - """ - flag = not reset - - for mode in modes: - self.set_mode(mode, flag, qmark, reset) - - def csi_set_scroll(self, top=0, bottom=0): - """ - Set scrolling region, 'top' is the line number of first line in the - scrolling region. 'bottom' is the line number of bottom line. If both - are set to 0, the whole screen will be used (default). - """ - if top == 0: - top = 1 - if bottom == 0: - bottom = self.height - - if top < bottom <= self.height: - self.scrollregion_start = self.constrain_coords( - 0, top - 1, ignore_scrolling=True - )[1] - self.scrollregion_end = self.constrain_coords( - 0, bottom - 1, ignore_scrolling=True - )[1] - - self.set_term_cursor(0, 0) - - def csi_clear_tabstop(self, mode=0): - """ - Clear tabstop at current position or if 'mode' is 3, delete all - tabstops. - """ - if mode == 0: - self.set_tabstop(remove=True) - elif mode == 3: - self.set_tabstop(clear=True) - - def csi_get_device_attributes(self, qmark): - """ - Report device attributes (what are you?). In our case, we'll report - ourself as a VT102 terminal. - """ - if not qmark: - self.widget.respond(ESC + '[?6c') - - def csi_status_report(self, mode): - """ - Report various information about the terminal status. - Information is queried by 'mode', where possible values are: - 5 -> device status report - 6 -> cursor position report - """ - if mode == 5: - # terminal OK - self.widget.respond(ESC + '[0n') - elif mode == 6: - x, y = self.term_cursor - self.widget.respond(ESC + '[%d;%dR' % (y + 1, x + 1)) - - def csi_erase_line(self, mode): - """ - Erase current line, modes are: - 0 -> erase from cursor to end of line. - 1 -> erase from start of line to cursor. - 2 -> erase whole line. - """ - x, y = self.term_cursor - - if mode == 0: - self.erase(self.term_cursor, (self.width - 1, y)) - elif mode == 1: - self.erase((0, y), (x, y)) - elif mode == 2: - self.blank_line(y) - - def csi_erase_display(self, mode): - """ - Erase display, modes are: - 0 -> erase from cursor to end of display. - 1 -> erase from start to cursor. - 2 -> erase the whole display. - """ - if mode == 0: - self.erase(self.term_cursor, (self.width - 1, self.height - 1)) - if mode == 1: - self.erase((0, 0), (self.term_cursor[0] - 1, self.term_cursor[1])) - elif mode == 2: - self.clear(cursor=self.term_cursor) - - def csi_set_keyboard_leds(self, mode=0): - """ - Set keyboard LEDs, modes are: - 0 -> clear all LEDs - 1 -> set scroll lock LED - 2 -> set num lock LED - 3 -> set caps lock LED - - This currently just emits a signal, so it can be processed by another - widget or the main application. - """ - states = { - 0: 'clear', - 1: 'scroll_lock', - 2: 'num_lock', - 3: 'caps_lock', - } - - if mode in states: - self.widget.leds(states[mode]) - - def clear(self, cursor=None): - """ - Clears the whole terminal screen and resets the cursor position - to (0, 0) or to the coordinates given by 'cursor'. - """ - self.term = [self.empty_line() for x in xrange(self.height)] - - if cursor is None: - self.set_term_cursor(0, 0) - else: - self.set_term_cursor(*cursor) - - def cols(self): - return self.width - - def rows(self): - return self.height - - def content(self, trim_left=0, trim_right=0, cols=None, rows=None, - attr_map=None): - if self.scrolling_up == 0: - for line in self.term: - yield line - else: - buf = self.scrollback_buffer + self.term - for line in buf[-(self.height+self.scrolling_up):-self.scrolling_up]: - yield line - - def content_delta(self, other): - if other is self: - return [self.cols()]*self.rows() - return self.content() - -class Terminal(Widget): - _selectable = True - _sizing = frozenset([BOX]) - - signals = ['closed', 'beep', 'leds', 'title'] - - def __init__(self, command, env=None, main_loop=None, escape_sequence=None): - """ - A terminal emulator within a widget. - - 'command' is the command to execute inside the terminal, provided as a - list of the command followed by its arguments. If 'command' is None, - the command is the current user's shell. You can also provide a callable - instead of a command, which will be executed in the subprocess. - - 'env' can be used to pass custom environment variables. If omitted, - os.environ is used. - - 'main_loop' should be provided, because the canvas state machine needs - to act on input from the PTY master device. This object must have - watch_file and remove_watch_file methods. - - 'escape_sequence' is the urwid key symbol which should be used to break - out of the terminal widget. If it's not specified, "ctrl a" is used. - """ - self.__super.__init__() - - if escape_sequence is None: - self.escape_sequence = "ctrl a" - else: - self.escape_sequence = escape_sequence - - if env is None: - self.env = dict(os.environ) - else: - self.env = dict(env) - - if command is None: - self.command = [self.env.get('SHELL', '/bin/sh')] - else: - self.command = command - - self.keygrab = False - self.last_key = None - - self.response_buffer = [] - - self.term_modes = TermModes() - - self.main_loop = main_loop - - self.master = None - self.pid = None - - self.width = None - self.height = None - self.term = None - self.has_focus = False - self.terminated = False - - def spawn(self): - env = self.env - env['TERM'] = 'linux' - - self.pid, self.master = pty.fork() - - if self.pid == 0: - if callable(self.command): - try: - try: - self.command() - except: - sys.stderr.write(traceback.format_exc()) - sys.stderr.flush() - finally: - os._exit(0) - else: - os.execvpe(self.command[0], self.command, env) - - if self.main_loop is None: - fcntl.fcntl(self.master, fcntl.F_SETFL, os.O_NONBLOCK) - - atexit.register(self.terminate) - - def terminate(self): - if self.terminated: - return - - self.terminated = True - self.remove_watch() - self.change_focus(False) - - if self.pid > 0: - self.set_termsize(0, 0) - for sig in (signal.SIGHUP, signal.SIGCONT, signal.SIGINT, - signal.SIGTERM, signal.SIGKILL): - try: - os.kill(self.pid, sig) - pid, status = os.waitpid(self.pid, os.WNOHANG) - except OSError: - break - - if pid == 0: - break - time.sleep(0.1) - try: - os.waitpid(self.pid, 0) - except OSError: - pass - - os.close(self.master) - - def beep(self): - self._emit('beep') - - def leds(self, which): - self._emit('leds', which) - - def respond(self, string): - """ - Respond to the underlying application with 'string'. - """ - self.response_buffer.append(string) - - def flush_responses(self): - for string in self.response_buffer: - os.write(self.master, string.encode('ascii')) - self.response_buffer = [] - - def set_termsize(self, width, height): - winsize = struct.pack("HHHH", height, width, 0, 0) - fcntl.ioctl(self.master, termios.TIOCSWINSZ, winsize) - - def touch_term(self, width, height): - process_opened = False - - if self.pid is None: - self.spawn() - process_opened = True - - if self.width == width and self.height == height: - return - - self.set_termsize(width, height) - - if not self.term: - self.term = TermCanvas(width, height, self) - else: - self.term.resize(width, height) - - self.width = width - self.height = height - - if process_opened: - self.add_watch() - - def set_title(self, title): - self._emit('title', title) - - def change_focus(self, has_focus): - """ - Ignore SIGINT if this widget has focus. - """ - if self.terminated or self.has_focus == has_focus: - return - - self.has_focus = has_focus - - if has_focus: - self.old_tios = RealTerminal().tty_signal_keys() - RealTerminal().tty_signal_keys(*(['undefined'] * 5)) - else: - RealTerminal().tty_signal_keys(*self.old_tios) - - def render(self, size, focus=False): - if not self.terminated: - self.change_focus(focus) - - width, height = size - self.touch_term(width, height) - - if self.main_loop is None: - self.feed() - - return self.term - - def add_watch(self): - if self.main_loop is None: - return - - self.main_loop.watch_file(self.master, self.feed) - - def remove_watch(self): - if self.main_loop is None: - return - - self.main_loop.remove_watch_file(self.master) - - def selectable(self): - return True - - def wait_and_feed(self, timeout=1.0): - while True: - try: - select.select([self.master], [], [], timeout) - break - except select.error as e: - if e.args[0] != 4: - raise - self.feed() - - def feed(self): - data = '' - - try: - data = os.read(self.master, 4096) - except OSError as e: - if e.errno == 5: # End Of File - data = '' - elif e.errno == errno.EWOULDBLOCK: # empty buffer - return - else: - raise - - if data == '': # EOF on BSD - self.terminate() - self._emit('closed') - return - - self.term.addstr(data) - - self.flush_responses() - - def keypress(self, size, key): - if self.terminated: - return key - - if key == "window resize": - width, height = size - self.touch_term(width, height) - return - - if (self.last_key == self.escape_sequence - and key == self.escape_sequence): - # escape sequence pressed twice... - self.last_key = key - self.keygrab = True - # ... so pass it to the terminal - elif self.keygrab: - if self.escape_sequence == key: - # stop grabbing the terminal - self.keygrab = False - self.last_key = key - return - else: - if key == 'page up': - self.term.scroll_buffer() - self.last_key = key - self._invalidate() - return - elif key == 'page down': - self.term.scroll_buffer(up=False) - self.last_key = key - self._invalidate() - return - elif (self.last_key == self.escape_sequence - and key != self.escape_sequence): - # hand down keypress directly after ungrab. - self.last_key = key - return key - elif self.escape_sequence == key: - # start grabbing the terminal - self.keygrab = True - self.last_key = key - return - elif self._command_map[key] is None or key == 'enter': - # printable character or escape sequence means: - # lock in terminal... - self.keygrab = True - # ... and do key processing - else: - # hand down keypress - self.last_key = key - return key - - self.last_key = key - - self.term.scroll_buffer(reset=True) - - if key.startswith("ctrl "): - if key[-1].islower(): - key = chr(ord(key[-1]) - ord('a') + 1) - else: - key = chr(ord(key[-1]) - ord('A') + 1) - else: - if self.term_modes.keys_decckm and key in KEY_TRANSLATIONS_DECCKM: - key = KEY_TRANSLATIONS_DECCKM.get(key) - else: - key = KEY_TRANSLATIONS.get(key, key) - - # ENTER transmits both a carriage return and linefeed in LF/NL mode. - if self.term_modes.lfnl and key == "\x0d": - key += "\x0a" - - if PYTHON3: - key = key.encode('ascii') - - os.write(self.master, key) diff --git a/urwid/web_display.py b/urwid/web_display.py deleted file mode 100755 index 44a505c..0000000 --- a/urwid/web_display.py +++ /dev/null @@ -1,1092 +0,0 @@ -#!/usr/bin/python -# -# Urwid web (CGI/Asynchronous Javascript) display module -# Copyright (C) 2004-2007 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/ - -""" -Urwid web application display module -""" -import os -import sys -import signal -import random -import select -import socket -import glob - -from urwid import util -_js_code = r""" -// Urwid web (CGI/Asynchronous Javascript) display module -// Copyright (C) 2004-2005 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/ - -colours = new Object(); -colours = { - '0': "black", - '1': "#c00000", - '2': "green", - '3': "#804000", - '4': "#0000c0", - '5': "#c000c0", - '6': "teal", - '7': "silver", - '8': "gray", - '9': "#ff6060", - 'A': "lime", - 'B': "yellow", - 'C': "#8080ff", - 'D': "#ff40ff", - 'E': "aqua", - 'F': "white" -}; - -keycodes = new Object(); -keycodes = { - 8: "backspace", 9: "tab", 13: "enter", 27: "esc", - 33: "page up", 34: "page down", 35: "end", 36: "home", - 37: "left", 38: "up", 39: "right", 40: "down", - 45: "insert", 46: "delete", - 112: "f1", 113: "f2", 114: "f3", 115: "f4", - 116: "f5", 117: "f6", 118: "f7", 119: "f8", - 120: "f9", 121: "f10", 122: "f11", 123: "f12" - }; - -var conn = null; -var char_width = null; -var char_height = null; -var screen_x = null; -var screen_y = null; - -var urwid_id = null; -var send_conn = null; -var send_queue_max = 32; -var send_queue = new Array(send_queue_max); -var send_queue_in = 0; -var send_queue_out = 0; - -var check_font_delay = 1000; -var send_more_delay = 100; -var poll_again_delay = 500; - -var document_location = null; - -var update_method = "multipart"; - -var sending = false; -var lastkeydown = null; - -function setup_connection() { - if (window.XMLHttpRequest) { - conn = new XMLHttpRequest(); - } else if (window.ActiveXObject) { - conn = new ActiveXObject("Microsoft.XMLHTTP"); - } - - if (conn == null) { - set_status("Connection Failed"); - alert( "Can't figure out how to send request." ); - return; - } - try{ - conn.multipart = true; - }catch(e){ - update_method = "polling"; - } - conn.onreadystatechange = handle_recv; - conn.open("POST", document_location, true); - conn.setRequestHeader("X-Urwid-Method",update_method); - conn.setRequestHeader("Content-type","text/plain"); - conn.send("window resize " +screen_x+" "+screen_y+"\n"); -} - -function do_poll() { - if (urwid_id == null){ - alert("that's unpossible!"); - return; - } - if (window.XMLHttpRequest) { - conn = new XMLHttpRequest(); - } else if (window.ActiveXObject) { - conn = new ActiveXObject("Microsoft.XMLHTTP"); - } - conn.onreadystatechange = handle_recv; - conn.open("POST", document_location, true); - conn.setRequestHeader("X-Urwid-Method","polling"); - conn.setRequestHeader("X-Urwid-ID",urwid_id); - conn.setRequestHeader("Content-type","text/plain"); - conn.send("eh?"); -} - -function handle_recv() { - if( ! conn ){ return;} - if( conn.readyState != 4) { - return; - } - if( conn.status == 404 && urwid_id != null) { - set_status("Connection Closed"); - return; - } - if( conn.status == 403 && update_method == "polling" ) { - set_status("Server Refused Connection"); - alert("This server does not allow polling clients.\n\n" + - "Please use a web browser with multipart support " + - "such as Mozilla Firefox"); - return; - } - if( conn.status == 503 ) { - set_status("Connection Failed"); - alert("The server has reached its maximum number of "+ - "connections.\n\nPlease try again later."); - return; - } - if( conn.status != 200) { - set_status("Connection Failed"); - alert("Error from server: "+conn.statusText); - return; - } - if( urwid_id == null ){ - urwid_id = conn.getResponseHeader("X-Urwid-ID"); - if( send_queue_in != send_queue_out ){ - // keys waiting - do_send(); - } - if(update_method=="polling"){ - set_status("Polling"); - }else if(update_method=="multipart"){ - set_status("Connected"); - } - - } - - if( conn.responseText == "" ){ - if(update_method=="polling"){ - poll_again(); - } - return; // keepalive - } - if( conn.responseText == "Z" ){ - set_status("Connection Closed"); - update_method = null; - return; - } - - var text = document.getElementById('text'); - - var last_screen = Array(text.childNodes.length); - for( var i=0; i k ){ - text.replaceChild(ln, text.childNodes[k]); - }else{ - text.appendChild(ln); - } - k = k+1; - ln = document.createElement('span'); - }else if( f.charAt(0) == "<" ){ - line_number = parseInt(f.substr(1)); - if( line_number == k ){ - k = k +1; - continue; - } - var clone = last_screen[line_number].cloneNode(true); - if( text.childNodes.length > k ){ - text.replaceChild(clone, text.childNodes[k]); - }else{ - text.appendChild(clone); - } - k = k+1; - }else{ - var span=make_span(f.substr(2),f.charAt(0),f.charAt(1)); - ln.appendChild( span ); - } - } - for( var i=k; i < text.childNodes.length; i++ ){ - text.removeChild(last_screen[i]); - } - - if(update_method=="polling"){ - poll_again(); - } -} - -function poll_again(){ - if(conn.status == 200){ - setTimeout("do_poll();",poll_again_delay); - } -} - - -function load_web_display(){ - if( document.documentURI ){ - document_location = document.documentURI; - }else{ - document_location = document.location; - } - - document.onkeypress = body_keypress; - document.onkeydown = body_keydown; - document.onresize = body_resize; - - body_resize(); - send_queue_out = send_queue_in; // don't queue the first resize - - set_status("Connecting"); - setup_connection(); - - setTimeout("check_fontsize();",check_font_delay); -} - -function set_status( status ){ - var s = document.getElementById('status'); - var t = document.createTextNode(status); - s.replaceChild(t, s.firstChild); -} - -function make_span(s, fg, bg){ - d = document.createElement('span'); - d.style.backgroundColor = colours[bg]; - d.style.color = colours[fg]; - d.appendChild(document.createTextNode(s)); - - return d; -} - -function body_keydown(e){ - if (conn == null){ - return; - } - if (!e) var e = window.event; - if (e.keyCode) code = e.keyCode; - else if (e.which) code = e.which; - - var mod = ""; - var key; - - if( e.ctrlKey ){ mod = "ctrl " + mod; } - if( e.altKey || e.metaKey ){ mod = "meta " + mod; } - if( e.shiftKey && e.charCode == 0 ){ mod = "shift " + mod; } - - key = keycodes[code]; - - if( key != undefined ){ - lastkeydown = key; - send_key( mod + key ); - stop_key_event(e); - return false; - } -} - -function body_keypress(e){ - if (conn == null){ - return; - } - - if (!e) var e = window.event; - if (e.keyCode) code = e.keyCode; - else if (e.which) code = e.which; - - var mod = ""; - var key; - - if( e.ctrlKey ){ mod = "ctrl " + mod; } - if( e.altKey || e.metaKey ){ mod = "meta " + mod; } - if( e.shiftKey && e.charCode == 0 ){ mod = "shift " + mod; } - - if( e.charCode != null && e.charCode != 0 ){ - key = String.fromCharCode(e.charCode); - }else if( e.charCode == null ){ - key = String.fromCharCode(code); - }else{ - key = keycodes[code]; - if( key == undefined || lastkeydown == key ){ - lastkeydown = null; - stop_key_event(e); - return false; - } - } - - send_key( mod + key ); - stop_key_event(e); - return false; -} - -function stop_key_event(e){ - e.cancelBubble = true; - if( e.stopPropagation ){ - e.stopPropagation(); - } - if( e.preventDefault ){ - e.preventDefault(); - } -} - -function send_key( key ){ - if( (send_queue_in+1)%send_queue_max == send_queue_out ){ - // buffer overrun - return; - } - send_queue[send_queue_in] = key; - send_queue_in = (send_queue_in+1)%send_queue_max; - - if( urwid_id != null ){ - if (send_conn == undefined || send_conn.ready_state != 4 ){ - send_more(); - return; - } - do_send(); - } -} - -function do_send() { - if( ! urwid_id ){ return; } - if( ! update_method ){ return; } // connection closed - if( send_queue_in == send_queue_out ){ return; } - if( sending ){ - //var queue_delta = send_queue_in - send_queue_out; - //if( queue_delta < 0 ){ queue_delta += send_queue_max; } - //set_status("Sending (queued "+queue_delta+")"); - return; - } - try{ - sending = true; - //set_status("starting send"); - if( send_conn == null ){ - if (window.XMLHttpRequest) { - send_conn = new XMLHttpRequest(); - } else if (window.ActiveXObject) { - send_conn = new ActiveXObject("Microsoft.XMLHTTP"); - } - }else if( send_conn.status != 200) { - alert("Error from server: "+send_conn.statusText); - return; - }else if(send_conn.readyState != 4 ){ - alert("not ready on send connection"); - return; - } - } catch(e) { - alert(e); - sending = false; - return; - } - send_conn.open("POST", document_location, true); - send_conn.onreadystatechange = send_handle_recv; - send_conn.setRequestHeader("Content-type","text/plain"); - send_conn.setRequestHeader("X-Urwid-ID",urwid_id); - var tmp_send_queue_in = send_queue_in; - var out = null; - if( send_queue_out > tmp_send_queue_in ){ - out = send_queue.slice(send_queue_out).join("\n") - if( tmp_send_queue_in > 0 ){ - out += "\n" + send_queue.slice(0,tmp_send_queue_in).join("\n"); - } - }else{ - out = send_queue.slice(send_queue_out, - tmp_send_queue_in).join("\n"); - } - send_queue_out = tmp_send_queue_in; - //set_status("Sending"); - send_conn.send( out +"\n" ); -} - -function send_handle_recv() { - if( send_conn.readyState != 4) { - return; - } - if( send_conn.status == 404) { - set_status("Connection Closed"); - update_method = null; - return; - } - if( send_conn.status != 200) { - alert("Error from server: "+send_conn.statusText); - return; - } - - sending = false; - - if( send_queue_out != send_queue_in ){ - send_more(); - } -} - -function send_more(){ - setTimeout("do_send();",send_more_delay); -} - -function check_fontsize(){ - body_resize() - setTimeout("check_fontsize();",check_font_delay); -} - -function body_resize(){ - var t = document.getElementById('testchar'); - var t2 = document.getElementById('testchar2'); - var text = document.getElementById('text'); - - var window_width; - var window_height; - if (window.innerHeight) { - window_width = window.innerWidth; - window_height = window.innerHeight; - }else{ - window_width = document.documentElement.clientWidth; - window_height = document.documentElement.clientHeight; - //var z = "CI:"; for(var i in bod){z = z + " " + i;} alert(z); - } - - char_width = t.offsetLeft / 44; - var avail_width = window_width-18; - var avail_width_mod = avail_width % char_width; - var x_size = (avail_width - avail_width_mod)/char_width; - - char_height = t2.offsetTop - t.offsetTop; - var avail_height = window_height-text.offsetTop-10; - var avail_height_mod = avail_height % char_height; - var y_size = (avail_height - avail_height_mod)/char_height; - - text.style.width = x_size*char_width+"px"; - text.style.height = y_size*char_height+"px"; - - if( screen_x != x_size || screen_y != y_size ){ - send_key("window resize "+x_size+" "+y_size); - } - screen_x = x_size; - screen_y = y_size; -} - -""" - -ALARM_DELAY = 60 -POLL_CONNECT = 3 -MAX_COLS = 200 -MAX_ROWS = 100 -MAX_READ = 4096 -BUF_SZ = 16384 - -_code_colours = { - 'black': "0", - 'dark red': "1", - 'dark green': "2", - 'brown': "3", - 'dark blue': "4", - 'dark magenta': "5", - 'dark cyan': "6", - 'light gray': "7", - 'dark gray': "8", - 'light red': "9", - 'light green': "A", - 'yellow': "B", - 'light blue': "C", - 'light magenta': "D", - 'light cyan': "E", - 'white': "F", -} - -# replace control characters with ?'s -_trans_table = "?" * 32 + "".join([chr(x) for x in range(32, 256)]) - -_css_style = """ -body { margin: 8px 8px 8px 8px; border: 0; - color: black; background-color: silver; - font-family: fixed; overflow: hidden; } - -form { margin: 0 0 8px 0; } - -#text { position: relative; - background-color: silver; - width: 100%; height: 100%; - margin: 3px 0 0 0; border: 1px solid #999; } - -#page { position: relative; width: 100%;height: 100%;} -""" - -# HTML Initial Page -_html_page = [ -""" - - -Urwid Web Display - """,""" - - - -
-
-
The quick brown fox jumps over the lazy dog.X
-Y
-
-Urwid Web Display - """,""" - -Status: Set up - -

-
-
-"""]
-
-class Screen:
-    def __init__(self):
-        self.palette = {}
-        self.has_color = True
-        self._started = False
-
-    started = property(lambda self: self._started)
-
-    def register_palette( self, l ):
-        """Register a list of palette entries.
-
-        l -- list of (name, foreground, background) or
-             (name, same_as_other_name) palette entries.
-
-        calls self.register_palette_entry for each item in l
-        """
-
-        for item in l:
-            if len(item) in (3,4):
-                self.register_palette_entry( *item )
-                continue
-            assert len(item) == 2, "Invalid register_palette usage"
-            name, like_name = item
-            if like_name not in self.palette:
-                raise Exception("palette entry '%s' doesn't exist"%like_name)
-            self.palette[name] = self.palette[like_name]
-
-    def register_palette_entry( self, name, foreground, background,
-        mono=None):
-        """Register a single palette entry.
-
-        name -- new entry/attribute name
-        foreground -- foreground colour
-        background -- background colour
-        mono -- monochrome terminal attribute
-
-        See curses_display.register_palette_entry for more info.
-        """
-        if foreground == "default":
-            foreground = "black"
-        if background == "default":
-            background = "light gray"
-        self.palette[name] = (foreground, background, mono)
-
-    def set_mouse_tracking(self, enable=True):
-        """Not yet implemented"""
-        pass
-
-    def tty_signal_keys(self, *args, **vargs):
-        """Do nothing."""
-        pass
-
-    def start(self):
-        """
-        This function reads the initial screen size, generates a
-        unique id and handles cleanup when fn exits.
-
-        web_display.set_preferences(..) must be called before calling
-        this function for the preferences to take effect
-        """
-        global _prefs
-
-        if self._started:
-            return util.StoppingContext(self)
-
-        client_init = sys.stdin.read(50)
-        assert client_init.startswith("window resize "),client_init
-        ignore1,ignore2,x,y = client_init.split(" ",3)
-        x = int(x)
-        y = int(y)
-        self._set_screen_size( x, y )
-        self.last_screen = {}
-        self.last_screen_width = 0
-
-        self.update_method = os.environ["HTTP_X_URWID_METHOD"]
-        assert self.update_method in ("multipart","polling")
-
-        if self.update_method == "polling" and not _prefs.allow_polling:
-            sys.stdout.write("Status: 403 Forbidden\r\n\r\n")
-            sys.exit(0)
-
-        clients = glob.glob(os.path.join(_prefs.pipe_dir,"urwid*.in"))
-        if len(clients) >= _prefs.max_clients:
-            sys.stdout.write("Status: 503 Sever Busy\r\n\r\n")
-            sys.exit(0)
-
-        urwid_id = "%09d%09d"%(random.randrange(10**9),
-            random.randrange(10**9))
-        self.pipe_name = os.path.join(_prefs.pipe_dir,"urwid"+urwid_id)
-        os.mkfifo(self.pipe_name+".in",0600)
-        signal.signal(signal.SIGTERM,self._cleanup_pipe)
-
-        self.input_fd = os.open(self.pipe_name+".in",
-            os.O_NONBLOCK | os.O_RDONLY)
-        self.input_tail = ""
-        self.content_head = ("Content-type: "
-            "multipart/x-mixed-replace;boundary=ZZ\r\n"
-            "X-Urwid-ID: "+urwid_id+"\r\n"
-            "\r\n\r\n"
-            "--ZZ\r\n")
-        if self.update_method=="polling":
-            self.content_head = (
-                "Content-type: text/plain\r\n"
-                "X-Urwid-ID: "+urwid_id+"\r\n"
-                "\r\n\r\n")
-
-        signal.signal(signal.SIGALRM,self._handle_alarm)
-        signal.alarm( ALARM_DELAY )
-        self._started = True
-
-        return util.StoppingContext(self)
-
-    def stop(self):
-        """
-        Restore settings and clean up.
-        """
-        if not self._started:
-            return
-
-        # XXX which exceptions does this actually raise? EnvironmentError?
-        try:
-            self._close_connection()
-        except Exception:
-            pass
-        signal.signal(signal.SIGTERM,signal.SIG_DFL)
-        self._cleanup_pipe()
-        self._started = False
-
-    def set_input_timeouts(self, *args):
-        pass
-
-    def run_wrapper(self,fn):
-        """
-        Run the application main loop, calling start() first
-        and stop() on exit.
-        """
-        try:
-            self.start()
-            return fn()
-        finally:
-            self.stop()
-
-
-    def _close_connection(self):
-        if self.update_method == "polling child":
-            self.server_socket.settimeout(0)
-            sock, addr = self.server_socket.accept()
-            sock.sendall("Z")
-            sock.close()
-
-        if self.update_method == "multipart":
-            sys.stdout.write("\r\nZ"
-                "\r\n--ZZ--\r\n")
-            sys.stdout.flush()
-
-    def _cleanup_pipe(self, *args):
-        if not self.pipe_name: return
-        # XXX which exceptions does this actually raise? EnvironmentError?
-        try:
-            os.remove(self.pipe_name+".in")
-            os.remove(self.pipe_name+".update")
-        except Exception:
-            pass
-
-    def _set_screen_size(self, cols, rows ):
-        """Set the screen size (within max size)."""
-
-        if cols > MAX_COLS:
-            cols = MAX_COLS
-        if rows > MAX_ROWS:
-            rows = MAX_ROWS
-        self.screen_size = cols, rows
-
-    def draw_screen(self, (cols, rows), r ):
-        """Send a screen update to the client."""
-
-        if cols != self.last_screen_width:
-            self.last_screen = {}
-
-        sendq = [self.content_head]
-
-        if self.update_method == "polling":
-            send = sendq.append
-        elif self.update_method == "polling child":
-            signal.alarm( 0 )
-            try:
-                s, addr = self.server_socket.accept()
-            except socket.timeout:
-                sys.exit(0)
-            send = s.sendall
-        else:
-            signal.alarm( 0 )
-            send = sendq.append
-            send("\r\n")
-            self.content_head = ""
-
-        assert r.rows() == rows
-
-        if r.cursor is not None:
-            cx, cy = r.cursor
-        else:
-            cx = cy = None
-
-        new_screen = {}
-
-        y = -1
-        for row in r.content():
-            y += 1
-            row = list(row)
-
-            l = []
-
-            sig = tuple(row)
-            if y == cy: sig = sig + (cx,)
-            new_screen[sig] = new_screen.get(sig,[]) + [y]
-            old_line_numbers = self.last_screen.get(sig, None)
-            if old_line_numbers is not None:
-                if y in old_line_numbers:
-                    old_line = y
-                else:
-                    old_line = old_line_numbers[0]
-                send( "<%d\n"%old_line )
-                continue
-
-            col = 0
-            for (a, cs, run) in row:
-                run = run.translate(_trans_table)
-                if a is None:
-                    fg,bg,mono = "black", "light gray", None
-                else:
-                    fg,bg,mono = self.palette[a]
-                if y == cy and col <= cx:
-                    run_width = util.calc_width(run, 0,
-                        len(run))
-                    if col+run_width > cx:
-                        l.append(code_span(run, fg, bg,
-                            cx-col))
-                    else:
-                        l.append(code_span(run, fg, bg))
-                    col += run_width
-                else:
-                    l.append(code_span(run, fg, bg))
-
-            send("".join(l)+"\n")
-        self.last_screen = new_screen
-        self.last_screen_width = cols
-
-        if self.update_method == "polling":
-            sys.stdout.write("".join(sendq))
-            sys.stdout.flush()
-            sys.stdout.close()
-            self._fork_child()
-        elif self.update_method == "polling child":
-            s.close()
-        else: # update_method == "multipart"
-            send("\r\n--ZZ\r\n")
-            sys.stdout.write("".join(sendq))
-            sys.stdout.flush()
-
-        signal.alarm( ALARM_DELAY )
-
-
-    def clear(self):
-        """
-        Force the screen to be completely repainted on the next
-        call to draw_screen().
-
-        (does nothing for web_display)
-        """
-        pass
-
-
-    def _fork_child(self):
-        """
-        Fork a child to run CGI disconnected for polling update method.
-        Force parent process to exit.
-        """
-        daemonize( self.pipe_name +".err" )
-        self.input_fd = os.open(self.pipe_name+".in",
-            os.O_NONBLOCK | os.O_RDONLY)
-        self.update_method = "polling child"
-        s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
-        s.bind( self.pipe_name+".update" )
-        s.listen(1)
-        s.settimeout(POLL_CONNECT)
-        self.server_socket = s
-
-    def _handle_alarm(self, sig, frame):
-        assert self.update_method in ("multipart","polling child")
-        if self.update_method == "polling child":
-            # send empty update
-            try:
-                s, addr = self.server_socket.accept()
-                s.close()
-            except socket.timeout:
-                sys.exit(0)
-        else:
-            # send empty update
-            sys.stdout.write("\r\n\r\n--ZZ\r\n")
-            sys.stdout.flush()
-        signal.alarm( ALARM_DELAY )
-
-
-    def get_cols_rows(self):
-        """Return the screen size."""
-        return self.screen_size
-
-    def get_input(self, raw_keys=False):
-        """Return pending input as a list."""
-        l = []
-        resized = False
-
-        try:
-            iready,oready,eready = select.select(
-                [self.input_fd],[],[],0.5)
-        except select.error as e:
-            # return on interruptions
-            if e.args[0] == 4:
-                if raw_keys:
-                    return [],[]
-                return []
-            raise
-
-        if not iready:
-            if raw_keys:
-                return [],[]
-            return []
-
-        keydata = os.read(self.input_fd, MAX_READ)
-        os.close(self.input_fd)
-        self.input_fd = os.open(self.pipe_name+".in",
-            os.O_NONBLOCK | os.O_RDONLY)
-        #sys.stderr.write( repr((keydata,self.input_tail))+"\n" )
-        keys = keydata.split("\n")
-        keys[0] = self.input_tail + keys[0]
-        self.input_tail = keys[-1]
-
-        for k in keys[:-1]:
-            if k.startswith("window resize "):
-                ign1,ign2,x,y = k.split(" ",3)
-                x = int(x)
-                y = int(y)
-                self._set_screen_size(x, y)
-                resized = True
-            else:
-                l.append(k)
-        if resized:
-            l.append("window resize")
-
-        if raw_keys:
-            return l, []
-        return l
-
-
-def code_span( s, fg, bg, cursor = -1):
-    code_fg = _code_colours[ fg ]
-    code_bg = _code_colours[ bg ]
-
-    if cursor >= 0:
-        c_off, _ign = util.calc_text_pos(s, 0, len(s), cursor)
-        c2_off = util.move_next_char(s, c_off, len(s))
-
-        return ( code_fg + code_bg + s[:c_off] + "\n" +
-             code_bg + code_fg + s[c_off:c2_off] + "\n" +
-             code_fg + code_bg + s[c2_off:] + "\n")
-    else:
-        return code_fg + code_bg + s + "\n"
-
-
-def html_escape(text):
-    """Escape text so that it will be displayed safely within HTML"""
-    text = text.replace('&','&')
-    text = text.replace('<','<')
-    text = text.replace('>','>')
-    return text
-
-
-def is_web_request():
-    """
-    Return True if this is a CGI web request.
-    """
-    return 'REQUEST_METHOD' in os.environ
-
-def handle_short_request():
-    """
-    Handle short requests such as passing keystrokes to the application
-    or sending the initial html page.  If returns True, then this
-    function recognised and handled a short request, and the calling
-    script should immediately exit.
-
-    web_display.set_preferences(..) should be called before calling this
-    function for the preferences to take effect
-    """
-    global _prefs
-
-    if not is_web_request():
-        return False
-
-    if os.environ['REQUEST_METHOD'] == "GET":
-        # Initial request, send the HTML and javascript.
-        sys.stdout.write("Content-type: text/html\r\n\r\n" +
-            html_escape(_prefs.app_name).join(_html_page))
-        return True
-
-    if os.environ['REQUEST_METHOD'] != "POST":
-        # Don't know what to do with head requests etc.
-        return False
-
-    if 'HTTP_X_URWID_ID' not in os.environ:
-        # If no urwid id, then the application should be started.
-        return False
-
-    urwid_id = os.environ['HTTP_X_URWID_ID']
-    if len(urwid_id)>20:
-        #invalid. handle by ignoring
-        #assert 0, "urwid id too long!"
-        sys.stdout.write("Status: 414 URI Too Long\r\n\r\n")
-        return True
-    for c in urwid_id:
-        if c not in "0123456789":
-            # invald. handle by ignoring
-            #assert 0, "invalid chars in id!"
-            sys.stdout.write("Status: 403 Forbidden\r\n\r\n")
-            return True
-
-    if os.environ.get('HTTP_X_URWID_METHOD',None) == "polling":
-        # this is a screen update request
-        s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
-        try:
-            s.connect( os.path.join(_prefs.pipe_dir,
-                "urwid"+urwid_id+".update") )
-            data = "Content-type: text/plain\r\n\r\n"+s.recv(BUF_SZ)
-            while data:
-                sys.stdout.write(data)
-                data = s.recv(BUF_SZ)
-            return True
-        except socket.error:
-            sys.stdout.write("Status: 404 Not Found\r\n\r\n")
-            return True
-
-    # this is a keyboard input request
-    try:
-        fd = os.open((os.path.join(_prefs.pipe_dir,
-            "urwid"+urwid_id+".in")), os.O_WRONLY)
-    except OSError:
-        sys.stdout.write("Status: 404 Not Found\r\n\r\n")
-        return True
-
-    # FIXME: use the correct encoding based on the request
-    keydata = sys.stdin.read(MAX_READ)
-    os.write(fd,keydata.encode('ascii'))
-    os.close(fd)
-    sys.stdout.write("Content-type: text/plain\r\n\r\n")
-
-    return True
-
-
-class _Preferences:
-    app_name = "Unnamed Application"
-    pipe_dir = "/tmp"
-    allow_polling = True
-    max_clients = 20
-
-_prefs = _Preferences()
-
-def set_preferences( app_name, pipe_dir="/tmp", allow_polling=True,
-    max_clients=20 ):
-    """
-    Set web_display preferences.
-
-    app_name -- application name to appear in html interface
-    pipe_dir -- directory for input pipes, daemon update sockets
-                and daemon error logs
-    allow_polling -- allow creation of daemon processes for
-                     browsers without multipart support
-    max_clients -- maximum concurrent client connections. This
-               pool is shared by all urwid applications
-               using the same pipe_dir
-    """
-    global _prefs
-    _prefs.app_name = app_name
-    _prefs.pipe_dir = pipe_dir
-    _prefs.allow_polling = allow_polling
-    _prefs.max_clients = max_clients
-
-
-class ErrorLog:
-    def __init__(self, errfile ):
-        self.errfile = errfile
-    def write(self, err):
-        open(self.errfile,"a").write(err)
-
-
-def daemonize( errfile ):
-    """
-    Detach process and become a daemon.
-    """
-    pid = os.fork()
-    if pid:
-        os._exit(0)
-
-    os.setsid()
-    signal.signal(signal.SIGHUP, signal.SIG_IGN)
-    os.umask(0)
-
-    pid = os.fork()
-    if pid:
-        os._exit(0)
-
-    os.chdir("/")
-    for fd in range(0,20):
-        try:
-            os.close(fd)
-        except OSError:
-            pass
-
-    sys.stdin = open("/dev/null","r")
-    sys.stdout = open("/dev/null","w")
-    sys.stderr = ErrorLog( errfile )
-
diff --git a/urwid/widget.py b/urwid/widget.py
deleted file mode 100644
index 661e7ed..0000000
--- a/urwid/widget.py
+++ /dev/null
@@ -1,1825 +0,0 @@
-#!/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 operator import attrgetter
-
-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(object):
-    """
-    Widget base class
-
-    .. attribute:: __metaclass__
-       :annotation: = urwid.WidgetMeta
-
-       See :class:`urwid.WidgetMeta` definition
-
-    .. 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
-    """
-    __metaclass__ = WidgetMeta
-
-    _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 paramter.
-
-        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 minumum
-        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 to listen for ``"change"`` events
-    sent when the value of edit_text changes.  See :func:``connect_signal``.
-    """
-    # (this variable is picked up by the MetaSignals metaclass)
-    signals = ["change"]
-
-    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 preceeding 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.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 preceeding 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)
-
-    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)
-
-    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)
-        self._edit_text = text
-        if self.edit_pos > len(text):
-            self.edit_pos = len(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="""
-        Read-only property returning 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, unicode)
-        cu = isinstance(self._caption, unicode)
-        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, unicode) and not
-                    isinstance(self._caption, unicode)):
-                # 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 long(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()
diff --git a/urwid/wimp.py b/urwid/wimp.py
deleted file mode 100755
index 03a3613..0000000
--- a/urwid/wimp.py
+++ /dev/null
@@ -1,664 +0,0 @@
-#!/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 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"]
-
-    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'``
-
-        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 supress 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
-        if do_callback and self._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
-
-    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'``
-
-        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 supress 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")
-