Add TextField widget, keypress event, cursor.
--- a/demos/03_application.py Fri Mar 28 14:58:20 2014 +0100
+++ b/demos/03_application.py Fri Mar 28 19:58:59 2014 +0100
@@ -5,6 +5,7 @@
from tuikit.core.application import Application
from tuikit.widgets.label import Label
from tuikit.widgets.button import Button
+from tuikit.widgets.textfield import TextField
label = Label('Hello there!')
label.pos.update(20, 10)
@@ -12,7 +13,12 @@
button = Button()
button.pos.update(20, 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(field)
+app.root_window.focus_child = field
app.start()
--- a/tuikit/core/application.py Fri Mar 28 14:58:20 2014 +0100
+++ b/tuikit/core/application.py Fri Mar 28 19:58:59 2014 +0100
@@ -61,6 +61,7 @@
screen = ProxyBuffer(self.driver)
while not self._quit:
self.window_manager.draw(screen)
+ self.driver.cursor = self.window_manager.cursor
self.driver.flush()
timeout = self.timer.nearest_timeout()
--- a/tuikit/core/container.py Fri Mar 28 14:58:20 2014 +0100
+++ b/tuikit/core/container.py Fri Mar 28 19:58:59 2014 +0100
@@ -1,4 +1,5 @@
from tuikit.core.widget import Widget
+from tuikit.core.coords import Point
from tuikit.layouts.fixed import FixedLayout
@@ -14,6 +15,7 @@
Widget.__init__(self)
#: List of child widgets.
self.children = []
+ self.focus_child = None
self.layout = layout_class()
def add(self, widget):
@@ -23,6 +25,8 @@
widget.window = self.window
widget.set_theme(self.theme)
self.layout.add(widget)
+ if self.focus_child is None:
+ self.focus_child = widget
def resize(self, w, h):
Widget.resize(self, w, h)
@@ -41,3 +45,19 @@
Widget.set_theme(self, theme)
for child in self.children:
child.set_theme(theme)
+
+ @property
+ def cursor(self):
+ if self.focus_child:
+ cursor = self.focus_child.cursor
+ if cursor is not None:
+ return cursor.moved(*self.focus_child.pos)
+ else:
+ if self._cursor is not None:
+ return Point(self._cursor)
+
+ ## input events ##
+
+ def keypress(self, keyname, char, mod=0):
+ if self.focus_child:
+ self.focus_child.keypress(keyname, char, mod)
--- a/tuikit/core/coords.py Fri Mar 28 14:58:20 2014 +0100
+++ b/tuikit/core/coords.py Fri Mar 28 19:58:59 2014 +0100
@@ -6,9 +6,10 @@
"""
- def __init__(self, x=0, y=0):
- self.x = x
- self.y = y
+ def __init__(self, *args, **kwargs):
+ self.x = 0
+ self.y = 0
+ self.update(*args, **kwargs)
def __getitem__(self, key):
return (self.x, self.y)[key]
--- a/tuikit/core/widget.py Fri Mar 28 14:58:20 2014 +0100
+++ b/tuikit/core/widget.py Fri Mar 28 19:58:59 2014 +0100
@@ -39,6 +39,11 @@
#: None means no maximum size (infinite).
self.sizemax = Size(None, None)
+ #: Cursor is position where text input will occur.
+ #: It is displayed on screen if widget is active.
+ #: None means no cursor (hidden).
+ self._cursor = None
+
## position and size ##
@property
@@ -86,6 +91,17 @@
"""
return buffer.clip_rect.moved(-buffer.origin.x, -buffer.origin.y)
+ @property
+ def cursor(self):
+ if self._cursor is not None:
+ return Point(self._cursor)
+
+ ## input events ##
+
+ def keypress(self, keyname, char, mod):
+ self._log.debug('%s keypress(%r, %r, %r)',
+ self.name, keyname, char, mod)
+
## timeouts ##
def add_timeout(self, delay, callback, *args):
--- a/tuikit/core/window.py Fri Mar 28 14:58:20 2014 +0100
+++ b/tuikit/core/window.py Fri Mar 28 19:58:59 2014 +0100
@@ -9,6 +9,8 @@
Widgets are drawn into window, events are routed to widgets through window.
+ Parent of Window is always WindowManager.
+
"""
def __init__(self, buffer=None):
@@ -45,6 +47,7 @@
def draw(self, buffer):
"""Draw this window into `buffer`."""
+ self.redraw()
buffer.draw(self.buffer)
@@ -57,8 +60,6 @@
def resize(self, w, h):
self.children[0].resize(w, h)
-# def keypress(self, keyname, char, mod=0):
-
def handle_event(self, event_name, *args):
"""Handle input event to managed windows."""
handler = getattr(self, event_name, None)
--- a/tuikit/driver/curses.py Fri Mar 28 14:58:20 2014 +0100
+++ b/tuikit/driver/curses.py Fri Mar 28 19:58:59 2014 +0100
@@ -139,7 +139,7 @@
def flush(self):
if self.cursor:
- self.stdscr.move(*self.cursor)
+ self.stdscr.move(self.cursor.y, self.cursor.x)
curses.curs_set(True)
else:
curses.curs_set(False)
@@ -175,18 +175,6 @@
res = res | self.attr_map[a]
return res
- ## cursor ##
-
- def showcursor(self, x, y):
- if self.clipstack.test(x, y):
- self.cursor = (y, x)
- else:
- self.cursor = None
-
- def hidecursor(self):
- curses.curs_set(False)
- self.cursor = None
-
## input, events ##
def getevents(self, timeout=None):
@@ -224,7 +212,7 @@
res += self._process_utf8_chars()
elif curses.ascii.isprint(c):
- res += [('keypress', None, str(chr(c)))]
+ res += [('keypress', None, str(chr(c)), set())]
else:
self._inputqueue_unget(c)
@@ -337,7 +325,7 @@
utf.append(c)
try:
uni = str(bytes(utf), 'utf-8')
- return [('keypress', None, uni)]
+ return [('keypress', None, uni, set())]
except UnicodeDecodeError:
continue
raise Exception('Invalid UTF-8 sequence: %r' % utf)
@@ -392,7 +380,7 @@
if match is None:
self.log.debug('Unknown control sequence: %s',
','.join(['0x%x' % x for x in consumed]))
- return [('keypress', 'Unknown', None)]
+ return [('keypress', 'Unknown', None, set())]
if keyname == 'mouse':
return self._process_xterm_mouse()
@@ -400,7 +388,7 @@
if keyname == 'CSI':
return self._process_control_sequence()
- return [('keypress', keyname, None)]
+ return [('keypress', keyname, None, set())]
def _process_xterm_mouse(self):
t = self._inputqueue_get_wait()
@@ -473,7 +461,7 @@
# no match -> unknown code
seq = ','.join(['0x%x' % x for x in debug_seq])
self.log.debug('Unknown control sequence: %s', seq)
- return [('keypress', 'Unknown:' + seq, None)]
+ return [('keypress', 'Unknown:' + seq, None, set())]
elif len(codes) == 1:
# one match -> we got the winner
break
@@ -496,7 +484,7 @@
# no match -> unknown code
seq = ','.join(['0x%x' % x for x in debug_seq])
self.log.debug('Unknown control sequence: %s', seq)
- return [('keypress', 'Unknown:' + seq, None)]
+ return [('keypress', 'Unknown:' + seq, None, set())]
if len(matching_codes) > 1:
raise Exception('Internal error: invalid csi_codes, more than one matching')
@@ -504,11 +492,17 @@
keyname = matching_codes[0][1]
# modifiers
- mod = 0
+ mod_bits = 0
if len(params) > 1:
- mod = params[1] - 1
+ mod_bits = params[1] - 1
- return [('keypress', keyname, None, mod)]
+ # convert modifiers from bit-map to set
+ mod_set = set()
+ for bit, name in enumerate(('shift', 'alt', 'ctrl', 'meta')):
+ if mod_bits & 1<<bit:
+ mod_set.add(name)
+
+ return [('keypress', keyname, None, mod_set)]
driver_class = CursesDriver
--- a/tuikit/widgets/button.py Fri Mar 28 14:58:20 2014 +0100
+++ b/tuikit/widgets/button.py Fri Mar 28 19:58:59 2014 +0100
@@ -43,6 +43,7 @@
self.sizereq.update(w, 1)
def set_theme(self, theme):
+ Widget.set_theme(self, theme)
self.color = theme.button
self.color_highlighted = theme.button_active
--- a/tuikit/widgets/textbox.py Fri Mar 28 14:58:20 2014 +0100
+++ b/tuikit/widgets/textbox.py Fri Mar 28 19:58:59 2014 +0100
@@ -61,28 +61,28 @@
buffer.puts(line, 0, j)
#self.cursor = (self._spot.x, self._spot.y)
- def on_keypress(self, ev):
- if ev.keyname:
- if ev.keyname == 'left': self.move_left()
- if ev.keyname == 'right': self.move_right()
- if ev.keyname == 'home': self.move_home()
- if ev.keyname == 'end': self.move_end()
- if ev.keyname == 'up': self.move_up()
- if ev.keyname == 'down': self.move_down()
- if ev.keyname == 'pageup': self.move_pageup()
- if ev.keyname == 'pagedown': self.move_pagedown()
- if ev.keyname == 'backspace': self.backspace()
- if ev.keyname == 'delete': self.del_char()
- if ev.keyname == 'enter': self.add_newline(move=True)
- if ev.mod == ev.MOD_CTRL:
- if ev.keyname == 'home': self.move_top()
- if ev.keyname == 'end': self.move_bottom()
+ 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 mod == MOD_CTRL:
+ if keyname == 'home': self.move_top()
+ if keyname == 'end': self.move_bottom()
- if ev.char:
- self.add_char(ev.char)
+ if char:
+ self.add_char(char)
self.move_right()
- self.redraw()
+ #self.redraw()
def on_mousedown(self, ev):
y = ev.wy
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/tuikit/widgets/textfield.py Fri Mar 28 19:58:59 2014 +0100
@@ -0,0 +1,102 @@
+import locale
+
+from tuikit.core.widget import Widget
+
+
+class TextField(Widget):
+
+ def __init__(self, value=''):
+ Widget.__init__(self)
+ self.sizereq.update(10, 1)
+
+ self.allow_focus = True
+
+ self.code = locale.getpreferredencoding()
+ if not isinstance(value, str):
+ value = str(value, self.code)
+
+ self.value = value
+ self.maxlen = None # unlimited
+
+ self.tw = 0 # real width of text field (minus space for arrows)
+ self.curspos = len(value) # position of cursor in value
+ self.ofs = 0 # position of value beginning on screen
+
+ def resize(self, w, h):
+ self.tw = self.width - 2
+
+ def set_theme(self, theme):
+ self.color = theme.normal
+
+ def draw(self, buffer):
+ with buffer.attr(self.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
+ buffer.puts(val.encode(self.code), 1)
+
+ # draw arrows if content overflows
+ c = ' '
+ if self.ofs > 0:
+ c = '<'
+ buffer.putch(c)
+
+ c = ' '
+ if len(self.value[self.ofs:]) > self.tw:
+ c = '>'
+ buffer.putch(c, self.width-1, 0)
+
+ self._cursor = (1 + self.curspos - self.ofs, 0)
+
+ def keypress(self, keyname, char, mod=0):
+ Widget.keypress(self, keyname, char, mod)
+ accepted = True
+ if keyname == 'left':
+ self.move_left()
+ elif keyname == 'right':
+ self.move_right()
+ elif keyname == 'backspace':
+ if self.curspos > 0:
+ self.move_left()
+ self.del_char()
+ elif keyname == 'delete':
+ self.del_char()
+ else:
+ accepted = False
+
+ if char:
+ self.add_char(char)
+ self.move_right()
+ accepted = True
+
+ #if accepted:
+ #self.redraw()
+ return accepted
+
+ def move_left(self):
+ if self.curspos - self.ofs > 1 or (self.ofs == 0 and self.curspos == 1):
+ # move cursor
+ self.curspos -= 1
+ else:
+ # move content in field
+ if self.ofs > 0:
+ self.curspos -= 1
+ self.ofs -= 1
+
+ def move_right(self):
+ if self.curspos < len(self.value):
+ if self.curspos - self.ofs < self.tw - 2 \
+ or (self.curspos - self.ofs == self.tw - 2 and self.curspos == len(self.value)-1):
+ # move cursor
+ self.curspos += 1
+ else:
+ # move content in field
+ self.curspos += 1
+ self.ofs += 1
+
+ def add_char(self, c):
+ self.value = self.value[:self.curspos] + c + self.value[self.curspos:]
+
+ def del_char(self):
+ self.value = self.value[:self.curspos] + self.value[self.curspos+1:]
+