Add ScrollWindow. Rewrite EditBox to work with OffsetLayout. Add propery "exposed" to DrawEvent. Add Widget._view_size. Add config file (driver, log_level).
--- 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.