# HG changeset patch # User Radek Brich # Date 1358545010 -3600 # Node ID 2f61931520c9119365663571536dc2b94cee93b6 # Parent 15088f62c4ac27463eb8345e7e08b69bef497603 Rework layouts: Layout is now normal Container which places its children upon resize event. Drop TopWindow, top is now any subclass of Container. Add floater concept: floaters are widgets drawn over normal widgets, not clipped by parent. Add HScrollbar and Scrollbar abstract base class. diff -r 15088f62c4ac -r 2f61931520c9 demo_anchorlayout.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/demo_anchorlayout.py Fri Jan 18 22:36:50 2013 +0100 @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import locale +import os + +from tuikit.application import Application +from tuikit.window import Window +from tuikit.label import Label +from tuikit.button import Button +from tuikit.layout import AnchorLayout +from tuikit.common import Borders + + +class MyApplication(Application): + def __init__(self): + Application.__init__(self) + self.top = AnchorLayout() + self.top.name = 'top' + self.top.add_handler('keypress', self.on_top_keypress) + + win = Window() + win.name = 'window' + win.title = 'AnchorLayout demo' + win.resize(80, 25) + self.top.add(win, halign='left', valign='top') + self.win = win + + button_valign = Button('valign: ' + self.win.hints['valign'].selected) + button_valign.name = 'button_valign' + button_valign.add_handler('click', self.on_button_align_click) + win.add(button_valign, halign='center', margin=Borders(t=2)) + + button_halign = Button('halign: ' + self.win.hints['halign'].selected) + button_halign.name = 'button_halign' + button_halign.add_handler('click', self.on_button_align_click) + win.add(button_halign, halign='center', margin=Borders(t=4)) + + label_margin = Label(str(self.win.hints['margin'])) + label_margin.name = 'label_margin' + label_margin.add_handler('draw', self.on_label_margin_draw) + win.add(label_margin, halign='center', margin=Borders(t=6)) + + def on_button_align_click(self, ev): + align_type = ev.originator.label.split(':', 1)[0] + align = self.win.hints[align_type] + align.select_next() + ev.originator.label = '%s: %s' % (align_type, align.selected) + self.top.emit('resize') + return True + + def on_label_margin_draw(self, ev): + ev.originator.label = str(self.win.hints['margin']) + + def on_top_keypress(self, ev): + if ev.keyname == 'escape': + self.terminate() + return True + + +if __name__ == '__main__': + locale.setlocale(locale.LC_ALL, '') + os.environ['ESCDELAY'] = '25' # do not wait 1 second after pressing Escape key + app = MyApplication() + app.start() diff -r 15088f62c4ac -r 2f61931520c9 demo_checkbox.py --- a/demo_checkbox.py Thu Jan 10 00:03:34 2013 +0100 +++ b/demo_checkbox.py Fri Jan 18 22:36:50 2013 +0100 @@ -4,16 +4,15 @@ import locale locale.setlocale(locale.LC_ALL, '') -from tuikit import * +from tuikit import Application, VerticalLayout, ComboBox, Checkbox class MyApplication(Application): def __init__(self): Application.__init__(self) + self.top = VerticalLayout(homogeneous=False) self.top.add_handler('keypress', self.on_top_keypress) - self.top.layout = VerticalLayout(homogeneous=False) - combo = ComboBox(items=['abc', 'xyz']) self.top.add(combo) diff -r 15088f62c4ac -r 2f61931520c9 demo_colors.py --- a/demo_colors.py Thu Jan 10 00:03:34 2013 +0100 +++ b/demo_colors.py Fri Jan 18 22:36:50 2013 +0100 @@ -4,27 +4,27 @@ import locale locale.setlocale(locale.LC_ALL, '') -from tuikit import Application, Window, Label, VerticalLayout +from tuikit import Application, Window, Label, AnchorLayout, VerticalLayout class MyApplication(Application): def __init__(self): - Application.__init__(self) + Application.__init__(self, top_layout=AnchorLayout) self.top.add_handler('keypress', self.on_top_keypress) - win = Window() - self.top.add(win) - win.layout = VerticalLayout() + win = Window(inner_layout=VerticalLayout) + win.resize(30,10) + win.title = 'styles' for attr in ['blink', 'bold', 'standout', 'underline']: label = Label(attr) label.color = 'test-' + attr - self.top.add(label) + win.add(label) - self.top.layout = VerticalLayout() + self.top.add(win) - def applytheme(self): - Application.applytheme(self) + def apply_theme(self): + Application.apply_theme(self) self.driver.setcolor('test-blink', 'cyan on blue, blink') self.driver.setcolor('test-bold', 'cyan on blue, bold') self.driver.setcolor('test-standout', 'cyan on blue, standout') diff -r 15088f62c4ac -r 2f61931520c9 demo_editor.py --- a/demo_editor.py Thu Jan 10 00:03:34 2013 +0100 +++ b/demo_editor.py Fri Jan 18 22:36:50 2013 +0100 @@ -7,7 +7,6 @@ import os from tuikit.application import Application -from tuikit.editfield import EditField from tuikit.textedit import TextEdit @@ -16,14 +15,9 @@ Application.__init__(self) self.top.add_handler('keypress', self.on_top_keypress) - #edit = EditField(50, 'DlouhyTest12') - #self.top.add(edit) - t = open('tuikit/widget.py').read() - textedit = TextEdit(100, 40, t) - self.top.add(textedit) - textedit.x = 2 - self.textedit = textedit + textedit = TextEdit(t) + self.top.add(textedit, halign='fill', valign='fill') def on_top_keypress(self, ev): if ev.keyname == 'escape': diff -r 15088f62c4ac -r 2f61931520c9 demo_input.py --- a/demo_input.py Thu Jan 10 00:03:34 2013 +0100 +++ b/demo_input.py Fri Jan 18 22:36:50 2013 +0100 @@ -12,9 +12,8 @@ Application.__init__(self) self.text = '' - self.textedit = TextEdit(100, 40, self.text) - self.top.add(self.textedit) - self.textedit.x = 2 + self.textedit = TextEdit(self.text) + self.top.add(self.textedit, halign='fill', valign='fill') editbox = self.textedit.editbox editbox.add_handler('keypress', self.on_any_input) diff -r 15088f62c4ac -r 2f61931520c9 demo_layout.py --- a/demo_layout.py Thu Jan 10 00:03:34 2013 +0100 +++ b/demo_layout.py Fri Jan 18 22:36:50 2013 +0100 @@ -10,12 +10,11 @@ class MyApplication(Application): def __init__(self): Application.__init__(self) + + self.top = VerticalLayout(homogeneous=False) self.top.add_handler('keypress', self.on_top_keypress) - #self.top.borders = (1,1,1,1) - self.top.layout = VerticalLayout(homogeneous=False) - self._row_num = 0 self.buildrow() self.buildrow(expand=True) @@ -26,9 +25,8 @@ self.buildrow(homogeneous=True, fill=True, spacing=2) def buildrow(self, homogeneous=False, spacing=0, expand=False, fill=False): - hbox = Container() - hbox.sizereq.h = 2 - hbox.layout = HorizontalLayout(homogeneous=homogeneous, spacing=spacing) + hbox = HorizontalLayout(homogeneous=homogeneous, spacing=spacing) + hbox.resize(h=2) self._row_num += 1 hbox.name = 'hbox' + str(self._row_num) self.top.add(hbox) diff -r 15088f62c4ac -r 2f61931520c9 demo_treeview.py --- a/demo_treeview.py Thu Jan 10 00:03:34 2013 +0100 +++ b/demo_treeview.py Fri Jan 18 22:36:50 2013 +0100 @@ -9,7 +9,7 @@ class MyApplication(Application): def __init__(self): - Application.__init__(self) + Application.__init__(self, top_layout=VerticalLayout) self.top.add_handler('keypress', self.on_top_keypress) model = TreeModel() @@ -29,8 +29,6 @@ scroll.add(view) self.top.add(scroll, expand=True, fill=True) - self.top.layout = VerticalLayout() - def on_top_keypress(self, ev): if ev.keyname == 'escape': self.terminate() diff -r 15088f62c4ac -r 2f61931520c9 demo_window.py --- a/demo_window.py Thu Jan 10 00:03:34 2013 +0100 +++ b/demo_window.py Fri Jan 18 22:36:50 2013 +0100 @@ -8,28 +8,33 @@ from tuikit.application import Application from tuikit.window import Window from tuikit.button import Button +from tuikit import AnchorLayout +from tuikit.common import Borders class MyApplication(Application): def __init__(self): Application.__init__(self) + self.top = AnchorLayout() + self.top.name = 'top' self.top.add_handler('keypress', self.on_top_keypress) #edit = EditField(50, 'DlouhyTest12') #self.top.add(edit) win = Window() - self.top.add(win) + win.title = 'demo_window' + win.resize(80, 25) + self.top.add(win, halign='left', valign='top') button = Button('click!') - win.add(button) - button.x = 10 - button.y = 7 +# win.add(button, x=10, y=6) button.add_handler('click', self.on_button_click) self.button = button - subwin = Window(8,8) + subwin = Window() + subwin.name = 'subwin' win.add(subwin) @@ -48,9 +53,9 @@ locale.setlocale(locale.LC_ALL, '') os.environ['ESCDELAY'] = '25' # do not wait 1 second after pressing Escape key app = MyApplication() - #app.start() + app.start() - cProfile.run('app.start()', 'demo_window.appstats') - p = pstats.Stats('demo_window.appstats') - p.sort_stats('time', 'cumulative').print_stats(20) + #cProfile.run('app.start()', 'demo_window.appstats') + #p = pstats.Stats('demo_window.appstats') + #p.sort_stats('time', 'cumulative').print_stats(20) diff -r 15088f62c4ac -r 2f61931520c9 docs/index.rst --- a/docs/index.rst Thu Jan 10 00:03:34 2013 +0100 +++ b/docs/index.rst Fri Jan 18 22:36:50 2013 +0100 @@ -18,19 +18,3 @@ colors driver -.. inheritance-diagram:: tuikit.application - tuikit.events - tuikit.widget - tuikit.container - tuikit.window - tuikit.button - tuikit.label - tuikit.scrollbar - tuikit.textedit - tuikit.editfield - tuikit.editbox - tuikit.combobox - tuikit.menu - tuikit.menubar - tuikit.treeview - diff -r 15088f62c4ac -r 2f61931520c9 docs/widget.rst --- a/docs/widget.rst Thu Jan 10 00:03:34 2013 +0100 +++ b/docs/widget.rst Fri Jan 18 22:36:50 2013 +0100 @@ -16,11 +16,11 @@ .. attribute:: x y - + Position inside parent widget. Modified by layout manager. - + .. attribute:: width height - + Actual size. Modified by layout manager. diff -r 15088f62c4ac -r 2f61931520c9 sdlterm/src/sdlterm.cc --- a/sdlterm/src/sdlterm.cc Thu Jan 10 00:03:34 2013 +0100 +++ b/sdlterm/src/sdlterm.cc Fri Jan 18 22:36:50 2013 +0100 @@ -371,9 +371,6 @@ bool Terminal::wait_event(Event &event, Uint32 timeout) { - static SDL_Event sdl_event; - bool event_ready = false; - // use timer to simulate SDL_WaitEventTimeout, which is not available in SDL 1.2 SDL_TimerID timer_id = NULL; if (timeout) @@ -381,6 +378,9 @@ timer_id = SDL_AddTimer(timeout, _wait_event_callback, NULL); } + // loop until we have something to return + SDL_Event sdl_event; + bool event_ready = false; while (SDL_WaitEvent(&sdl_event) && !event_ready) { switch (sdl_event.type) diff -r 15088f62c4ac -r 2f61931520c9 tuikit/__init__.py --- a/tuikit/__init__.py Thu Jan 10 00:03:34 2013 +0100 +++ b/tuikit/__init__.py Fri Jan 18 22:36:50 2013 +0100 @@ -9,7 +9,7 @@ from tuikit.editbox import EditBox from tuikit.editfield import EditField from tuikit.label import Label -from tuikit.layout import VerticalLayout, HorizontalLayout, GridLayout +from tuikit.layout import VerticalLayout, HorizontalLayout, GridLayout, AnchorLayout from tuikit.menu import Menu from tuikit.menubar import MenuBar from tuikit.pager import Pager diff -r 15088f62c4ac -r 2f61931520c9 tuikit/application.py --- a/tuikit/application.py Thu Jan 10 00:03:34 2013 +0100 +++ b/tuikit/application.py Fri Jan 18 22:36:50 2013 +0100 @@ -3,7 +3,7 @@ import logging import time -from tuikit.container import Container +from tuikit.layout import AnchorLayout class Timer: @@ -51,66 +51,64 @@ timeout[0] = adjusted_delay -class TopWindow(Container): - - '''Top window of an application. Covers entire screen.''' - - def __init__(self): - '''Create top window.''' - Container.__init__(self) - self.top = self - self.timer = Timer() - - def draw(self, driver, x=0, y=0): - """Draw the top window and its children. - - This is entry point for full screen redraw. - - """ - driver.erase() - Container.draw(self, driver, x, y) - - class Application: '''Application class. Defines main loop.''' - def __init__(self, driver = 'sdl'): + def __init__(self, top_layout=AnchorLayout, driver='curses'): '''Create application.''' - - self.top = TopWindow() - '''Top window.''' + self._setup_logging() + + # Top widget + self._top = None + self._timer = Timer() + self.top = top_layout() self.quit = False + #: Driver class instance (render + input), e.g. DriverCurses. self.driver = self.get_driver_instance(driver) - '''Driver class instance (render + input), e.g. DriverCurses.''' + def _setup_logging(self): self.log = logging.getLogger('tuikit') self.log.setLevel(logging.DEBUG) handler = logging.FileHandler('./tuikit.log') formatter = logging.Formatter('%(asctime)s %(levelname)-5s %(message)s', '%y-%m-%d %H:%M:%S') handler.setFormatter(formatter) self.log.addHandler(handler) - self.log.info('=== start ===') + + @property + def top(self): + return self._top + + @top.setter + def top(self, value): + self._top = value + self._top.top = value + self._top.timer = self._timer + self._top.add_handler('draw', self._on_top_draw) + + def _on_top_draw(self, ev): + ev.driver.erase() def start(self): '''Start application. Runs main loop.''' - self.driver.start(self.mainloop) + self.log.info('=== start ===') + self.driver.start(self.main_loop) def terminate(self): '''Terminate application.''' self.quit = True - def mainloop(self): + def main_loop(self): '''The main loop.''' - self.applytheme() - self.top.size = self.driver.size # link top widget size to screen size - self.top.emit('resize') - timer = self.top.timer + self.apply_theme() + self._top._size = self.driver.size # link top widget size to screen size + self._top.emit('resize') + timer = self._timer while True: - self.top.draw(self.driver) + self._top.draw(self.driver, 0, 0) self.driver.commit() timeout = timer.nearest_timeout() @@ -121,13 +119,14 @@ if event[0] == 'quit': self.quit = True else: - self.top.emit(event[0], *event[1:]) + self._top.emit(event[0], *event[1:]) if self.quit: break + self.log.info('=== quit ===') - def applytheme(self): + def apply_theme(self): #TODO: allow custom colors: # e.g. "blue (#2020FF) on black (#101010), underline" # color in brackets is used when driver supports custom colors diff -r 15088f62c4ac -r 2f61931520c9 tuikit/button.py --- a/tuikit/button.py Thu Jan 10 00:03:34 2013 +0100 +++ b/tuikit/button.py Fri Jan 18 22:36:50 2013 +0100 @@ -10,18 +10,18 @@ def __init__(self, label=''): '''Create button with given label, size according to label.''' - w = len(label) + 4 - h = 1 - Widget.__init__(self, w, h) + Widget.__init__(self) #: Button label. - self.label = label + self._label = '' #: Text or graphics to be added before label self.prefix = '[' #: Text or graphics to be added after label self.suffix = ']' #: How should label be aligned if button has excess space - center | left | right self.align = 'center' + #: Padding between prefix/suffix and label + self.padding = 1 self.allow_focus = True @@ -29,12 +29,20 @@ self.bghi = 'button-active' self.highlight = False - # size - self.sizereq.w = w - self.sizereq.h = h - self.add_events('click', Event) + self.label = label + + @property + def label(self): + """Button label.""" + return self._label + + @label.setter + def label(self, value): + self._label = value + w = len(value) + len(self.prefix) + len(self.suffix) + 2 * self.padding + self._default_size.update(w, 1) def on_draw(self, ev): pad = self.width - len(self.label) - len(self.prefix) - len(self.suffix) diff -r 15088f62c4ac -r 2f61931520c9 tuikit/combobox.py --- a/tuikit/combobox.py Thu Jan 10 00:03:34 2013 +0100 +++ b/tuikit/combobox.py Fri Jan 18 22:36:50 2013 +0100 @@ -1,38 +1,40 @@ # -*- coding: utf-8 -*- -from tuikit.container import Container +from tuikit.layout import HorizontalLayout from tuikit.editfield import EditField from tuikit.button import Button from tuikit.menu import Menu +import logging -class ComboBox(Container): - def __init__(self, width=15, value='', items=[]): - Container.__init__(self, width, 1) - +class ComboBox(HorizontalLayout): + def __init__(self, value='', items=[]): + HorizontalLayout.__init__(self) + self._default_size.update(15, 1) self.allow_focus = True - self.colorprefix = 'combo:' - self._edit = EditField(width - 3, value) - self.add(self._edit) + self._edit = EditField(value) + self._edit.resize(self.width - 3, 1) + self.add(self._edit, expand=True, fill=True) self._btn = Button('v') self._btn.prefix = '' self._btn.suffix = '' - self._btn.x = width - 3 - self._btn.width = 3 + self._btn.resize(3, 1) self._btn.add_handler('click', self._on_btn_click) - self.add(self._btn) + self.add(self._btn, expand=False) self._menu = Menu(items) self._menu.hide() - self._menu.allow_layout = False - - def _set_top(self, value): - Container._set_top(self, value) - self.top.add(self._menu) + self.add_floater(self._menu) def _on_btn_click(self, ev): self._menu.show() + def on_resize(self, ev): + HorizontalLayout.on_resize(self, ev) + logging.getLogger('tuikit').info('w = %s', self.width) + self._menu.move(self.x, self.y + 1) + self._menu.resize(w=self.width) + diff -r 15088f62c4ac -r 2f61931520c9 tuikit/common.py --- a/tuikit/common.py Thu Jan 10 00:03:34 2013 +0100 +++ b/tuikit/common.py Fri Jan 18 22:36:50 2013 +0100 @@ -3,6 +3,37 @@ from tuikit.events import Event, Emitter +class Select: + def __init__(self, selected=None): + if not hasattr(self, '_options'): + raise TypeError('Cannot instantiate Select class.') + self._selected = selected or self._options[0] + + def update(self, selected): + if selected not in self._options: + raise ValueError('Select: %r not in options %r' % (selected, self._options)) + self._selected = selected + + def select_next(self): + i = self._options.index(self._selected) + try: + self._selected = self._options[i+1] + except IndexError: + self._selected = self._options[0] + + @property + def selected(self): + return self._selected + + def __repr__(self): + return self.__class__.__name__ + '(%r)' % self._selected + + +def make_select(*args): + name = ''.join([x.capitalize() for x in args]) + 'Select' + return type(name, (Select,), {'_options': args}) + + class Coords: '''2D coordinates.''' @@ -28,6 +59,19 @@ def __repr__(self): return 'Coords(x={0.x},y={0.y})'.format(self) + def update(self, x=None, y=None): + if isinstance(x, Coords) and y is None: + self.x, self.y = x + else: + if isinstance(x, int): + 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 + elif y is not None: + raise ValueError('Coords.update(): second parameter must be int') + class Size(Emitter): @@ -78,6 +122,22 @@ def __repr__(self): return 'Size(w={0._w},h={0._h})'.format(self) + def update(self, w=None, h=None): + old_w, old_h = self._w, self._h + if isinstance(w, Size) and h is None: + self._w, self._h = w + else: + if isinstance(w, int): + self._w = w + elif w is not None: + raise ValueError('Size.update(): first parameter must be int or Size') + if isinstance(h, int): + self._h = h + elif h is not None: + raise ValueError('Size.update(): second parameter must be int') + if self._w != old_w or self._h != old_h: + self.emit('change') + class Rect: @@ -103,11 +163,13 @@ ''' - def __init__(self, l=0, t=0, r=0, b=0): + def __init__(self, l=0, t=0, r=0, b=0, full=None): self.l = l # left self.t = t # top self.r = r # right self.b = b # bottom + if full is not None: + self.l = self.t = self.r = self.b = full def __getitem__(self, key): try: @@ -120,6 +182,16 @@ def __repr__(self): return 'Borders(l={0.l},t={0.t},r={0.r},b={0.b})'.format(self) + def update(self, *args, **kwargs): + if len(args) == 4: + self.l, self.t, self.r, self.b = args + elif len(args) == 1 and isinstance(args[0], Borders): + self.l, self.t, self.r, self.b = args[0] + elif len(args): + raise ValueError('Borders.update() takes exactly 4 positional arguments.') + for arg in kwargs: + setattr(self, arg, kwargs[arg]) + class ClipStack: @@ -176,6 +248,11 @@ # http://en.wikipedia.org/wiki/List_of_Unicode_characters#Geometric_shapes UP_ARROW = '▲' #curses.ACS_UARROW DOWN_ARROW = '▼' #curses.ACS_DARROW + LEFT_ARROW = '◀' + RIGHT_ARROW = '▶' + CIRCLE = '●' + DIAMOND = '◆' + MIDDLE_DOT = '·' # http://en.wikipedia.org/wiki/Box-drawing_characters LIGHT_SHADE = '░' #curses.ACS_BOARD diff -r 15088f62c4ac -r 2f61931520c9 tuikit/container.py --- a/tuikit/container.py Thu Jan 10 00:03:34 2013 +0100 +++ b/tuikit/container.py Fri Jan 18 22:36:50 2013 +0100 @@ -2,18 +2,20 @@ from tuikit.widget import Widget from tuikit.common import Borders, Coords +import logging class Container(Widget): '''Container widget. Base for any widget which can contain other widgets.''' - def __init__(self, width = 10, height = 10): + def __init__(self): '''Create container of requested size.''' - Widget.__init__(self, width, height) + Widget.__init__(self) #: List of child widgets. self.children = [] + self._hint_class = {} #: Offset for child widgets self.offset = Coords() @@ -36,16 +38,29 @@ self.trap_focus = False # if True, tab cycles inside container - def add(self, widget, **kwargs): '''Add widget into this container.''' self.children.append(widget) widget.parent = self widget.top = self.top - widget.hints.update(kwargs) + widget.hints.update({k:v() for k,v in self._hint_class.items()}) + for key in kwargs: + try: + widget.hints[key].update(kwargs[key]) + except AttributeError: + widget.hints[key] = widget.hints[key].__class__(kwargs[key]) if self.focuschild is None and widget.can_focus(): self.focuschild = widget + def add_floater(self, widget, **kwargs): + widget.floater = True + widget._size.update(widget._sizereq) + self.add(widget, **kwargs) + + def register_hints(self, *hints): + for hint_name, hint_class in zip(hints[::2], hints[1::2]): + self._hint_class[hint_name] = hint_class + @property def layout(self): return self._layout @@ -57,6 +72,9 @@ self._layout.container = self + def move_child(self, child, x, y): + pass + def _set_top(self, value): self._top = value for child in self.children: @@ -112,23 +130,37 @@ Widget.draw(self, driver, x, y) - for child in [ch for ch in self.children if not ch.allow_layout]: - child.draw(driver, x + child.x, y + child.y) - l, t, r, b = self.borders driver.clipstack.push(x+l, y+t, self.width-l-r, self.height-t-b) - for child in [ch for ch in self.children if ch.allow_layout]: + for child in [ch for ch in self.children if not ch.floater]: child.draw(driver, x + self.offset.x + child.x, y + self.offset.y + child.y) driver.clipstack.pop() + # draw floaters - no clipping, no colorprefix + if self.parent is None: + self.draw_floaters(driver, x, y) + if self.colorprefix: driver.popcolorprefix() driver.clipstack.pop() + def draw_floaters(self, driver, x, y): + logging.getLogger('tuikit').info('draw_floaters %s %r %r', self, x ,y ) + # draw floaters in this container + for child in [ch for ch in self.children if ch.floater]: + child.draw(driver, + x + self.offset.x + child.x, + y + self.offset.y + child.y) + # delve into child containers, draw their floaters + for child in [ch for ch in self.children if not ch.floater and isinstance(ch, Container)]: + child.draw_floaters(driver, + x + self.offset.x + child.x, + y + self.offset.y + child.y) + def on_resize(self, ev): for child in self.children: child.emit('resize') @@ -144,9 +176,9 @@ def on_mousedown(self, ev): handled = False for child in reversed(self.children): - if child.enclose(ev.wx, ev.wy): - childev = ev.childevent(child) - child.emit('mousedown', childev) + if child.enclose(ev.wx - self.offset.x, ev.wy - self.offset.y): + child_ev = ev.make_child_event(self, child) + child.emit('mousedown', child_ev) self.mousechild = child handled = True break @@ -154,23 +186,23 @@ def on_mouseup(self, ev): if self.mousechild: - childev = ev.childevent(self.mousechild) - self.mousechild.emit('mouseup', childev) + child_ev = ev.make_child_event(self, self.mousechild) + self.mousechild.emit('mouseup', child_ev) self.mousechild = None return True def on_mousemove(self, ev): if self.mousechild: - childev = ev.childevent(self.mousechild) - self.mousechild.emit('mousemove', childev) + child_ev = ev.make_child_event(self, self.mousechild) + self.mousechild.emit('mousemove', child_ev) return True def on_mousewheel(self, ev): handled = False for child in reversed(self.children): - if child.enclose(ev.wx, ev.wy): - childev = ev.childevent(child) - child.emit('mousewheel', childev) + if child.enclose(ev.wx - self.offset.x, ev.wy - self.offset.y): + child_ev = ev.make_child_event(self, child) + child.emit('mousewheel', child_ev) handled = True break return handled diff -r 15088f62c4ac -r 2f61931520c9 tuikit/driver_sdl.py --- a/tuikit/driver_sdl.py Thu Jan 10 00:03:34 2013 +0100 +++ b/tuikit/driver_sdl.py Fri Jan 18 22:36:50 2013 +0100 @@ -131,7 +131,7 @@ if len(self.colorstack): attr = self.colorstack[-1] else: - attr = 0 + attr = 7 self.sdlterm.set_attr(attr) def _color_by_name(self, name): diff -r 15088f62c4ac -r 2f61931520c9 tuikit/editbox.py --- a/tuikit/editbox.py Thu Jan 10 00:03:34 2013 +0100 +++ b/tuikit/editbox.py Fri Jan 18 22:36:50 2013 +0100 @@ -5,9 +5,9 @@ class EditBox(Widget): - def __init__(self, width=20, height=20, text=''): - Widget.__init__(self, width, height) - + def __init__(self, text=''): + Widget.__init__(self) + self._default_size.update(20, 20) self.allow_focus = True self.xofs = 0 diff -r 15088f62c4ac -r 2f61931520c9 tuikit/editfield.py --- a/tuikit/editfield.py Thu Jan 10 00:03:34 2013 +0100 +++ b/tuikit/editfield.py Fri Jan 18 22:36:50 2013 +0100 @@ -6,8 +6,9 @@ class EditField(Widget): - def __init__(self, width=10, value=''): - Widget.__init__(self, width, 1) + def __init__(self, value=''): + Widget.__init__(self) + self._default_size.update(10, 1) self.allow_focus = True diff -r 15088f62c4ac -r 2f61931520c9 tuikit/events.py --- a/tuikit/events.py Thu Jan 10 00:03:34 2013 +0100 +++ b/tuikit/events.py Fri Jan 18 22:36:50 2013 +0100 @@ -20,6 +20,9 @@ def __getitem__(self, key): return self.__dict__[key] + def __repr__(self): + return 'Event()' + class DrawEvent(Event): def __init__(self, driver, x, y): @@ -66,7 +69,7 @@ #: Mouse button: left=1, middle=2, right=3, wheelup=4 wheeldown=5 self.button = button - def childevent(self, child): + def make_child_event(self, container, child): ev = MouseEvent(self.x, self.y, self.button) # original local coordinates are new parent coordinates ev.px = self.wx @@ -97,7 +100,7 @@ It also serves as initializer, other methods of Emitter will not work if add_events was not called. - *events -- Arguments must be given in pairs. + events -- Arguments must be given in pairs. Each pair consists of event_name, event_class: event_name -- a string used in add_handler(), emit() etc. @@ -185,10 +188,6 @@ is passed to handlers. """ - logging.getLogger('tuikit').debug('Emit "%s" on %s %s', - event_name, - self.__class__.__name__, - getattr(self, 'name', None) or id(self)) # create event from specified event class, or use first argument if len(args) == 1 and isinstance(args[0], Event): event = args[0] @@ -197,6 +196,11 @@ # set originator to instance on which emit() was called event.originator = self event.event_name = event_name + # log + logging.getLogger('tuikit').debug('Emit %r %r on %s %s', + event_name, event, + self.__class__.__name__, + getattr(self, 'name', None) or id(self)) # call handlers from first to last, stop if satisfied for handler in self._event_handlers[event_name]: handled = handler(event) diff -r 15088f62c4ac -r 2f61931520c9 tuikit/label.py --- a/tuikit/label.py Thu Jan 10 00:03:34 2013 +0100 +++ b/tuikit/label.py Fri Jan 18 22:36:50 2013 +0100 @@ -5,8 +5,8 @@ class Label(Widget): def __init__(self, label=''): - Widget.__init__(self, len(label), 1) - + Widget.__init__(self) + self._default_size.update(len(label), 1) self.label = label self.color = 'normal' diff -r 15088f62c4ac -r 2f61931520c9 tuikit/layout.py --- a/tuikit/layout.py Thu Jan 10 00:03:34 2013 +0100 +++ b/tuikit/layout.py Fri Jan 18 22:36:50 2013 +0100 @@ -8,50 +8,119 @@ ''' import math +import logging -from tuikit.common import Rect +from tuikit.common import Coords, Size, Rect, Borders, make_select +from tuikit.container import Container -class Layout: - def __init__(self): - self._container = None - - @property - def container(self): - return self._container +class Layout(Container): + def _get_children(self): + return [child for child in self.children + if not child.floater and not child.hidden] - @container.setter - def container(self, value): - self._container = value - self._container.add_handler('resize', self._on_container_resize) - - def _on_container_resize(self, ev): - self.resize() - - def _getchildren(self): - return [child for child in self.container.children - if child.allow_layout and not child.hidden] - - def _getregion(self): + def _get_region(self): c = self.container bl, bt, br, bb = c.borders return Rect(bl, bt, c.width - bl - br, c.height - bt - bb) +class AnchorLayout(Layout): + + """Attach widgets to borders of container. + + Also allows absolute positioning when possible (not for center, fill). + + """ + + def __init__(self): + Layout.__init__(self) + self.register_hints( + 'halign', make_select('left', 'right', 'fill', 'center'), + 'valign', make_select('top', 'bottom', 'fill', 'center'), + 'margin', Borders, + ) + + def on_resize(self, ev): + for child in self._get_children(): + reqw = max(child.sizereq.w, child.sizemin.w) + reqh = max(child.sizereq.h, child.sizemin.h) + ha = child.hints['halign'].selected + va = child.hints['valign'].selected + margin = child.hints['margin'] + if ha == 'left': + x = margin.l + w = reqw + if ha == 'right': + x = self.width - margin.r - reqw + w = reqw + if ha == 'fill': + x = margin.l + w = self.width - margin.l - margin.r + if ha == 'center': + x = (self.width - reqw) // 2 + w = reqw + if va == 'top': + y = margin.t + h = reqh + if va == 'bottom': + y = self.height - margin.b - reqh + h = reqh + if va == 'fill': + y = margin.t + h = self.height - margin.t - margin.b + if va == 'center': + y = (self.height - reqh) // 2 + h = reqh + child._pos.update(x=x, y=y) + child._size.update(w=w, h=h) + + def move_child(self, child, x, y): + if not child in self.children: + raise ValueError('AnchorLayout.move(): Cannot move foreign child.') + margin = child.hints['margin'] + ha = child.hints['halign'].selected + va = child.hints['valign'].selected + ofsx = x - child.x + ofsy = y - child.y + if ha == 'left': + margin.l += ofsx + newx = margin.l + elif ha == 'right': + margin.r -= ofsx + newx = self.width - margin.r - child.sizereq.w + else: + # do not move when halign is center,fill + newx = child.x + if va == 'top': + margin.t += ofsy + newy = margin.t + elif va == 'bottom': + margin.b -= ofsy + newy = self.height - margin.b - child.sizereq.h + else: + # do not move when valign is center,fill + newy = child.y + child._pos.update(x=newx, y=newy) + + class LinearLayout(Layout): def __init__(self, homogeneous=False, spacing=0): Layout.__init__(self) self.homogeneous = homogeneous self.spacing = spacing + self.register_hints( + 'expand', bool, + 'fill', bool, + ) def _resize(self, ax1, ax2): - children = self._getchildren() - c = self.container - b1 = c.borders[ax1] - b2 = c.borders[ax2] + children = self._get_children() + b1 = self.borders[ax1] + b2 = self.borders[ax2] # available space - space1 = c.size[ax1] - b1 - c.borders[ax1+2] - space2 = c.size[ax2] - b2 - c.borders[ax2+2] + space1 = self._size[ax1] - b1 - self.borders[ax1+2] + space2 = self._size[ax2] - b2 - self.borders[ax2+2] # all space minus spacing space_to_divide = space1 - (len(children) - 1) * self.spacing @@ -59,7 +128,7 @@ # reduce by space acquired by children space_to_divide -= sum([child.sizereq[ax1] for child in children]) # number of children with expanded hint - expanded_num = len([ch for ch in children if ch.hint('expand')]) + expanded_num = len([ch for ch in children if ch.hints['expand']]) else: # all children are implicitly expanded expanded_num = len(children) @@ -70,40 +139,42 @@ offset = 0. for child in children: - child.position[ax1] = b1 + int(offset) - child.position[ax2] = b2 - child.size[ax1] = child.sizereq[ax1] - child.size[ax2] = space2 + pos = Coords() + pos[ax1] = b1 + int(offset) + pos[ax2] = b2 + size = Size() + size[ax1] = max(child.sizereq[ax1], child.sizemin[ax1]) + size[ax2] = space2 - if child.hint('expand') or self.homogeneous: + if child.hints['expand'] or self.homogeneous: maxsize = int(round(space_child + math.modf(offset)[0], 2)) offset += space_child + self.spacing if not self.homogeneous: maxsize += child.sizereq[ax1] offset += child.sizereq[ax1] - if child.hint('fill'): - child.size[ax1] = maxsize + if child.hints['fill']: + size[ax1] = maxsize else: - child.position[ax1] += int((maxsize - child.size[ax1])/2) + pos[ax1] += int((maxsize - size[ax1])/2) else: - offset += child.size[ax1] + offset += size[ax1] - child.emit('resize') - c.redraw() + child._pos.update(pos) + child._size.update(size) class VerticalLayout(LinearLayout): - def resize(self): - ax1 = 1 # primary dimension - y - ax2 = 0 # secondary dimension - x + def on_resize(self, ev): + ax1 = 1 # primary dimension = y + ax2 = 0 # secondary dimension = x self._resize(ax1, ax2) class HorizontalLayout(LinearLayout): - def resize(self): - ax1 = 0 # primary dimension - x - ax2 = 1 # secondary dimension - y + def on_resize(self, ev): + ax1 = 0 # primary dimension = x + ax2 = 1 # secondary dimension = y self._resize(ax1, ax2) diff -r 15088f62c4ac -r 2f61931520c9 tuikit/menu.py --- a/tuikit/menu.py Thu Jan 10 00:03:34 2013 +0100 +++ b/tuikit/menu.py Fri Jan 18 22:36:50 2013 +0100 @@ -2,13 +2,15 @@ from tuikit.widget import Widget from tuikit.events import GenericEvent +import logging class Menu(Widget): def __init__(self, items=[]): Widget.__init__(self) - self.width = max([len(x[0]) for x in items if x is not None]) + 4 - self.height = len(items) + 2 + w = max([len(x[0]) for x in items if x is not None]) + 4 + h = len(items) + 2 + self._default_size.update(w, h) self.allow_focus = True @@ -21,6 +23,7 @@ self.add_events('activate', GenericEvent) def on_draw(self, ev): + logging.getLogger('tuikit').info('menu draw %s %s %s', ev, ev.x, ev.y) ev.driver.pushcolor(self.bg) ev.driver.frame(ev.x, ev.y, self.width, self.height) i = 1 diff -r 15088f62c4ac -r 2f61931520c9 tuikit/scrollbar.py --- a/tuikit/scrollbar.py Thu Jan 10 00:03:34 2013 +0100 +++ b/tuikit/scrollbar.py Fri Jan 18 22:36:50 2013 +0100 @@ -2,67 +2,133 @@ from tuikit.widget import Widget from tuikit.events import Event +from tuikit.common import UnicodeGraphics -class VScrollbar(Widget): - def __init__(self, height=10): - Widget.__init__(self, 1, height) +class Scrollbar(Widget): - self._max = height - 3 - self._pos = 0 - self._thumbpos = 0 + """Abstract base class for scrollbars.""" + + def __init__(self): + Widget.__init__(self) - # interval for continuous scroll when user holds mouse on scrollbar arrow - self.scroll_delay = 0.150 - self.scroll_interval = 0.030 + # 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 - self.dragging = False - 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 + #: scrollbar styling + unigraph = UnicodeGraphics() + self.track_char = unigraph.MIDDLE_DOT + self.thumb_char = unigraph.CIRCLE + # change event is emitted when user moves scrollbar (even programmatically) self.add_events('change', Event) @property - def max(self): + def scroll_max(self): """Maximum for scrolling position.""" - return self._max + return self._scroll_max - @max.setter - def max(self, value): - self._max = value - self._update_thumbpos() + @scroll_max.setter + def scroll_max(self, value): + self._scroll_max = value + self._update_thumb_pos() @property - def pos(self): + def scroll_pos(self): """Scrolling position. Integer number between 0 and 'max'. """ - return self._pos + return self._scroll_pos - @pos.setter - def pos(self, value): - if self._pos != value: - self._pos = value - self._update_thumbpos() + @scroll_pos.setter + def scroll_pos(self, value): + if self._scroll_pos != value: + self._scroll_pos = value + self._update_thumb_pos() self.emit('change') - def _update_thumbpos(self): - self._thumbpos = 0 - if self._max and self._pos <= self._max: - self._thumbpos = int(round(self._pos / self._max * (self.height - 3))) + 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._default_size.update(1, 20) + def on_draw(self, ev): + ev.driver.pushcolor('normal') ev.driver.putch(ev.x, ev.y, ev.driver.unigraph.UP_ARROW) - for i in range(ev.y + 1, ev.y + self.height - 1): - ev.driver.putch(ev.x, i, ev.driver.unigraph.LIGHT_SHADE) - ev.driver.putch(ev.x, ev.y + 1 + self._thumbpos, ev.driver.unigraph.BLOCK) + for i in range(1, self.height - 1): + ev.driver.putch(ev.x, ev.y + i, self.track_char) + ev.driver.putch(ev.x, ev.y + 1 + self._thumb_pos, self.thumb_char) ev.driver.putch(ev.x, ev.y + self.height - 1, ev.driver.unigraph.DOWN_ARROW) + ev.driver.popcolor() def on_mousedown(self, ev): - self.dragging = False - self.move = None + self._dragging = False + self._move = None # arrow buttons if ev.wy == 0 or ev.wy == self.height - 1: if ev.wy == 0: @@ -72,47 +138,72 @@ self.add_timeout(self.scroll_delay, self._continuous_scroll) return # thumb bar - if ev.wy == 1 + self._thumbpos: - self.dragging = True - return - - 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 + if ev.wy == 1 + self._thumb_pos: + self._dragging = True return def on_mousemove(self, ev): - if self.dragging: + if self._dragging: self.drag(ev.wy) - def move_up(self): - if self._pos > 0: - self.pos = self._pos - 1 - self.move = 'up' + 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 + - def move_down(self): - if self._pos < self._max: - self.pos = self._pos + 1 - self.move = 'down' +class HScrollbar(Scrollbar): + def __init__(self): + Scrollbar.__init__(self) + self._default_size.update(20, 1) + + def on_draw(self, ev): + ev.driver.pushcolor('normal') + ev.driver.putch(ev.x, ev.y, ev.driver.unigraph.LEFT_ARROW) + for i in range(1, self.width - 1): + ev.driver.putch(ev.x + i, ev.y, self.track_char) + ev.driver.putch(ev.x + 1 + self._thumb_pos, ev.y, self.thumb_char) + ev.driver.putch(ev.x + self.width - 1, ev.y, ev.driver.unigraph.RIGHT_ARROW) + ev.driver.popcolor() - def drag(self, wy): - newpos = int(round((wy - 1) / (self.height - 3) * self._max)) - if newpos < 0: - newpos = 0 - if newpos > self._max: - newpos = self._max - if self._pos != newpos: - self.pos = newpos + 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 _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) + 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 15088f62c4ac -r 2f61931520c9 tuikit/scrollview.py --- a/tuikit/scrollview.py Thu Jan 10 00:03:34 2013 +0100 +++ b/tuikit/scrollview.py Fri Jan 18 22:36:50 2013 +0100 @@ -12,12 +12,13 @@ """ - def __init__(self, width=20, height=20): - Container.__init__(self, width, height) + def __init__(self): + Container.__init__(self) + self._default_size.update(20, 20) - self.vscroll = VScrollbar(height) + self.vscroll = VScrollbar() self.vscroll.add_handler('change', self._on_vscroll_change) - self.vscroll.allow_layout = False + self.vscroll.evade_layout = 2 self.add(self.vscroll) def add(self, widget, **kwargs): @@ -27,8 +28,8 @@ widget.add_handler('spotmove', self._on_child_spotmove) def on_resize(self, ev): - self.vscroll.x = self.size.w - 1 - self.vscroll.height = self.height + self.vscroll.move(x = self.size.w - 1) + self.vscroll.resize(h = self.height) self._update_vscroll_max() def _on_vscroll_change(self, ev): diff -r 15088f62c4ac -r 2f61931520c9 tuikit/textedit.py --- a/tuikit/textedit.py Thu Jan 10 00:03:34 2013 +0100 +++ b/tuikit/textedit.py Fri Jan 18 22:36:50 2013 +0100 @@ -1,26 +1,28 @@ # -*- coding: utf-8 -*- -from tuikit.container import Container +from tuikit.layout import AnchorLayout from tuikit.editbox import EditBox -from tuikit.scrollbar import VScrollbar +from tuikit.scrollbar import VScrollbar, HScrollbar +from tuikit.common import Borders -class TextEdit(Container): - def __init__(self, width=20, height=20, text=''): - Container.__init__(self, width, height) +class TextEdit(AnchorLayout): + def __init__(self, text=''): + AnchorLayout.__init__(self) + self._default_size.update(20, 20) - self.editbox = EditBox(width-2, height-2, text) - self.add(self.editbox) - self.editbox.x = 1 - self.editbox.y = 1 + self.editbox = EditBox(text) self.editbox.add_handler('scroll', self.on_editbox_scroll) self.editbox.add_handler('areasize', self.on_editbox_areasize) + self.add(self.editbox, halign='fill', valign='fill', margin=Borders(full=1)) - self.vscroll = VScrollbar(height - 2) - self.add(self.vscroll) - self.vscroll.x = width - 1 - self.vscroll.y = 1 + self.vscroll = VScrollbar() self.vscroll.add_handler('change', self.on_vscroll_change) + self.add(self.vscroll, halign='right', valign='fill', margin=Borders(t=1, b=1)) + + self.hscroll = HScrollbar() + self.hscroll.add_handler('change', self.on_hscroll_change) + self.add(self.hscroll, halign='fill', valign='bottom', margin=Borders(l=1, r=1)) self.on_editbox_areasize(None) @@ -34,15 +36,18 @@ ev.driver.frame(ev.x, ev.y, self.width, self.height) def on_editbox_scroll(self, ev): - self.vscroll.pos = self.editbox.yofs + self.vscroll.scroll_pos = self.editbox.yofs def on_editbox_areasize(self, ev): smax = len(self.editbox.lines) - self.editbox.height if smax < 0: smax = 0 - self.vscroll.max = smax + self.vscroll.scroll_max = smax def on_vscroll_change(self, ev): - self.editbox.set_yofs(self.vscroll.pos) + self.editbox.set_yofs(self.vscroll.scroll_pos) self.editbox.redraw() + def on_hscroll_change(self, ev): + self.editbox.xofs = self.hscroll.scroll_pos + self.editbox.redraw() diff -r 15088f62c4ac -r 2f61931520c9 tuikit/treeview.py --- a/tuikit/treeview.py Thu Jan 10 00:03:34 2013 +0100 +++ b/tuikit/treeview.py Fri Jan 18 22:36:50 2013 +0100 @@ -175,8 +175,10 @@ class TreeView(Widget): """Tree view displays data from tree model.""" - def __init__(self, model=None, width=20, height=20): - Widget.__init__(self, width, height) + def __init__(self, model=None): + Widget.__init__(self) + self._default_size.update(20, 20) + self.allow_focus = True # model diff -r 15088f62c4ac -r 2f61931520c9 tuikit/widget.py --- a/tuikit/widget.py Thu Jan 10 00:03:34 2013 +0100 +++ b/tuikit/widget.py Fri Jan 18 22:36:50 2013 +0100 @@ -3,12 +3,14 @@ from tuikit.events import Emitter, Event, DrawEvent, FocusEvent, KeyboardEvent, MouseEvent from tuikit.common import Coords, Size +import logging + class Widget(Emitter): '''Base class for all widgets.''' - def __init__(self, width = 10, height = 10): + def __init__(self): #: Widget name is used for logging etc. Not visible anywhere. self.name = None @@ -18,28 +20,30 @@ #: Top widget (same for every widget in one application). self._top = None - # Position inside parent widget. Modified by layout manager. - self.position = Coords() + #: Floating widget + self.floater = False - # Actual size. Modified by layout manager. - self.size = Size(width, height) - + ### placing and size + # Position inside parent widget. Modified by layout manager. + self._pos = Coords() + # Actual size. Modified only by layout manager. + self._size = Size(10, 10) + self._size.add_handler('change', lambda ev: self.emit('resize')) + #: 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)) + #: Size request. Equals to default size, unless changed by user. + #: Size request will be fulfilled by layout manager when possible. + self._sizereq = Size(1, 1) + self._sizereq.add_handler('change', lambda ev: self.emit('sizereq')) #: Minimal size of widget. Under normal circumstances #: widget will never be sized smaller than this. #: Tuple (w, h). Both must be integers >= 1. self.sizemin = Size(1, 1) - #: Maximum size of widget. Widget will never be sized bigger than this. #: Tuple (w, h). Integers >= 1 or None (meaning no maximum size or infinite). self.sizemax = Size(None, None) - #: Size request. This is default size of the widget. Will be fulfilled if possible. - #: Size(w, h). Integers >= 1 or None (meaning use minumal size). - self._sizereq = Size(10, 10) - self._sizereq.add_handler('change', lambda ev: self.emit('sizereq')) - - #: When false, the widget is not considered in layout. - self.allow_layout = True #: Allow keyboard focus for this widget. self.allow_focus = False @@ -75,41 +79,39 @@ 'focus', FocusEvent, 'unfocus', FocusEvent) + ### placing and size @property def x(self): - return self.position.x - - @x.setter - def x(self, value): - self.position.x = value + return self._pos.x @property def y(self): - return self.position.y + return self._pos.y - @y.setter - def y(self, value): - self.position.y = value - + def move(self, x, y): + if self.floater: + self._pos.update(x, y) + if self.parent: + self.parent.move_child(self, x, y) @property def width(self): - return self.size.w - - @width.setter - def width(self, value): - self.size.w = value - self.emit('resize') + return self._size.w @property def height(self): - return self.size.h + return self._size.h + + def resize(self, w=None, h=None): + """Set size request. - @height.setter - def height(self, value): - self.size.h = value - self.emit('resize') + It's up to parent container if request will be fulfilled. + + """ + self._sizereq.update(w, h) + #if self.parent: + # self.parent.emit('resize') @property def sizereq(self): @@ -121,6 +123,15 @@ """ return self._sizereq + def on_sizereq(self, ev): + # floater is not managet by layout, + # always set its size to sizereq + if self.floater: + logging.getLogger('tuikit').info('xy') + self._size.update(self._sizereq) + + ### misc + @property def spot(self): """Spot of visibility. @@ -220,10 +231,6 @@ ### - def hint(self, key): - if key in self.hints: - return self.hints[key] - def enclose(self, x, y): if self.hidden: return False diff -r 15088f62c4ac -r 2f61931520c9 tuikit/window.py --- a/tuikit/window.py Thu Jan 10 00:03:34 2013 +0100 +++ b/tuikit/window.py Fri Jan 18 22:36:50 2013 +0100 @@ -2,45 +2,48 @@ from tuikit.container import Container from tuikit.button import Button - +from tuikit.layout import AnchorLayout +from tuikit.common import Borders -class Window(Container): +import logging + +class Window(AnchorLayout): '''Window widget. It represents part of screen with border, close button and contents. Window can be moved, resized or closed by user.''' - def __init__(self, width=40, height=10): + def __init__(self, inner_layout=AnchorLayout): '''Create window of requested size.''' - Container.__init__(self, width, height) + AnchorLayout.__init__(self) + self.sizemin.update(8, 3) #: Window title. self.title = '' - self._closebutton = True - #: Allow user to resize window. self.resizable = True - + self._resizing = False #: Allow user to move window. self.movable = True - - self.resizing = False - self.moving = False + self._moving = False self.closebtn = Button('x') - self.closebtn.allow_layout = False - self.closebtn.x = self.width - 5 - self.closebtn.width = 3 + self.closebtn.sizereq.w = 3 self.closebtn.add_handler('click', self.on_closebtn_click) self.closebtn.bg = 'controls' self.closebtn.bghi = 'controls-active' - self.add(self.closebtn) + Container.add(self, self.closebtn, halign='right', margin=Borders(r=1)) + self._closebutton = True - self.borders = (1, 1, 1, 1) self.colorprefix = 'window:' + self._inner = inner_layout() + Container.add(self, self._inner, halign='fill', valign='fill', margin=Borders(1,1,1,1)) + + def add(self, widget, **kwargs): + self._inner.add(widget, **kwargs) @property def closebutton(self): @@ -58,7 +61,7 @@ ev.driver.frame(ev.x, ev.y, self.width, self.height) if self.resizable: - if self.resizing: + if self._resizing: ev.driver.pushcolor('controls-active') else: ev.driver.pushcolor('controls') @@ -66,7 +69,17 @@ ev.driver.popcolor() if self.title: - ev.driver.puts(ev.x + (self.width - len(self.title))//2, ev.y, self.title) + title = ' ' + self.title + ' ' + left = max(1, (self.width - len(title))//2) + right = self.width - len(title) - left + maxlen = self.width - 5 + if left > 1 and right < 4: + # move space from left to right to make space for close button + left -= 1 + if len(title) > maxlen: + title = title[:maxlen] + title = title[:-1] + ' ' + ev.driver.puts(ev.x + left, ev.y, title) ev.driver.fill(ev.x+1, ev.y+1, self.width-2, self.height-2) ev.driver.popcolor() @@ -75,24 +88,23 @@ def after_mousedown(self, ev): self.dragstart = (ev.wx, ev.wy) if self.resizable and ev.wx >= self.width - 1 and ev.wy >= self.height - 1: - self.resizing = True + self._resizing = True elif self.movable: - self.moving = True + self._moving = True self.origsize = (self.width, self.height) self.redraw(True) def after_mouseup(self, ev): - if self.resizing: - self.width = self.origsize[0] + ev.wx - self.dragstart[0] - self.height = self.origsize[1] + ev.wy - self.dragstart[1] - self.resizing = False - self.emit('resize') - elif self.moving: - self.x = ev.px - self.dragstart[0] - self.y = ev.py - self.dragstart[1] - self.moving = False + if self._resizing: + self.resize(self.origsize[0] + ev.wx - self.dragstart[0], + self.origsize[1] + ev.wy - self.dragstart[1]) + self._resizing = False + elif self._moving: + self.move(ev.px - self.dragstart[0], + ev.py - self.dragstart[1]) + self._moving = False self.redraw(True) @@ -105,21 +117,16 @@ #if x > self.parent.width-self.width: # return - if self.resizing: - self.width = self.origsize[0] + ev.wx - self.dragstart[0] - self.height = self.origsize[1] + ev.wy - self.dragstart[1] - self.emit('resize') - elif self.moving: - self.x = ev.px - self.dragstart[0] - self.y = ev.py - self.dragstart[1] + if self._resizing: + self.resize(self.origsize[0] + ev.wx - self.dragstart[0], + self.origsize[1] + ev.wy - self.dragstart[1]) + elif self._moving: + self.move(ev.px - self.dragstart[0], + ev.py - self.dragstart[1]) self.redraw(True) - def on_resize(self, ev): - self.closebtn.x = self.width - 5 - - def on_closebtn_click(self, ev): self.hide()