Source code for albow.widget
from __future__ import division
import sys
import albow # used for translation update
from pygame import Rect, Surface, image
from pygame.locals import K_RETURN, K_KP_ENTER, K_ESCAPE, K_TAB, KEYDOWN, SRCALPHA
from pygame.mouse import set_cursor
from pygame.cursors import arrow as arrow_cursor
from pygame.transform import rotozoom
from vectors import add, subtract
from utils import frame_rect
import theme
from theme import ThemeProperty, FontProperty
import resource
from numpy import fromstring
debug_rect = False
debug_tab = True
root_widget = None
current_cursor = None
[docs]def overridable_property(name, doc=None):
"""Creates a property which calls methods get_xxx and set_xxx of
the underlying object to get and set the property value, so that
the property's behaviour may be easily overridden by subclasses."""
getter_name = intern('get_' + name)
setter_name = intern('set_' + name)
return property(
lambda self: getattr(self, getter_name)(),
lambda self, value: getattr(self, setter_name)(value),
None,
doc)
[docs]def rect_property(name):
def get(self):
return getattr(self._rect, name)
def set(self, value):
r = self._rect
old_size = r.size
setattr(r, name, value)
new_size = r.size
if old_size != new_size:
self._resized(old_size)
return property(get, set)
# noinspection PyPropertyAccess
[docs]class Widget(object):
# rect Rect bounds in parent's coordinates
# parent Widget containing widget
# subwidgets [Widget] contained widgets
# focus_switch Widget subwidget to receive key events
# fg_color color or None to inherit from parent
# bg_color color to fill background, or None
# visible boolean
# border_width int width of border to draw around widget, or None
# border_color color or None to use widget foreground color
# tab_stop boolean stop on this widget when tabbing
# anchor string of 'ltrb'
font = FontProperty('font')
fg_color = ThemeProperty('fg_color')
bg_color = ThemeProperty('bg_color')
bg_image = ThemeProperty('bg_image')
scale_bg = ThemeProperty('scale_bg')
border_width = ThemeProperty('border_width')
border_color = ThemeProperty('border_color')
sel_color = ThemeProperty('sel_color')
margin = ThemeProperty('margin')
menu_bar = overridable_property('menu_bar')
is_gl_container = overridable_property('is_gl_container')
tab_stop = False
enter_response = None
cancel_response = None
anchor = 'ltwh'
debug_resize = False
_menubar = None
_visible = True
_is_gl_container = False
redraw_every_event = True
tooltip = None
tooltipText = None
doNotTranslate = False
# 'name' is used to track widgets without parent
name = 'Widget'
def __init__(self, rect=None, **kwds):
if rect and not isinstance(rect, Rect):
raise TypeError("Widget rect not a pygame.Rect")
self._rect = Rect(rect or (0, 0, 100, 100))
#-# Translation live update preparation
self.__lang = albow.translate.getLang()
self.__update_translation = False
self.shrink_wrapped = False
#-#
self.parent = None
self.subwidgets = []
self.focus_switch = None
self.is_modal = False
self.set(**kwds)
self.root = self.get_root()
self.setup_spacings()
#-# Translation live update preparation
@property
def get_update_translation(self):
return self.__update_translation
[docs] def set_update_ui(self, v):
if v:
self.font = self.predict_font({})
for widget in self.subwidgets:
widget.set_update_ui(v)
if self.shrink_wrapped:
self.shrink_wrap()
if hasattr(self, 'calc_size'):
self.calc_size()
self.invalidate()
self.__update_translation = v
#-#
[docs] def setup_spacings(self):
def new_size(size):
size = float(size * 1000)
size = size / float(100)
size = int(size * resource.font_proportion / 1000)
return size
self.margin = new_size(self.margin)
if hasattr(self, 'spacing'):
self.spacing = new_size(self.spacing)
[docs] def set(self, **kwds):
for name, value in kwds.iteritems():
if not hasattr(self, name):
raise TypeError("Unexpected keyword argument '%s'" % name)
setattr(self, name, value)
[docs] def set_rect(self, x):
old_size = self._rect.size
self._rect = Rect(x)
self._resized(old_size)
# def get_anchor(self):
# if self.hstretch:
# chars ='lr'
# elif self.hmove:
# chars = 'r'
# else:
# chars = 'l'
# if self.vstretch:
# chars += 'tb'
# elif self.vmove:
# chars += 'b'
# else:
# chars += 't'
# return chars
#
# def set_anchor(self, chars):
# self.hmove = 'r' in chars and not 'l' in chars
# self.vmove = 'b' in chars and not 't' in chars
# self.hstretch = 'r' in chars and 'l' in chars
# self.vstretch = 'b' in chars and 't' in chars
#
# anchor = property(get_anchor, set_anchor)
resizing_axes = {'h': 'lr', 'v': 'tb'}
resizing_values = {'': [0], 'm': [1], 's': [0, 1]}
[docs] def set_resizing(self, axis, value):
chars = self.resizing_axes[axis]
anchor = self.anchor
for c in chars:
anchor = anchor.replace(c, '')
for i in self.resizing_values[value]:
anchor += chars[i]
self.anchor = anchor + value
def _resized(self, (old_width, old_height)):
new_width, new_height = self._rect.size
dw = new_width - old_width
dh = new_height - old_height
if dw or dh:
self.resized(dw, dh)
[docs] def resized(self, dw, dh):
if self.debug_resize:
print "Widget.resized:", self, "by", (dw, dh), "to", self.size
for widget in self.subwidgets:
widget.parent_resized(dw, dh)
[docs] def parent_resized(self, dw, dh):
debug_resize = self.debug_resize or getattr(self.parent, 'debug_resize', False)
if debug_resize:
print "Widget.parent_resized:", self, "by", (dw, dh)
left, top, width, height = self._rect
move = False
resize = False
anchor = self.anchor
if dw:
factors = [1, 1, 1] # left, width, right
if 'r' in anchor:
factors[2] = 0
if 'w' in anchor:
factors[1] = 0
if 'l' in anchor:
factors[0] = 0
if any(factors):
resize = factors[1]
move = factors[0] or factors[2]
#print "lwr", factors
left += factors[0] * dw / sum(factors)
width += factors[1] * dw / sum(factors)
#left = (left + width) + factors[2] * dw / sum(factors) - width
if dh:
factors = [1, 1, 1] # bottom, height, top
if 't' in anchor:
factors[2] = 0
if 'h' in anchor:
factors[1] = 0
if 'b' in anchor:
factors[0] = 0
if any(factors):
resize = factors[1]
move = factors[0] or factors[2]
#print "bht", factors
top += factors[2] * dh / sum(factors)
height += factors[1] * dh / sum(factors)
#top = (top + height) + factors[0] * dh / sum(factors) - height
if resize:
if debug_resize:
print "Widget.parent_resized: changing rect to", (left, top, width, height)
r = Rect((left, top, width, height))
self.rect = Rect((left, top, width, height))
elif move:
if debug_resize:
print "Widget.parent_resized: moving to", (left, top)
self._rect.topleft = (left, top)
rect = property(get_rect, set_rect)
left = rect_property('left')
right = rect_property('right')
top = rect_property('top')
bottom = rect_property('bottom')
width = rect_property('width')
height = rect_property('height')
size = rect_property('size')
topleft = rect_property('topleft')
topright = rect_property('topright')
bottomleft = rect_property('bottomleft')
bottomright = rect_property('bottomright')
midleft = rect_property('midleft')
midright = rect_property('midright')
midtop = rect_property('midtop')
midbottom = rect_property('midbottom')
center = rect_property('center')
centerx = rect_property('centerx')
centery = rect_property('centery')
visible = overridable_property('visible')
[docs] def add(self, arg, index=None):
if arg:
if isinstance(arg, Widget):
if index is not None:
arg.set_parent(self, index)
else:
arg.set_parent(self)
else:
for item in arg:
self.add(item)
[docs] def add_centered(self, widget):
w, h = self.size
widget.center = w // 2, h // 2
self.add(widget)
[docs] def set_parent(self, parent, index=None):
if parent is not self.parent:
if self.parent:
self.parent._remove(self)
self.parent = parent
if parent:
parent._add(self, index)
[docs] def all_parents(self):
widget = self
parents = []
while widget.parent:
parents.append(widget.parent)
widget = widget.parent
return parents
def _add(self, widget, index=None):
if index is not None:
self.subwidgets.insert(index, widget)
else:
self.subwidgets.append(widget)
if hasattr(widget, "idleevent"):
#print "Adding idle handler for ", widget
self.root.add_idle_handler(widget)
def _remove(self, widget):
if hasattr(widget, "idleevent"):
#print "Removing idle handler for ", widget
self.root.remove_idle_handler(widget)
self.subwidgets.remove(widget)
if self.focus_switch is widget:
self.focus_switch = None
[docs] def draw_all(self, surface):
if self.visible:
surf_rect = surface.get_rect()
bg_image = self.bg_image
if bg_image:
assert isinstance(bg_image, Surface)
if self.scale_bg:
bg_width, bg_height = bg_image.get_size()
width, height = self.size
if width > bg_width or height > bg_height:
hscale = width / bg_width
vscale = height / bg_height
bg_image = rotozoom(bg_image, 0.0, max(hscale, vscale))
r = bg_image.get_rect()
r.center = surf_rect.center
surface.blit(bg_image, r)
else:
bg = self.bg_color
if bg:
surface.fill(bg)
self.draw(surface)
bw = self.border_width
if bw:
bc = self.border_color or self.fg_color
frame_rect(surface, bc, surf_rect, bw)
for widget in self.subwidgets:
sub_rect = widget.rect
if debug_rect:
print "Widget: Drawing subwidget %s of %s with rect %s" % (
widget, self, sub_rect)
sub_rect = surf_rect.clip(sub_rect)
if sub_rect.width > 0 and sub_rect.height > 0:
try:
sub = surface.subsurface(sub_rect)
except ValueError, e:
if str(e) == "subsurface rectangle outside surface area":
self.diagnose_subsurface_problem(surface, widget)
else:
raise
else:
widget.draw_all(sub)
self.draw_over(surface)
[docs] def diagnose_subsurface_problem(self, surface, widget):
mess = "Widget %s %s outside parent surface %s %s" % (
widget, widget.rect, self, surface.get_rect())
sys.stderr.write("%s\n" % mess)
surface.fill((255, 0, 0), widget.rect)
[docs] def find_widget(self, pos):
for widget in self.subwidgets[::-1]:
if widget.visible:
r = widget.rect
if r.collidepoint(pos):
return widget.find_widget(subtract(pos, r.topleft))
return self
[docs] def handle_mouse(self, name, event):
self.augment_mouse_event(event)
self.call_handler(name, event)
self.setup_cursor(event)
[docs] def setup_cursor(self, event):
global current_cursor
cursor = self.get_cursor(event) or arrow_cursor
if cursor is not current_cursor:
set_cursor(*cursor)
current_cursor = cursor
[docs] def dispatch_key(self, name, event):
if self.visible:
if event.type == KEYDOWN:
menubar = self._menubar
if menubar and menubar.handle_command_key(event):
return
widget = self.focus_switch
if widget:
widget.dispatch_key(name, event)
else:
self.call_handler(name, event)
else:
self.call_parent_handler(name, event)
[docs] def get_focus(self):
widget = self
while 1:
focus = widget.focus_switch
if not focus:
break
widget = focus
return widget
[docs] def notify_attention_loss(self):
widget = self
while 1:
if widget.is_modal:
break
parent = widget.parent
if not parent:
break
focus = parent.focus_switch
if focus and focus is not widget:
self.root.notMove = False
focus.dispatch_attention_loss()
widget = parent
[docs] def dispatch_attention_loss(self):
widget = self
while widget:
widget.attention_lost()
widget = widget.focus_switch
[docs] def handle_command(self, name, *args):
method = getattr(self, name, None)
if method:
return method(*args)
else:
parent = self.next_handler()
if parent:
return parent.handle_command(name, *args)
[docs] def call_handler(self, name, *args):
method = getattr(self, name, None)
if method:
return method(*args)
else:
return 'pass'
[docs] def call_parent_handler(self, name, *args):
parent = self.next_handler()
if parent:
parent.call_handler(name, *args)
[docs] def local_to_global_offset(self):
d = self.topleft
parent = self.parent
if parent:
d = add(d, parent.local_to_global_offset())
return d
[docs] def key_down(self, event):
k = event.key
#print "Widget.key_down:", k ###
if k == K_RETURN or k == K_KP_ENTER:
if self.enter_response is not None:
self.dismiss(self.enter_response)
return
elif k == K_ESCAPE:
self.root.fix_sticky_ctrl()
if self.cancel_response is not None:
self.dismiss(self.cancel_response)
return
elif k == K_TAB:
self.tab_to_next()
return
self.call_parent_handler('key_down', event)
[docs] def is_inside(self, container):
widget = self
while widget:
if widget is container:
return True
widget = widget.parent
return False
@property
def is_hover(self):
return self.root.hover_widget is self
[docs] def present(self, centered=True):
#print "Widget: presenting with rect", self.rect
if self.root is None:
self.root = self.get_root()
if "ControlPanel" not in str(self):
self.root.notMove = True
if centered:
self.center = self.root.center
self.root.add(self)
try:
self.root.run_modal(self)
self.dispatch_attention_loss()
finally:
self.root.remove(self)
#print "Widget.present: returning", self.modal_result
if "ControlPanel" not in str(self):
self.root.notMove = False
return self.modal_result
[docs] def get_top_widget(self):
top = self
while top.parent and not top.is_modal:
top = top.parent
return top
[docs] def focus_on(self, subwidget):
old_focus = self.focus_switch
if old_focus is not subwidget:
if old_focus:
old_focus.dispatch_attention_loss()
self.focus_switch = subwidget
self.focus()
[docs] def focus_chain(self):
result = []
widget = self
while widget:
result.append(widget)
widget = widget.focus_switch
return result
[docs] def shrink_wrap(self):
contents = self.subwidgets
if contents:
rects = [widget.rect for widget in contents]
#rmax = Rect.unionall(rects) # broken in PyGame 1.7.1
rmax = rects.pop()
for r in rects:
rmax = rmax.union(r)
self._rect.size = add(rmax.topleft, rmax.bottomright)
#-# Translation live update preparation
self.shrink_wrapped = True
#-#
@staticmethod
[docs] def predict(self, kwds, name):
try:
return kwds[name]
except KeyError:
return theme.root.get(self.__class__, name)
[docs] def predict_attr(self, kwds, name):
try:
return kwds[name]
except KeyError:
return getattr(self, name)
[docs] def init_attr(self, kwds, name):
try:
return kwds.pop(name)
except KeyError:
return getattr(self, name)
[docs] def predict_font(self, kwds, name='font'):
return kwds.get(name) or theme.root.get_font(self.__class__, name)
[docs] def get_margin_rect(self):
r = Rect((0, 0), self.size)
d = -2 * self.margin
r.inflate_ip(d, d)
return r
[docs] def set_size_for_text(self, width, nlines=1):
if width is not None:
font = self.font
d = 2 * self.margin
if isinstance(width, basestring):
width, height = font.size(width)
width += d + 2
else:
height = font.size("X")[1]
self.size = (width, height * nlines + d)
[docs] def tab_to_next(self):
top = self.get_top_widget()
chain = top.get_tab_order()
try:
i = chain.index(self)
except ValueError:
return
target = chain[(i + 1) % len(chain)]
target.focus()
[docs] def collect_tab_order(self, result):
if self.visible:
if self.tab_stop:
result.append(self)
for child in self.subwidgets:
child.collect_tab_order(result)
# def tab_to_first(self, start = None):
# if debug_tab:
# print "Enter Widget.tab_to_first:", self ###
# print "...start =", start ###
# if not self.visible:
# if debug_tab: print "...invisible" ###
# self.tab_to_next_in_parent(start)
# elif self.tab_stop:
# if debug_tab: print "...stopping here" ###
# self.focus()
# else:
# if debug_tab: print "...tabbing to next" ###
# self.tab_to_next(start or self)
# if debug_tab: print "Exit Widget.tab_to_first:", self ###
#
# def tab_to_next(self, start = None):
# if debug_tab:
# print "Enter Widget.tab_to_next:", self ###
# print "...start =", start ###
# sub = self.subwidgets
# if sub:
# if debug_tab: print "...tabbing to first subwidget" ###
# sub[0].tab_to_first(start or self)
# else:
# if debug_tab: print "...tabbing to next in parent" ###
# self.tab_to_next_in_parent(start)
# if debug_tab: print "Exit Widget.tab_to_next:", self ###
#
# def tab_to_next_in_parent(self, start):
# if debug_tab:
# print "Enter Widget.tab_to_next_in_parent:", self ###
# print "...start =", start ###
# parent = self.parent
# if parent and not self.is_modal:
# if debug_tab: print "...telling parent to tab to next" ###
# parent.tab_to_next_after(self, start)
# else:
# if self is not start:
# if debug_tab: print "...wrapping back to first" ###
# self.tab_to_first(start)
# if debug_tab: print "Exit Widget.tab_to_next_in_parent:", self ###
#
# def tab_to_next_after(self, last, start):
# if debug_tab:
# print "Enter Widget.tab_to_next_after:", self, last ###
# print "...start =", start ###
# sub = self.subwidgets
# i = sub.index(last) + 1
# if debug_tab: print "...next index =", i, "of", len(sub) ###
# if i < len(sub):
# if debug_tab: print "...tabbing there" ###
# sub[i].tab_to_first(start)
# else:
# if debug_tab: print "...tabbing to next in parent" ###
# self.tab_to_next_in_parent(start)
# if debug_tab: print "Exit Widget.tab_to_next_after:", self, last ###
[docs] def inherited(self, attribute):
value = getattr(self, attribute)
if value is not None:
return value
else:
parent = self.next_handler()
if parent:
return parent.inherited(attribute)
def __contains__(self, event):
r = Rect(self._rect)
r.left = 0
r.top = 0
p = self.global_to_local(event.pos)
return r.collidepoint(p)
[docs] def gl_draw_all(self, root, offset):
if not self.visible:
return
from OpenGL import GL, GLU
rect = self.rect.move(offset)
if self.is_gl_container:
self.gl_draw_self(root, offset)
suboffset = rect.topleft
for subwidget in self.subwidgets:
subwidget.gl_draw_all(root, suboffset)
else:
try:
surface = Surface(self.size, SRCALPHA)
except Exception:
#size error?
return
self.draw_all(surface)
data = image.tostring(surface, 'RGBA', 1)
w, h = root.size
GL.glViewport(0, 0, w, h)
GL.glMatrixMode(GL.GL_PROJECTION)
GL.glLoadIdentity()
GLU.gluOrtho2D(0, w, 0, h)
GL.glMatrixMode(GL.GL_MODELVIEW)
GL.glLoadIdentity()
GL.glRasterPos2i(max(rect.left, 0), max(h - rect.bottom, 0))
GL.glPushAttrib(GL.GL_COLOR_BUFFER_BIT)
GL.glEnable(GL.GL_BLEND)
GL.glBlendFunc(GL.GL_SRC_ALPHA, GL.GL_ONE_MINUS_SRC_ALPHA)
GL.glDrawPixels(self.width, self.height,
GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, fromstring(data, dtype='uint8'))
GL.glPopAttrib()
GL.glFlush()