tuikit/backend_curses.py
changeset 0 a35731b5e31a
child 2 684cdc352562
--- /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