tuikit
authorRadek Brich <radek.brich@devl.cz>
Wed, 16 Feb 2011 23:51:30 +0100
changeset 0 a35731b5e31a
child 1 69318aba22bf
tuikit
.hgignore
docs/Makefile
docs/conf.py
docs/focus.rst
docs/index.rst
docs/redraw.rst
example.py
tuikit/__init__.py
tuikit/application.py
tuikit/backend_curses.py
tuikit/button.py
tuikit/common.py
tuikit/container.py
tuikit/editbox.py
tuikit/editfield.py
tuikit/layout.py
tuikit/menu.py
tuikit/menubar.py
tuikit/scrollbar.py
tuikit/textedit.py
tuikit/widget.py
tuikit/window.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.hgignore	Wed Feb 16 23:51:30 2011 +0100
@@ -0,0 +1,3 @@
+.*~
+tuikit/.*\.pyc
+docs/_build
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/docs/Makefile	Wed Feb 16 23:51:30 2011 +0100
@@ -0,0 +1,89 @@
+# Makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line.
+SPHINXOPTS    =
+SPHINXBUILD   = sphinx-build
+PAPER         =
+BUILDDIR      = _build
+
+# Internal variables.
+PAPEROPT_a4     = -D latex_paper_size=a4
+PAPEROPT_letter = -D latex_paper_size=letter
+ALLSPHINXOPTS   = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+
+.PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest
+
+help:
+	@echo "Please use \`make <target>' where <target> is one of"
+	@echo "  html      to make standalone HTML files"
+	@echo "  dirhtml   to make HTML files named index.html in directories"
+	@echo "  pickle    to make pickle files"
+	@echo "  json      to make JSON files"
+	@echo "  htmlhelp  to make HTML files and a HTML help project"
+	@echo "  qthelp    to make HTML files and a qthelp project"
+	@echo "  latex     to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
+	@echo "  changes   to make an overview of all changed/added/deprecated items"
+	@echo "  linkcheck to check all external links for integrity"
+	@echo "  doctest   to run all doctests embedded in the documentation (if enabled)"
+
+clean:
+	-rm -rf $(BUILDDIR)/*
+
+html:
+	$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
+	@echo
+	@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
+
+dirhtml:
+	$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
+	@echo
+	@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
+
+pickle:
+	$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
+	@echo
+	@echo "Build finished; now you can process the pickle files."
+
+json:
+	$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
+	@echo
+	@echo "Build finished; now you can process the JSON files."
+
+htmlhelp:
+	$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
+	@echo
+	@echo "Build finished; now you can run HTML Help Workshop with the" \
+	      ".hhp project file in $(BUILDDIR)/htmlhelp."
+
+qthelp:
+	$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
+	@echo
+	@echo "Build finished; now you can run "qcollectiongenerator" with the" \
+	      ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
+	@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Tuikit.qhcp"
+	@echo "To view the help file:"
+	@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Tuikit.qhc"
+
+latex:
+	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+	@echo
+	@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
+	@echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \
+	      "run these through (pdf)latex."
+
+changes:
+	$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
+	@echo
+	@echo "The overview file is in $(BUILDDIR)/changes."
+
+linkcheck:
+	$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
+	@echo
+	@echo "Link check complete; look for any errors in the above output " \
+	      "or in $(BUILDDIR)/linkcheck/output.txt."
+
+doctest:
+	$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
+	@echo "Testing of doctests in the sources finished, look at the " \
+	      "results in $(BUILDDIR)/doctest/output.txt."
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/docs/conf.py	Wed Feb 16 23:51:30 2011 +0100
@@ -0,0 +1,194 @@
+# -*- coding: utf-8 -*-
+#
+# Tuikit documentation build configuration file, created by
+# sphinx-quickstart on Wed Feb  2 23:35:28 2011.
+#
+# This file is execfile()d with the current directory set to its containing dir.
+#
+# Note that not all possible configuration values are present in this
+# autogenerated file.
+#
+# All configuration values have a default; values that are commented out
+# serve to show the default.
+
+import sys, os
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+#sys.path.append(os.path.abspath('.'))
+
+# -- General configuration -----------------------------------------------------
+
+# Add any Sphinx extension module names here, as strings. They can be extensions
+# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
+extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.todo']
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# The suffix of source filenames.
+source_suffix = '.rst'
+
+# The encoding of source files.
+#source_encoding = 'utf-8'
+
+# The master toctree document.
+master_doc = 'index'
+
+# General information about the project.
+project = u'Tuikit'
+copyright = u'2011, Radek Brich'
+
+# The version info for the project you're documenting, acts as replacement for
+# |version| and |release|, also used in various other places throughout the
+# built documents.
+#
+# The short X.Y version.
+version = '0.5'
+# The full version, including alpha/beta/rc tags.
+release = '0.5a'
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+#language = None
+
+# There are two options for replacing |today|: either, you set today to some
+# non-false value, then it is used:
+#today = ''
+# Else, today_fmt is used as the format for a strftime call.
+#today_fmt = '%B %d, %Y'
+
+# List of documents that shouldn't be included in the build.
+#unused_docs = []
+
+# List of directories, relative to source directory, that shouldn't be searched
+# for source files.
+exclude_trees = ['_build']
+
+# The reST default role (used for this markup: `text`) to use for all documents.
+#default_role = None
+
+# If true, '()' will be appended to :func: etc. cross-reference text.
+#add_function_parentheses = True
+
+# If true, the current module name will be prepended to all description
+# unit titles (such as .. function::).
+#add_module_names = True
+
+# If true, sectionauthor and moduleauthor directives will be shown in the
+# output. They are ignored by default.
+#show_authors = False
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = 'sphinx'
+
+# A list of ignored prefixes for module index sorting.
+#modindex_common_prefix = []
+
+
+# -- Options for HTML output ---------------------------------------------------
+
+# The theme to use for HTML and HTML Help pages.  Major themes that come with
+# Sphinx are currently 'default' and 'sphinxdoc'.
+html_theme = 'default'
+
+# Theme options are theme-specific and customize the look and feel of a theme
+# further.  For a list of options available for each theme, see the
+# documentation.
+#html_theme_options = {}
+
+# Add any paths that contain custom themes here, relative to this directory.
+#html_theme_path = []
+
+# The name for this set of Sphinx documents.  If None, it defaults to
+# "<project> v<release> documentation".
+#html_title = None
+
+# A shorter title for the navigation bar.  Default is the same as html_title.
+#html_short_title = None
+
+# The name of an image file (relative to this directory) to place at the top
+# of the sidebar.
+#html_logo = None
+
+# The name of an image file (within the static path) to use as favicon of the
+# docs.  This file should be a Windows icon file (.ico) being 16x16 or 32x32
+# pixels large.
+#html_favicon = None
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ['_static']
+
+# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
+# using the given strftime format.
+#html_last_updated_fmt = '%b %d, %Y'
+
+# If true, SmartyPants will be used to convert quotes and dashes to
+# typographically correct entities.
+#html_use_smartypants = True
+
+# Custom sidebar templates, maps document names to template names.
+#html_sidebars = {}
+
+# Additional templates that should be rendered to pages, maps page names to
+# template names.
+#html_additional_pages = {}
+
+# If false, no module index is generated.
+#html_use_modindex = True
+
+# If false, no index is generated.
+#html_use_index = True
+
+# If true, the index is split into individual pages for each letter.
+#html_split_index = False
+
+# If true, links to the reST sources are added to the pages.
+#html_show_sourcelink = True
+
+# If true, an OpenSearch description file will be output, and all pages will
+# contain a <link> tag referring to it.  The value of this option must be the
+# base URL from which the finished HTML is served.
+#html_use_opensearch = ''
+
+# If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml").
+#html_file_suffix = ''
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'Tuikitdoc'
+
+
+# -- Options for LaTeX output --------------------------------------------------
+
+# The paper size ('letter' or 'a4').
+#latex_paper_size = 'letter'
+
+# The font size ('10pt', '11pt' or '12pt').
+#latex_font_size = '10pt'
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title, author, documentclass [howto/manual]).
+latex_documents = [
+  ('index', 'Tuikit.tex', u'Tuikit Documentation',
+   u'Radek Brich', 'manual'),
+]
+
+# The name of an image file (relative to this directory) to place at the top of
+# the title page.
+#latex_logo = None
+
+# For "manual" documents, if this is true, then toplevel headings are parts,
+# not chapters.
+#latex_use_parts = False
+
+# Additional stuff for the LaTeX preamble.
+#latex_preamble = ''
+
+# Documents to append as an appendix to all manuals.
+#latex_appendices = []
+
+# If false, no module index is generated.
+#latex_use_modindex = True
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/docs/focus.rst	Wed Feb 16 23:51:30 2011 +0100
@@ -0,0 +1,14 @@
+Focus
+=====
+
+Only one non-container widget can have focus at the time.
+All parent containers also have focus.
+
+Events emitted on change: focus, unfocus
+
+mousedown - focus widget under mouse or its parent if canfocus() == false
+
+tab - focus next child in container
+shift-tab - previous child
+
+hide() -> unfocus
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/docs/index.rst	Wed Feb 16 23:51:30 2011 +0100
@@ -0,0 +1,22 @@
+.. Tuikit documentation master file, created by
+   sphinx-quickstart on Wed Feb  2 23:35:28 2011.
+   You can adapt this file completely to your liking, but it should at least
+   contain the root `toctree` directive.
+
+Welcome to Tuikit's documentation!
+==================================
+
+Contents:
+
+.. toctree::
+   :maxdepth: 2
+
+   redraw
+
+Indices and tables
+==================
+
+* :ref:`genindex`
+* :ref:`modindex`
+* :ref:`search`
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/example.py	Wed Feb 16 23:51:30 2011 +0100
@@ -0,0 +1,76 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+import locale
+locale.setlocale(locale.LC_ALL, '')
+
+import os
+
+from tuikit.application import Application
+from tuikit.editfield import EditField
+from tuikit.window import Window
+from tuikit.button import Button
+from tuikit.scrollbar import VScrollbar
+from tuikit.textedit import TextEdit
+from tuikit.menubar import MenuBar
+from tuikit.menu import Menu
+from tuikit.layout import VerticalLayout
+
+
+class MyApplication(Application):
+    def __init__(self):
+        Application.__init__(self)
+        self.top.connect('keypress', self.globalkeypress)
+
+        menubar = MenuBar()
+        self.top.add(menubar)
+
+        filemenu = Menu(['New', '-', 'Open', 'Save', '-', 'Quit'])
+        self.top.add(filemenu)
+        filemenu.allowlayout = False
+        filemenu.hidden = True
+
+        editmenu = Menu(['Copy', 'Paste'])
+        self.top.add(editmenu)
+        editmenu.allowlayout = False
+        editmenu.hidden = True
+
+        helpmenu = Menu(['About'])
+        self.top.add(helpmenu)
+        helpmenu.allowlayout = False
+        helpmenu.hidden = True
+
+        menubar.setitems([
+            ('File', filemenu),
+            ('Edit', editmenu),
+            ('Help', helpmenu)
+            ])
+
+        vert = VerticalLayout()
+        self.top.layout(vert)
+
+
+        #win = Window()
+        #self.top.add(win)
+
+        #button = Button('click!')
+        #win.add(button)
+        #button.x = 10
+        #button.y = 7
+
+        #button.connect('click', self.buttonclick)
+        #self.button = button
+
+        #subwin = Window(8,8)
+        #win.add(subwin)
+
+
+    def globalkeypress(self, keyname, char):
+        if keyname == 'escape':
+            self.terminate()
+
+
+if __name__ == '__main__':
+    app = MyApplication()
+    app.start()
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tuikit/application.py	Wed Feb 16 23:51:30 2011 +0100
@@ -0,0 +1,110 @@
+# -*- coding: utf-8 -*-
+
+import curses
+import curses.wrapper
+import logging
+import time
+import math
+
+from .container import Container
+from .backend_curses import BackendCurses
+
+
+class TopWindow(Container):
+    def __init__(self):
+        Container.__init__(self)
+
+        self.top = self
+
+        self.timeout = []
+        self.timelast = None
+
+        self.connect('draw', self.on_draw)
+
+
+    def on_draw(self, screen, x, y):
+        screen.erase()
+
+
+    def add_timeout(self, s, func):
+        if not len(self.timeout):
+            self.timelast = time.time()
+        self.timeout += [[s, func]]
+
+
+    def remove_timeout(self, func):
+        for to in self.timeout[:]:
+            if to[1] == func:
+                self.timeout.remove(to)
+
+
+    def has_timeout(self):
+        return len(self.timeout) and True or False
+
+
+    def nearest_timeout(self):
+        return min(self.timeout)[0]
+
+
+    def process_timeout(self):
+        now = time.time()
+        lasted = now - self.timelast
+        self.timelast = now
+
+        for to in self.timeout[:]:
+            newt = to[0] - lasted
+            if newt <= 0.0:
+                to[1]()
+                self.timeout.remove(to)
+            else:
+                to[0] = newt
+
+
+class Application:
+    def __init__(self):
+        self.top = TopWindow()
+        self.quit = False
+        self.screen = None
+
+        self.log = logging.getLogger('tuikit')
+        self.log.setLevel(logging.DEBUG)
+        handler = logging.FileHandler('./tuikit.log')
+        format = logging.Formatter('%(asctime)s %(levelname)-5s %(message)s', '%y-%m-%d %H:%M:%S')
+        handler.setFormatter(format)
+        self.log.addHandler(handler)
+        self.log.info('start')
+
+
+    def start(self):
+        curses.wrapper(self.mainloop)
+
+
+    def terminate(self):
+        self.quit = True
+
+
+    def mainloop(self, screen):
+        self.screen = BackendCurses(screen)
+        self.top.width, self.top.height = self.screen.width, self.screen.height
+        self.top.emit('resize')
+        self.top.setfocus()
+
+        while True:
+            self.top.draw(self.screen)
+            self.screen.commit()
+
+            timeout = None
+            if self.top.has_timeout():
+                timeout = int(math.ceil(self.top.nearest_timeout() * 10))
+
+            events = self.screen.process_input(timeout)
+
+            if self.top.has_timeout():
+                self.top.process_timeout()
+
+            for event in events:
+                self.top.emit(event[0], *event[1:])
+
+            if self.quit:
+                break
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tuikit/backend_curses.py	Wed Feb 16 23:51:30 2011 +0100
@@ -0,0 +1,408 @@
+# -*- coding: utf-8 -*-
+
+import curses
+import curses.ascii
+import locale
+import logging
+
+from .common import Rect
+
+
+class MouseEvent:
+    def __init__(self, x=0, y=0):
+        self.x = x   # global coordinates
+        self.y = y
+        self.wx = x  # local widget coordinates
+        self.wy = y
+        self.px = 0  # parent coordinates
+        self.py = 0
+        self.button = 0
+
+
+    def childevent(self, child):
+        ev = MouseEvent(self.x, self.y)
+        # original local coordinates are new parent coordinates
+        ev.px = self.wx
+        ev.py = self.wy
+        # update local coordinates
+        ev.wx = self.wx - child.x
+        ev.wy = self.wy - child.y
+
+        return ev
+
+
+class BackendCurses:
+    xterm_codes = (
+        (0x09,                      'tab'           ),
+        (0x0a,                      'enter'         ),
+        (0x7f,                      'backspace'     ),
+        (0x1b,0x4f,0x50,            'f1'            ),
+        (0x1b,0x4f,0x51,            'f2'            ),
+        (0x1b,0x4f,0x52,            'f3'            ),
+        (0x1b,0x4f,0x53,            'f4'            ),
+        (0x1b,0x5b,0x4d,            'mouse'         ),
+        (0x1b,0x5b,0x41,            'up'            ),
+        (0x1b,0x5b,0x42,            'down'          ),
+        (0x1b,0x5b,0x43,            'right'         ),
+        (0x1b,0x5b,0x44,            'left'          ),
+        (0x1b,0x5b,0x31,0x35,0x7e,  'f5'            ),
+        (0x1b,0x5b,0x31,0x37,0x7e,  'f6'            ),
+        (0x1b,0x5b,0x31,0x38,0x7e,  'f7'            ),
+        (0x1b,0x5b,0x31,0x39,0x7e,  'f8'            ),
+        (0x1b,0x5b,0x32,0x30,0x7e,  'f9'            ),
+        (0x1b,0x5b,0x32,0x31,0x7e,  'f10'           ),
+        (0x1b,0x5b,0x32,0x33,0x7e,  'f11'           ),
+        (0x1b,0x5b,0x32,0x34,0x7e,  'f12'           ),
+        (0x1b,0x5b,0x32,0x7e,       'insert'        ),
+        (0x1b,0x5b,0x33,0x7e,       'delete'        ),
+        (0x1b,0x5b,0x35,0x7e,       'pageup'        ),
+        (0x1b,0x5b,0x36,0x7e,       'pagedown'      ),
+        (0x1b,0x5b,0x46,            'end'           ),
+        (0x1b,0x5b,0x48,            'home'          ),
+        (0x1b,                      'escape'        ),
+    )
+
+    def __init__(self, screen):
+        self.screen = screen
+        self.height, self.width = screen.getmaxyx()
+
+        self.clipstack = []
+        self.colorstack = []
+        self.inputqueue = []
+        self.mbtnstack = []
+
+        self.log = logging.getLogger('tuikit')
+
+        # initialize curses
+        curses.curs_set(False)
+        curses.mousemask(curses.ALL_MOUSE_EVENTS | curses.REPORT_MOUSE_POSITION)
+        curses.mouseinterval(0)  # do not wait to detect clicks, we use only press/release
+
+        screen.immedok(0)
+        screen.keypad(0)
+
+        curses.init_pair(1, curses.COLOR_WHITE, curses.COLOR_BLACK)
+        curses.init_pair(2, curses.COLOR_BLACK, curses.COLOR_CYAN)
+        curses.init_pair(3, curses.COLOR_WHITE, curses.COLOR_CYAN)
+
+        self.BOLD = curses.A_BOLD
+        self.BLINK = curses.A_BLINK
+        self.UNDERLINE = curses.A_UNDERLINE
+
+        # http://en.wikipedia.org/wiki/List_of_Unicode_characters#Geometric_shapes
+        self.UP_ARROW = '▲' #curses.ACS_UARROW
+        self.DOWN_ARROW = '▼' #curses.ACS_DARROW
+
+        # http://en.wikipedia.org/wiki/Box-drawing_characters
+        self.LIGHT_SHADE = '░' #curses.ACS_BOARD
+        self.MEDIUM_SHADE = '▒'
+        self.DARK_SHADE = '▓'
+        self.BLOCK = '█'
+
+        self.COLUMN = '▁▂▃▄▅▆▇█'
+        self.CORNER_ROUND = '╭╮╰╯'
+        self.CORNER = '┌┐└┘'
+        self.LINE = '─━│┃┄┅┆┇┈┉┊┋'
+
+        self.HLINE = '─' # curses.ACS_HLINE
+        self.VLINE = '│' # curses.ACS_VLINE
+        self.ULCORNER = '┌' # curses.ACS_ULCORNER
+        self.URCORNER = '┐' # curses.ACS_URCORNER
+        self.LLCORNER = '└' # curses.ACS_LLCORNER
+        self.LRCORNER = '┘' # curses.ACS_LRCORNER
+        self.LTEE = '├'
+        self.RTEE = '┤'
+
+
+    ## clip operations ##
+
+    def pushclip(self, x, y, w, h):
+        newclip = Rect(x, y, w, h)
+        if len(self.clipstack):
+            oldclip = self.clipstack[-1]
+            newclip = self.intersect(oldclip, newclip)
+        self.clipstack.append(newclip)
+
+
+    def popclip(self):
+        self.clipstack.pop()
+
+
+    def testclip(self, x, y):
+        # no clip rectangle on stack => passed
+        if not len(self.clipstack):
+            return True
+        # test against top clip rect from stack
+        clip = self.clipstack[-1]
+        if x < clip.x or y < clip.y \
+        or x >= clip.x + clip.w or y >= clip.y + clip.h:
+            return False
+        # passed
+        return True
+
+
+    def intersect(self, r1, r2):
+        x1 = max(r1.x, r2.x)
+        y1 = max(r1.y, r2.y)
+        x2 = min(r1.x + r1.w, r2.x + r2.w)
+        y2 = min(r1.y + r1.h, r2.y + r2.h)
+        if x1 >= x2 or y1 >= y2:
+            return Rect()
+        return Rect(x1, y1, x2-x1, y2-y1)
+
+
+    def union(self, r1, r2):
+        x = min(r1.x, r2.x)
+        y = min(r1.y, r2.y)
+        w = max(r1.x + r1.w, r2.x + r2.w) - x
+        h = max(r1.y + r1.h, r2.y + r2.h) - y
+        return Rect(x, y, w, h)
+
+
+    ## attributes ##
+
+    def pushcolor(self, col, attr=0):
+        self.screen.attrset(curses.color_pair(col) | attr)
+        self.colorstack.append((col, attr))
+
+
+    def popcolor(self):
+        self.colorstack.pop()
+        if len(self.colorstack):
+            col, attr = self.colorstack[-1]
+        else:
+            col, attr = 0, 0
+        self.screen.attrset(curses.color_pair(col) | attr)
+
+
+    ## drawing ##
+
+    def putch(self, x, y, c):
+        if not self.testclip(x, y):
+            return
+        try:
+            if type(c) is str and len(c) == 1:
+                self.screen.addstr(y, x, c)
+            else:
+                self.screen.addch(y, x, c)
+        except curses.error:
+            pass
+
+
+    def puts(self, x, y, s):
+        for c in s:
+            self.putch(x, y, c)
+            x += 1
+
+
+    def hline(self, x, y, w, c=' '):
+        if type(c) is str:
+            s = c*w
+        else:
+            s = [c]*w
+        self.puts(x, y, s)
+
+
+    def vline(self, x, y, h, c=' '):
+        for i in range(h):
+            self.putch(x, y+i, c)
+
+
+    def frame(self, x, y, w, h):
+        self.putch(x, y, self.ULCORNER)
+        self.putch(x+w-1, y, self.URCORNER)
+        self.putch(x, y+h-1, self.LLCORNER)
+        self.putch(x+w-1, y+h-1, self.LRCORNER)
+        self.hline(x+1, y, w-2, self.HLINE)
+        self.hline(x+1, y+h-1, w-2, self.HLINE)
+        self.vline(x, y+1, h-2, self.VLINE)
+        self.vline(x+w-1, y+1, h-2, self.VLINE)
+
+
+    def fill(self, x, y, w, h, c=' '):
+        for i in range(h):
+            self.hline(x, y + i, w, c)
+
+
+    def erase(self):
+        curses.curs_set(False)
+        self.cursor = None
+        self.screen.erase()
+
+
+    def commit(self):
+        if self.cursor:
+            self.screen.move(*self.cursor)
+            curses.curs_set(True)
+        else:
+            curses.curs_set(False)
+        self.screen.refresh()
+
+
+    ## cursor ##
+
+    def showcursor(self, x, y):
+        if not self.testclip(x, y):
+            return
+        self.cursor = (y, x)
+
+
+    ## input ##
+
+    def inputqueue_fill(self):
+        self.screen.nodelay(1)
+
+        while True:
+            c = self.screen.getch()
+            if c == -1:
+                break
+            self.inputqueue.insert(0, c)
+
+        self.screen.nodelay(0)
+
+
+    def inputqueue_next(self):
+        c = None
+        while c is None:
+            try:
+                c = self.inputqueue.pop()
+            except IndexError:
+                curses.napms(25)
+                self.inputqueue_fill()
+        return c
+
+
+    def process_input(self, timeout=None):
+        if len(self.inputqueue) > 0:
+            c = self.inputqueue_next()
+        else:
+            if not timeout is None:
+                curses.halfdelay(timeout)
+                c = self.screen.getch()
+                curses.cbreak()
+                if c == -1:
+                    return []
+            else:
+                c = self.screen.getch()
+
+        if c == curses.KEY_MOUSE:
+            return self.process_mouse()
+
+        elif curses.ascii.isctrl(c):
+            self.inputqueue.append(c)
+            self.inputqueue_fill()
+            return self.process_control_chars()
+
+        elif c >= 192 and c <= 255:
+            self.inputqueue.append(c)
+            self.inputqueue_fill()
+            return self.process_utf8_chars()
+
+        elif curses.ascii.isprint(c):
+            return [('keypress', None, str(chr(c)))]
+
+        else:
+            #self.top.keypress(None, unicode(chr(c)))
+            self.inputqueue.append(c)
+            self.inputqueue_fill()
+            return self.process_control_chars()
+
+
+    def process_mouse(self):
+        id, x, y, z, bstate = curses.getmouse()
+        ev = MouseEvent(x, y)
+
+        out = []
+
+        if bstate & curses.REPORT_MOUSE_POSITION:
+            out += [('mousemove', ev)]
+
+        if bstate & curses.BUTTON1_PRESSED:
+            ev.button = 1
+            out += [('mousedown', ev)]
+
+        if bstate & curses.BUTTON3_PRESSED:
+            ev.button = 3
+            out += [('mousedown', ev)]
+
+        if bstate & curses.BUTTON1_RELEASED:
+            ev.button = 1
+            out += [('mouseup', ev)]
+
+        if bstate & curses.BUTTON3_RELEASED:
+            ev.button = 3
+            out += [('mouseup', ev)]
+
+        return out
+
+
+    def process_utf8_chars(self):
+        #FIXME read exact number of chars as defined by utf-8
+        utf = ''
+        while len(utf) <= 6:
+            c = self.inputqueue_next()
+            utf += chr(c)
+            try:
+                uni = str(utf, 'utf-8')
+                return [('keypress', None, uni)]
+            except UnicodeDecodeError:
+                continue
+        raise Exception('Invalid UTF-8 sequence: %r' % utf)
+
+
+    def process_control_chars(self):
+        keyname = None
+        for code in self.xterm_codes:
+            ok = False
+            if len(self.inputqueue) >= len(code) - 1:
+                ok = True
+                for i in range(len(code)-1):
+                    if self.inputqueue[-i-1] != code[i]:
+                        ok = False
+                        break
+
+            if ok:
+                keyname = code[-1]
+                self.inputqueue = self.inputqueue[:-len(code)+1]
+
+        if keyname is None:
+            self.log.debug('Unknown control sequence: %s',
+                ','.join(reversed(['0x%x'%x for x in self.inputqueue])))
+            c = self.inputqueue_next()
+            return [('keypress', 'Unknown%x' % c, None)]
+
+        if keyname == 'mouse':
+           return self.process_xterm_mouse()
+
+        return [('keypress', keyname, None)]
+
+
+    def process_xterm_mouse(self):
+        t = self.inputqueue_next()
+        x = self.inputqueue_next() - 0x21
+        y = self.inputqueue_next() - 0x21
+
+        ev = MouseEvent(x, y)
+        out = []
+
+        if t in (0x20, 0x21, 0x22): # button press
+            btn = t - 0x1f
+            ev.button = btn
+            if not btn in self.mbtnstack:
+                self.mbtnstack.append(btn)
+                out += [('mousedown', ev)]
+            else:
+                out += [('mousemove', ev)]
+
+        elif t == 0x23: # button release
+            ev.button = self.mbtnstack.pop()
+            out += [('mouseup', ev)]
+
+        elif t in (0x60, 0x61): # wheel up, down
+            ev.button = 4 + t - 0x60
+            out += [('mousewheel', ev)]
+
+        else:
+            raise Exception('Unknown mouse event: %x' % t)
+
+        return out
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tuikit/button.py	Wed Feb 16 23:51:30 2011 +0100
@@ -0,0 +1,47 @@
+# -*- coding: utf-8 -*-
+
+import curses
+import locale
+
+from .widget import Widget
+
+
+class Button(Widget):
+    def __init__(self, label=''):
+        Widget.__init__(self, len(label) + 4, 1)
+
+        self.label = label
+        self.bg = 1
+
+        self.connect('draw', self.on_draw)
+        self.connect('mousedown', self.on_mousedown)
+        self.connect('mouseup', self.on_mouseup)
+
+        self.newevent('click')
+
+
+    def on_draw(self, screen, x, y):
+        l = (self.width - len(self.label)) // 2
+        screen.pushcolor(self.bg)
+        screen.putch(x, y, '<')
+        for i in range(x+1, x+l):
+            screen.putch(i, y, ' ')
+        screen.puts(x + l, y, self.label)
+        for i in range(x+l+len(self.label), x+self.width-1):
+            screen.putch(i, y, ' ')
+        screen.putch(x + self.width - 1, y, '>')
+        screen.popcolor()
+
+
+    def on_mousedown(self, ev):
+        self.bg = 2
+        self.redraw()
+
+
+    def on_mouseup(self, ev):
+        self.bg = 1
+        self.redraw()
+
+        if self.enclose(ev.px, ev.py):
+            self.handle('click')
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tuikit/common.py	Wed Feb 16 23:51:30 2011 +0100
@@ -0,0 +1,13 @@
+# -*- coding: utf-8 -*-
+
+class Rect:
+    def __init__(self, x=0, y=0, w=0, h=0):
+        self.x = x
+        self.y = y
+        self.w = w
+        self.h = h
+
+
+    def __repr__(self):
+        return 'Rect(%(x)s,%(y)s,%(w)s,%(h)s)' % self.__dict__
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tuikit/container.py	Wed Feb 16 23:51:30 2011 +0100
@@ -0,0 +1,139 @@
+# -*- coding: utf-8 -*-
+
+from .widget import Widget
+
+
+class Container(Widget):
+    def __init__(self, width = 10, height = 10):
+        Widget.__init__(self, width, height)
+
+        self.children = []
+        self.focuschild = None
+        self.mousechild = None
+
+        self.borders = (0, 0, 0, 0) # left, top, right, bottom
+        self.widthrequest = (None, None)
+        self.heightrequest = (None, None)
+
+
+    def add(self, widget):
+        self.children.append(widget)
+        widget.parent = self
+        widget.settop(self.top)
+
+
+    def layout(self, layout):
+        self.layout = layout
+        layout.container = self
+        self.connect('resize', layout.resize)
+
+
+    def settop(self, top):
+        self.top = top
+        for child in self.children:
+            child.settop(top)
+
+
+    ### focus
+
+
+    def canfocus(self):
+        return True
+
+
+    def setfocus(self):
+        self.focus = True
+        if self.focuschild is None and len(self.children) > 0:
+            for child in self.children:
+                if self.focuschild is None:
+                    if child.canfocus():
+                        self.focuschild = child
+                        child.setfocus()
+                else:
+                    if child.focus:
+                        child.unfocus()
+
+    def unfocus(self):
+        self.focus = False
+        for child in self.children:
+            child.unfocus()
+
+
+    ###
+
+
+    def draw(self, screen, x=0, y=0):
+        #if self._redraw:
+            #self.fill(screen, self.y, self.x, self.height, self.width)
+        self.handle('draw', screen, x, y)
+
+        l, t, r, b = self.borders
+        screen.pushclip(x + l, y + t, self.width - l - r, self.height - t - b)
+
+        for child in self.children:
+            #if self._redraw:
+             #   child._redraw = True
+            child.draw(screen, x + child.x, y + child.y)
+
+        screen.popclip()
+
+        #self._redraw = False
+
+
+    def keypress(self, keyname, char):
+        # always relay key event to some child
+        if self.focus:
+            self.handle('keypress', keyname, char)
+            if self.focuschild:
+                self.focuschild.keypress(keyname, char)
+
+
+    def mousedown(self, ev):
+        handled = False
+        for child in reversed(self.children):
+            if child.enclose(ev.wx, ev.wy):
+                childev = ev.childevent(child)
+                child.mousedown(childev)
+                self.mousechild = child
+                handled = True
+                break
+        if not handled:
+            self.handle('mousedown', ev)
+
+
+    def mouseup(self, ev):
+        if self.mousechild:
+            childev = ev.childevent(self.mousechild)
+            self.mousechild.mouseup(childev)
+            self.mousechild = None
+        else:
+            self.handle('mouseup', ev)
+        #handled = False
+        #for child in self.children:
+            #if child.enclose(ev.wx, ev.wy):
+                #childev = ev.childevent(child)
+                #child.mouseup(childev)
+                #self.mousechild = child
+                #handled = True
+        #if not handled:
+            #self.handle('mouseup', ev)
+
+
+    def mousemove(self, ev):
+        if self.mousechild:
+            childev = ev.childevent(self.mousechild)
+            self.mousechild.mousemove(childev)
+        else:
+            self.handle('mousemove', ev)
+
+
+    def mousewheel(self, ev):
+        handled = False
+        for child in reversed(self.children):
+            if child.enclose(ev.wx, ev.wy):
+                childev = ev.childevent(child)
+                child.mousewheel(childev)
+                handled = True
+                break
+        if not handled:
+            self.handle('mousewheel', ev)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tuikit/editbox.py	Wed Feb 16 23:51:30 2011 +0100
@@ -0,0 +1,210 @@
+# -*- coding: utf-8 -*-
+
+from .widget import Widget
+
+class EditBox(Widget):
+    def __init__(self, width=20, height=20, text=''):
+        Widget.__init__(self, width, height)
+
+        self.set_text(text)
+
+        self.xofs = 0
+        self.yofs = 0
+
+        # cursor
+        self.cline = 0
+        self.cpos = 0
+
+        # selection
+        self.sline = 0
+        self.spos = 0
+
+        self.connect('draw', self.on_draw)
+        self.connect('keypress', self.on_keypress)
+        self.connect('mousewheel', self.on_mousewheel)
+
+        self.newevent('scroll')
+        self.newevent('areasize')
+
+
+    def on_draw(self, screen, x, y):
+        for j in range(self.height):
+            if self.yofs + j >= len(self.lines):
+                break
+            line = self.lines[self.yofs + j]
+            #if len(line) < self.width:
+                #line += ' ' * (self.width - len(line))
+            #else:
+                #line = line[:self.width]
+            screen.puts(x, y + j, line)
+
+        screen.showcursor(x + self.get_cpos() - self.xofs, y + self.cline - self.yofs)
+
+
+    def on_keypress(self, keyname, char):
+        if keyname:
+            if keyname == 'left':
+                self.move_left()
+
+            if keyname == 'right':
+                self.move_right()
+
+            if keyname == 'home':
+                self.move_home()
+
+            if keyname == 'end':
+                self.move_end()
+
+            if keyname == 'up':
+                self.move_up()
+
+            if keyname == 'down':
+                self.move_down()
+
+            if keyname == 'pageup':
+                self.move_pageup()
+
+            if keyname == 'pagedown':
+                self.move_pagedown()
+
+            if keyname == 'backspace':
+                if self.cline > 0 or self.cpos > 0:
+                    self.move_left()
+                    self.del_char()
+
+            if keyname == 'delete':
+                self.del_char()
+
+            if keyname == 'enter':
+                self.add_newline()
+                self.move_right()
+
+        if char:
+            self.add_char(char)
+            self.move_right()
+
+        self.redraw()
+
+
+    def on_mousewheel(self, ev):
+        # up
+        if ev.button == 4:
+            self.move_up()
+        # down
+        if ev.button == 5:
+            self.move_down()
+        self.redraw()
+
+
+    def set_text(self, text):
+        self.lines = text.split('\n')
+
+
+    def get_text(self):
+        return '\n'.join(self.lines)
+
+
+    def get_linelen(self):
+        return len(self.lines[self.cline])
+
+
+    def get_cpos(self):
+        if self.cpos > self.get_linelen():
+            return self.get_linelen()
+        return self.cpos
+
+
+    def set_yofs(self, yofs):
+        if yofs < 0:
+            yofs = 0
+        if yofs > len(self.lines) - self.height:
+            yofs = len(self.lines) - self.height
+        self.yofs = yofs
+        self.handle('scroll')
+
+
+    def move_left(self):
+        if self.cpos > 0:
+            self.cpos = self.get_cpos() - 1
+        else:
+            if self.move_up():
+                self.cpos = self.get_linelen()
+
+
+    def move_right(self):
+        if self.cpos < self.get_linelen():
+            self.cpos += 1
+        else:
+            if self.move_down():
+                self.cpos = 0
+
+
+    def move_home(self):
+        self.cpos = 0
+
+
+    def move_end(self):
+        self.cpos = self.get_linelen()
+
+
+    def move_up(self):
+        if self.cline > 0:
+            self.cline -= 1
+            if self.cline < self.yofs:
+                self.set_yofs(self.cline)
+            return True
+        return False
+
+
+    def move_down(self):
+        if self.cline < len(self.lines) - 1:
+            self.cline += 1
+            if self.cline > self.yofs + self.height - 1:
+                self.set_yofs(self.cline - (self.height - 1))
+            return True
+        return False
+
+
+    def move_pageup(self):
+        if self.cline >= self.height - 1:
+            self.cline -= self.height - 1
+            self.set_yofs(self.yofs - (self.height - 1))
+        else:
+            self.cline = 0
+            self.set_yofs(0)
+
+
+    def move_pagedown(self):
+        if self.cline <= len(self.lines) - (self.height - 1):
+            self.cline += self.height - 1
+            self.set_yofs(self.yofs + (self.height - 1))
+        else:
+            self.cline = len(self.lines) - 1
+            self.set_yofs(self.cline)
+
+
+    def add_char(self, c):
+        ln = self.lines[self.cline]
+        cpos = self.get_cpos()
+        self.lines[self.cline] = ln[:cpos] + c + ln[cpos:]
+        self.cpos = cpos
+
+
+    def add_newline(self):
+        ln = self.lines[self.cline]
+        cpos = self.get_cpos()
+        self.lines[self.cline] = ln[cpos:]
+        self.lines.insert(self.cline, ln[:cpos])
+        self.handle('areasize')
+
+
+    def del_char(self):
+        ln = self.lines[self.cline]
+        cpos = self.get_cpos()
+        if cpos == self.get_linelen():
+            if self.cline + 1 < len(self.lines):
+                self.lines[self.cline] = self.lines[self.cline] + self.lines[self.cline+1]
+                del self.lines[self.cline+1]
+                self.handle('areasize')
+        else:
+            self.lines[self.cline] = ln[:cpos] + ln[cpos+1:]
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tuikit/editfield.py	Wed Feb 16 23:51:30 2011 +0100
@@ -0,0 +1,104 @@
+# -*- coding: utf-8 -*-
+
+import curses
+import locale
+
+from .widget import Widget
+
+
+class EditField(Widget):
+    def __init__(self, width=10, value=''):
+        Widget.__init__(self, width, 1)
+
+        self.code = locale.getpreferredencoding()
+        if not type(value) is str:
+            value = str(value, self.code)
+
+        self.value = value
+        self.maxlen = None   # unlimited
+
+        self.tw = self.width - 2  # real width of text field (minus space for arrows)
+        self.cursor = 0   # position of cursor on screen
+        self.pos = 0      # position of cursor in value
+        self.ofs = 0      # position of value beginning on screen
+
+        self.connect('draw', self.on_draw)
+        self.connect('keypress', self.on_keypress)
+
+
+    def on_draw(self, screen, x, y):
+        # draw value
+        val = self.value + ' ' * self.tw         # add spaces to fill rest of field
+        val = val[self.ofs : self.ofs + self.tw]  # cut value - begin from ofs, limit to tw chars
+        screen.puts(x + 1, y, val.encode(self.code))
+
+        # draw arrows if content overflows
+        c = ' '
+        if self.ofs > 0:
+            c = '<'
+        screen.putch(x, y, c)
+
+        c = ' '
+        if len(self.value[self.ofs:]) > self.tw:
+            c = '>'
+        screen.putch(x + self.width-1, y, c)
+
+        # move cursor to the position
+        screen.showcursor(x + 1 + self.cursor, y)
+
+
+    def on_keypress(self, keyname, char):
+        if keyname:
+            if keyname == 'left':
+                self.move_left()
+
+            if keyname == 'right':
+                self.move_right()
+
+            if keyname == 'backspace':
+                if self.pos > 0:
+                    self.move_left()
+                    self.del_char()
+
+            if keyname == 'delete':
+                self.del_char()
+
+        if char:
+            self.add_char(char)
+            self.move_right()
+
+        self.redraw()
+
+
+    def move_left(self):
+        if self.cursor > 1 or (self.cursor == 1 and self.pos == 1):
+            # move cursor
+            self.pos -= 1
+            self.cursor -= 1
+        else:
+            # move content in field
+            if self.pos > self.cursor:
+                self.pos -= 1
+                self.ofs -= 1
+
+
+    def move_right(self):
+        if self.pos < len(self.value):
+            if self.cursor < self.tw - 2 \
+            or (self.cursor == self.tw - 2 and self.pos == len(self.value)-1):
+                # move cursor
+                self.pos += 1
+                self.cursor += 1
+            else:
+                # move content in field
+                self.pos += 1
+                self.ofs += 1
+
+
+    def add_char(self, c):
+        self.value = self.value[:self.pos] + c + self.value[self.pos:]
+
+
+    def del_char(self):
+        self.value = self.value[:self.pos] + self.value[self.pos+1:]
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tuikit/layout.py	Wed Feb 16 23:51:30 2011 +0100
@@ -0,0 +1,16 @@
+# -*- coding: utf-8 -*-
+
+
+class VerticalLayout:
+    def resize(self):
+        v = 0
+        c = self.container
+        for child in c.children:
+            if not child.allowlayout:
+                continue
+            child.x = 0
+            child.width = c.width
+            child.y = v
+            v += child.height
+            child.handle('resize')
+        c.redraw()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tuikit/menu.py	Wed Feb 16 23:51:30 2011 +0100
@@ -0,0 +1,34 @@
+# -*- coding: utf-8 -*-
+
+from .widget import Widget
+
+
+class Menu(Widget):
+    def __init__(self, items=[]):
+        Widget.__init__(self)
+        self.width = max([len(x) for x in items]) + 4
+        self.height = len(items) + 2
+
+        self.bg = 2
+        self.items = items
+
+        self.connect('draw', self.on_draw)
+        self.connect('keypress', self.on_keypress)
+
+
+    def on_draw(self, screen, x, y):
+        screen.pushcolor(self.bg)
+        screen.frame(x, y, self.width, self.height)
+        i = 1
+        for item in self.items:
+            if item == '-':
+                screen.puts(x, y + i, screen.LTEE + screen.HLINE * (self.width - 2) + screen.RTEE)
+            else:
+                screen.puts(x + 1, y + i, ' ' + item + ' ' * (self.width - 3 - len(item)))
+            i += 1
+        screen.popcolor()
+
+
+    def on_keypress(self, keyname, char):
+        self.redraw()
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tuikit/menubar.py	Wed Feb 16 23:51:30 2011 +0100
@@ -0,0 +1,61 @@
+# -*- coding: utf-8 -*-
+
+from .widget import Widget
+
+
+class MenuBar(Widget):
+    def __init__(self, items = []):
+        Widget.__init__(self, 0, 1)
+
+        self.bg = 2
+        self.highlight = 3
+
+        self.setitems(items)
+        self.selected = None
+
+        self.connect('draw', self.on_draw)
+        self.connect('keypress', self.on_keypress)
+        self.connect('mousedown', self.on_mousedown)
+
+
+    def setitems(self, items):
+        self.items = items
+
+
+    def on_draw(self, screen, x, y):
+        screen.pushcolor(self.bg)
+        i = 0
+        for item in self.items:
+            if self.selected == item:
+                screen.pushcolor(self.highlight, screen.BOLD)
+                screen.puts(x + i, y, '  ' + item[0] + '  ')
+                screen.popcolor()
+            else:
+                screen.puts(x + i, y, '  ' + item[0] + '  ')
+            i += len(item[0]) + 4
+        if i < self.width:
+            screen.puts(x + i, y, ' ' * (self.width - i))
+        screen.popcolor()
+
+
+    def on_keypress(self, keyname, char):
+        self.redraw()
+
+
+    def on_mousedown(self, ev):
+        i = 0
+        if self.selected:
+            if isinstance(self.selected[1], Widget):
+                self.selected[1].hidden = True
+            self.selected = None
+        for item in self.items:
+            w = len(item[0]) + 4
+            if ev.wx >= i and ev.wx < i + w:
+                self.selected = item
+                if isinstance(item[1], Widget):
+                    item[1].x = i
+                    item[1].y = self.y + 1
+                    item[1].hidden = False
+                    item[1].redraw()
+            i += w
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tuikit/scrollbar.py	Wed Feb 16 23:51:30 2011 +0100
@@ -0,0 +1,107 @@
+# -*- coding: utf-8 -*-
+
+from .widget import Widget
+
+
+class VScrollbar(Widget):
+    def __init__(self, height=10):
+        Widget.__init__(self, 1, height)
+
+        self.max = height - 3
+        self.pos = 0
+        self.thumbpos = 0
+
+        self.interval = 0.1
+
+        self.dragging = False
+        self.move = None
+
+        self.connect('draw', self.on_draw)
+        self.connect('mousedown', self.on_mousedown)
+        self.connect('mouseup', self.on_mouseup)
+        self.connect('mousemove', self.on_mousemove)
+
+        self.newevent('change')
+
+
+    def setpos(self, pos):
+        self.pos = pos
+        self.thumbpos = int(round(self.pos / self.max * (self.height - 3)))
+
+
+    def on_draw(self, screen, x, y):
+        screen.putch(x, y, screen.UP_ARROW)
+        for i in range(y + 1, y + self.height - 1):
+            screen.putch(x, i, screen.LIGHT_SHADE)
+        screen.putch(x, y + 1 + self.thumbpos, screen.BLOCK)
+        screen.putch(x, y + self.height - 1, screen.DOWN_ARROW)
+
+
+    def on_mousedown(self, ev):
+        self.dragging = False
+        self.move = None
+        # arrow buttons
+        if ev.wy == 0 or ev.wy == self.height - 1:
+            if ev.wy == 0:
+                self.move_up()
+            else:
+                self.move_down()
+            self.top.add_timeout(self.interval * 2, self.on_timeout)
+            return
+        # thumb bar
+        if ev.wy == 1 + self.thumbpos:
+            self.dragging = True
+            return
+
+
+    def on_mouseup(self, ev):
+        if self.dragging:
+            self.drag(ev.wy)
+            self.dragging = False
+            return
+        if self.move:
+            self.top.remove_timeout(self.on_timeout)
+            self.move = None
+            return
+
+
+    def on_mousemove(self, ev):
+        if self.dragging:
+            self.drag(ev.wy)
+
+
+    def on_timeout(self):
+        if self.move == 'up':
+            self.move_up()
+        if self.move == 'down':
+            self.move_down()
+        self.top.add_timeout(self.interval, self.on_timeout)
+
+
+    def move_up(self):
+        if self.pos > 0:
+            self.setpos(self.pos - 1)
+        self.move = 'up'
+        self.redraw()
+        self.handle('change')
+
+
+    def move_down(self):
+        if self.pos < self.max:
+            self.setpos(self.pos + 1)
+        self.move = 'down'
+        self.redraw()
+        self.handle('change')
+
+
+    def drag(self, wy):
+        newpos = int(round((wy - 1) / (self.height - 3) * self.max))
+        if newpos < 0:
+            newpos = 0
+        if newpos > self.max:
+            newpos = self.max
+        if self.pos != newpos:
+            self.setpos(newpos)
+            self.redraw()
+            self.handle('change')
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tuikit/textedit.py	Wed Feb 16 23:51:30 2011 +0100
@@ -0,0 +1,48 @@
+# -*- coding: utf-8 -*-
+
+from .container import Container
+from .editbox import EditBox
+from .scrollbar import VScrollbar
+
+
+class TextEdit(Container):
+    def __init__(self, width=20, height=20, text=''):
+        Container.__init__(self, width, height)
+
+        self.editbox = EditBox(width-2, height-2, text)
+        self.add(self.editbox)
+        self.editbox.x = 1
+        self.editbox.y = 1
+        self.editbox.connect('scroll', self.on_editbox_scroll)
+        self.editbox.connect('areasize', self.on_editbox_areasize)
+
+        self.vscroll = VScrollbar(height - 2)
+        self.add(self.vscroll)
+        self.vscroll.x = width - 1
+        self.vscroll.y = 1
+        self.vscroll.connect('change', self.on_vscroll_change)
+
+        self.on_editbox_areasize()
+
+        self.connect('draw', self.on_draw)
+
+
+    def settext(self, text):
+        self.editbox.set_text(text)
+
+
+    def on_draw(self, screen, x, y):
+        screen.frame(x, y, self.width, self.height)
+
+
+    def on_editbox_scroll(self):
+        self.vscroll.setpos(self.editbox.yofs)
+
+
+    def on_editbox_areasize(self):
+        self.vscroll.max = len(self.editbox.lines) - self.editbox.height
+
+
+    def on_vscroll_change(self):
+        self.editbox.yofs = self.vscroll.pos
+        self.editbox.redraw()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tuikit/widget.py	Wed Feb 16 23:51:30 2011 +0100
@@ -0,0 +1,144 @@
+# -*- coding: utf-8 -*-
+
+import curses
+
+
+class Widget:
+    def __init__(self, width = 10, height = 10):
+        self.parent = None
+        self.top = None
+        # placing - set by parent widget
+        self.x = 0
+        self.y = 0
+        # size
+        self.width = width
+        self.height = height
+        self.allowlayout = True
+        # redraw request
+        self._redraw = True
+        self.focus = False
+        self.hidden = False
+        # event handlers
+        self.event = {
+            'resize' : [],
+            'draw' : [],
+            'keypress' : [],
+            'mousedown' : [],
+            'mouseup' : [],
+            'mousemove' : [],
+            'mousewheel' : []}
+
+
+    def settop(self, top):
+        self.top = top
+
+
+    def newevent(self, event):
+        self.event[event] = []
+
+
+    def connect(self, event, handler):
+        if event in list(self.event.keys()):
+            self.event[event] += [handler]
+
+
+    def disconnect(self, event, handler=None):
+        if event in list(self.event.keys()):
+            if handler:
+                i = self.event[event].index(handler)
+                del self.event[event][i]
+            else:
+                self.event[event] = []
+
+
+    def handle(self, event, *args, **kwargs):
+        for handler in self.event[event]:
+            handler(*args, **kwargs)
+
+
+    def emit(self, event, *args, **kwargs):
+        getattr(self, event)(*args, **kwargs)
+
+
+    def resize(self):
+        self.handle('resize')
+
+
+    def redraw(self, parent=True):
+        self._redraw = True
+        if parent and self.parent:
+            self.parent._redraw = True
+
+
+    def draw(self, screen, x=0, y=0):
+        #if self._redraw:
+        if not self.hidden:
+            self.handle('draw', screen, x, y)
+            #self._redraw = False
+
+
+    ### focus
+
+
+    def canfocus(self):
+        return bool(self.event['keypress'])
+
+
+    def setfocus(self):
+        self.focus = True
+
+
+    def unfocus(self):
+        self.focus = False
+
+
+    def grabfocus(self):
+        if not self.focus:
+            self.parent.grabfocus(self) # grab focus for me
+
+
+    ###
+
+
+    def keypress(self, keyname, char):
+        if self.focus:
+            self.handle('keypress', keyname, char)
+
+
+    def mousedown(self, ev):
+        self.handle('mousedown', ev)
+
+
+    def mouseup(self, ev):
+        self.handle('mouseup', ev)
+
+
+    def mousemove(self, ev):
+        self.handle('mousemove', ev)
+
+
+    def mousewheel(self, ev):
+        self.handle('mousewheel', ev)
+
+
+    def enclose(self, x, y):
+        if self.hidden:
+            return False
+        if x < self.x or y < self.y \
+        or x >= self.x + self.width or y >= self.y + self.height:
+            return False
+        return  True
+
+
+    def screentest(self, y, x):
+        sy, sx = self.screenyx()
+        if y < sy or x < sx or y >= sy + self.height or x >= sx + self.width:
+            return False
+        return True
+
+
+    def screenyx(self):
+        if self.parent:
+            y,x = self.parent.screenyx()
+            return self.y + y, self.x + x
+        return self.y, self.x
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tuikit/window.py	Wed Feb 16 23:51:30 2011 +0100
@@ -0,0 +1,65 @@
+# -*- coding: utf-8 -*-
+
+from .container import Container
+
+
+class Window(Container):
+    def __init__(self, width=40, height=10):
+        Container.__init__(self, width, height)
+
+        self.connect('draw', self.on_draw)
+        self.connect('mousedown', self.on_mousedown)
+        self.connect('mouseup', self.on_mouseup)
+        self.connect('mousemove', self.on_mousemove)
+
+        self.title = ' '
+
+
+    def on_draw(self, screen, x, y):
+        screen.frame(x, y, self.width, self.height)
+        screen.fill(x+1, y+1, self.width-2, self.height-2)
+    #    screen.addstr(self.title)
+
+
+    def on_mousedown(self, ev):
+        self.dragstart = (ev.wx, ev.wy)
+        if ev.wx >= self.width - 1 and ev.wy >= self.height - 1:
+            self.resizing = True
+        else:
+            self.resizing = False
+        self.origsize = (self.width, self.height)
+        #self.title += '%dP%d ' % (x,y)
+        self.redraw(True)
+
+
+    def on_mouseup(self, ev):
+        #self.title += '%dR%d ' % (x,y)
+
+        if self.resizing:
+            self.width = self.origsize[0] + ev.wx - self.dragstart[0]
+            self.height = self.origsize[1] + ev.wy - self.dragstart[1]
+        else:
+            self.x = ev.px - self.dragstart[0]
+            self.y = ev.py - self.dragstart[1]
+
+        self.redraw(True)
+
+
+    def on_mousemove(self, ev):
+     #   self.title += '%dM%d ' % (x,y)
+        if ev.px == self.x + self.dragstart[0] \
+        and ev.py == self.y + self.dragstart[1]:
+            return
+
+        #if x > self.parent.width-self.width:
+         #   return
+
+        if self.resizing:
+            self.width = self.origsize[0] + ev.wx - self.dragstart[0]
+            self.height = self.origsize[1] + ev.wy - self.dragstart[1]
+        else:
+            self.x = ev.px - self.dragstart[0]
+            self.y = ev.py - self.dragstart[1]
+
+        self.redraw(True)
+