Merge.
Due to my schizophrenia, I've accidentally forked my own code. The other set of changes were found in another computer.
--- a/demos/03_application.py Wed Sep 03 08:57:24 2014 +0200
+++ b/demos/03_application.py Wed Sep 03 21:56:20 2014 +0200
@@ -10,15 +10,26 @@
label = Label('Hello there!')
label.pos.update(20, 10)
-button = Button()
-button.pos.update(20, 20)
+button1 = Button()
+button1.pos.update(20, 20)
+button2 = Button()
+button2.pos.update(30, 20)
field = TextField('text field')
field.pos.update(20, 30)
app = Application()
app.root_window.add(label)
-app.root_window.add(button)
+app.root_window.add(button1)
+app.root_window.add(button2)
app.root_window.add(field)
-app.root_window.focus_child = field
+app.root_window.focus_widget = field
+
+def on_keypress(ev):
+ if ev.keyname == 'escape':
+ app.stop()
+
+app.window_manager.sig_keypress.connect(on_keypress)
+
app.start()
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/curses_get_wch.py Wed Sep 03 21:56:20 2014 +0200
@@ -0,0 +1,18 @@
+#!/usr/bin/python3
+
+import curses
+import locale
+
+locale.setlocale(locale.LC_ALL, "")
+
+def doStuff(stdscr):
+ stdscr.keypad(1)
+ message = "Press 'q' to quit.\n"
+ stdscr.addstr(0, 0, message, 0)
+ while True:
+ c = stdscr.get_wch() # pauses until a key's hit
+ if c == 'q':
+ break
+ stdscr.addstr('%s %r\n' % (c, c))
+
+curses.wrapper(doStuff)
--- a/tests/curses_getkey.py Wed Sep 03 08:57:24 2014 +0200
+++ b/tests/curses_getkey.py Wed Sep 03 21:56:20 2014 +0200
@@ -1,5 +1,4 @@
-#!/usr/bin/python
-# coding=UTF-8
+#!/usr/bin/python3
import curses
import locale
--- a/tests/curses_keycodes.py Wed Sep 03 08:57:24 2014 +0200
+++ b/tests/curses_keycodes.py Wed Sep 03 21:56:20 2014 +0200
@@ -1,5 +1,4 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
+#!/usr/bin/python3
import curses
import locale
--- a/tests/curses_mouse.py Wed Sep 03 08:57:24 2014 +0200
+++ b/tests/curses_mouse.py Wed Sep 03 21:56:20 2014 +0200
@@ -1,5 +1,4 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
+#!/usr/bin/python3
import curses
import locale
@@ -21,8 +20,8 @@
screen.addstr('key: %x %s\n' % (c, char))
if c == curses.KEY_MOUSE:
- m = curses.getmouse()
- screen.addstr('(%d %d %d %d %x)\n' % m)
+ id_, x, y, z, bstate = curses.getmouse()
+ screen.addstr('(%d %d %d %d %s)\n' % (id_, x, y, z, bin(bstate)))
screen.refresh()
--- a/tests/curses_unicode.py Wed Sep 03 08:57:24 2014 +0200
+++ b/tests/curses_unicode.py Wed Sep 03 21:56:20 2014 +0200
@@ -1,5 +1,4 @@
-#!/usr/bin/python
-# coding=UTF-8
+#!/usr/bin/python3
import curses
import locale
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test_event_class.py Wed Sep 03 21:56:20 2014 +0200
@@ -0,0 +1,13 @@
+#!/usr/bin/env python3
+
+import sys
+sys.path.append('..')
+
+from tuikit.core.events import KeypressEvent
+
+x = KeypressEvent('tab', '\t', set(['shift', 'ctrl']))
+print(repr(x))
+print(x.mod_key())
+
+x = KeypressEvent(None, 'c', set(['shift', 'ctrl']))
+print(x.mod_key())
--- a/tuikit/core/application.py Wed Sep 03 08:57:24 2014 +0200
+++ b/tuikit/core/application.py Wed Sep 03 21:56:20 2014 +0200
@@ -2,6 +2,7 @@
from tuikit.core.theme import default_theme
from tuikit.core.timer import Timer
from tuikit.core.buffer import ProxyBuffer
+from tuikit.core.events import ResizeEvent
import logging
@@ -15,7 +16,7 @@
"""
- def __init__(self, driver='curses'):
+ def __init__(self, driver='cursesw'):
self.log = logging.getLogger(__name__)
self.driver = None
self.timer = Timer()
@@ -56,12 +57,12 @@
def main_loop(self):
"""The main loop."""
self._started = True
- self.window_manager.handle_event('resize', *self.driver.size)
+ self.window_manager.handle_event(ResizeEvent(*self.driver.size))
screen = ProxyBuffer(self.driver)
while not self._quit:
self.window_manager.draw(screen)
- self.driver.cursor = self.window_manager.cursor
+ self.driver.cursor = self.window_manager.get_cursor_if_visible()
self.driver.flush()
timeout = self.timer.nearest_timeout()
@@ -69,7 +70,7 @@
self.timer.process_timeouts()
for event in events:
- self.window_manager.handle_event(event[0], *event[1:])
+ self.window_manager.handle_event(event)
self._started = False
self.log.info('=== End ===')
--- a/tuikit/core/container.py Wed Sep 03 08:57:24 2014 +0200
+++ b/tuikit/core/container.py Wed Sep 03 21:56:20 2014 +0200
@@ -14,20 +14,24 @@
def __init__(self, layout_class=FixedLayout):
Widget.__init__(self)
#: List of child widgets.
- self.children = []
- self.focus_child = None
- self.mouse_child = None
+ self._widgets = []
+ #: Widget with keyboard focus
+ self.focus_widget = None
+ #: Widget on last mouse position
+ self.mouse_widget = None
+ #: If True, tab cycles inside container
+ self.trap_focus = False
self.layout = layout_class()
def add(self, widget):
"""Add widget into container."""
- self.children.append(widget)
+ self._widgets.append(widget)
widget.parent = self
widget.window = self.window
widget.set_theme(self.theme)
self.layout.add(widget)
- if self.focus_child is None:
- self.focus_child = widget
+ if self.focus_widget is None and widget.can_focus():
+ self.focus_widget = widget
def resize(self, w, h):
Widget.resize(self, w, h)
@@ -36,7 +40,7 @@
def draw(self, buffer):
"""Draw child widgets."""
Widget.draw(self, buffer)
- for child in self.children:
+ for child in self._widgets:
with buffer.moved_origin(child.x, child.y):
with buffer.clip(buffer.origin.x, buffer.origin.y,
child.width, child.height):
@@ -44,7 +48,7 @@
def set_theme(self, theme):
Widget.set_theme(self, theme)
- for child in self.children:
+ for child in self._widgets:
child.set_theme(theme)
@property
@@ -55,21 +59,37 @@
If this container has child with focus, return its cursor position instead.
"""
- if self.focus_child:
- cursor = self.focus_child.cursor
+ if self.focus_widget:
+ cursor = self.focus_widget.cursor
if not cursor:
return None
- cursor = cursor.moved(*self.focus_child.pos)
+ cursor = cursor.moved(*self.focus_widget.pos)
else:
cursor = self._cursor.immutable()
if cursor in Rect._make((0, 0), self._size):
return cursor
+ @property
+ def cursor_visible(self):
+ if self.focus_widget:
+ return self.focus_widget.cursor_visible
+ else:
+ return self._cursor_visible
+
## input events ##
- def keypress(self, keyname, char, mod=0):
- if self.focus_child:
- self.focus_child.keypress(keyname, char, mod)
+ def keypress_event(self, ev):
+ # First, handle the keypress event to focused child widget
+ if self.focus_widget is not None:
+ if self.focus_widget.keypress_event(ev):
+ return True
+ # Next, handle default key behaviour by Container
+ if ev.keyname == 'tab':
+ return self.focus_next(-1 if 'shift' in ev.mods else 1)
+ # Finally, handle default keys by Widget
+ # and send keypress signal
+ if Widget.keypress_event(self, ev):
+ return True
def mousedown(self, button, pos):
self.mouse_child = None
@@ -87,3 +107,47 @@
self.mouse_child.mousemove(button,
pos - self.mouse_child.pos, relpos)
+ ## focus ##
+
+ def focus_next(self, step=1):
+ """Focus next child.
+
+ Sets focus to next child, if there is one
+ which can be focused. Cycles from last child
+ to first when needed. Return value depends on
+ this cycling:
+
+ * False means there wasn't any child to focus
+ before end of list. Focus was either not changed
+ or first child was focused.
+
+ * True when focus is set to next child in normal
+ way or when self.trap_focus is set.
+
+ Return value is supposed to be returned from keypress
+ event - in that case, True stops event propagation.
+
+ """
+ if self.focus_widget is None:
+ idx_current = 0
+ else:
+ idx_current = self._widgets.index(self.focus_widget)
+ idx_new = idx_current
+ cycled = False
+ while True:
+ idx_new += step
+ if idx_new >= len(self._widgets):
+ idx_new = 0
+ cycled = True
+ if idx_new < 0: # for focus_previous
+ idx_new = len(self._widgets) - 1
+ cycled = True
+ if idx_current == idx_new:
+ return False
+ if self._widgets[idx_new].can_focus():
+ self.focus_widget = self._widgets[idx_new]
+ return self.trap_focus or not cycled
+
+ def focus_previous(self):
+ """Focus previous child."""
+ self.focus_next(-1)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/tuikit/core/events.py Wed Sep 03 21:56:20 2014 +0200
@@ -0,0 +1,53 @@
+from collections import OrderedDict
+
+
+class Event:
+
+ """Base class for events."""
+
+ def __init__(self, event_name=None, arg_names=(), arg_values=()):
+ self.name = event_name
+ self.args = OrderedDict(zip(arg_names, arg_values))
+
+ def __getattr__(self, item):
+ if item in self.args:
+ return self.args[item]
+ else:
+ raise AttributeError(item)
+
+ def __getitem__(self, key):
+ return self.args[key]
+
+ def __repr__(self):
+ return '{}({})'.format(self.__class__.__name__,
+ ', '.join("%s=%r" % (k,v) for k,v
+ in self.args.items()))
+
+
+class ResizeEvent(Event):
+
+ def __init__(self, w, h):
+ Event.__init__(self, 'resize', ('w', 'h'), (w, h))
+
+
+class KeypressEvent(Event):
+
+ def __init__(self, keyname, char, mods):
+ Event.__init__(self, 'keypress',
+ ('keyname', 'char', 'mods'),
+ (keyname, char, mods))
+
+ def mod_key(self, sep='+'):
+ """Return combined key with modifiers.
+
+ E.g. "shift+tab"
+
+ Order of modifiers is fixed: ctrl, alt, meta, shift
+
+ """
+ res = []
+ for mod in ('ctrl', 'alt', 'meta', 'shift'):
+ if mod in self.mods:
+ res.append(mod)
+ res.append(self.keyname or self.char)
+ return sep.join(res)
--- a/tuikit/core/signal.py Wed Sep 03 08:57:24 2014 +0200
+++ b/tuikit/core/signal.py Wed Sep 03 21:56:20 2014 +0200
@@ -15,13 +15,19 @@
"""
- def __init__(self):
+ def __init__(self, allow_stop=False):
self._handlers = []
+ #: Allow one of the handlers to stop processing signal
+ #: The handler should return True value,
+ #: then other handlers will not be called
+ self.allow_stop = allow_stop
def __call__(self, *args, **kwargs):
"""Emit the signal to all connected handlers."""
for handler in self._handlers:
- handler(*args, **kwargs)
+ res = handler(*args, **kwargs)
+ if self.allow_stop and res:
+ return True
def connect(self, handler):
if not handler in self._handlers:
--- a/tuikit/core/theme.py Wed Sep 03 08:57:24 2014 +0200
+++ b/tuikit/core/theme.py Wed Sep 03 21:56:20 2014 +0200
@@ -6,6 +6,7 @@
"""Default color style"""
normal = 'lightgray'
+ active = 'black on cyan'
button = 'black on lightgray'
button_active = 'black on cyan'
--- a/tuikit/core/widget.py Wed Sep 03 08:57:24 2014 +0200
+++ b/tuikit/core/widget.py Wed Sep 03 21:56:20 2014 +0200
@@ -1,5 +1,6 @@
from tuikit.core.coords import Point, Size, Rect
from tuikit.core.theme import default_theme
+from tuikit.core.signal import Signal
import logging
@@ -37,13 +38,17 @@
self.sizemax = Size(None, None)
#: Cursor is position where text input will occur.
- #: It is displayed on screen if widget is active.
#: The cursor coordinates are relative to widget.
- #: Position outside of widget boundaries means no cursor (hidden).
- self._cursor = Point(-1, -1)
+ self._cursor = Point()
+ #: Cursor is displayed on screen only when the widget is focused.
+ self._cursor_visible = False
- #: Logger name contains full module name, class name and instance number
- self._log = logging.getLogger('%s.%s' % (self.__module__, self.name))
+ #: Hidden widget does not affect layout.
+ self.hidden = False
+ #: Allow keyboard focus for this widget.
+ self.allow_focus = False
+
+ self.sig_keypress = Signal(allow_stop=True)
## position and size ##
@@ -78,8 +83,8 @@
def draw(self, buffer):
"""Draw self into buffer."""
- self._log.debug('draw into %r at %s (exposed %s)',
- buffer, buffer.origin, self.exposed(buffer))
+ self.log.debug('Draw into %r at %s (exposed %s)',
+ buffer, buffer.origin, self.exposed(buffer))
def set_theme(self, theme):
self.theme = theme
@@ -98,16 +103,37 @@
@property
def cursor(self):
- """Return cursor coordinates or None if cursor is not set
- or is set outside of widget boundaries."""
+ """Return cursor coordinates.
+
+ Returns None if cursor is set outside of widget boundaries.
+
+ """
if self._cursor in Rect._make((0, 0), self._size):
return self._cursor
- ## input events ##
+ @property
+ def cursor_visible(self):
+ return self._cursor_visible
+
+ ## events ##
+
+ def resize_event(self, ev):
+ self.resize(ev.w, ev.h)
+
+ def keypress_event(self, ev):
+ """Keypress event handler.
- def keypress(self, keyname, char, mod):
- self._log.debug('keypress(keyname=%r, char=%r, mod=%r)',
- keyname, char, mod)
+ Override to accept keyboard input.
+
+ Returns True if event was consumed.
+
+ Call this implementation from inherited classes
+ if it does not consume the event.
+
+ """
+ if self.sig_keypress(ev):
+ return True
+ self.log.debug('Not consumed: %s', ev)
def mousedown(self, button, pos):
self._log.debug('mousedown(btn=%r, pos=%r)',
@@ -134,3 +160,25 @@
"""
self.parent.remove_timeout(self, callback, *args)
+
+ ## focus ##
+
+ def can_focus(self):
+ return not self.hidden and self.allow_focus
+
+ def has_focus(self):
+ if self.parent is None:
+ return True
+ return (self.parent.has_focus()
+ and self.parent.focus_widget == self)
+
+ ## utilities ##
+
+ @property
+ def log(self):
+ """Logger for widget debugging.
+
+ Logger name contains full module name, class name and instance number.
+
+ """
+ return logging.getLogger('%s.%s' % (self.__module__, self.name))
--- a/tuikit/core/window.py Wed Sep 03 08:57:24 2014 +0200
+++ b/tuikit/core/window.py Wed Sep 03 21:56:20 2014 +0200
@@ -17,6 +17,8 @@
"""New buffer for the window will be created unless given existing
`buffer` as parameter."""
Container.__init__(self)
+ self.allow_focus = True
+ self.trap_focus = True
self._buffer = None
self.buffer = buffer or Buffer()
@@ -57,15 +59,35 @@
Container.__init__(self)
self.timer = timer
+ def draw(self, buffer):
+ Container.draw(self, buffer)
+ self.log.debug('%s has focus.', self.get_focused_widget().name)
+
def resize(self, w, h):
Container.resize(self, w, h)
- self.children[0].resize(w, h)
+ self._widgets[0].resize(w, h)
- def handle_event(self, event_name, *args):
+ def keypress_event(self, ev):
+ self.log.debug('%s', ev)
+ return Container.keypress_event(self, ev)
+
+ def handle_event(self, event):
"""Handle input event to managed windows."""
- self._log.debug('Handle event: %s %r', event_name, args)
- handler = getattr(self, event_name, None)
+ self.log.debug('Handle event: %r', event)
+ handler = getattr(self, event.name + '_event', None)
if handler:
- handler(*args)
+ handler(event)
else:
- raise Exception('Unknown event: %r %r' % (event_name, args))
+ raise Exception('Unknown event: %r' % event)
+
+ def get_focused_widget(self):
+ """Traverse the widget hierarchy to bottom
+ and return actually focused Widget."""
+ node = self
+ while isinstance(node, Container) and node.focus_widget:
+ node = node.focus_widget
+ return node
+
+ def get_cursor_if_visible(self):
+ if self.cursor_visible:
+ return self.cursor
--- a/tuikit/driver/curses.py Wed Sep 03 08:57:24 2014 +0200
+++ b/tuikit/driver/curses.py Wed Sep 03 21:56:20 2014 +0200
@@ -44,6 +44,7 @@
(0x44, 1, 'left' ),
(0x46, 1, 'end' ), # xterm
(0x48, 1, 'home' ), # xterm
+ (0x5a, 1, 'shift+tab' ), # xterm
(0x5b, 0x41, 1, 'f1' ), # linux
(0x5b, 0x42, 1, 'f2' ), # linux
(0x5b, 0x43, 1, 'f3' ), # linux
@@ -80,7 +81,7 @@
def __init__(self):
Driver.__init__(self)
- self._log = logging.getLogger('tuikit')
+ self._log = logging.getLogger(__name__)
self.stdscr = None
self.cursor = None
self.colors = {} # maps names to curses attributes
@@ -379,7 +380,7 @@
keyname = match[-1]
if match is None:
- self.log.debug('Unknown control sequence: %s',
+ self._log.debug('Unknown control sequence: %s',
','.join(['0x%x' % x for x in consumed]))
return [('keypress', 'Unknown', None, set())]
@@ -461,7 +462,7 @@
if len(codes) == 0:
# no match -> unknown code
seq = ','.join(['0x%x' % x for x in debug_seq])
- self.log.debug('Unknown control sequence: %s', seq)
+ self._log.debug('Unknown control sequence: %s', seq)
return [('keypress', 'Unknown:' + seq, None, set())]
elif len(codes) == 1:
# one match -> we got the winner
@@ -484,7 +485,7 @@
if len(matching_codes) == 0:
# no match -> unknown code
seq = ','.join(['0x%x' % x for x in debug_seq])
- self.log.debug('Unknown control sequence: %s', seq)
+ self._log.debug('Unknown control sequence: %s', seq)
return [('keypress', 'Unknown:' + seq, None, set())]
if len(matching_codes) > 1:
@@ -503,6 +504,14 @@
if mod_bits & 1<<bit:
mod_set.add(name)
+ # parse keynames in form "shift+tab"
+ if '+' in keyname:
+ parts = keyname.split('+')
+ for mod in parts[:-1]:
+ assert(mod in ('shift', 'alt', 'ctrl', 'meta'))
+ mod_set.add(mod)
+ keyname = parts[-1]
+
return [('keypress', keyname, None, mod_set)]
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/tuikit/driver/cursesw.py Wed Sep 03 21:56:20 2014 +0200
@@ -0,0 +1,258 @@
+import curses.ascii
+import math
+import logging
+
+from tuikit.driver.driver import Driver
+from tuikit.core.events import ResizeEvent, KeypressEvent
+from tuikit.core.coords import Point
+
+
+class CursesWDriver(Driver):
+
+ key_names = {
+ '\t': 'tab',
+ '\n': 'enter',
+ '\x1b': 'escape',
+ }
+
+ key_map = {
+ curses.KEY_UP: 'up',
+ curses.KEY_DOWN: 'down',
+ curses.KEY_LEFT: 'left',
+ curses.KEY_RIGHT: 'right',
+ curses.KEY_IC: 'insert',
+ curses.KEY_DC: 'delete',
+ curses.KEY_HOME: 'home',
+ curses.KEY_END: 'end',
+ curses.KEY_PPAGE: 'pageup',
+ curses.KEY_NPAGE: 'pagedown',
+ curses.KEY_BACKSPACE: 'backspace',
+ curses.KEY_BTAB: 'shift+tab',
+ }
+ for _i in range(1, 13):
+ key_map[curses.KEY_F0 + _i] = 'f' + str(_i)
+
+ color_map = {
+ 'default': (-1, 0),
+ 'black': (curses.COLOR_BLACK, 0),
+ 'blue': (curses.COLOR_BLUE, 0),
+ 'green': (curses.COLOR_GREEN, 0),
+ 'cyan': (curses.COLOR_CYAN, 0),
+ 'red': (curses.COLOR_RED, 0),
+ 'magenta': (curses.COLOR_MAGENTA, 0),
+ 'brown': (curses.COLOR_YELLOW, 0),
+ 'lightgray': (curses.COLOR_WHITE, 0),
+ 'gray': (curses.COLOR_BLACK, curses.A_BOLD),
+ 'lightblue': (curses.COLOR_BLUE, curses.A_BOLD),
+ 'lightgreen': (curses.COLOR_GREEN, curses.A_BOLD),
+ 'lightcyan': (curses.COLOR_CYAN, curses.A_BOLD),
+ 'lightred': (curses.COLOR_RED, curses.A_BOLD),
+ 'lightmagenta': (curses.COLOR_MAGENTA, curses.A_BOLD),
+ 'yellow': (curses.COLOR_YELLOW, curses.A_BOLD),
+ 'white': (curses.COLOR_WHITE, curses.A_BOLD),
+ }
+
+ attr_map = {
+ 'bold': curses.A_BOLD,
+ 'underline': curses.A_UNDERLINE,
+ 'standout': curses.A_STANDOUT, # inverse bg/fg
+ 'blink': curses.A_BLINK,
+ }
+
+ def __init__(self):
+ Driver.__init__(self)
+ self._log = logging.getLogger(__name__)
+ self.stdscr = None
+ self.cursor = None
+ self.colors = {} # maps names to curses attributes
+ self.colorpairs = {} # maps tuple (fg,bg) to curses color_pair
+ self._mouse_last_pos = None # Point
+ self._mouse_last_bstate = None
+
+ ## initialization, finalization ##
+
+ def init(self):
+ """Initialize curses"""
+ self.stdscr = curses.initscr()
+ curses.start_color()
+ curses.use_default_colors()
+ curses.noecho()
+ curses.cbreak()
+ self.stdscr.keypad(1)
+ self.stdscr.immedok(0)
+
+ self.size.h, self.size.w = self.stdscr.getmaxyx()
+
+ 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 close(self):
+ self.stdscr.keypad(0)
+ curses.echo()
+ curses.nocbreak()
+ curses.endwin()
+
+ ## drawing ##
+
+ def clear(self):
+ self.stdscr.erase()
+
+ def putch(self, ch, x, y):
+ try:
+ if isinstance(ch, int):
+ self.stdscr.addch(y, x, ch)
+ elif isinstance(ch, str) and len(ch) == 1:
+ self.stdscr.addstr(y, x, ch)
+ else:
+ raise TypeError('Integer or one-char string is required.')
+ except curses.error as e:
+ self._log.exception('putch(%r, %s, %s) error:' % (ch, x, y))
+
+ def draw(self, buffer, x=0, y=0):
+ for bufy in range(buffer.size.h):
+ for bufx in range(buffer.size.w):
+ char, attr_desc = buffer.get(bufx, bufy)
+ self.setattr(attr_desc)
+ self.putch(char, x + bufx, y + bufy)
+
+ def flush(self):
+ if self.cursor:
+ self.stdscr.move(self.cursor.y, self.cursor.x)
+ curses.curs_set(True)
+ else:
+ curses.curs_set(False)
+ self.stdscr.refresh()
+
+ ## colors, attributes ##
+
+ def setattr(self, attr_desc):
+ """Set attribute to be used for subsequent draw operations."""
+ attr = self.colors.get(attr_desc, None)
+ if attr is None:
+ # first encountered `attr_desc`, initialize
+ fg, bg, attrs = self._parse_attr_desc(attr_desc)
+ fgcol, fgattr = self.color_map[fg]
+ bgcol, _bgattr = self.color_map[bg]
+ colpair = self._getcolorpair(fgcol, bgcol)
+ attr = curses.color_pair(colpair) | self._parseattrs(attrs) | fgattr
+ self.colors[attr_desc] = attr
+ self.stdscr.attrset(attr)
+
+ def _getcolorpair(self, fgcol, bgcol):
+ pair = (fgcol, bgcol)
+ if pair in self.colorpairs:
+ return self.colorpairs[pair]
+ num = len(self.colorpairs) + 1
+ curses.init_pair(num, fgcol, bgcol)
+ self.colorpairs[pair] = num
+ return num
+
+ def _parseattrs(self, attrs):
+ res = 0
+ for a in attrs:
+ res = res | self.attr_map[a]
+ return res
+
+ ## input, events ##
+
+ def getevents(self, timeout=None):
+ """Process input, return list of events.
+
+ timeout -- float, in seconds (None=infinite)
+
+ Returns:
+ [('event', param1, ...), ...]
+
+ """
+ # Set timeout
+ if timeout is None:
+ # wait indefinitely
+ curses.cbreak()
+ elif timeout > 0:
+ # wait
+ timeout_tenths = math.ceil(timeout * 10)
+ curses.halfdelay(timeout_tenths)
+ else:
+ # timeout = 0 -> no wait
+ self.stdscr.nodelay(1)
+
+ # Get key or char
+ c = self.stdscr.get_wch()
+
+ res = []
+
+ if c == -1:
+ # Timeout
+ return res
+ elif c == curses.KEY_MOUSE:
+ res += self._process_mouse()
+ elif c == curses.KEY_RESIZE:
+ self.size.h, self.size.w = self.stdscr.getmaxyx()
+ res.append(ResizeEvent(self.size.w, self.size.h))
+ elif isinstance(c, int):
+ keyname, mods = self._split_keyname_mods(self.key_map[c])
+ res.append(KeypressEvent(keyname, None, mods))
+ else:
+ keyname = self.key_names.get(c)
+ res.append(KeypressEvent(keyname, c, set()))
+
+ return res
+
+ def _process_mouse(self):
+ out = []
+ try:
+ _id, x, y, _z, bstate = curses.getmouse()
+ except curses.error:
+ return out
+
+ pos = Point(x, y)
+ if bstate & curses.REPORT_MOUSE_POSITION:
+ if self._mouse_last_pos != pos:
+ if self._mouse_last_pos:
+ relpos = pos - self._mouse_last_pos
+ out += [('mousemove', 0, pos, relpos)]
+ self._mouse_last_pos = pos
+
+ # we are interested only in changes, not buttons already pressed before event
+ if self._mouse_last_bstate is not None:
+ old = self._mouse_last_bstate
+ new = bstate
+ bstate = ~old & new
+ self._mouse_last_bstate = new
+ else:
+ self._mouse_last_bstate = bstate
+
+ if bstate & curses.BUTTON1_PRESSED:
+ out += [('mousedown', 1, pos)]
+ if bstate & curses.BUTTON2_PRESSED:
+ out += [('mousedown', 2, pos)]
+ if bstate & curses.BUTTON3_PRESSED:
+ out += [('mousedown', 3, pos)]
+ if bstate & curses.BUTTON1_RELEASED:
+ out += [('mouseup', 1, pos)]
+ if bstate & curses.BUTTON2_RELEASED:
+ out += [('mouseup', 2, pos)]
+ if bstate & curses.BUTTON3_RELEASED:
+ out += [('mouseup', 3, pos)]
+
+ # reset last pos when pressed/released
+ if len(out) > 0 and out[-1][0] in ('mousedown', 'mouseup'):
+ self._mouse_last_pos = None
+
+ return out
+
+ def _split_keyname_mods(self, keyname):
+ """Parse keynames in form "shift+tab", return (keyname, mod)."""
+ mod_set = set()
+ if '+' in keyname:
+ parts = keyname.split('+')
+ for mod in parts[:-1]:
+ assert(mod in ('shift', 'alt', 'ctrl', 'meta'))
+ mod_set.add(mod)
+ keyname = parts[-1]
+
+ return keyname, mod_set
+
+
+driver_class = CursesWDriver
--- a/tuikit/widgets/button.py Wed Sep 03 08:57:24 2014 +0200
+++ b/tuikit/widgets/button.py Wed Sep 03 21:56:20 2014 +0200
@@ -9,6 +9,7 @@
def __init__(self, label='btn'):
"""Create button with given label, size according to label."""
Widget.__init__(self)
+ self.allow_focus = True
#: Button label.
self._label = ''
@@ -21,10 +22,8 @@
#: Padding between prefix/suffix and label
self.padding = 1
- self.allow_focus = True
-
self.color = 'default'
- self.color_highlighted = 'default on red'
+ self.color_active = 'default on red'
self.highlight = False
self.sig_clicked = Signal()
@@ -45,11 +44,11 @@
def set_theme(self, theme):
Widget.set_theme(self, theme)
self.color = theme.button
- self.color_highlighted = theme.button_active
+ self.color_active = theme.button_active
def _get_color(self):
- if self.highlight: # or self.has_focus():
- return self.color_highlighted
+ if self.has_focus():
+ return self.color_active
return self.color
def draw(self, buffer):
--- a/tuikit/widgets/textbox.py Wed Sep 03 08:57:24 2014 +0200
+++ b/tuikit/widgets/textbox.py Wed Sep 03 21:56:20 2014 +0200
@@ -4,25 +4,20 @@
class TextBox(Widget):
- """Multiline text view/edit widget.
-
- Cursor is used for text cursor position.
-
- """
+ """Multiline text view/edit widget."""
def __init__(self, text=''):
Widget.__init__(self)
+ self.allow_focus = True
# Text content, splitted as lines
self._lines = []
self.text = text
- self.allow_focus = True
-
- # This variable rememberes horizontal position of cursor
+ self._cursor_visible = True
+ # This variable remembers horizontal cursor position
# for the case when cursor moves to shorter line.
self.cursor_column = 0
- self._cursor.update(0, 0)
# selection - line and column of selection start
self.sel_line = 0
self.sel_column = 0
@@ -47,41 +42,47 @@
def cur_line(self, value):
self._lines[self._cursor.y] = value
- def set_theme(self, theme):
- Widget.set_theme(self, theme)
- self.color = theme.normal
-
def draw(self, buffer):
exposed = self.exposed(buffer)
- with buffer.attr(self.color):
+ with buffer.attr(self.theme.normal):
buffer.fill()
end_y = min(len(self._lines), exposed.y + exposed.h)
for j in range(exposed.y, end_y):
line = self._lines[j]
buffer.puts(line, 0, j)
- def keypress(self, keyname, char, mod=0):
- 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': self.backspace()
- if keyname == 'delete': self.del_char()
- if keyname == 'enter': self.add_newline(move=True)
- if 'ctrl' in mod:
- if keyname == 'home': self.move_top()
- if keyname == 'end': self.move_bottom()
+ def keypress_event(self, ev):
+ if ev.keyname and not ev.mods:
+ consumed = True
+ if ev.keyname == 'left': self.move_left()
+ elif ev.keyname == 'right': self.move_right()
+ elif ev.keyname == 'home': self.move_home()
+ elif ev.keyname == 'end': self.move_end()
+ elif ev.keyname == 'up': self.move_up()
+ elif ev.keyname == 'down': self.move_down()
+ elif ev.keyname == 'pageup': self.move_pageup()
+ elif ev.keyname == 'pagedown': self.move_pagedown()
+ elif ev.keyname == 'backspace': self.backspace()
+ elif ev.keyname == 'delete': self.del_char()
+ elif ev.keyname == 'enter': self.add_newline(move=True)
+ else:
+ consumed = False
+ if consumed:
+ return True
+ if ev.mods:
+ consumed = True
+ mk = ev.mod_key()
+ if mk == 'ctrl+home': self.move_top()
+ elif mk == 'ctrl+end': self.move_bottom()
+ else:
+ consumed = False
+ if consumed:
+ return True
- if char:
- self.add_char(char)
+ if ev.char and not ev.keyname:
+ self.add_char(ev.char)
self.move_right()
-
- #self.redraw()
+ return True
def on_mousedown(self, ev):
y = ev.wy
@@ -169,7 +170,7 @@
sx = self._cursor.x
self.cur_line = ln[sx:]
self._lines.insert(self._cursor.y, ln[:sx])
- self._default_size.update(h=len(self._lines))
+ self.sizereq.update(h=len(self._lines))
if move:
self.move_right()
@@ -181,8 +182,8 @@
self.cursor_column = 0
self._cursor.x = 0
self._cursor.y += 1
- w = max(self._default_size.w, len(ln[:sx] + text))
- self._default_size.update(w=w, h=len(self._lines))
+ w = max(self.sizereq.w, len(ln[:sx] + text))
+ self.sizereq.update(w=w, h=len(self._lines))
def backspace(self):
if self._cursor.y > 0 or self._cursor.x > 0:
@@ -196,7 +197,7 @@
if self._cursor.y + 1 < len(self._lines):
self.cur_line += self._lines[self._cursor.y+1]
del self._lines[self._cursor.y+1]
- self._default_size.update(h=len(self._lines))
+ self.sizereq.update(h=len(self._lines))
else:
self.cur_line = ln[:sx] + ln[sx+1:]
--- a/tuikit/widgets/textfield.py Wed Sep 03 08:57:24 2014 +0200
+++ b/tuikit/widgets/textfield.py Wed Sep 03 21:56:20 2014 +0200
@@ -7,10 +7,9 @@
def __init__(self, value=''):
Widget.__init__(self)
+ self.allow_focus = True
self.sizereq.update(10, 1)
- self.allow_focus = True
-
self.code = locale.getpreferredencoding()
if not isinstance(value, str):
value = str(value, self.code)
@@ -22,6 +21,7 @@
self.curspos = 0 # position of cursor in value
self.ofs = 0 # position of value beginning on screen
+ self._cursor_visible = True
self.move_end()
def resize(self, w, h):
@@ -29,11 +29,9 @@
if self.curspos >= self.tw:
self.ofs = self.curspos - self.tw
- def set_theme(self, theme):
- self.color = theme.normal
-
def draw(self, buffer):
- with buffer.attr(self.color):
+ color = self.theme.active if self.has_focus() else self.theme.normal
+ with buffer.attr(color):
# 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
@@ -52,8 +50,7 @@
self._cursor.update(1 + self.curspos - self.ofs, 0)
- def keypress(self, keyname, char, mod=0):
- Widget.keypress(self, keyname, char, mod)
+ def keypress_event(self, ev):
map_keyname_to_func = {
'left': self.move_left,
'right': self.move_right,
@@ -62,17 +59,18 @@
'backspace': self.move_left_and_del,
'delete': self.del_char,
}
- accepted = False
- if keyname in map_keyname_to_func:
- map_keyname_to_func[keyname]()
- accepted = True
- if char:
- self.add_char(char)
+ consumed = False
+ if ev.keyname in map_keyname_to_func:
+ map_keyname_to_func[ev.keyname]()
+ consumed = True
+ if not ev.keyname and ev.char:
+ self.add_char(ev.char)
self.move_right()
- accepted = True
- #if accepted:
+ consumed = True
+ if consumed:
#self.redraw()
- return accepted
+ return True
+ Widget.keypress_event(self, ev)
def move_left(self):
if self.curspos - self.ofs > 1 or (self.ofs == 0 and self.curspos == 1):