Add ScrollWindow. Rewrite EditBox to work with OffsetLayout. Add propery "exposed" to DrawEvent. Add Widget._view_size. Add config file (driver, log_level).
authorRadek Brich <radek.brich@devl.cz>
Fri, 01 Feb 2013 00:24:02 +0100
changeset 74 23767a33a781
parent 73 85a282b5e4fc
child 75 2430c643838a
Add ScrollWindow. Rewrite EditBox to work with OffsetLayout. Add propery "exposed" to DrawEvent. Add Widget._view_size. Add config file (driver, log_level).
demo_window.py
tuikit.conf
tuikit/application.py
tuikit/common.py
tuikit/editbox.py
tuikit/events.py
tuikit/layout.py
tuikit/scrollwindow.py
tuikit/textedit.py
tuikit/treeview.py
tuikit/widget.py
--- a/demo_window.py	Wed Jan 30 20:21:08 2013 +0100
+++ b/demo_window.py	Fri Feb 01 00:24:02 2013 +0100
@@ -7,9 +7,11 @@
 
 from tuikit.application import Application
 from tuikit.window import Window
+from tuikit.scrollwindow import ScrollWindow
 from tuikit.button import Button
-from tuikit import AnchorLayout
+from tuikit.layout import AnchorLayout
 from tuikit.common import Borders
+from tuikit.editbox import EditBox
 
 
 class MyApplication(Application):
@@ -23,8 +25,8 @@
         #self.top.add(edit)
 
         win = Window()
-        win.title = 'demo_window'
-        win.resize(80, 25)
+        win.title = 'demo window'
+        win.resize(40, 25)
         self.top.add(win, halign='left', valign='top')
 
         button = Button('click!')
@@ -37,6 +39,16 @@
         subwin.name = 'subwin'
         win.add(subwin)
 
+        swin = ScrollWindow()
+        swin.title = 'scroll window'
+        swin.resize(40, 25)
+        self.top.add(swin)
+
+        swin.move(x=40)
+
+        text = open('tuikit/widget.py').read()
+        editbox = EditBox(text)
+        swin.add(editbox)
 
     def on_button_click(self, ev):
         self.button.label = 'YES'
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tuikit.conf	Fri Feb 01 00:24:02 2013 +0100
@@ -0,0 +1,5 @@
+# rendering driver: curses | sdl (default: curses)
+#driver='sdl'
+
+# log level: info | debug (default: info)
+#log_level='debug'
--- a/tuikit/application.py	Wed Jan 30 20:21:08 2013 +0100
+++ b/tuikit/application.py	Fri Feb 01 00:24:02 2013 +0100
@@ -57,6 +57,11 @@
 
     def __init__(self, top_layout=AnchorLayout, driver='curses'):
         '''Create application.'''
+        self.cfg = {
+            'driver': driver,
+            'log_level': 'info',
+            }
+        self._load_conf('tuikit.conf')
         self._setup_logging()
 
         # Top widget
@@ -67,11 +72,19 @@
         self.quit = False
 
         #: Driver class instance (render + input), e.g. DriverCurses.
-        self.driver = self.get_driver_instance(driver)
+        self.driver = self.get_driver_instance(self.cfg['driver'])
+
+    def _load_conf(self, file_name):
+        try:
+            with open(file_name, 'r') as f:
+                content = f.read()
+            exec(content, self.cfg)
+        except IOError:
+            pass
 
     def _setup_logging(self):
         self.log = logging.getLogger('tuikit')
-        self.log.setLevel(logging.DEBUG)
+        self.log.setLevel(self.cfg['log_level'].upper())
         handler = logging.FileHandler('./tuikit.log')
         formatter = logging.Formatter('%(asctime)s %(levelname)-5s %(message)s', '%y-%m-%d %H:%M:%S')
         handler.setFormatter(formatter)
