Rename BackendCurses to DriverCurses. Add DriverDummy - dummy driver for debugging purposes. Move clipping stack from driver to common.ClipStack class.
--- a/tuikit/application.py Sun Oct 09 11:17:42 2011 +0200
+++ b/tuikit/application.py Sun Oct 09 13:06:58 2011 +0200
@@ -1,12 +1,12 @@
# -*- coding: utf-8 -*-
-import curses.wrapper
import logging
import time
import math
from tuikit.container import Container
-from tuikit.backend_curses import BackendCurses
+from tuikit.driver_curses import DriverCurses
+from tuikit.driver_dummy import DriverDummy
class TopWindow(Container):
@@ -79,12 +79,14 @@
def __init__(self):
'''Create application.'''
+ self.top = TopWindow()
'''Top window.'''
- self.top = TopWindow()
+
self.quit = False
- '''Renderer class, i.e. BackendCurses.'''
- self.screen = None
+ #self.driver = DriverDummy()
+ self.driver = DriverCurses()
+ '''Driver class (render + input), i.e. DriverCurses.'''
self.log = logging.getLogger('tuikit')
self.log.setLevel(logging.DEBUG)
@@ -94,40 +96,34 @@
self.log.addHandler(handler)
self.log.info('=== start ===')
-
def start(self):
'''Start application. Runs main loop.'''
- curses.wrapper(self.mainloop)
-
+ self.driver.start(self.mainloop)
def terminate(self):
'''Terminate application.'''
self.quit = True
-
- def mainloop(self, screen):
+ def mainloop(self):
'''The main loop.'''
- self.screen = BackendCurses(screen)
self.applytheme()
- self.top.width, self.top.height = self.screen.width, self.screen.height
+ self.top.size = self.driver.size # link top widget size to screen size
self.top.emit('resize')
while True:
- self.top.draw(self.screen)
- self.screen.commit()
+ self.top.draw(self.driver)
+ self.driver.commit()
timeout = None
if self.top.has_timeout():
timeout = int(math.ceil(self.top.nearest_timeout() * 10))
- events = self.screen.process_input(timeout)
+ events = self.driver.process_input(timeout)
if self.top.has_timeout():
self.top.process_timeout()
for event in events:
- if event[0] == 'resize':
- self.top.width, self.top.height = self.screen.width, self.screen.height
self.top.emit(event[0], *event[1:])
if self.quit:
@@ -135,15 +131,15 @@
def applytheme(self):
- screen = self.screen
- screen.setcolor('normal', 'white on black')
- screen.setcolor('strong', 'white on black, bold')
- screen.setcolor('active', 'black on cyan')
- screen.setcolor('window:normal', 'white on blue')
- screen.setcolor('window:controls', 'white on blue, bold')
- screen.setcolor('window:controls-active', 'cyan on blue, bold')
- screen.setcolor('button', 'black on white')
- screen.setcolor('button-active', 'black on cyan')
- screen.setcolor('menu', 'black on cyan')
- screen.setcolor('menu-active', 'white on cyan, bold')
+ driver = self.driver
+ driver.setcolor('normal', 'white on black')
+ driver.setcolor('strong', 'white on black, bold')
+ driver.setcolor('active', 'black on cyan')
+ driver.setcolor('window:normal', 'white on blue')
+ driver.setcolor('window:controls', 'white on blue, bold')
+ driver.setcolor('window:controls-active', 'cyan on blue, bold')
+ driver.setcolor('button', 'black on white')
+ driver.setcolor('button-active', 'black on cyan')
+ driver.setcolor('menu', 'black on cyan')
+ driver.setcolor('menu-active', 'white on cyan, bold')
--- a/tuikit/backend_curses.py Sun Oct 09 11:17:42 2011 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,549 +0,0 @@
-# -*- coding: utf-8 -*-
-
-import curses
-import curses.ascii
-import locale
-import logging
-
-from tuikit.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, 'escape' ),
- (0x1b,0x4f,0x50, 'f1' ),
- (0x1b,0x4f,0x51, 'f2' ),
- (0x1b,0x4f,0x52, 'f3' ),
- (0x1b,0x4f,0x53, 'f4' ),
- (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,0x31,0x7e, 'home' ), # linux
- (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,0x34,0x7e, 'end' ), # linux
- (0x1b,0x5b,0x35,0x7e, 'pageup' ),
- (0x1b,0x5b,0x36,0x7e, 'pagedown' ),
- (0x1b,0x5b,0x41, 'up' ),
- (0x1b,0x5b,0x42, 'down' ),
- (0x1b,0x5b,0x43, 'right' ),
- (0x1b,0x5b,0x44, 'left' ),
- (0x1b,0x5b,0x46, 'end' ),
- (0x1b,0x5b,0x48, 'home' ),
- (0x1b,0x5b,0x4d, 'mouse' ),
- (0x1b,0x5b,0x5b,0x41, 'f1' ), # linux
- (0x1b,0x5b,0x5b,0x42, 'f2' ), # linux
- (0x1b,0x5b,0x5b,0x43, 'f3' ), # linux
- (0x1b,0x5b,0x5b,0x44, 'f4' ), # linux
- (0x1b,0x5b,0x5b,0x45, 'f5' ), # linux
- )
-
- color_names = {
- 'black' : curses.COLOR_BLACK,
- 'blue' : curses.COLOR_BLUE,
- 'cyan' : curses.COLOR_CYAN,
- 'green' : curses.COLOR_GREEN,
- 'magenta' : curses.COLOR_MAGENTA,
- 'red' : curses.COLOR_RED,
- 'white' : curses.COLOR_WHITE,
- 'yellow' : curses.COLOR_YELLOW,
- }
-
- def __init__(self, screen):
- self.screen = screen
- self.height, self.width = screen.getmaxyx()
-
- self.cursor = None
- self.clipstack = []
-
- self.colors = {} # maps names to curses attributes
- self.colorpairs = {} # maps tuple (fg,bg) to curses color_pair
- self.colorstack = [] # pushcolor/popcolor puts or gets attributes from this
- self.colorprefix = [] # stack of color prefixes
-
- 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)
-
- # 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)
-
-
- ## colors, attributes ##
-
- def _parsecolor(self, name):
- name = name.lower().strip()
- return self.color_names[name]
-
-
- def _getcolorpair(self, fg, bg):
- pair = (fg, bg)
- if pair in self.colorpairs:
- return self.colorpairs[pair]
- num = len(self.colorpairs) + 1
- curses.init_pair(num, fg, bg)
- self.colorpairs[pair] = num
- return num
-
-
- def _parseattrs(self, attrs):
- res = 0
- for a in attrs:
- a = a.lower().strip()
- trans = {
- 'blink' : curses.A_BLINK,
- 'bold' : curses.A_BOLD,
- 'dim' : curses.A_DIM,
- 'standout' : curses.A_STANDOUT,
- 'underline' : curses.A_UNDERLINE,
- }
- res = res | trans[a]
- return res
-
-
- def setcolor(self, name, desc):
- parts = desc.split(',')
- fg, bg = parts[0].split(' on ')
- attrs = parts[1:]
- fg = self._parsecolor(fg)
- bg = self._parsecolor(bg)
- col = self._getcolorpair(fg, bg)
- attr = self._parseattrs(attrs)
- self.colors[name] = curses.color_pair(col) | attr
-
-
- def pushcolor(self, name):
- # add prefix if available
- if len(self.colorprefix):
- prefixname = self.colorprefix[-1] + name
- if prefixname in self.colors:
- name = prefixname
- attr = self.colors[name]
- self.screen.attrset(attr)
- self.colorstack.append(attr)
-
-
- def popcolor(self):
- self.colorstack.pop()
- if len(self.colorstack):
- attr = self.colorstack[-1]
- else:
- attr = 0
- self.screen.attrset(attr)
-
-
- def pushcolorprefix(self, name):
- self.colorprefix.append(name)
-
-
- def popcolorprefix(self):
- self.colorprefix.pop()
-
-
- ## drawing ##
-
- def putch(self, x, y, c):
- if not self.testclip(x, y):
- return
- try:
- if isinstance(c, 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 isinstance(c, 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):
- 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)
-
-
- def hidecursor(self):
- curses.curs_set(False)
- self.cursor = None
-
-
- ## input ##
-
- def inputqueue_fill(self, timeout=None):
- if timeout is None:
- # wait indefinitely
- c = self.screen.getch()
- self.inputqueue.insert(0, c)
-
- elif timeout > 0:
- # wait
- curses.halfdelay(timeout)
- c = self.screen.getch()
- curses.cbreak()
- if c == -1:
- return
- self.inputqueue.insert(0, c)
-
- # timeout = 0 -> no wait
-
- 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_top(self, num=0):
- return self.inputqueue[-1-num]
-
-
- def inputqueue_get(self):
- c = None
- try:
- c = self.inputqueue.pop()
- except IndexError:
- pass
- return c
-
-
- def inputqueue_get_wait(self):
- c = None
- while c is None:
- try:
- c = self.inputqueue.pop()
- except IndexError:
- curses.napms(25)
- self.inputqueue_fill(0)
- return c
-
-
- def inputqueue_unget(self, c):
- self.inputqueue.append(c)
-
-
- def process_input(self, timeout=None):
- # empty queue -> fill
- if len(self.inputqueue) == 0:
- self.inputqueue_fill(timeout)
-
- res = []
- while len(self.inputqueue):
- c = self.inputqueue_get()
-
- if c == curses.KEY_MOUSE:
- res += self.process_mouse()
-
- elif c == curses.KEY_RESIZE:
- self.height, self.width = self.screen.getmaxyx()
- res.append(('resize',))
-
- elif curses.ascii.isctrl(c):
- self.inputqueue_unget(c)
- res += self.process_control_chars()
-
- elif c >= 192 and c <= 255:
- self.inputqueue_unget(c)
- res += self.process_utf8_chars()
-
- elif curses.ascii.isprint(c):
- res += [('keypress', None, str(chr(c)))]
-
- else:
- #self.top.keypress(None, unicode(chr(c)))
- self.inputqueue_unget(c)
- res += self.process_control_chars()
-
- return res
-
-
- def process_mouse(self):
- try:
- id, x, y, z, bstate = curses.getmouse()
- except curses.error:
- return []
-
- 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_get_wait()
- utf.append(c)
- try:
- uni = str(bytes(utf), 'utf-8')
- return [('keypress', None, uni)]
- except UnicodeDecodeError:
- continue
- raise Exception('Invalid UTF-8 sequence: %r' % utf)
-
-
- def process_control_chars(self):
- codes = self.xterm_codes
- matchingcodes = []
- match = None
- consumed = []
-
- # consume next char, filter out matching codes
- c = self.inputqueue_get_wait()
- consumed.append(c)
-
- while True:
- self.log.debug('c=%s len=%s', c, len(codes))
- for code in codes:
- if c == code[len(consumed)-1]:
- if len(code) - 1 == len(consumed):
- match = code
- else:
- matchingcodes += [code]
-
- self.log.debug('matching=%s', len(matchingcodes))
-
- # match found, or no matching code found -> stop
- if len(matchingcodes) == 0:
- break
-
- # match found and some sequencies still match -> continue
- if len(matchingcodes) > 0:
- if len(self.inputqueue) == 0:
- self.inputqueue_fill(1)
-
- c = self.inputqueue_get()
- if c:
- consumed.append(c)
- codes = matchingcodes
- matchingcodes = []
- else:
- break
-
- keyname = None
- if match:
- # compare match to consumed, return unused chars
- l = len(match) - 1
- while len(consumed) > l:
- self.inputqueue_unget(consumed[-1])
- del consumed[-1]
- keyname = match[-1]
-
- if match is None:
- self.log.debug('Unknown control sequence: %s',
- ','.join(['0x%x'%x for x in consumed]))
- return [('keypress', 'Unknown', None)]
-
- if keyname == 'mouse':
- return self.process_xterm_mouse()
-
- return [('keypress', keyname, None)]
-
-
- def process_xterm_mouse(self):
- t = self.inputqueue_get_wait()
- x = self.inputqueue_get_wait() - 0x21
- y = self.inputqueue_get_wait() - 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
--- a/tuikit/common.py Sun Oct 09 11:17:42 2011 +0200
+++ b/tuikit/common.py Sun Oct 09 13:06:58 2011 +0200
@@ -2,6 +2,9 @@
class Coords:
+
+ '''2D coordinates.'''
+
def __init__(self, x=0, y=0):
self.x = x
self.y = y
@@ -25,6 +28,14 @@
class Size:
+
+ '''Size class.
+
+ Implements attribute access (.w, .h), list-like access([0],[1])
+ and dict-like access (['w'],['h']).
+
+ '''
+
def __init__(self, w=None, h=None):
self.w = w
self.h = h
@@ -38,9 +49,9 @@
return self.__dict__[key]
def __setitem__(self, key, value):
- if key == 0:
+ if key in [0, 'w']:
self.w = value
- if key == 1:
+ if key in [1, 'h']:
self.h = value
def __repr__(self):
@@ -48,6 +59,9 @@
class Rect:
+
+ '''Rectangle is defined by 2D coordinates and size.'''
+
def __init__(self, x=0, y=0, w=0, h=0):
self.x = x
self.y = y
@@ -59,6 +73,15 @@
class Borders:
+
+ '''Borders are defined by left, top, right, bottom border size.
+
+ Ordering is clock-wise, starting with left. This may seem weird,
+ but it corresponds to X/Y or W/H used elsewhere. Left and right are
+ on X axis, so they are defined first.
+
+ '''
+
def __init__(self, l=0, t=0, r=0, b=0):
self.l = l # left
self.t = t # top
@@ -76,3 +99,49 @@
def __repr__(self):
return 'Borders(l={0.l},t={0.t},r={0.r},b={0.b})'.format(self)
+
+class ClipStack:
+
+ '''Stack of clipping regions.'''
+
+ def __init__(self):
+ self.stack = []
+
+ def push(self, x, y, w, h):
+ newclip = Rect(x, y, w, h)
+ if len(self.stack):
+ oldclip = self.stack[-1]
+ newclip = self.intersect(oldclip, newclip)
+ self.stack.append(newclip)
+
+ def pop(self):
+ self.stack.pop()
+
+ def test(self, x, y):
+ # no clip rectangle on stack => passed
+ if not len(self.stack):
+ return True
+ # test against top clip rect from stack
+ clip = self.stack[-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)
+
--- a/tuikit/container.py Sun Oct 09 11:17:42 2011 +0200
+++ b/tuikit/container.py Sun Oct 09 13:06:58 2011 +0200
@@ -81,30 +81,30 @@
child.emit('resize')
- def draw(self, screen, x=0, y=0):
+ def draw(self, driver, x=0, y=0):
if self.hidden:
return
- screen.pushclip(x, y, self.width, self.height)
+ driver.clipstack.push(x, y, self.width, self.height)
if self.colorprefix:
- screen.pushcolorprefix(self.colorprefix)
+ driver.pushcolorprefix(self.colorprefix)
- Widget.draw(self, screen, x, y)
+ Widget.draw(self, driver, x, y)
for child in [x for x in self.children if not x.allowlayout]:
- child.draw(screen, x + child.x, y + child.y)
+ child.draw(driver, x + child.x, y + child.y)
l, t, r, b = self.borders
- screen.pushclip(x+l, y+t, self.width-l-r, self.height-t-b)
+ driver.clipstack.push(x+l, y+t, self.width-l-r, self.height-t-b)
for child in [x for x in self.children if x.allowlayout]:
- child.draw(screen, x + child.x, y + child.y)
+ child.draw(driver, x + child.x, y + child.y)
- screen.popclip()
+ driver.clipstack.pop()
if self.colorprefix:
- screen.popcolorprefix()
- screen.popclip()
+ driver.popcolorprefix()
+ driver.clipstack.pop()
def mousedown(self, ev):
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/tuikit/driver_curses.py Sun Oct 09 13:06:58 2011 +0200
@@ -0,0 +1,511 @@
+# -*- coding: utf-8 -*-
+
+import curses
+import curses.ascii
+import curses.wrapper
+import locale
+import logging
+
+from tuikit.common import Size, Rect, ClipStack
+
+
+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 DriverCurses:
+ xterm_codes = (
+ (0x09, 'tab' ),
+ (0x0a, 'enter' ),
+ (0x7f, 'backspace' ),
+ (0x1b, 'escape' ),
+ (0x1b,0x4f,0x50, 'f1' ),
+ (0x1b,0x4f,0x51, 'f2' ),
+ (0x1b,0x4f,0x52, 'f3' ),
+ (0x1b,0x4f,0x53, 'f4' ),
+ (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,0x31,0x7e, 'home' ), # linux
+ (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,0x34,0x7e, 'end' ), # linux
+ (0x1b,0x5b,0x35,0x7e, 'pageup' ),
+ (0x1b,0x5b,0x36,0x7e, 'pagedown' ),
+ (0x1b,0x5b,0x41, 'up' ),
+ (0x1b,0x5b,0x42, 'down' ),
+ (0x1b,0x5b,0x43, 'right' ),
+ (0x1b,0x5b,0x44, 'left' ),
+ (0x1b,0x5b,0x46, 'end' ),
+ (0x1b,0x5b,0x48, 'home' ),
+ (0x1b,0x5b,0x4d, 'mouse' ),
+ (0x1b,0x5b,0x5b,0x41, 'f1' ), # linux
+ (0x1b,0x5b,0x5b,0x42, 'f2' ), # linux
+ (0x1b,0x5b,0x5b,0x43, 'f3' ), # linux
+ (0x1b,0x5b,0x5b,0x44, 'f4' ), # linux
+ (0x1b,0x5b,0x5b,0x45, 'f5' ), # linux
+ )
+
+ color_names = {
+ 'black' : curses.COLOR_BLACK,
+ 'blue' : curses.COLOR_BLUE,
+ 'cyan' : curses.COLOR_CYAN,
+ 'green' : curses.COLOR_GREEN,
+ 'magenta' : curses.COLOR_MAGENTA,
+ 'red' : curses.COLOR_RED,
+ 'white' : curses.COLOR_WHITE,
+ 'yellow' : curses.COLOR_YELLOW,
+ }
+
+ def __init__(self):
+ '''Set driver attributes to default values.'''
+ self.screen = None
+ self.size = Size()
+ self.cursor = None
+ self.clipstack = ClipStack()
+ self.colors = {} # maps names to curses attributes
+ self.colorpairs = {} # maps tuple (fg,bg) to curses color_pair
+ self.colorstack = [] # pushcolor/popcolor puts or gets attributes from this
+ self.colorprefix = [] # stack of color prefixes
+ self.inputqueue = []
+ self.mbtnstack = []
+
+ self.log = logging.getLogger('tuikit')
+
+ # 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 = '┤'
+
+ def init(self):
+ '''Initialize curses'''
+ self.size.h, self.size.w = self.screen.getmaxyx()
+ self.screen.immedok(0)
+ self.screen.keypad(0)
+ curses.curs_set(False) # hide cursor
+ curses.mousemask(curses.ALL_MOUSE_EVENTS | curses.REPORT_MOUSE_POSITION)
+ curses.mouseinterval(0) # do not wait to detect clicks, we use only press/release
+
+ def start(self, mainfunc):
+ def main(screen):
+ self.screen = screen
+ self.init()
+ mainfunc()
+ curses.wrapper(main)
+
+
+ ## colors, attributes ##
+
+ def _parsecolor(self, name):
+ name = name.lower().strip()
+ return self.color_names[name]
+
+
+ def _getcolorpair(self, fg, bg):
+ pair = (fg, bg)
+ if pair in self.colorpairs:
+ return self.colorpairs[pair]
+ num = len(self.colorpairs) + 1
+ curses.init_pair(num, fg, bg)
+ self.colorpairs[pair] = num
+ return num
+
+
+ def _parseattrs(self, attrs):
+ res = 0
+ for a in attrs:
+ a = a.lower().strip()
+ trans = {
+ 'blink' : curses.A_BLINK,
+ 'bold' : curses.A_BOLD,
+ 'dim' : curses.A_DIM,
+ 'standout' : curses.A_STANDOUT,
+ 'underline' : curses.A_UNDERLINE,
+ }
+ res = res | trans[a]
+ return res
+
+
+ def setcolor(self, name, desc):
+ parts = desc.split(',')
+ fg, bg = parts[0].split(' on ')
+ attrs = parts[1:]
+ fg = self._parsecolor(fg)
+ bg = self._parsecolor(bg)
+ col = self._getcolorpair(fg, bg)
+ attr = self._parseattrs(attrs)
+ self.colors[name] = curses.color_pair(col) | attr
+
+
+ def pushcolor(self, name):
+ # add prefix if available
+ if len(self.colorprefix):
+ prefixname = self.colorprefix[-1] + name
+ if prefixname in self.colors:
+ name = prefixname
+ attr = self.colors[name]
+ self.screen.attrset(attr)
+ self.colorstack.append(attr)
+
+
+ def popcolor(self):
+ self.colorstack.pop()
+ if len(self.colorstack):
+ attr = self.colorstack[-1]
+ else:
+ attr = 0
+ self.screen.attrset(attr)
+
+
+ def pushcolorprefix(self, name):
+ self.colorprefix.append(name)
+
+
+ def popcolorprefix(self):
+ self.colorprefix.pop()
+
+
+ ## drawing ##
+
+ def putch(self, x, y, c):
+ if not self.clipstack.test(x, y):
+ return
+ try:
+ if isinstance(c, 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 isinstance(c, 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):
+ 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.clipstack.test(x, y):
+ return
+ self.cursor = (y, x)
+
+
+ def hidecursor(self):
+ curses.curs_set(False)
+ self.cursor = None
+
+
+ ## input ##
+
+ def inputqueue_fill(self, timeout=None):
+ if timeout is None:
+ # wait indefinitely
+ c = self.screen.getch()
+ self.inputqueue.insert(0, c)
+
+ elif timeout > 0:
+ # wait
+ curses.halfdelay(timeout)
+ c = self.screen.getch()
+ curses.cbreak()
+ if c == -1:
+ return
+ self.inputqueue.insert(0, c)
+
+ # timeout = 0 -> no wait
+
+ 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_top(self, num=0):
+ return self.inputqueue[-1-num]
+
+
+ def inputqueue_get(self):
+ c = None
+ try:
+ c = self.inputqueue.pop()
+ except IndexError:
+ pass
+ return c
+
+
+ def inputqueue_get_wait(self):
+ c = None
+ while c is None:
+ try:
+ c = self.inputqueue.pop()
+ except IndexError:
+ curses.napms(25)
+ self.inputqueue_fill(0)
+ return c
+
+
+ def inputqueue_unget(self, c):
+ self.inputqueue.append(c)
+
+
+ def process_input(self, timeout=None):
+ # empty queue -> fill
+ if len(self.inputqueue) == 0:
+ self.inputqueue_fill(timeout)
+
+ res = []
+ while len(self.inputqueue):
+ c = self.inputqueue_get()
+
+ if c == curses.KEY_MOUSE:
+ res += self.process_mouse()
+
+ elif c == curses.KEY_RESIZE:
+ self.size.h, self.size.w = self.screen.getmaxyx()
+ res.append(('resize',))
+
+ elif curses.ascii.isctrl(c):
+ self.inputqueue_unget(c)
+ res += self.process_control_chars()
+
+ elif c >= 192 and c <= 255:
+ self.inputqueue_unget(c)
+ res += self.process_utf8_chars()
+
+ elif curses.ascii.isprint(c):
+ res += [('keypress', None, str(chr(c)))]
+
+ else:
+ #self.top.keypress(None, unicode(chr(c)))
+ self.inputqueue_unget(c)
+ res += self.process_control_chars()
+
+ return res
+
+
+ def process_mouse(self):
+ try:
+ id, x, y, z, bstate = curses.getmouse()
+ except curses.error:
+ return []
+
+ 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_get_wait()
+ utf.append(c)
+ try:
+ uni = str(bytes(utf), 'utf-8')
+ return [('keypress', None, uni)]
+ except UnicodeDecodeError:
+ continue
+ raise Exception('Invalid UTF-8 sequence: %r' % utf)
+
+
+ def process_control_chars(self):
+ codes = self.xterm_codes
+ matchingcodes = []
+ match = None
+ consumed = []
+
+ # consume next char, filter out matching codes
+ c = self.inputqueue_get_wait()
+ consumed.append(c)
+
+ while True:
+ self.log.debug('c=%s len=%s', c, len(codes))
+ for code in codes:
+ if c == code[len(consumed)-1]:
+ if len(code) - 1 == len(consumed):
+ match = code
+ else:
+ matchingcodes += [code]
+
+ self.log.debug('matching=%s', len(matchingcodes))
+
+ # match found, or no matching code found -> stop
+ if len(matchingcodes) == 0:
+ break
+
+ # match found and some sequencies still match -> continue
+ if len(matchingcodes) > 0:
+ if len(self.inputqueue) == 0:
+ self.inputqueue_fill(1)
+
+ c = self.inputqueue_get()
+ if c:
+ consumed.append(c)
+ codes = matchingcodes
+ matchingcodes = []
+ else:
+ break
+
+ keyname = None
+ if match:
+ # compare match to consumed, return unused chars
+ l = len(match) - 1
+ while len(consumed) > l:
+ self.inputqueue_unget(consumed[-1])
+ del consumed[-1]
+ keyname = match[-1]
+
+ if match is None:
+ self.log.debug('Unknown control sequence: %s',
+ ','.join(['0x%x'%x for x in consumed]))
+ return [('keypress', 'Unknown', None)]
+
+ if keyname == 'mouse':
+ return self.process_xterm_mouse()
+
+ return [('keypress', keyname, None)]
+
+
+ def process_xterm_mouse(self):
+ t = self.inputqueue_get_wait()
+ x = self.inputqueue_get_wait() - 0x21
+ y = self.inputqueue_get_wait() - 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/driver_dummy.py Sun Oct 09 13:06:58 2011 +0200
@@ -0,0 +1,75 @@
+# -*- coding: utf-8 -*-
+'''Dummy driver.
+
+Implements basic driver interface.
+This is useful for debugging or when writing new driver.
+
+@author: Radek Brich <radek.brich@devl.cz>
+
+'''
+
+import logging
+
+from tuikit.common import Size, ClipStack
+
+
+class DriverDummy:
+
+ '''Dummy driver class'''
+
+ def __init__(self):
+ '''Initialize instance attributes'''
+ self.log = logging.getLogger('tuikit')
+ self.size = Size()
+ self.clipstack = ClipStack()
+
+ def start(self, mainfunc):
+ '''Start driver and run mainfunc.'''
+ self.size.w, self.size.h = 80, 25
+ mainfunc()
+
+
+ # colors
+
+ def setcolor(self, name, desc):
+ '''Define color name.
+
+ name - name of color (e.g. 'normal', 'active')
+ desc - color description - foreground, background, attributes (e.g. 'black on white, bold')
+
+ '''
+ self.log.info('DummyDriver.setcolor(name=%r, desc=%r)', name, desc)
+
+ def pushcolor(self, name):
+ self.log.info('DummyDriver.pushcolor(name=%r)', name)
+
+ def popcolor(self):
+ self.log.info('DummyDriver.popcolor()')
+
+
+ # drawing
+
+ def erase(self):
+ '''Clear screen.'''
+ self.log.info('DummyDriver.erase()')
+
+ def puts(self, x, y, s):
+ '''Output string to specified coordinates.'''
+ self.log.info('DummyDriver.puts(x=%r, y=%r, s=%r)', x, y, s)
+
+ def commit(self):
+ '''Commit changes to the screen.'''
+ self.log.info('DummyDriver.commit()')
+
+
+ # input
+
+ def process_input(self, timeout=None):
+ '''Process input, return list of events.
+
+ This dummy implementation just returns 'q' and Escape key presses.
+
+ '''
+ events = [('keypress', None, 'q'), ('keypress', 'escape', None)]
+ return events
+