Add TextField widget, keypress event, cursor.
authorRadek Brich <radek.brich@devl.cz>
Fri, 28 Mar 2014 19:58:59 +0100
changeset 97 0c2e0c09ba5c
parent 96 68c562e0eb1f
child 100 3b2df86d8f94
child 109 105b1affc3c2
Add TextField widget, keypress event, cursor.
demos/03_application.py
tuikit/core/application.py
tuikit/core/container.py
tuikit/core/coords.py
tuikit/core/widget.py
tuikit/core/window.py
tuikit/driver/curses.py
tuikit/widgets/button.py
tuikit/widgets/textbox.py
tuikit/widgets/textfield.py
--- 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:]
+