--- /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