# HG changeset patch # User Radek Brich # Date 1424516517 -3600 # Node ID 8c79705206324481021fbbe1a0cc47aec70664bf # Parent 8680c23335467dd7d24548e3c340785a55e7b2a3 Add mouse events, event demo. diff -r 8680c2333546 -r 8c7970520632 demos/03_application.py --- a/demos/03_application.py Mon Feb 16 21:17:43 2015 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,27 +0,0 @@ -#!/usr/bin/env python3 - -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!') -button1 = Button() -button2 = Button() -field = TextField('text field') - -app = Application() -app.root_window.add(label, 20, 10) -app.root_window.add(button1, 20, 20) -app.root_window.add(button2, 30, 20) -app.root_window.add(field, 20, 30) -app.root_window.focus_widget = field - -def on_keypress(ev): - if ev.keyname == 'escape': - app.stop() - -app.window_manager.sig_keypress.connect(on_keypress) - -app.start() - diff -r 8680c2333546 -r 8c7970520632 demos/03_event.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/demos/03_event.py Sat Feb 21 12:01:57 2015 +0100 @@ -0,0 +1,19 @@ +#!/usr/bin/env python3 + +from tuikit.core.buffer import Buffer +from tuikit.driver.cursesw import CursesWDriver + +buffer = Buffer() +line = 0 +with CursesWDriver() as driver: + buffer.resize(*driver.size) + buffer.puts(str(driver.size), 0, 10) + while True: + for event in driver.getevents(): + buffer.puts(str(event), 0, line) + line += 1 + driver.draw(buffer) + driver.flush() + if line >= buffer.size.h: + buffer.fill() + line = 0 diff -r 8680c2333546 -r 8c7970520632 demos/04_texteditor.py --- a/demos/04_texteditor.py Mon Feb 16 21:17:43 2015 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,34 +0,0 @@ -#!/usr/bin/env python3 - -import sys - -from tuikit.core.application import Application -#from tuikit.scrollview import ScrollView -from tuikit.widgets.textbox import TextBox - - -class MyApplication(Application): - - def __init__(self): - Application.__init__(self) - self.window_manager.sig_keypress.connect(self.on_wm_keypress) - #self.top.add_handler('keypress', self.on_top_keypress) - - t = open(sys.argv[0]).read() - editbox = TextBox(t) - - #scroll = ScrollView() - #scroll.add(editbox) - - self.root_window.add(editbox) - #self.root_window.add(scroll, halign='fill', valign='fill') - - def on_wm_keypress(self, ev): - if ev.keyname == 'escape': - self.stop() - return True - -if __name__ == '__main__': - app = MyApplication() - app.start() - diff -r 8680c2333546 -r 8c7970520632 demos/05_fixedlayout.py --- a/demos/05_fixedlayout.py Mon Feb 16 21:17:43 2015 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,21 +0,0 @@ -#!/usr/bin/env python3 - -from tuikit.core.application import Application -from tuikit.widgets.button import Button - -app = Application() -app.add(Button('left=0'), left=0) -app.add(Button('left=5'), left=5, top=2) -app.add(Button('right=0'), right=0) -app.add(Button('right=5'), right=5, top=2) -app.add(Button('center=x'), center='x', top=3) -app.add(Button('center=x, left=5'), center='x', left=5, top=5) -app.add(Button('center=x, right=5'), center='x', right=5, top=7) -app.add(Button('center=xy'), center='xy') -app.add(Button('fill=x'), fill='x', top=9) -app.add(Button('fill=x, left=5'), fill='x', left=5, top=11) -app.add(Button('fill=x, right=5'), fill='x', right=5, top=13) -app.add(Button('fill=x, left=5, right=5'), fill='x', left=5, right=5, top=15) - -app.window_manager.sig_keypress.connect(lambda ev: app.stop()) -app.start() diff -r 8680c2333546 -r 8c7970520632 demos/06_gridlayout.py --- a/demos/06_gridlayout.py Mon Feb 16 21:17:43 2015 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,24 +0,0 @@ -#!/usr/bin/env python3 - -from tuikit.layouts.grid import GridLayout -from tuikit.widgets.label import Label - -l1 = Label('Hello') -l1.sizemin.update(10, 1) - -grid = GridLayout() -grid.add(l1, 1, 1) -grid.update(10, 10) - -print(grid._grid_size) -print(grid._grid) - -for row in range(grid.row_count): - for col in range(grid.column_count): - w = grid.get_widget_at(row, col) - name = w.name if w else '--' - print(name.center(16), end='') - print() - -print(l1.pos) -print(l1.size) \ No newline at end of file diff -r 8680c2333546 -r 8c7970520632 demos/10_application.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/demos/10_application.py Sat Feb 21 12:01:57 2015 +0100 @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 + +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!') +button1 = Button() +button2 = Button() +field = TextField('text field') + +app = Application() +app.root_window.add(label, 20, 10) +app.root_window.add(button1, 20, 20) +app.root_window.add(button2, 30, 20) +app.root_window.add(field, 20, 30) +app.root_window.focus_widget = field + +def on_keypress(ev): + if ev.keyname == 'escape': + app.stop() + +app.window_manager.sig_keypress.connect(on_keypress) + +app.start() + diff -r 8680c2333546 -r 8c7970520632 demos/11_fixedlayout.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/demos/11_fixedlayout.py Sat Feb 21 12:01:57 2015 +0100 @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 + +from tuikit.core.application import Application +from tuikit.widgets.button import Button + +app = Application() +app.add(Button('left=0'), left=0) +app.add(Button('left=5'), left=5, top=2) +app.add(Button('right=0'), right=0) +app.add(Button('right=5'), right=5, top=2) +app.add(Button('center=x'), center='x', top=3) +app.add(Button('center=x, left=5'), center='x', left=5, top=5) +app.add(Button('center=x, right=5'), center='x', right=5, top=7) +app.add(Button('center=xy'), center='xy') +app.add(Button('fill=x'), fill='x', top=9) +app.add(Button('fill=x, left=5'), fill='x', left=5, top=11) +app.add(Button('fill=x, right=5'), fill='x', right=5, top=13) +app.add(Button('fill=x, left=5, right=5'), fill='x', left=5, right=5, top=15) + +app.window_manager.sig_keypress.connect(lambda ev: app.stop()) +app.start() diff -r 8680c2333546 -r 8c7970520632 demos/12_gridlayout.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/demos/12_gridlayout.py Sat Feb 21 12:01:57 2015 +0100 @@ -0,0 +1,24 @@ +#!/usr/bin/env python3 + +from tuikit.layouts.grid import GridLayout +from tuikit.widgets.label import Label + +l1 = Label('Hello') +l1.sizemin.update(10, 1) + +grid = GridLayout() +grid.add(l1, 1, 1) +grid.update(10, 10) + +print(grid._grid_size) +print(grid._grid) + +for row in range(grid.row_count): + for col in range(grid.column_count): + w = grid.get_widget_at(row, col) + name = w.name if w else '--' + print(name.center(16), end='') + print() + +print(l1.pos) +print(l1.size) \ No newline at end of file diff -r 8680c2333546 -r 8c7970520632 demos/20_texteditor.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/demos/20_texteditor.py Sat Feb 21 12:01:57 2015 +0100 @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 + +import sys + +from tuikit.core.application import Application +#from tuikit.scrollview import ScrollView +from tuikit.widgets.textbox import TextBox + + +class MyApplication(Application): + + def __init__(self): + Application.__init__(self) + self.window_manager.sig_keypress.connect(self.on_wm_keypress) + #self.top.add_handler('keypress', self.on_top_keypress) + + t = open(sys.argv[0]).read() + editbox = TextBox(t) + + #scroll = ScrollView() + #scroll.add(editbox) + + self.root_window.add(editbox) + #self.root_window.add(scroll, halign='fill', valign='fill') + + def on_wm_keypress(self, ev): + if ev.keyname == 'escape': + self.stop() + return True + +if __name__ == '__main__': + app = MyApplication() + app.start() + diff -r 8680c2333546 -r 8c7970520632 tuikit/core/container.py --- a/tuikit/core/container.py Mon Feb 16 21:17:43 2015 +0100 +++ b/tuikit/core/container.py Sat Feb 21 12:01:57 2015 +0100 @@ -91,21 +91,20 @@ if Widget.keypress_event(self, ev): return True - def mousedown(self, button, pos): - self.mouse_child = None - for child in reversed(self.children): - if pos in child.boundaries: - child.mousedown(button, pos - child.pos) - self.mouse_child = child + def mousedown_event(self, ev): + self.mouse_widget = None + for child in reversed(self._widgets): + if ev.pos in child.boundaries: + child.mousedown_event(ev.rebase(child.pos)) + self.mouse_widget = child - def mouseup(self, button, pos): - if self.mouse_child: - self.mouse_child.mouseup(button, pos - self.mouse_child.pos) + def mouseup_event(self, ev): + if self.mouse_widget: + self.mouse_widget.mouseup_event(ev.rebase(self.mouse_widget.pos)) - def mousemove(self, button, pos, relpos): - if self.mouse_child: - self.mouse_child.mousemove(button, - pos - self.mouse_child.pos, relpos) + def mousemove_event(self, ev): + if self.mouse_widget: + self.mouse_widget.mousemove_event(ev.rebase(self.mouse_widget.pos)) ## focus ## diff -r 8680c2333546 -r 8c7970520632 tuikit/core/events.py --- a/tuikit/core/events.py Mon Feb 16 21:17:43 2015 +0100 +++ b/tuikit/core/events.py Sat Feb 21 12:01:57 2015 +0100 @@ -51,3 +51,16 @@ res.append(mod) res.append(self.keyname or self.char) return sep.join(res) + + +class MouseEvent(Event): + + def __init__(self, name, button, pos, relpos=None): + Event.__init__(self, name, + ('name', 'button', 'pos', 'relpos'), + (name, button, pos, relpos)) + + def rebase(self, zero_pos): + """Return new MouseEvent with position rebased to zero_pos.""" + pos = self.pos - zero_pos + return MouseEvent(self.name, self.button, pos, self.relpos) diff -r 8680c2333546 -r 8c7970520632 tuikit/core/widget.py --- a/tuikit/core/widget.py Mon Feb 16 21:17:43 2015 +0100 +++ b/tuikit/core/widget.py Sat Feb 21 12:01:57 2015 +0100 @@ -130,27 +130,24 @@ Override to accept keyboard input. - Returns True if event was consumed. + Return True if event was consumed. - Call this implementation from inherited classes - if it does not consume the event. + Call parent implementation from inherited classes + when not consuming the event. """ if self.sig_keypress(ev): return True self.log.debug('Not consumed: %s', ev) - def mousedown(self, button, pos): - self._log.debug('mousedown(btn=%r, pos=%r)', - button, pos) + def mousedown_event(self, ev): + self.log.debug('Not consumed: %s', ev) - def mouseup(self, button, pos): - self._log.debug('mouseup(btn=%r, pos=%r)', - button, pos) + def mouseup_event(self, ev): + self.log.debug('Not consumed: %s', ev) - def mousemove(self, button, pos, relpos): - self._log.debug('mousemove(btn=%r, pos=%r, relpos=%r)', - button, pos, relpos) + def mousemove_event(self, ev): + self.log.debug('Not consumed: %s', ev) ## timeouts ## diff -r 8680c2333546 -r 8c7970520632 tuikit/driver/cursesw.py --- a/tuikit/driver/cursesw.py Mon Feb 16 21:17:43 2015 +0100 +++ b/tuikit/driver/cursesw.py Sat Feb 21 12:01:57 2015 +0100 @@ -3,7 +3,7 @@ import logging from tuikit.driver.driver import Driver -from tuikit.core.events import ResizeEvent, KeypressEvent +from tuikit.core.events import ResizeEvent, KeypressEvent, MouseEvent from tuikit.core.coords import Point @@ -106,7 +106,12 @@ self.stdscr.addstr(y, x, ch) else: raise TypeError('Integer or one-char string is required.') - except curses.error as e: + except curses.error: + if x == self.size.w - 1 and y == self.size.h - 1: + # Curses putch to lower-right corner gives error because + # scrolling is disabled and cursor cannot move to next char. + # Let's just ignore that for now. + return self._log.exception('putch(%r, %s, %s) error:' % (ch, x, y)) def draw(self, buffer, x=0, y=0): @@ -162,7 +167,7 @@ timeout -- float, in seconds (None=infinite) Returns: - [('event', param1, ...), ...] + List of Event objects. """ # Set timeout @@ -200,21 +205,21 @@ return res def _process_mouse(self): - out = [] + res = [] try: _id, x, y, _z, bstate = curses.getmouse() except curses.error: - return out + return res pos = Point(x, y) if bstate & curses.REPORT_MOUSE_POSITION: if self._mouse_last_pos != pos: if self._mouse_last_pos: relpos = pos - self._mouse_last_pos - out += [('mousemove', 0, pos, relpos)] + res.append(MouseEvent('mousemove', 0, pos, relpos)) self._mouse_last_pos = pos - # we are interested only in changes, not buttons already pressed before event + # We are interested only in changes, not buttons already pressed before event if self._mouse_last_bstate is not None: old = self._mouse_last_bstate new = bstate @@ -224,23 +229,23 @@ self._mouse_last_bstate = bstate if bstate & curses.BUTTON1_PRESSED: - out += [('mousedown', 1, pos)] + res.append(MouseEvent('mousedown', 1, pos)) if bstate & curses.BUTTON2_PRESSED: - out += [('mousedown', 2, pos)] + res.append(MouseEvent('mousedown', 2, pos)) if bstate & curses.BUTTON3_PRESSED: - out += [('mousedown', 3, pos)] + res.append(MouseEvent('mousedown', 3, pos)) if bstate & curses.BUTTON1_RELEASED: - out += [('mouseup', 1, pos)] + res.append(MouseEvent('mouseup', 1, pos)) if bstate & curses.BUTTON2_RELEASED: - out += [('mouseup', 2, pos)] + res.append(MouseEvent('mouseup', 2, pos)) if bstate & curses.BUTTON3_RELEASED: - out += [('mouseup', 3, pos)] + res.append(MouseEvent('mouseup', 3, pos)) - # reset last pos when pressed/released - if len(out) > 0 and out[-1][0] in ('mousedown', 'mouseup'): + # Reset last pos when pressed/released + if len(res) > 0 and res[-1].name in ('mousedown', 'mouseup'): self._mouse_last_pos = None - return out + return res def _split_keyname_mods(self, keyname): """Parse keynames in form "shift+tab", return (keyname, mod).""" diff -r 8680c2333546 -r 8c7970520632 tuikit/driver/driver.py --- a/tuikit/driver/driver.py Mon Feb 16 21:17:43 2015 +0100 +++ b/tuikit/driver/driver.py Sat Feb 21 12:01:57 2015 +0100 @@ -86,6 +86,7 @@ def __enter__(self): self.init() + return self def __exit__(self, exc_type, exc_val, exc_tb): self.close() diff -r 8680c2333546 -r 8c7970520632 tuikit/scrollwindow.py --- a/tuikit/scrollwindow.py Mon Feb 16 21:17:43 2015 +0100 +++ b/tuikit/scrollwindow.py Sat Feb 21 12:01:57 2015 +0100 @@ -14,4 +14,3 @@ def add(self, widget, **kwargs): Window.add(self, widget, **kwargs) self._connect_child(widget) - diff -r 8680c2333546 -r 8c7970520632 tuikit/widgets/button.py --- a/tuikit/widgets/button.py Mon Feb 16 21:17:43 2015 +0100 +++ b/tuikit/widgets/button.py Sat Feb 21 12:01:57 2015 +0100 @@ -24,6 +24,7 @@ self.color = 'default' self.color_active = 'default on red' + self.color_highlight = 'default on yellow' self.highlight = False self.sig_clicked = Signal() @@ -47,6 +48,8 @@ self.color_active = theme.button_active def _get_color(self): + if self.highlight: + return self.color_highlight if self.has_focus(): return self.color_active return self.color @@ -71,20 +74,18 @@ # suffix buffer.puts(self.suffix, pos) - def on_mousedown(self, ev): + def mousedown_event(self, ev): self.highlight = True - self.redraw() + #self.redraw() - def on_mouseup(self, ev): + def mouseup_event(self, ev): self.highlight = False - self.redraw() + #self.redraw() + self.sig_clicked() - if self.enclose(ev.px, ev.py): - self.emit('click') - - def on_keypress(self, ev): + def keypress_event(self, ev): if ev.keyname == 'enter': - self.emit('click') + self.sig_clicked() def _divide_padding(self, pad): # default is 'left' diff -r 8680c2333546 -r 8c7970520632 tuikit/widgets/scrollbar.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tuikit/widgets/scrollbar.py Sat Feb 21 12:01:57 2015 +0100 @@ -0,0 +1,212 @@ +# -*- coding: utf-8 -*- + +from tuikit.core.widget import Widget +from tuikit.core.signal import Signal + + +class Scrollbar(Widget): + + """Abstract base class for scrollbars.""" + + def __init__(self): + Widget.__init__(self) + + # Scrolling range is 0 .. _scroll_max + self._scroll_max = self._get_length() - 3 + # Current position of scrollbar in scrolling range. + self._scroll_pos = 0 + # Current position of thumb on scrollbar - used for draw. + self._thumb_pos = 0 + # Auxilliary variable, True when user holds mouse on thumb. + self._dragging = False + # Auxilliary variable, 'up' or 'down' depending on last move direction. + self._move = None + + #: delay before continuous scrolling when user holds mouse on scrollbar arrow + self.scroll_delay = 0.150 + #: interval for continuous scrolling + self.scroll_interval = 0.030 + + # change event is emitted when user moves scrollbar (even programmatically) + self.sig_changed = Signal() + + @property + def scroll_max(self): + """Maximum for scrolling position.""" + return self._scroll_max + + @scroll_max.setter + def scroll_max(self, value): + if value < 0: + value = 0 + self._scroll_max = value + if self._scroll_pos > value: + self._scroll_pos = value + self.sig_changed() + self._update_thumb_pos() + + @property + def scroll_pos(self): + """Scrolling position. + + Integer number between 0 and 'max'. + + """ + return self._scroll_pos + + @scroll_pos.setter + def scroll_pos(self, value): + if self._scroll_pos != value: + self._scroll_pos = value + self._update_thumb_pos() + self.sig_changed() + + def move_up(self): + """Move scrolling position up/left.""" + if self._scroll_pos > 0: + self.scroll_pos = self._scroll_pos - 1 + self._move = 'up' + + def move_down(self): + """Move scrolling position down/right.""" + if self._scroll_pos < self._scroll_max: + self.scroll_pos = self._scroll_pos + 1 + self._move = 'down' + + def drag(self, mouse_pos): + """Scroll using mouse drag on thumb. + + Args: + mouse_pos: new position of mouse, in range 0 .. self._get_length() + + """ + new_pos = int(round((mouse_pos - 1) / (self._get_length() - 3) * self._scroll_max)) + if new_pos < 0: + new_pos = 0 + if new_pos > self._scroll_max: + new_pos = self._scroll_max + if self._scroll_pos != new_pos: + self.scroll_pos = new_pos + + def _get_length(self): + """Return length of scrollbar. + + This will be widget height for vertical scrollbar, + width for horizontal scrollbar. + + """ + raise NotImplementedError() + + def _update_thumb_pos(self): + """Update value of internal variable _thumb_pos.""" + self._thumb_pos = 0 + if self._scroll_max and self._scroll_pos <= self._scroll_max: + self._thumb_pos = int(round(self._scroll_pos / self._scroll_max * (self._get_length() - 3))) + #self.redraw() + + def _continuous_scroll(self): + if self._move == 'up': + self.move_up() + if self._move == 'down': + self.move_down() + self.add_timeout(self.scroll_interval, self._continuous_scroll) + + +class VScrollbar(Scrollbar): + def __init__(self): + Scrollbar.__init__(self) + self.sizereq.update(1, 20) + + def draw(self, buffer): + Widget.draw(self, buffer) + ug = ev.driver.unigraph + ev.driver.pushcolor('normal') + ev.driver.putch(ev.x, ev.y, ug.get_char('sb_up')) + for i in range(1, self.height - 1): + ev.driver.putch(ev.x, ev.y + i, ug.get_char('sb_vtrack')) + ev.driver.putch(ev.x, ev.y + 1 + self._thumb_pos, ug.get_char('sb_thumb')) + ev.driver.putch(ev.x, ev.y + self.height - 1, ug.get_char('sb_down')) + ev.driver.popcolor() + + def on_mousedown(self, ev): + self._dragging = False + self._move = None + # arrow buttons + if ev.wy == 0 or ev.wy == self.height - 1: + if ev.wy == 0: + self.move_up() + else: + self.move_down() + self.add_timeout(self.scroll_delay, self._continuous_scroll) + return + # thumb bar + if ev.wy == 1 + self._thumb_pos: + self._dragging = True + return + + def on_mousemove(self, ev): + if self._dragging: + self.drag(ev.wy) + + def on_mouseup(self, ev): + if self._dragging: + self.drag(ev.wy) + self._dragging = False + return + if self._move: + self.remove_timeout(self._continuous_scroll) + self._move = None + return + + def _get_length(self): + return self.height + + +class HScrollbar(Scrollbar): + def __init__(self): + Scrollbar.__init__(self) + self.sizereq.update(20, 1) + + def on_draw(self, ev): + ug = ev.driver.unigraph + ev.driver.pushcolor('normal') + ev.driver.putch(ev.x, ev.y, ug.get_char('sb_left')) + for i in range(1, self.width - 1): + ev.driver.putch(ev.x + i, ev.y, ug.get_char('sb_htrack')) + ev.driver.putch(ev.x + 1 + self._thumb_pos, ev.y, ug.get_char('sb_thumb')) + ev.driver.putch(ev.x + self.width - 1, ev.y, ug.get_char('sb_right')) + ev.driver.popcolor() + + def on_mousedown(self, ev): + self._dragging = False + self._move = None + # arrow buttons + if ev.wx == 0 or ev.wx == self.width - 1: + if ev.wx == 0: + self.move_up() + else: + self.move_down() + self.add_timeout(self.scroll_delay, self._continuous_scroll) + return + # thumb bar + if ev.wx == 1 + self._thumb_pos: + self._dragging = True + return + + def on_mousemove(self, ev): + if self._dragging: + self.drag(ev.wx) + + def on_mouseup(self, ev): + if self._dragging: + self.drag(ev.wx) + self._dragging = False + return + if self._move: + self.remove_timeout(self._continuous_scroll) + self._move = None + return + + def _get_length(self): + return self.width + diff -r 8680c2333546 -r 8c7970520632 tuikit/widgets/textbox.py --- a/tuikit/widgets/textbox.py Mon Feb 16 21:17:43 2015 +0100 +++ b/tuikit/widgets/textbox.py Sat Feb 21 12:01:57 2015 +0100 @@ -84,11 +84,11 @@ self.move_right() return True - def on_mousedown(self, ev): - y = ev.wy - x = min(ev.wx, len(self._lines[y])) + def mousedown_event(self, ev): + y = ev.pos.y + x = min(ev.pos.x, len(self._lines[y])) self._cursor.update(x=x, y=y) - self.redraw() + #self.redraw() def on_mousewheel(self, ev): if ev.button == 4: