Add core Application (adjusted), Window (new version), Signal (replaces Emitter), Size (adjusted). Add application demo.
authorRadek Brich <radek.brich@devl.cz>
Mon, 17 Mar 2014 20:40:04 +0100
changeset 86 0978fb755d31
parent 85 6828c5b16087
child 87 ee5ea9671f28
Add core Application (adjusted), Window (new version), Signal (replaces Emitter), Size (adjusted). Add application demo.
demos/03_application.py
tuikit/core/application.py
tuikit/core/buffer.py
tuikit/core/signal.py
tuikit/core/size.py
tuikit/core/window.py
tuikit/driver/curses.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()
--- /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