#file: widget.py #Copyright (C) 2008 FunnyMan3595 #This file is part of Endgame: Singularity. #Endgame: Singularity is free software; you can redistribute it and/or modify #it under the terms of the GNU General Public License as published by #the Free Software Foundation; either version 2 of the License, or #(at your option) any later version. #Endgame: Singularity is distributed in the hope that it will be useful, #but WITHOUT ANY WARRANTY; without even the implied warranty of #MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #GNU General Public License for more details. #You should have received a copy of the GNU General Public License #along with Endgame: Singularity; if not, write to the Free Software #Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA #This file contains the widget class. import pygame from numpy import array import g import constants def unmask(widget): """Causes the widget to exist above its parent's fade mask. The widget's children will still be masked, unless they are unmasked themselves.""" unmask_all(widget) widget.mask_children = True def unmask_all(widget): """Causes the widget to exist above its parent's fade mask. The widget's children will not be masked.""" widget.self_mask = True widget.do_mask = lambda: None def call_on_change(data_member, call_me, *args, **kwargs): """Creates a data member that calls a function when changed.""" def get(self): return getattr(self, data_member) def set(self, my_value): if data_member in self.__dict__: change = (my_value != self.__dict__[data_member]) else: change = True if change: setattr(self, data_member, my_value) call_me(self, *args, **kwargs) return property(get, set) def set_on_change(data_member, set_me, set_value = True): """Creates a data member that sets another data member to a given value when changed.""" return call_on_change(data_member, setattr, set_me, set_value) def causes_rebuild(data_member): """Creates a data member that sets needs_rebuild to True when changed.""" return set_on_change(data_member, "needs_rebuild") def causes_redraw(data_member): """Creates a data member that sets needs_redraw to True when changed.""" return set_on_change(data_member, "needs_redraw") def causes_resize(data_member): """Creates a data member that sets needs_resize to True when changed.""" return set_on_change(data_member, "needs_resize") def causes_reposition(data_member): """Creates a data member that sets needs_reposition to True when changed.""" return set_on_change(data_member, "needs_reposition") def causes_update(data_member): """Creates a data member that sets needs_update to True when changed.""" return set_on_change(data_member, "needs_update") def propogate_need(data_member): """Creates a function that can be passed to call_on_change. When the data member changes to True, needs_update is set, and the True value is passed to all descendants.""" def do_propogate(self): if getattr(self, data_member, False): self.needs_update = True descendants = self.children[:] while descendants: child = descendants.pop() # Propogate to this child and its descendants, if needed. if not getattr(child, data_member, False): setattr(child, data_member, True) child._needs_update = True descendants += child.children return do_propogate class Widget(object): """A Widget is a GUI element. It can have one parent and any number of children.""" needs_redraw = call_on_change("_needs_redraw", propogate_need("_needs_redraw")) needs_resize = call_on_change("_needs_resize", propogate_need("_needs_resize")) needs_reposition = call_on_change("_needs_reposition", propogate_need("_needs_reposition")) needs_rebuild = causes_update("_needs_rebuild") def _propogate_update(self): if self._needs_update: target = self.parent while target and not target._needs_update: target._needs_update = True target = target.parent needs_update = call_on_change("_needs_update", _propogate_update) pos = causes_reposition("_pos") size = causes_resize("_size") anchor = causes_reposition("_anchor") visible = causes_redraw("_visible") def __init__(self, parent, pos, size, anchor = constants.TOP_LEFT): self.parent = parent self.children = [] self.pos = pos self.size = size self.anchor = anchor # "It's a widget!" self.add_hooks() self.is_above_mask = False self.self_mask = False self.mask_children = False self.visible = True self.needs_rebuild = True self.collision_rect = None # Set automatically by other properties. #self.needs_redraw = True #self.needs_full_redraw = True def add_hooks(self): if self.parent is not None: self.parent.children.append(self) # Won't trigger on the call from __init__, since there are no # children yet, but add_hooks may be explicitly called elsewhere to # undo remove_hooks. for child in self.children: child.add_hooks() def remove_hooks(self): # Localize the children list to avoid index corruption and O(N^2) time. children = self.children self.children = [] # Recurse to the children. for child in children: child.remove_hooks() if self.parent is not None: try: self.parent.children.remove(self) except ValueError: pass # Wasn't there to start with. def _parent_size(self): if self.parent == None: return g.real_screen_size else: return self.parent.real_size def _calc_size(self): """Internal method. Calculates and returns the real size of this widget. Override to create a dynamically-sized widget.""" parent_size = self._parent_size() size = list(self.size) for i in range(2): if size[i] > 0: size[i] = int(size[i] * g.real_screen_size[i]) elif size[i] < 0: size[i] = int( (-size[i]) * parent_size[i] ) return tuple(size) def get_real_size(self): """Returns the real size of this widget. To implement a dynamically-sized widget, override _calc_size, which will be called whenever the widget is resized, and set needs_resize when appropriate.""" return self._real_size real_size = property(get_real_size) def get_real_pos(self): """Returns the real position of this widget on its parent.""" vanchor, hanchor = self.anchor parent_size = self._parent_size() my_size = self.real_size if self.pos[0] >= 0: hpos = int(self.pos[0] * g.real_screen_size[0]) else: hpos = - int(self.pos[0] * parent_size[0]) if hanchor == constants.LEFT: pass elif hanchor == constants.CENTER: hpos -= my_size[0] // 2 elif hanchor == constants.RIGHT: hpos -= my_size[0] if self.pos[1] >= 0: vpos = int(self.pos[1] * g.real_screen_size[1]) else: vpos = - int(self.pos[1] * parent_size[1]) if vanchor == constants.TOP: pass elif vanchor == constants.MID: vpos -= my_size[1] // 2 elif vanchor == constants.BOTTOM: vpos -= my_size[1] return (hpos, vpos) real_pos = property(get_real_pos) def _make_collision_rect(self): """Creates and returns a collision rect for this widget.""" pos = array(self.real_pos) if self.parent: pos += self.parent.collision_rect[:2] return pygame.Rect(pos, self.real_size) def is_over(self, position): if position != (0,0): return self.collision_rect.collidepoint(position) else: return False def remake_surfaces(self): """Recreates the surfaces that this widget will draw on.""" size = self.real_size pos = self.real_pos if self.parent != None: try: self.surface = self.parent.surface.subsurface(pos + size) except ValueError: print "Warning: %r can't fit on its parent." % self print pos, size, self.parent.pos, self.parent.size wanted_rect = pos + size available_rect = self.parent.surface.get_rect() compromise = available_rect.clip(wanted_rect) self.surface = self.parent.surface.subsurface(compromise) else: # Recreate using the abstracted screen size, NOT the real one # g.set_screen() will calculate the proper g.real_screen_size self.surface = g.set_mode() self.surface.fill( (0,0,0,255) ) g.fade_mask = pygame.Surface(size, 0, g.ALPHA) g.fade_mask.fill( (0,0,0,175) ) def prepare_for_redraw(self): # First we handle any substance changes. if self.needs_rebuild: self.rebuild() # Then size changes. if self.needs_resize: self.resize() # Then position changes. if self.needs_reposition: self.reposition() # And finally we recurse to our descendants. for child in self.children: if child.needs_update and child.visible: child.prepare_for_redraw() def maybe_update(self): if self.needs_update: self.update() def update(self): # First we prepare everything for its redraw (if needed). self.prepare_for_redraw() self._update() # Oh, and if this is the top-level widget, we should flip the display. if not self.parent: pygame.display.flip() def _update(self): redrew_self = self.needs_redraw if self.needs_redraw: self.redraw() # Then we update any children below our fade mask. check_mask = [] above_mask = [] for child in self.children: if child.needs_update and child.visible: if child.is_above_mask: above_mask.append(child) else: check_mask += child._update() # Next, we handle the fade mask. if getattr(self, "faded", False): while check_mask: child = check_mask.pop() if not child.self_mask: child.surface.blit(g.fade_mask, (0,0)) elif child.mask_children: check_mask += child.children # And finally we update any children above the fade mask. for child in above_mask: child._update() # Update complete. self.needs_update = False # Any descendants we didn't check for masking get passed upwards. if redrew_self: # If we redrew this widget, we tell our parent to consider it # instead. The parent will recurse down to any descendants if # needed, and redraw already propogated down to them. check_mask = [self] return check_mask def rebuild(self): self.needs_rebuild = False def resize(self): self._real_size = self._calc_size() self.needs_resize = False self.needs_reposition = True self.needs_redraw = True def reposition(self): self.needs_reposition = False old_rect = self.collision_rect self.collision_rect = self._make_collision_rect() if not self.parent: self.remake_surfaces() self.needs_redraw = True elif ( (getattr(self, "surface", None) is None) or (old_rect is None) or (self.surface.get_parent() is not self.parent.surface) or (not self.collision_rect.contains(old_rect)) ): self.remake_surfaces() self.parent.needs_redraw = True elif self.collision_rect != old_rect: self.remake_surfaces() self.needs_redraw = True def redraw(self): self.needs_redraw = False if self.parent is None: self.surface.fill((0,0,0,255)) def add_handler(self, *args, **kwargs): """Handler pass-through.""" if self.parent: self.parent.add_handler(*args, **kwargs) def remove_handler(self, *args, **kwargs): """Handler pass-through.""" if self.parent: self.parent.remove_handler(*args, **kwargs) def add_key_handler(self, *args, **kwargs): """Handler pass-through.""" if self.parent: self.parent.add_key_handler(*args, **kwargs) def remove_key_handler(self, *args, **kwargs): """Handler pass-through.""" if self.parent: self.parent.remove_key_handler(*args, **kwargs) def add_focus_widget(self, *args, **kwargs): """Focus pass-through.""" if self.parent: self.parent.add_focus_widget(*args, **kwargs) def remove_focus_widget(self, *args, **kwargs): """Focus pass-through.""" if self.parent: self.parent.remove_focus_widget(*args, **kwargs) def took_focus(self, *args, **kwargs): """Focus pass-through.""" if self.parent: self.parent.took_focus(*args, **kwargs) class BorderedWidget(Widget): borders = causes_redraw("_borders") border_color = causes_redraw("_border_color") background_color = causes_redraw("_background_color") def __init__(self, parent, *args, **kwargs): self.parent = parent self.children = [] self.borders = kwargs.pop("borders", ()) self.border_color = kwargs.pop("border_color", g.colors["white"]) self.background_color = kwargs.pop("background_color", g.colors["blue"]) super(BorderedWidget, self).__init__(parent, *args, **kwargs) def rebuild(self): super(BorderedWidget, self).rebuild() if self.parent and self.background_color == g.colors["clear"]: self.parent.needs_redraw = True def reposition(self): super(BorderedWidget, self).reposition() if self.parent and self.background_color == g.colors["clear"]: self.parent.needs_redraw = True def redraw(self): super(BorderedWidget, self).redraw() # Fill the background. if self.background_color != g.colors["clear"]: self.surface.fill( self.background_color ) self.draw_borders() def draw_borders(self): my_size = self.real_size for edge in self.borders: if edge == constants.TOP: self.surface.fill(self.border_color, (0, 0, my_size[0], 1) ) elif edge == constants.LEFT: self.surface.fill(self.border_color, (0, 0, 1, my_size[1]) ) elif edge == constants.RIGHT: self.surface.fill(self.border_color, (my_size[0]-1, 0) + my_size) elif edge == constants.BOTTOM: self.surface.fill(self.border_color, (0, my_size[1]-1) + my_size) class FocusWidget(Widget): has_focus = causes_redraw("_has_focus") def __init__(self, *args, **kwargs): super(FocusWidget, self).__init__(*args, **kwargs) self.has_focus = True self.took_focus(self) def add_hooks(self): super(FocusWidget, self).add_hooks() self.parent.add_focus_widget(self) def remove_hooks(self): super(FocusWidget, self).remove_hooks() self.parent.remove_focus_widget(self)