--- a/tuikit/common.py	Wed Jan 30 20:21:08 2013 +0100
+++ b/tuikit/common.py	Fri Feb 01 00:24:02 2013 +0100
@@ -81,14 +81,14 @@
     def update(self, x=None, y=None):
         old_x, old_y = self._x, self._y
         if isinstance(x, Coords) and y is None:
-            self.x, self.y = x
+            self._x, self._y = x
         else:
             if isinstance(x, int):
-                self.x = x
+                self._x = x
             elif x is not None:
                 raise ValueError('Coords.update(): first parameter must be int or Coords')
             if isinstance(y, int):
-                self.y = y
+                self._y = y
             elif y is not None:
                 raise ValueError('Coords.update(): second parameter must be int')
         if self._x != old_x or self._y != old_y:
@@ -225,19 +225,22 @@
     def push(self, x, y, w, h):
         newclip = Rect(x, y, w, h)
         if len(self.stack):
-            oldclip = self.stack[-1]
+            oldclip = self.top()
             newclip = self.intersect(oldclip, newclip)
         self.stack.append(newclip)
 
     def pop(self):
         self.stack.pop()
 
+    def top(self):
+        return self.stack[-1]
+
     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]
+        clip = self.top()
         if x < clip.x or y < clip.y \
         or x >= clip.x + clip.w or y >= clip.y + clip.h:
             return False
--- a/tuikit/editbox.py	Wed Jan 30 20:21:08 2013 +0100
+++ b/tuikit/editbox.py	Fri Feb 01 00:24:02 2013 +0100
@@ -5,80 +5,73 @@
 
 
 class EditBox(Widget):
+
+    """Multiline text edit widget.
+
+    Spot is used for text cursor position.
+
+    """
+
     def __init__(self, text=''):
         Widget.__init__(self)
-        self._default_size.update(20, 20)
+        self._default_size.update(40, 40)
         self.allow_focus = True
 
-        self.xofs = 0
-        self.yofs = 0
+        # Cursor position is same as spot.
+        # This variable rememberes horizontal position
+        # for the case when cursor moves to shorter line.
+        self.cursor_column = 0
+        # selection - line and column of selection start
+        self.sel_line = 0
+        self.sel_column = 0
 
-        # cursor
-        self.cline = 0
-        self.cpos = 0
+        self.add_events('scroll', Event)
+
+        self.text = text
 
-        # selection
-        self.sline = 0
-        self.spos = 0
+    @property
+    def text(self):
+        return '\n'.join(self.lines)
 
-        self.add_events(
-            'scroll', Event,
-            'areasize', Event)
+    @text.setter
+    def text(self, value):
+        self.lines = value.split('\n')
+        maxlen = max([len(line) for line in self.lines])
+        self._default_size.update(w=maxlen, h=len(self.lines))
 
-        self.set_text(text)
+    @property
+    def cur_line(self):
+        return self.lines[self._spot.y]
 
+    @cur_line.setter
+    def cur_line(self, value):
+        self.lines[self._spot.y] = value
 
     def on_draw(self, ev):
-        for j in range(self.height):
-            if self.yofs + j >= len(self.lines):
-                break
-            line = self.lines[self.yofs + j]
-            #if len(line) < self.width:
-                #line += ' ' * (self.width - len(line))
-            #else:
-                #line = line[:self.width]
+        ev.driver.pushcolor('normal')
+        end_y = min(len(self.lines), ev.exposed.y + ev.exposed.h)
+        for j in range(ev.exposed.y, end_y):
+            line = self.lines[j]
             ev.driver.puts(ev.x, ev.y + j, line)
-
-        self.cursor = (self.get_cpos() - self.xofs, self.cline - self.yofs)
-
+        self.cursor = (self._spot.x, self._spot.y)
+        ev.driver.popcolor()
 
     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':
