# HG changeset patch # User Radek Brich # Date 1395085204 -3600 # Node ID 0978fb755d31b1c12f991af0f9250ac5d3cf97b0 # Parent 6828c5b1608723bac844130c741ad6bd4300b616 Add core Application (adjusted), Window (new version), Signal (replaces Emitter), Size (adjusted). Add application demo. diff -r 6828c5b16087 -r 0978fb755d31 demos/03_application.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/demos/03_application.py Mon Mar 17 20:40:04 2014 +0100 @@ -0,0 +1,10 @@ +#!/usr/bin/env python3 + +import sys +sys.path.append('..') + +from tuikit.core.application import Application + + +app = Application() +app.start() diff -r 6828c5b16087 -r 0978fb755d31 tuikit/core/application.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tuikit/core/application.py Mon Mar 17 20:40:04 2014 +0100 @@ -0,0 +1,69 @@ +from tuikit.core.window import Window + +import logging + + +class Application: + + """Tuikit application helper. + + Joins all required pieces to build complete application + using TUI widgets and event loop. + + """ + + def __init__(self, driver='curses'): + # logger + self.log = logging.getLogger('tuikit') + # Driver + self.driver = None + # root Buffer and Window + self.root_window = Window() + # flags + self._started = False + self._quit = False + # find and initialize driver + self.use_driver(driver) + + def use_driver(self, driver_name): + """Select driver to be used for rendering and input. + + `driver_name` should be one of: 'base', 'curses', 'sdl' + + """ + if self._started: + raise Exception('Cannot change driver after starting the application.') + module = __import__('tuikit.driver.' + driver_name, fromlist=['driver_class']) + self.driver = module.driver_class() + + def start(self): + """Start application. Runs main loop.""" + self.log.info('=== start ===') + with self.driver: + self.main_loop() + + def stop(self): + """Terminate application.""" + self._quit = True + + def main_loop(self): + """The main loop.""" + self._started = True + self.root_window.resize(*self.driver.size) +# timer = self._timer + self.root_window.buffer.frame() + + while not self._quit: + self.root_window.draw(self.driver) + self.driver.flush() + + #timeout = timer.nearest_timeout() + events = self.driver.getevents()#timeout) + self._quit = True + #timer.process_timeouts() + +# for event in events: + # self._top.emit(event[0], *event[1:]) + self._started = False + self.log.info('=== quit ===') + diff -r 6828c5b16087 -r 0978fb755d31 tuikit/core/buffer.py --- a/tuikit/core/buffer.py Sat Mar 15 15:28:20 2014 +0100 +++ b/tuikit/core/buffer.py Mon Mar 17 20:40:04 2014 +0100 @@ -1,4 +1,5 @@ -from tuikit.common import Size +from tuikit.core.signal import Signal +from tuikit.core.size import Size from tuikit.core.unigraph import unigraph_default @@ -21,7 +22,62 @@ self.attr = attr -class Buffer: +class BufferOperationsMixin: + + def draw(self, buffer, x=0, y=0): + """Draw another buffer onto this buffer at x/y coords.""" + for bufy in range(buffer.size.h): + for bufx in range(buffer.size.w): + char, attr_desc = buffer.get(bufx, bufy) + self.setattr(attr_desc) + self.putch(x + bufx, y + bufy, char) + + def puts(self, x, y, s): + """Output string of characters.""" + for c in s: + self.putch(x, y, c) + x += 1 + + def hline(self, x, y, w, c=' '): + """Draw horizontal line.""" + self.puts(x, y, [c] * w) + + def vline(self, x, y, h, c=' '): + """Draw vertical line.""" + for i in range(h): + self.putch(x, y + i, c) + + def fill(self, x=0, y=0, w=0, h=0, c=' '): + """Fill rectangular area. + + Fill whole buffer if width or height is not specified (zero). + + """ + w = self.size.w if not w else w + h = self.size.h if not h else h + for i in range(h): + self.hline(x, y + i, w, c) + + def frame(self, x=0, y=0, w=0, h=0, style=unigraph_default): + """Draw rectangular frame. + + Frame whole buffer if width or height is not specified (zero). + Use line-drawing characters from `style` bank. + + """ + w = self.size.w if not w else w + h = self.size.h if not h else h + self.putch(x, y, style.frame_ulcorner) + self.putch(x+w-1, y, style.frame_urcorner) + self.putch(x, y+h-1, style.frame_llcorner) + self.putch(x+w-1, y+h-1, style.frame_lrcorner) + self.hline(x+1, y, w-2, style.frame_hline) + self.hline(x+1, y+h-1, w-2, style.frame_hline) + self.vline(x, y+1, h-2, style.frame_vline) + self.vline(x+w-1, y+1, h-2, style.frame_vline) + + +class Buffer(BufferOperationsMixin): """Rectangular character buffer. @@ -37,7 +93,7 @@ self._attr_map = {'default': 0} self._current_attr = 0 self.clear() - self._size.add_handler('change', self._on_resize) + self.sig_resized = Signal() @property def size(self): @@ -46,10 +102,8 @@ def resize(self, w, h): """Resize buffer.""" self._size.update(w, h) - - def _on_resize(self): - """Reset contents of buffer when resized.""" self.clear() + self.sig_resized(w, h) def clear(self): """Reset buffer data.""" @@ -78,47 +132,38 @@ """Set character on xy coords to c.""" self._data[y * self._size.w + x].set(ch, self._current_attr) - def puts(self, x, y, s): - """Output string of characters.""" - for c in s: - self.putch(x, y, c) - x += 1 + +class ProxyBuffer(BufferOperationsMixin): - def hline(self, x, y, w, c=' '): - """Draw horizontal line.""" - self.puts(x, y, [c] * w) + """Special buffer which proxies the operations + to another buffer or buffer-like class.""" - def vline(self, x, y, h, c=' '): - """Draw vertical line.""" - for i in range(h): - self.putch(x, y + i, c) + def __init__(self, target): + self.target = target + self.sig_resized = Signal() - def fill(self, x=0, y=0, w=0, h=0, c=' '): - """Fill rectangular area. - - Fill whole buffer if width or height is not specified (zero). + @property + def size(self): + return self.target.size - """ - w = self._size.w if not w else w - h = self._size.h if not h else h - for i in range(h): - self.hline(x, y + i, w, c) + def resize(self, w, h): + """Resize buffer.""" + self.target.resize(w, h) + self.sig_resized(w, h) - def frame(self, x=0, y=0, w=0, h=0, style=unigraph_default): - """Draw rectangular frame. - - Frame whole buffer if width or height is not specified (zero). - Use line-drawing characters from `style` bank. + def clear(self): + """Reset buffer data.""" + self.target.clear() - """ - w = self._size.w if not w else w - h = self._size.h if not h else h - self.putch(x, y, style.frame_ulcorner) - self.putch(x+w-1, y, style.frame_urcorner) - self.putch(x, y+h-1, style.frame_llcorner) - self.putch(x+w-1, y+h-1, style.frame_lrcorner) - self.hline(x+1, y, w-2, style.frame_hline) - self.hline(x+1, y+h-1, w-2, style.frame_hline) - self.vline(x, y+1, h-2, style.frame_vline) - self.vline(x+w-1, y+1, h-2, style.frame_vline) + def get(self, x, y): + """Get cell data at `x`, `y` coords.""" + return self.target.get(x, y) + def setattr(self, attr_desc): + """Set attribute to be used for subsequent draw operations.""" + self.target.setattr(attr_desc) + + def putch(self, x, y, ch): + """Set character on xy coords to c.""" + self.target.putch(x, y, ch) + diff -r 6828c5b16087 -r 0978fb755d31 tuikit/core/signal.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tuikit/core/signal.py Mon Mar 17 20:40:04 2014 +0100 @@ -0,0 +1,31 @@ +class Signal: + + """Simple implementation of signal/slot concept. + + In signalling class, add attribute with "sig_" prefix: + self.sig_clicked = Signal() + + In listening class, add normal method, e.g. "close()" and connect it, e.g: + button.sig_clicked.connect(window.close) + + When button gets clicked, it should call the signal: + self.sig_clicked() + + Then window.close() will be called. + + """ + + def __init__(self): + self._handlers = [] + + def __call__(self, *args, **kwargs): + """Emit the signal to all connected handlers.""" + for handler in self._handlers: + handler(*args, **kwargs) + + def connect(self, handler): + if not handler in self._handlers: + self._handlers.append(handler) + + def disconnect(self, handler): + self._handlers.remove(handler) diff -r 6828c5b16087 -r 0978fb755d31 tuikit/core/size.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tuikit/core/size.py Mon Mar 17 20:40:04 2014 +0100 @@ -0,0 +1,58 @@ +class Size: + + """Size class. + + Implements attribute access (.w, .h) and list-like access([0],[1]). + + """ + + def __init__(self, *args, **kwargs): + self.w = 0 + self.h = 0 + self.update(*args, **kwargs) + + def __getitem__(self, key): + return (self.w, self.h)[key] + + def __repr__(self): + return 'Size(w={0.w},h={0.h})'.format(self) + + def update(self, *args, **kwargs): + if len(args) == 2: + # (w:int, h:int) + self.w, self.h = args + elif len(args) == 1: + # (size:Size) + self.w, self.h = args[0] + elif len(args): + raise ValueError('Too many args.') + for key, val in kwargs.items(): + if key in ('w', 'h'): + setattr(self, key, val) + else: + raise ValueError('Bad keyword arg: %r' % key) + + def readonly(self): + return ReadonlySize(self) + + +class ReadonlySize: + + """Wrapper for Size which makes it read-only.""" + + def __init__(self, size): + self._size = size + + @property + def w(self): + return self._size.w + + @property + def h(self): + return self._size.h + + def __getitem__(self, key): + return self._size[key] + + def __repr__(self): + return 'ReadonlySize(w={0.w},h={0.h})'.format(self._size) diff -r 6828c5b16087 -r 0978fb755d31 tuikit/core/window.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tuikit/core/window.py Mon Mar 17 20:40:04 2014 +0100 @@ -0,0 +1,46 @@ +from tuikit.core.buffer import Buffer +from tuikit.core.signal import Signal + + +class Window: + + """Window is rectangular part of screen containing widgets. + + Widgets are drawn into window, events are routed to widgets through window. + + """ + + def __init__(self, buffer=None): + """New buffer for the window will be created unless given existing + `buffer` as parameter.""" + self._buffer = None + self._size = None + self.sig_resized = Signal() + self.buffer = buffer or Buffer() + + @property + def buffer(self): + return self._buffer + + @buffer.setter + def buffer(self, buffer): + # disconnect signals from old buffer + if self._buffer: + self.sig_resized.disconnect(self._buffer.resize) + # replace the buffer + self._buffer = buffer + self._size = buffer.size + # resize buffer when window gets resized + self.sig_resized.connect(buffer.resize) + + @property + def size(self): + return self._size.readonly() + + def resize(self, w, h): + self._buffer.resize(w, h) + self.sig_resized(w, h) + + def draw(self, buffer, x=0, y=0): + """Draw this window into buffer at x, y coords.""" + buffer.draw(self.buffer, x, y) diff -r 6828c5b16087 -r 0978fb755d31 tuikit/driver/curses.py --- a/tuikit/driver/curses.py Sat Mar 15 15:28:20 2014 +0100 +++ b/tuikit/driver/curses.py Mon Mar 17 20:40:04 2014 +0100 @@ -121,14 +121,14 @@ def erase(self): self.stdscr.erase() - def putch(self, x, y, c): + def putch(self, x, y, ch): if not self.clipstack.test(x, y): return try: - if isinstance(c, str) and len(c) == 1: - self.stdscr.addstr(y, x, c) + if isinstance(ch, str) and len(ch) == 1: + self.stdscr.addstr(y, x, ch) else: - self.stdscr.addch(y, x, c) + self.stdscr.addch(y, x, ch) except curses.error: pass @@ -511,3 +511,6 @@ mod = params[1] - 1 return [('keypress', keyname, None, mod)] + + +driver_class = CursesDriver