# HG changeset patch # User Radek Brich # Date 1359674642 -3600 # Node ID 23767a33a781a422bb8071ad4d9a735aba0bcb03 # Parent 85a282b5e4fc83c6e8caeaf7eeaee98e6a6548af Add ScrollWindow. Rewrite EditBox to work with OffsetLayout. Add propery "exposed" to DrawEvent. Add Widget._view_size. Add config file (driver, log_level). diff -r 85a282b5e4fc -r 23767a33a781 demo_window.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' diff -r 85a282b5e4fc -r 23767a33a781 tuikit.conf --- /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' diff -r 85a282b5e4fc -r 23767a33a781 tuikit/application.py --- 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) diff -r 85a282b5e4fc -r 23767a33a781 tuikit/common.py --- 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 diff -r 85a282b5e4fc -r 23767a33a781 tuikit/editbox.py --- 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 + diff -r 85a282b5e4fc -r 23767a33a781 tuikit/events.py --- 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.""" diff -r 85a282b5e4fc -r 23767a33a781 tuikit/layout.py --- 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) diff -r 85a282b5e4fc -r 23767a33a781 tuikit/scrollwindow.py --- /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() + diff -r 85a282b5e4fc -r 23767a33a781 tuikit/textedit.py --- 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) diff -r 85a282b5e4fc -r 23767a33a781 tuikit/treeview.py --- 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') diff -r 85a282b5e4fc -r 23767a33a781 tuikit/widget.py --- 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.