-                if self.cline > 0 or self.cpos > 0:
-                    self.move_left()
-                    self.del_char()
-
-            if ev.keyname == 'delete':
-                self.del_char()
-
-            if ev.keyname == 'enter':
-                self.add_newline()
-                self.move_right()
+            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()
 
         if ev.char:
             self.add_char(ev.char)
@@ -86,139 +79,114 @@
 
         self.redraw()
 
+    def on_mousedown(self, ev):
+        y = ev.wy
+        x = min(ev.wx, len(self.lines[y]))
+        self._spot.update(x=x, y=y)
 
     def on_mousewheel(self, ev):
-        # up
         if ev.button == 4:
-            self.move_up()
-        # down
+            # wheel up
+            self.emit('scrollreq', -5)
         if ev.button == 5:
-            self.move_down()
+            # wheel down
+            self.emit('scrollreq', +5)
         self.redraw()
 
-
-    def set_text(self, text):
-        self.lines = text.split('\n')
-        self.emit('areasize')
-
-
-    def get_text(self):
-        return '\n'.join(self.lines)
-
-
-    def get_linelen(self):
-        return len(self.lines[self.cline])
-
-
-    def get_cpos(self):
-        if self.cpos > self.get_linelen():
-            return self.get_linelen()
-        return self.cpos
-
-
-    def set_yofs(self, yofs):
-        if yofs > len(self.lines) - self.height:
-            yofs = len(self.lines) - self.height
-        if yofs < 0:
-            yofs = 0
-        if self.yofs != yofs:
-            self.yofs = yofs
-            self.emit('scroll')
-
-
     def move_left(self):
-        if self.cpos > 0:
-            self.cpos = self.get_cpos() - 1
+        if self._spot.x > 0:
+            self._spot.x -= 1
         else:
-            if self.move_up():
-                self.cpos = self.get_linelen()
-
+            if self._spot.y > 0:
+                self._spot.y -= 1
+                self._spot.x = len(self.cur_line)
+        self.cursor_column = self._spot.x
 
     def move_right(self):
-        if self.cpos < self.get_linelen():
-            self.cpos += 1
+        if self._spot.x < len(self.cur_line):
+            self._spot.x += 1
         else:
-            if self.move_down():
-                self.cpos = 0
-
+            if self._spot.y < len(self.lines) - 1:
+                self._spot.y += 1
+                self._spot.x = 0
+        self.cursor_column = self._spot.x
 
     def move_home(self):
-        self.cpos = 0
-
+        self._spot.x = 0
+        self.cursor_column = self._spot.x
 
     def move_end(self):
-        self.cpos = self.get_linelen()
-
+        self._spot.x = len(self.cur_line)
+        self.cursor_column = self._spot.x
 
     def move_up(self):
-        if self.cline > 0:
-            self.cline -= 1
-            if self.cline < self.yofs:
-                self.set_yofs(self.cline)
-            return True
-        return False
-
+        if self._spot.y > 0:
+            self._spot.y -= 1
+        self._update_spot_x()
 
     def move_down(self):
-        if self.cline < len(self.lines) - 1:
-            self.cline += 1
-            if self.cline > self.yofs + self.height - 1:
-                self.set_yofs(self.cline - (self.height - 1))
-            return True
-        return False
-
+        if self._spot.y < len(self.lines) - 1:
+            self._spot.y += 1
+        self._update_spot_x()
 
     def move_pageup(self):
-        if self.cline >= self.height - 1:
-            self.cline -= self.height - 1
-            self.set_yofs(self.yofs - (self.height - 1))
+        if self._spot.y >= self.view_height - 1:
+            self.emit('scrollreq', - (self.view_height - 1))
+            self._spot.y -= self.view_height - 1
         else:
-            self.cline = 0
-            self.set_yofs(0)
-
+            self._spot.y = 0
+        self._update_spot_x()
 
     def move_pagedown(self):
