Add core Application (adjusted), Window (new version), Signal (replaces Emitter), Size (adjusted). Add application demo.
--- /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()
--- /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 ===')
+
--- 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)
+
--- /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)
--- /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)
--- /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)
--- 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