############################################################################# ## This file is part of TunnelHack II. ## ##-------------------------------------------------------------------------## ## Copyright 2007-2008 Bryce Schroeder ## ## bryce.schroeder@gmail.com ## ## http://www.ferazelhosting.net/~bryce/ ## ##-------------------------------------------------------------------------## ## This program 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 3 of the License, or ## ## (at your option) any later version. ## ## This program 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 this program. If not, see . ## ############################################################################# from g import g import pygame, random from copy import copy ############################################################################## # Class: WindowGroup Specialized PyGame Sprite Group for windows # ############################################################################## class WindowGroup(pygame.sprite.OrderedUpdates): def __init__(self, *sprites): pygame.sprite.OrderedUpdates.__init__(self, *sprites) self._spritelist = [None] self.top_sprite = None self.modal_focus = None self.configurations = {} self.confignames = 0 # supplementary event handlers self.event_handler = [] self.drag_window = None self.allow_focus_changes = True def update(self, *args): self.request_focus(self.get_window_at(*pygame.mouse.get_pos())) last = len(self._spritelist) - 1 for e, s in enumerate(self._spritelist): if not s: continue if e == last: s.update(last=True, *args) else: s.update(*args) for handler in self.event_handler: handler.update() def suspend_focus_changes(self): self.allow_focus_changes = False def resume_focus_changes(self): self.allow_focus_changes = True def modal_busy(self): """ Does something have modal focus now? """ return self.modal_focus def get_at(self, x, y, exclude=None): """ Identify the widget at absolute coordinates x, y. Similar to an event in how it's handled, but different in its effect. Optionally, you may exclude a window or widget from consideration. """ # we itereate backwards so that we hit the last-rendered # window first (correctly). for window in reversed(self.sprites()): if exclude is window: continue if window.rect.collidepoint(x,y): return window.process_get_at(x,y) return None def get_window_at(self, x, y, exclude=None): """ Identify the widget at absolute coordinates x, y. Similar to an event in how it's handled, but different in its effect. Optionally, you may exclude a window or widget from consideration. """ # we itereate backwards so that we hit the last-rendered # window first (correctly). for window in reversed(self.sprites()): if exclude is window: continue if window.rect.collidepoint(x, y): return window return None def handle_events(self, events=None): """ Process events gotten from pygame. If the events parameter is None, it will fetch them itself. """ if not events: events = pygame.event.get() if self.modal_focus: s = [self.modal_focus] else: s = self.sprites() for w in reversed(s): if not w.accept_events: continue events = w.process_events(events) if self.event_handler: for handler in self.event_handler: events = handler.process_events(events) # catch unsucessful drags if self.drag_window: for evt in events: if evt.type == pygame.MOUSEBUTTONUP: self.drag_window.was_dropped(False) p=self.get_at(evt.pos[0], evt.pos[1], self.drag_window) if p: p.transition('mouse') print self.drag_window, self.top_sprite return events def add_event_handler(self, handler): """ Create a handler for events not captured by any window. Should be conformant to the API used by IObj. In fact, it may -be- an IEventHandler, which IObj itself is a subclass of. It should also include a method raw_event(pygameevent) that accepts pygame events that aren't of the type recognized by the IObj, e.g. quit events. """ self.event_handler.append(handler) handler.window_manager = self def set_top(self, sprite): """ Make this sprite the top one. (Last rendered). Precondition: sprite is a member of this group. """ if sprite is self.top_sprite: return if self.top_sprite: self.top_sprite.lose_window_focus() self._spritelist.remove(sprite) self.top_sprite = sprite self._spritelist.append(sprite) if self.top_sprite: self.top_sprite.gain_window_focus() def request_focus(self, sprite): """ Request the sprite to be made the top window. Right now it always goes along with the request unless the current one is being mouse-carried. """ if not self.allow_focus_changes: return #print "granting focus to", sprite if not self.top_sprite or self.top_sprite.release_focus(): self.set_top(sprite) def has_modal_focus(self): """ Does the window manager have a modally focused window? If so, returns it, else None. """ return self.modal_focus def add(self, *items): """ Add a new item (or several) to the group. """ pygame.sprite.OrderedUpdates.add(self, *items) for item in items: item.bind_wm(self) def get_by_name(self, name): """ Get a window by searching for its name. """ #replace by filter? for window in self.sprites(): if window.name == name: return window return None def add_configuration(self, name=None, config=None): """ Create a new stored window configuration. If config=None, then the current configuration will be saved. If name=None, then a new name will be generated and returned by this method. The configuration is a list of 3-tuples (windowname, x, y). You can also use the actual window objects rather than their names. """ if not name: name = 'config%d'%self.confignames self.confignames += 1 if not config: config = [] for window in self.sprites(): config.append((window, window.rect.x, window.rect.y)) else: ncfg = [] for window in config: if isinstance(window[0], str): ncfg.append((self.get_by_name(window[0]), window[1], window[2])) else: ncfg.append(window) config = ncfg self.configurations[name] = config return name def use_configuration(self, config, frames=20): """ Go to a window configuration. It can either be a list, or a string. If it is a string, it goes to a previously created named configuration. If it is a list, it is assumed to be a configuration (as specified above.) It must use windows themselves, not windownames, however. frames is how many frames it takes to go into the configuraiton. """ if isinstance(config, str): config = self.configurations[config] for window in config: window[0].go_to(window[1], window[2], frames) def start_drag(self, draggable, src, dc=None, offset=None): """ Start a drag and drop sequence using the draggable. (It ends with a drop event being generated when the user unclicks.) """ dragwin = dragging.IDraggable( None, source=src, delivery_confirmation=dc, draggable=draggable, offset=offset, name='_drag_item_' ) g.interface_layer.add(dragwin) self.suspend_focus_changes() self.drag_window = dragwin self.highlight_drag_receptors(draggable) assert self.drag_window != None def highlight_drag_receptors(self, item): """ Go through the windows and highlight all the drag receptors of item. """ for w in self.sprites(): w.highlight_drag_receptors(item) def unhighlight_drag_receptors(self, exclude=None): """ Go and unfocus the drag receptors. """ for w in self.sprites(): w.unhighlight_drag_receptors(exclude) #g.interface_layer.give_modal_focus(dragwin) def end_drag(self, target = None): self.remove(self.drag_window) self.resume_focus_changes() self.drag_window = None self.unhighlight_drag_receptors(target) self.drop_receiver = None def give_modal_focus(self, window): """ Give modal/exclusive focus to one window. """ if isinstance(window, str): window = self.get_by_name(window) self.set_top(window) self.modal_focus = window window.get_modal_focus() for win in self.sprites(): win.dirty_all() self.suspend_focus_changes() def release_modal_focus(self): """ Take away modal focus and return to normal. """ if not self.modal_focus: return self.modal_focus.lose_modal_focus() for win in self.sprites(): win.dirty_all() self.modal_focus = None self.resume_focus_changes() def sprites(self): return [s for s in self._spritelist if s] ############################################################################## # Class: IEventHandler Parent of all event-handling objects (including IObj) # ############################################################################## class IEventHandler(object): """ This class is an event handler skeleton. It has a method process_events that takes a list of raw pygame events, and turns them into function calls to the appropriate functions, e.g. it gets an event of type pygame.KEY and calls accept_key then, if it is accepted, handle_key. IObj is a subclass of this class. """ accept_events = True window_manager = None def __init__(self): self.accepted_keys = {} def update(self): pass def load(self): pass def process_events(self, events): """ Process a list of pygame Event objects. """ # TODO: it was a design mistake to not have seperate "internal rect" # veruss pygame external .rect. caused all sorts of trouble. change? # anyhow for now the archon can't be clicked. Well, technically # it can be, but only if it is at 0,0 or thereabouts. # REDESIGN - doing this now. -bryce remaining = [] for event in events: if event.type == pygame.MOUSEMOTION: if not self.process_mouse(*event.pos): remaining.append(event) else: self.request_focus() elif event.type == pygame.MOUSEBUTTONDOWN: if self.window_manager and self.window_manager.drag_window: pass elif event.button == 2 or ( event.button==1 and pygame.key.get_mods()&pygame.KMOD_CTRL): if not self.process_rightclick(*event.pos): remaining.append(event) elif event.button == 1: if not self.process_click(*event.pos): remaining.append(event) elif event.type == pygame.MOUSEBUTTONUP: if self.window_manager and self.window_manager.drag_window: if not self.process_drop(self.window_manager.drag_window, *event.pos): remaining.append(event) elif event.button == 2 or ( event.button==1 and pygame.key.get_mods()&pygame.KMOD_CTRL): if not self.process_rightunclick(*event.pos): remaining.append(event) elif event.button == 1: if not self.process_unclick(*event.pos): remaining.append(event) elif event.type == pygame.KEYDOWN: if not self.process_key(event.unicode, event.key, event.mod): remaining.append(event) elif hasattr(self, 'raw_event'): if not self.raw_event(event): remaining.append(event) else: remaining.append(event) return remaining def process_click(self, x, y): """ * This is a general description of all the process_* functions. * This documentation may be out of date. * The event system has been changed to not use pygame events. * All the process_* functions should return True if the event * was consumed. Process a single event. If the event is not to be consumed, return True, else False. Each handler has an acceptor function and an actor function. The acceptor either accepts or rejects the event. If it is accepted, the actor carries out whatever action should be performed. Note that an acceptor may take an event and then the actor may not do the full action we normally associate with something, e.g. a button acceptor may accept an unclick event but not be cliccked because it was not in the mouse-down state (it was a stray click.) Note also that if this object is an archon (great ancestor object, e.g. the main window), it will perform dynamic translation of the coordinates of mouse events. """ if self.accept_click(x,y): self.handle_click(x-self.rect.x,y-self.rect.y) return True else: return False def process_unclick(self, x, y): """ Check to see if this event is handled by the object's contents or iteself. """ if self.accept_unclick(x,y): self.handle_unclick(x-self.rect.x,y-self.rect.y) return True else: return False def process_get_at(self, x, y): """ process a get-at query to find a widget this click intersects with. Overload me! """ if self.rect.collidepoint(x,y): return self else: return None def process_rightclick(self, x, y): """ Porcess rightclick. """ if self.accept_rightclick(x,y): self.handle_rightclick(x-self.rect.x,y-self.rect.y) return True else: return False def process_rightunclick(self, x, y): """ Check to see if this event is handled by the object's contents or iteself. """ if self.accept_rightunclick(x,y): self.handle_rightunclick(x-self.rect.x,y-self.rect.y) return True else: return False def process_mouse(self, x, y): """ Check to see if this event is handled by the object's contents or iteself. """ if self.accept_mouse(x,y): self.handle_mouse(x-self.rect.x,y-self.rect.y) return True else: return False def process_key(self, uni, key, mods): """ Check to see if this event is handled by the object's contents or iteself. """ if self.accept_key(uni, key, mods): self.handle_key(uni, key, mods) return True else: return False def process_drop(self, dragwin, x, y): """ Check to see if this event is handled by the object's contents or iteself. """ if self.accept_drop(dragwin.draggable, x, y): self.handle_drop(dragwin.draggable) dragwin.was_dropped(True) return True else: return False def accept_click(self, x, y): """ See if this object will absorb particular click event. All sub-objects have already been given a chance to accept or reject it. Generally, this function will see if the click falls within the rect of this widget. x and y - coordinates within the archon. """ return False def accept_unclick(self, x, y): """ Accept an unclick event? The default version of this handler rejects incomplete clicks. """ return False def accept_rightclick(self, x, y): """ See if this object will absorb particular click event. All sub-objects have already been given a chance to accept or reject it. Generally, this function will see if the click falls within the rect of this widget. x and y - coordinates within the archon. """ return False def accept_rightunclick(self, x, y): """ Accept an unclick event? The default version of this handler rejects incomplete clicks. """ return False def accept_mouse(self, x, y): """ Accept mouse-over? x and y are coordinates within the archon. """ # cords are right. return False def accept_key(self, unicode, key, mods): """ Accept a key press event? """ return False #return unicode in self.accepted_keys def accept_drop(self, item, x, y): """ Accept or reject an item from being dropped into this object. (The drag-and-drop mechanism.) """ return False def handle_click(self, lx, ly): """ Called when the object is clicked. As with all event handlers, events (clicks, in this case) destined for objects contained within this widget (if such there be) have already been filtered out - it is this object itself being clicked. lx and ly are the local coordinates, inside the object. In implementing drag and drop, this function should handle the mechanics of dealing with the archon's drag-and-drop. """ pass def handle_unclick(self, lx, ly): """ Called when the object is no longer clicked, that is, when the button is realeased. You probably want to overload this function to do something useful! """ pass def handle_rightclick(self, lx, ly): """ Called when the object is right clicked. As with all event handlers, events (clicks, in this case) destined for objects contained within this widget (if such there be) have already been filtered out - it is this object itself being clicked. lx and ly are the local coordinates, inside the object. """ pass def handle_rightunclick(self, lx, ly): """ Called when the object is no longer clicked, that is, when the button is realeased. You probably want to overload this function to do something useful! """ pass def handle_key(self, unicode_str, key, mods): """ Called when a key is pressed. You probably want to replace this method by something more useful. """ pass def handle_mouse(self, lx, ly): """ Invoked when you mouse over this object. """ pass def handle_drop(self, item): """ This method is called when something is dropped on the widget (for drag and drop or pickup-drop.) """ pass ############################################################################## # Class: PropertyDict Convenience class for IObj properties. # ############################################################################## class PropertyDict(dict): def __getattribute__(self, k): if hasattr(dict, k): return dict.__getattribute__(self, k) else: return self[k] def __setattr__(self, k, v): self[k] = v ############################################################################## # Class: IObj Parent of all Interface Object classes. # ############################################################################## class IObj(pygame.sprite.Sprite, IEventHandler): """ This is the class from which other interface objects are derrived. When making a new interface object class, you should probably not derrive directly from this one, but rather a more similar object. attribute defaults: default values for parameters. The instance attribute parent is the immediate predecessor in the hierarchy of widgets (n.b. that the hierarchy of widgets - object _instances_ - is independent of the hierarchy of classes), the attribute archon designates the head of a 'tribe' of widgets, e.g. the main window. The archon manages some functionality like drag-and-drop. """ # States of the button # FOCUSED == mouse over it, or otherwise has focus. DISABLED = 0; NORMAL = 1; FOCUSED = 2; TOGGLED=3; CLICKED = 4 defaults = {'h': 1, 'w': 1, 'x':0, 'y':0, 'theme': 'Default', 'color': (40,42,40)} # just a little Finite State Machine. unsticky_transition_function = { #(DISABLED, None ): DISABLED, (FOCUSED, 'click' ): CLICKED, (CLICKED, 'unclick'): FOCUSED, (NORMAL, 'mouse' ): FOCUSED, (FOCUSED, 'unmouse'): NORMAL, (CLICKED, 'unmouse'): NORMAL } sticky_transition_function = { #(DISABLED, None ): DISABLED, (NORMAL, 'mouse' ): FOCUSED, (TOGGLED, 'mouse' ): CLICKED, (TOGGLED, 'click' ): NORMAL, (FOCUSED, 'unmouse'): NORMAL, (CLICKED, 'unmouse'): TOGGLED, (FOCUSED, 'click' ): CLICKED, (CLICKED, 'click' ): FOCUSED } transition_function = {True: sticky_transition_function, False:unsticky_transition_function} drop_destination = False last = None window_focused = False accept_keys = False def __init__(self, parent, **parameters): pygame.sprite.Sprite.__init__(self) IEventHandler.__init__(self) self.parent = parent self.window_manager = None self.links = [] # self.contents = [] self.state = self.NORMAL self.dirty = True # Velocity, for animating window movment. self.velocity = None self.vcount = 0 self.vdest = None # self.value = 0 # p is for 'parameters' self.p = PropertyDict(self.defaults) # bring in simple defaults for k,v in self.__class__.__dict__.items(): if k.startswith('p_'): self.p[k[2:]] = v self.p.update(parameters) self.name = self.p.get('name', '_untitled_') if not self.p.get('auto', False): self.compute_rect() if parent: self.archon = parent.archon else: self.archon = self self.theme = g.resources.get("Theme", self.p.theme) self.focus_item = None self.set_target(0,0, pygame.Surface((self.rect.w, self.rect.h), pygame.SRCALPHA, 32)#.convert_alpha() ) #self.image.fill((255,0,255)) #self.image.set_colorkey((255,0,255)) self.floaters = pygame.sprite.RenderUpdates() # for drag and drop, used only by archon # not used anymore #self.drag_window = None # MISC state self.mouse_follow = False # x-locked mouse follow. self.mouse_follow_y = False # self.init() self.load() if not self.archon is self: parent.add(self) def compute_rect(self): """ Compute the rectangle from the p size parameters. """ self.rect = pygame.rect.Rect(self.p.x*16, self.p.y*16, self.p.w*16, self.p.h*16) def init(self): """ User initalization function called at the end of the constructor. """ pass def update(self, last=False): """ Part of the pygame sprite interface. """ if self.animate(): self.dirty = True if self.archon is self: if self.mouse_follow: x, y = pygame.mouse.get_pos() self.set_target(x + self.mouse_follow[0], y + self.mouse_follow[1]) elif self.mouse_follow_y: x, y = pygame.mouse.get_pos() self.set_target(self.rect.x, y + self.mouse_follow_y[1]) if self.is_dirty(last): self.render(0, 0, last=last) self.floaters.update() if self.velocity: if self.vcount == 0: if self.vdest: self.rect.x, self.rect.y = self.vdest self.velocity = None self.vdest = None self.finish_movement() else: if self.focus_item: self.focus_item.transition('unmouse') self.true_position = (self.true_position[0]+self.velocity[0], self.true_position[1]+self.velocity[1]) self.rect.topleft = map(round, self.true_position) self.vcount -= 1 for child in self.contents: child.update(last) def animate(self): """ If your widget needs to change its appearence frame-by-frame, do so here. Remember to dirty the widget if you change the appearance! """ pass def get_absolute_position(self): if self.archon is self: return self.rect.x,self.rect.y x,y = self.parent.get_absolute_position() return x + self.rect.x, y+self.rect.y ############################################################################## # Class: IObj Methods related to event handling. # ############################################################################## # I wish python had lisp macros... def process_click(self, x, y): """ * This is a general description of all the process_* functions. * This documentation may be out of date. * The event system has been changed to not use pygame events. * All the process_* functions should return True if the event * was consumed. Process a single event. If the event is not to be consumed, return True, else False. Each handler has an acceptor function and an actor function. The acceptor either accepts or rejects the event. If it is accepted, the actor carries out whatever action should be performed. Note that an acceptor may take an event and then the actor may not do the full action we normally associate with something, e.g. a button acceptor may accept an unclick event but not be cliccked because it was not in the mouse-down state (it was a stray click.) Note also that if this object is an archon (great ancestor object, e.g. the main window), it will perform dynamic translation of the coordinates of mouse events. """ for child in self.contents: if child.process_click(x-self.rect.x,y-self.rect.y): return True if self.accept_click(x,y): self.transition('click') self.handle_click(x-self.rect.x,y-self.rect.y) return True else: return False def process_get_at(self, x, y, exclude=None): """ process a get-at query to find a widget this click intersects with. """ for child in self.contents: if child is exclude: continue if child.process_click(x-self.rect.x,y-self.rect.y): return child if self.rect.collidepoint(x,y): return self else: return None def process_unclick(self, x, y): """ Check to see if this event is handled by the object's contents or iteself. """ for child in self.contents: if child.process_unclick(x-self.rect.x,y-self.rect.y): return True if self.accept_unclick(x,y): self.transition('unclick') self.handle_unclick(x-self.rect.x,y-self.rect.y) return True else: return False def process_rightclick(self, x, y): """ *Rightclick """ for child in self.contents: if child.process_rightclick(x-self.rect.x,y-self.rect.y): return True if self.accept_rightclick(x,y): self.transition('click') self.handle_rightclick(x-self.rect.x,y-self.rect.y) return True else: return False def process_rightunclick(self, x, y): """ Check to see if this event is handled by the object's contents or iteself. """ for child in self.contents: if child.process_rightunclick(x-self.rect.x,y-self.rect.y): return True if self.accept_rightunclick(x,y): self.transition('unclick') self.handle_rightunclick(x-self.rect.x,y-self.rect.y) return True else: return False def process_mouse(self, x, y): """ Check to see if this event is handled by the object's contents or iteself. """ for child in self.contents: if child.process_mouse(x-self.rect.x,y-self.rect.y): return True if (self.drag_in_progress() and not self.accept_drop(self.drag_in_progress().draggable,x,y)): return False if self.accept_mouse(x,y): self.transition('mouse') self.handle_mouse(x-self.rect.x,y-self.rect.y) return True else: return False def process_key(self, uni, key, mod): """ Check to see if this event is handled by the object's contents or iteself. """ for child in self.contents: if child.process_key(uni, key, mod): return True if self.accept_key(uni, key, mod): self.handle_key(uni, key, mod) return True else: return False def process_drop(self, dragwin, x, y): """ Check to see if this event is handled by the object's contents or iteself. """ for child in self.contents: if child.process_drop(dragwin, x-self.rect.x, y-self.rect.y): return True if self.accept_drop(dragwin.draggable, x, y): self.handle_drop(dragwin.draggable) dragwin.was_dropped(self) return True elif self.archon is self and self.rect.collidepoint(x,y): dragwin.was_dropped(False) return True else: return False def accept_click(self, x, y): """ See if this object will absorb particular click event. All sub-objects have already been given a chance to accept or reject it. Generally, this function will see if the click falls within the rect of this widget. x and y - coordinates within the archon. """ return self.rect.collidepoint(x,y) def accept_unclick(self, x, y): """ Accept an unclick event? The default version of this handler rejects incomplete clicks. """ return self.state is self.CLICKED and self.rect.collidepoint(x,y) def accept_rightclick(self, x, y): """ See if this object will absorb particular click event. All sub-objects have already been given a chance to accept or reject it. Generally, this function will see if the click falls within the rect of this widget. x and y - coordinates within the archon. """ return self.rect.collidepoint(x,y) def accept_rightunclick(self, x, y): """ Accept an unclick event? The default version of this handler rejects incomplete clicks. """ return self.state is self.CLICKED and self.rect.collidepoint(x,y) def accept_mouse(self, x, y): """ Accept mouse-over? x and y are coordinates within the archon. """ # cords are right. return self.state is not self.DISABLED and self.rect.collidepoint(x,y) def accept_key(self, unicode, key, mods): """ Accept a key press event? """ return self.accept_keys and self.state in [self.CLICKED, self.FOCUSED] def accept_drop(self, item, x, y): """ Accept or reject an item from being dropped into this object. (The drag-and-drop mechanism.) """ return self.drop_destination and self.rect.collidepoint(x,y) def handle_click(self, lx, ly): """ Called when the object is clicked. As with all event handlers, events (clicks, in this case) destined for objects contained within this widget (if such there be) have already been filtered out - it is this object itself being clicked. lx and ly are the local coordinates, inside the object. In implementing drag and drop, this function should handle the mechanics of dealing with the archon's drag-and-drop. """ pass def handle_unclick(self, lx, ly): """ Called when the object is no longer clicked, that is, when the button is realeased. You probably want to overload this function to do something useful! """ pass def handle_key(self, unicode, key, mods): """ Called when a key is pressed. You probably want to replace this method by something more useful. """ #print self.accept_keys if unicode in self.accept_keys: self.accept_keys[unicode](mods) def handle_mouse(self, lx, ly): """ Invoked when you mouse over this object. """ pass def handle_drop(self, item): """ This method is called when something is dropped on the widget (for drag and drop or pickup-drop.) """ pass def lose_focus(self): """ Called when this object no longer has focus. """ if self.state in [self.FOCUSED, self.CLICKED]: self.set_state(self.NORMAL) ############################################################################## # Class: IObj Methods related to drag and drop. # ############################################################################## def drag_in_progress(self): """ Returns the window object that is currently "picked up", i.e. being dragged-and-dropped, or None if there is none. """ return self.archon.window_manager.drag_window def start_drag(self, item, offset=None): """ Pick up an item for drag-and-drop. If there was already an item, it is returned to sender - an error occurs if this cannot take place! """ if self.drag_in_progress(): self.drag_in_progress().was_dropped(False) self.archon.window_manager.start_drag(item, self, offset=offset) def return_to_sender(self, item): """ Return the drag and dropped item to it's origin. This is invoked when a drag and drop operation is aborted. """ self.picked_up_origin.handle_drop(item) def highlight_drag_receptors(self, item): for child in self.contents: child.highlight_drag_receptors(item) if self.accept_drop(item, self.rect.x+1,self.rect.y+1): self.transition('mouse') def unhighlight_drag_receptors(self, exclude=None): for child in self.contents: child.unhighlight_drag_receptors(exclude) if not self is exclude and self.drop_destination: self.transition('unmouse') ############################################################################## # Class: IObj Methods related to object state/value and linking. # ############################################################################## def add_link(self, linkee): """ Add linkee to a list of objects to receive messages from this object. """ self.links.append(linkee) def set_value(self, v): """ Set the object's value Widgets should use this when setting their own value. """ self.dirty = True self.value = v for link in self.links: link.get_message(self, value_change=self.value) def get_value(self): """ Return the widget's value. (E.g. the text of a textentry, a bool for a button's clicked status and so forth.) """ return self.value def get_message(self, sender, **kwargs): """ Called when the widget gets a message, as the result of being the link of some other object. """ pass def set_state(self, state): """ Change the state of an object directly. """ if self.state == state: return self.dirty = True self.state = state def transition(self, edge): """ Transition function for the state machine for clicking state. """ tf = self.transition_function[self.p.get('sticky',False)] transtate = (self.state, edge) if tf.has_key(transtate): self.set_state(tf[transtate]) if edge == 'mouse' and not self.archon.focus_item is self: if self.archon.focus_item: self.archon.focus_item.transition('unmouse') self.archon.focus_item = self ############################################################################## # Class: IObj Methods related to children. # ############################################################################## def add(self, child): """ Add a child object to this object's contents. """ self.contents.append(child) def load(self): """ Overload this method to add sub-objects. It will be called after init. """ pass def finish(self): """ Let the object know you are done adding children. This is optional for most objects. An exception is tabbedwindows. """ pass ############################################################################## # Class: IObj Methods related to rendering. # ############################################################################## def render(self, x, y, force=False, last=False): """ Draw the interface onto the target of the archon. NOTE that although it is intexpensive to do the rendering if nothing has changed - the object keeps track of it's 'dirty' state - rendering the object still requires blitting it to the target surface. The object's dirty state can be accessed via the function is_dirty(). You can avoid calling render at all if the object is not dirty, and spare yourself some blitting, provided that the interface has not been sullied with external changes. You probably want to overload render_self when making a new widget, not render, as render_self impelments widget-specific functionality. A note to people who may be surprised to see that this method takes no parameters - objects always render to a surface of their archon, typically a window of some sort. """ #print "RENDERING", self, self.rect, x, y self.window_focused = last if self.dirty or force or self.last != last: if self.archon is self: #self.image.fill((255,0,255)) self.image.fill((0,0,0,0)) force = self.render_self( self.archon.theme, self.archon.get_target(), x, y) else: force = self.render_self( self.archon.theme, self.archon.get_target(), x + self.rect.x, y + self.rect.y) self.dirty = False self.last = last else: force = False if not self.archon is self: x += self.rect.x y += self.rect.y for child in self.contents: child.render(x, y, force, last) self.render_overlay( self.archon.theme, self.archon.get_target(), x, y) def render_floaters(self, display): """ Render the floating items onto a display.. Floaters include menus, drag-and-dropped items, etc. Archon only. Returns list of update areas. """ assert self.archon is self return self.floaters.draw(display) def render_self(self, theme, target, px, py): """ Draw the widget. This is the bread-and-butter rendering function. It is called before the rendering function of the children, but if you clobber the child objects that may already be rendered (e.g. by blitting over the areas where they may be), you need to return True from this function, forcing the rerendering of children in spite of their possibly non-dirty status. If your widget is not supposed to have children or doesn't clobber them, return False. The px and py parame4ters refer to the coordinates to which you are to render. They have been translated from the local coordinates within the archon that are in the rect of an instance. """ return False def render_overlay(self, theme, target, px, py): """ Called after the children have been drawn, gives this widget a chance to draw over them (presumably to some useful end.) """ pass def is_dirty(self, last=False): """ Has this object's appearnce changed since the last time it was rendered? """ if self.last != last: return True if self.dirty: return True for child in self.contents: if child.is_dirty(): return True return False def dirty_all(self): """ Touch this object and all its children. Use this to signal external contamination of the surface onto which the interface is drawn, since such comtamination rarely respects widget boundries. """ self.dirty = True for child in self.contents: child.dirty_all() def set_target(self, x, y, surface=None): """ Set the pygame surface onto which this object and it's (direct and indirect) descendents will be drawn, and the x and y position on that surface. Typically the surface is the drawing buffer of the archon itself, but it can be drawn onto other things. This method should only be called on an archon. If surface is None, the surface will not be changed. """ assert self.archon is self if surface: self.image = surface self.rect.x = x self.rect.y = y self.dirty_all() def get_target(self): return self.archon.image def draw_rect(self, frame, target, px, py, width, height, stateful=False, state_dict=None): """ Draw a rectangle onto the target. frame is the (x,y) tuple of the upper-left corner of the desired frame graphic. px and py are the destination, in pixels. w and h are the width and height, in tiles. Minimum size of w and h is 2. """ state = self.state if stateful else 0 if state_dict is not None: state = state_dict.get(state, state) fx, fy = frame assert width >= 2 and height >= 2 theme_blit = self.archon.theme.blit # Draw the corners. for tx, ty, x, y in ( (0, 0, 0, 0), (0, 2, 0, height - 1), (2, 0, width - 1, 0), (2, 2, width - 1, height - 1)): theme_blit((fx + tx + 3 * state, fy + ty), target, px + x * 16, py + y * 16) # Draw the top and bottom. for x in xrange(1, width - 1): theme_blit((fx + 1 + 3 * state, fy), target, px + x * 16, py) theme_blit((fx + 1 + 3 * state, fy+2), target, px + x * 16, py + (height - 1) * 16) # Draw the sides. for y in xrange(1, height - 1): theme_blit((fx + 3 * state, fy+1), target, px, py + y * 16) theme_blit((fx +2 + 3 * state, fy+1), target, px + (width - 1) * 16, py + y * 16) # Draw the innards. for x in xrange(1, width - 1): for y in xrange(1, height - 1): theme_blit((fx + 1 + 3 * state, fy+1), target, px + x * 16, py + y * 16) def draw_empty_rect(self, frame, target, px, py, width, height, stateful=False, state_dict=None): """ Draw an empty rectangle onto the target. frame is the (x,y) tuple of the upper-left corner of the desired frame graphic. px and py are the destination, in pixels. w and h are the width and height, in tiles. Minimum size of w and h is 2. """ state = self.state if stateful else 0 if state_dict is not None: state = state_dict.get(state, state) fx, fy = frame assert width >= 2 and height >= 2 theme_blit = self.archon.theme.blit # Draw the corners. for tx, ty, x, y in ( (0, 0, 0, 0), (0, 2, 0, height - 1), (2, 0, width - 1, 0), (2, 2, width - 1, height - 1)): theme_blit((fx + tx + 3 * state, fy + ty), target, px + x * 16, py + y * 16) # Draw the top and bottom. for x in xrange(1, width - 1): theme_blit((fx + 1 + 3 * state, fy), target, px + x * 16, py) theme_blit((fx + 1 + 3 * state, fy+2), target, px + x * 16, py + (height - 1) * 16) # Draw the sides. for y in xrange(1, height - 1): theme_blit((fx + 3 * state, fy+1), target, px, py + y * 16) theme_blit((fx +2 + 3 * state, fy+1), target, px + (width - 1) * 16, py + y * 16) ############################################################################## # Class: IObj Methods related to window manager. # ############################################################################## def gain_window_focus(self): """ This method is called by the window manager when this window gains window focus (becomes the top window). """ pass def lose_window_focus(self): """ This method is called by the window manager when this window looses window focus (is no longer the top window). """ pass def release_focus(self): """ Asks the window to release focus. Should generally return True. Modal dialogs return False. """ True def get_modal_focus(self): """ Called by the window manager to inform the object that it has been given exclusive (modal) focus. """ return True def lose_modal_focus(self): """ Called by the wm to inform the object that it has lost modal focus. """ pass def close(self): """ Close the window. """ self.window_manager.remove(self) def bind_wm(self, wm): """ Bind a window manager. It will be kept aprised of any event receptions that should trigger focus changes. """ self.window_manager = wm def request_focus(self): """ Try to get the wm to focus the window containing this widget. """ if self.archon.window_manager: self.archon.window_manager.request_focus(self.archon) def pick_up(self, item): """ Pass an item up to the window manager (for drag and drop) """ self.window_manager.get_drag_item(item) def set_velocity(self, velocity, vcount): """ Set the velocity of the window, and how long it is to travel at that speed/direction. """ self.velocity = velocity self.vcount = vcount def go_to(self, x, y, time): """ Travel to x, y in time frames. """ time = float(time) dx = x - self.rect.x dy = y - self.rect.y self.set_velocity((dx / time, dy / time), time) self.vdest = x, y self.true_position = float(self.rect.x), float(self.rect.y) self.max_vcount = time def finish_movement(self): """ This method is called when a movement has ended. """ pass ############################################################################## # Class: IExclusiveGroup Class for radio buttons, etc # ############################################################################## class IExclusiveGroup(IObj): """ Class for exclusive groups, in which only one object can be clicked at a time. (Example: radio buttons.) """ def process_key(self, uni, key, mod): """ Check to see if this event is handled by the object's contents or iteself. """ if key == self.p.get('tab_key', None): current = self.get_current_selection() self.reset_all(current) current.process_click(0,0) return True for child in self.contents: if child.process_key(uni, key, mod): return True if self.accept_key(uni, key, mod): self.handle_key(uni, key, mod) return True else: return False def process_click(self, x, y): """ Process a click. If it registered with a child, unclick all the others. """ for child in self.contents: if child.process_click(x-self.rect.x,y-self.rect.y): self.reset_all(child) if self.accept_click(x,y): self.transition('click') self.handle_click(x-self.rect.x,y-self.rect.y) return True else: return False def reset_all(self, clicked): """ Reset the clicked state of all items except the one designated by clicked. (Slightly more robust than storing a "current clicked" variable """ for child in self.contents: if child is clicked: continue if child.state in [self.CLICKED,self.TOGGLED]: child.transition('click') def get_current_selection(self): for child in self.contents: if child.state in [self.CLICKED, self.TOGGLED]: return child return None def render_self(self, theme, target, x, y): return True import interface.IWindow as window import interface.IButton as button import interface.IText as text import interface.IBar as bar import interface.IModal as modal import interface.IDragging as dragging