-        if self.cline <= len(self.lines) - (self.height - 1):
-            self.cline += self.height - 1
-            self.set_yofs(self.yofs + (self.height - 1))
+        if len(self.lines) - self._spot.y > (self.view_height - 1):
+            self.emit('scrollreq', (self.view_height - 1))
+            self._spot.y += self.view_height - 1
         else:
-            self.cline = len(self.lines) - 1
-            self.set_yofs(self.cline)
-
+            self._spot.y = len(self.lines) - 1
+        self._update_spot_x()
 
-    def move_pagefirst(self):
-        self.cline = 0
-        self.set_yofs(0)
-
+    def move_top(self):
+        self._spot.y = 0
+        self._update_spot_x()
 
-    def move_pagelast(self):
-        self.cline = len(self.lines) - 1
-        self.set_yofs(self.cline)
-
+    def move_bottom(self):
+        self._spot.y = len(self.lines) - 1
+        self._update_spot_x()
 
     def add_char(self, c):
-        ln = self.lines[self.cline]
-        cpos = self.get_cpos()
-        self.lines[self.cline] = ln[:cpos] + c + ln[cpos:]
-        self.cpos = cpos
-
+        ln = self.cur_line
+        sx = self._spot.x
+        self.cur_line = ln[:sx] + c + ln[sx:]
+        self.cursor_column = sx
 
-    def add_newline(self):
-        ln = self.lines[self.cline]
-        cpos = self.get_cpos()
-        self.lines[self.cline] = ln[cpos:]
-        self.lines.insert(self.cline, ln[:cpos])
-        self.emit('areasize')
+    def add_newline(self, move=False):
+        ln = self.cur_line
+        sx = self._spot.x
+        self.cur_line = ln[sx:]
+        self.lines.insert(self._spot.y, ln[:sx])
+        self._default_size.update(h=len(self.lines))
+        if move:
+            self.move_right()
 
+    def backspace(self):
+        if self._spot.y > 0 or self._spot.x > 0:
+            self.move_left()
+            self.del_char()
 
     def del_char(self):
-        ln = self.lines[self.cline]
-        cpos = self.get_cpos()
-        if cpos == self.get_linelen():
-            if self.cline + 1 < len(self.lines):
-                self.lines[self.cline] = self.lines[self.cline] + self.lines[self.cline+1]
-                del self.lines[self.cline+1]
-                self.emit('areasize')
+        ln = self.cur_line
+        sx = self._spot.x
+        if sx == len(self.cur_line):
+            if self._spot.y + 1 < len(self.lines):
+                self.cur_line += self.lines[self._spot.y+1]
+                del self.lines[self._spot.y+1]
+                self._default_size.update(h=len(self.lines))
         else:
-            self.lines[self.cline] = ln[:cpos] + ln[cpos+1:]
+            self.cur_line = ln[:sx] + ln[sx+1:]
 
+    def _update_spot_x(self):
+        if self.cursor_column > len(self.cur_line):
+            self._spot.x = len(self.cur_line)
+        else:
+            self._spot.x = self.cursor_column
+
--- a/tuikit/events.py	Wed Jan 30 20:21:08 2013 +0100
+++ b/tuikit/events.py	Fri Feb 01 00:24:02 2013 +0100
@@ -10,6 +10,7 @@
 
 import logging
 import inspect
+from copy import copy
 
 
 class Event:
@@ -30,6 +31,21 @@
         self.driver = driver
         self.x = x
         self.y = y
+        self._exposed = None
+
+    @property
+    def exposed(self):
+        """Exposed part of widget - only this area needs to be drawn.
+
+        In widget's local coordinates, 0,0 is left top corner of widget to be drawn.
+
+        """
+        if self._exposed is None:
+            rect = copy(self.driver.clipstack.top())
+            rect.x -= self.x
+            rect.y -= self.y
+            self._exposed = rect
+        return self._exposed
 
     def __repr__(self):
         return 'DrawEvent(x={0.x},y={0.y})'.format(self)
@@ -154,7 +170,7 @@
             else:
                 self._event_handlers[event_name].insert(0, handler)
         else:
-            raise KeyError('Unknown event: %s', event_name)
+            raise KeyError('Unknown event: ' + event_name)
 
     def remove_handler(self, event_name, handler):
         """Remove event handler from the list."""
--- a/tuikit/layout.py	Wed Jan 30 20:21:08 2013 +0100
+++ b/tuikit/layout.py	Fri Feb 01 00:24:02 2013 +0100
@@ -81,6 +81,7 @@
                 h = reqh
             child._pos.update(x=x, y=y)
             child._size.update(w=w, h=h)
+            child._view_size.update(w=w, h=h)
 
     def move_child(self, child, x=None, y=None):
         """Move child inside container by adjusting its margin.
@@ -176,6 +177,7 @@
 
             child._pos.update(pos)
             child._size.update(size)
+            child._view_size.update(size)
 
 
 class VerticalLayout(LinearLayout):
@@ -211,6 +213,7 @@
         for child in self._get_children():
             w, h = child.sizereq
             child._size.update(w=w, h=h)
+            child._view_size.update(w=self.width, h=self.height)
 
     def move_child(self, child, x, y):
         child.hints('position').update(x, y)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tuikit/scrollwindow.py	Fri Feb 01 00:24:02 2013 +0100
@@ -0,0 +1,82 @@
+from tuikit.window import Window
+from tuikit.layout import OffsetLayout, AnchorLayout
+from tuikit.scrollbar import VScrollbar, HScrollbar
+from tuikit.common import Borders
+
+
+class ScrollWindow(Window):
+    def __init__(self):
+        Window.__init__(self, inner_layout=OffsetLayout)
+
+        self.vscroll = VScrollbar()
+        self.vscroll.add_handler('change', self._on_vscroll_change)
+        AnchorLayout.add(self, self.vscroll, halign='right', valign='fill', margin=Borders(t=1, b=1))
+
+        self.hscroll = HScrollbar()
+        self.hscroll.add_handler('change', self._on_hscroll_change)
+        AnchorLayout.add(self, self.hscroll, halign='fill', valign='bottom', margin=Borders(l=1, r=2))
+
+    def add(self, widget, **kwargs):
+        Window.add(self, widget, **kwargs)
+        widget.add_handler('sizereq', self._on_child_sizereq)
+        widget.add_handler('spotmove', self._on_child_spotmove)
+        widget.add_handler('scrollreq', self._on_child_scrollreq)
+
+    def on_resize(self, ev):
+        self._update_scroll_max()
+
+    def _on_child_sizereq(self, ev):
+        self._update_scroll_max()
+
+    def _on_vscroll_change(self, ev):
+        self._inner.offset.y = - self.vscroll.scroll_pos
+
+    def _on_hscroll_change(self, ev):
+        self._inner.offset.x = - self.hscroll.scroll_pos
+
+    def _on_child_spotmove(self, ev):
+        child = ev.originator
+        # x
+        spot_x = child.x - self._inner.offset.x + child.spot.x
+        if spot_x < self.hscroll.scroll_pos:
+            self.hscroll.scroll_pos = spot_x
+        if spot_x > (self._inner.width - 1) + self.hscroll.scroll_pos:
+            self.hscroll.scroll_pos = spot_x - (self._inner.width - 1)
+        # y
+        spot_y = child.y - self._inner.offset.y + child.spot.y
+        if spot_y < self.vscroll.scroll_pos:
+            self.vscroll.scroll_pos = spot_y
+        if spot_y > (self._inner.height - 1) + self.vscroll.scroll_pos:
+            self.vscroll.scroll_pos = spot_y - (self._inner.height - 1)
+
+    def _on_child_scrollreq(self, ev):
+        new_scroll_pos = self.vscroll.scroll_pos + ev.data
+        if new_scroll_pos > self.vscroll.scroll_max:
+            self.vscroll.scroll_pos = self.vscroll.scroll_max
+        elif new_scroll_pos < 0:
+            self.vscroll.scroll_pos = 0
+        else:
+            self.vscroll.scroll_pos = new_scroll_pos
+
+    def _update_scroll_max(self):
+        max_width = 0
+        max_height = 0
+        for child in self._inner.children:
+            child_width = child.x - self._inner.offset.x + child.sizereq.w
+            if child_width > max_width:
+                max_width = child_width
+            child_height = child.y - self._inner.offset.y + child.sizereq.h
+            if child_height > max_height:
+                max_height = child_height
+        max_width += 1
+        if max_width < self._inner.width:
+            self.hscroll.hide()
+        else:
+            self.hscroll.scroll_max = max_width - self._inner.width
+            self.hscroll.show()
+        if max_height < self._inner.height:
+            self.vscroll.hide()
+        else:
+            self.vscroll.scroll_max = max_height - self._inner.height
+            self.vscroll.show()
+
--- a/tuikit/textedit.py	Wed Jan 30 20:21:08 2013 +0100
+++ b/tuikit/textedit.py	Fri Feb 01 00:24:02 2013 +0100
@@ -30,7 +30,7 @@
         self.editbox.set_text(text)
 
     def scrolltoend(self):
-        self.editbox.move_pagelast()
+        self.editbox.move_bottom()
 
     def on_draw(self, ev):
         ev.driver.frame(ev.x, ev.y, self.width, self.height)
--- a/tuikit/treeview.py	Wed Jan 30 20:21:08 2013 +0100
+++ b/tuikit/treeview.py	Fri Feb 01 00:24:02 2013 +0100
@@ -356,5 +356,4 @@
             if node is self.cursor_node:
                 self._spot.x = _level * 2
                 self._spot.y = num
-        self.emit('spotmove')
 
--- a/tuikit/widget.py	Wed Jan 30 20:21:08 2013 +0100
+++ b/tuikit/widget.py	Fri Feb 01 00:24:02 2013 +0100
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 
-from tuikit.events import Emitter, Event, DrawEvent, FocusEvent, KeyboardEvent, MouseEvent
+from tuikit.events import Emitter, Event, DrawEvent, FocusEvent, KeyboardEvent, MouseEvent, GenericEvent
 from tuikit.common import Coords, Size
 
 import logging
@@ -24,11 +24,13 @@
         self.floater = False
 
         ### placing and size
-        # Position inside parent widget. Modified by layout manager.
+        #: Position inside parent widget. Modified by layout manager.
         self._pos = Coords()
-        # Actual size. Modified only by layout manager.
+        #: Actual size. Modified only by layout manager.
         self._size = Size(10, 10)
         self._size.add_handler('change', lambda ev: self.emit('resize'))
+        #: Size of visible part of widget. Used in OffsetLayout. Modified only by layout manager.
+        self._view_size = Size(10, 10)
         #: Default (natural) size of Widget.
         self._default_size = Size(1, 1)
         self._default_size.add_handler('change', lambda ev: self._sizereq.update(self._default_size))
@@ -61,6 +63,7 @@
 
         # See spot property.
         self._spot = Coords()
+        self._spot.add_handler('change', lambda ev: self.emit('spotmove'))
 
         # redraw request
         self._redraw = True
@@ -76,6 +79,7 @@
             'mousehover', MouseEvent,
             'mousewheel', MouseEvent,
             'sizereq', Event,
+            'scrollreq', GenericEvent,
             'spotmove', Event,
             'focus', FocusEvent,
             'unfocus', FocusEvent)
@@ -113,6 +117,14 @@
         self._sizereq.update(w, h)
 
     @property
+    def view_width(self):
+        return self._view_size.w
+
+    @property
+    def view_height(self):
+        return self._view_size.h
+
+    @property
     def sizereq(self):
         """Size request.