Exemplo n.º 1
0
    def __init__(self, text = '', pos=None, size=None, font_size=40, start_pos=None):
        self.size = MovingValue(Point((20,20)), get_default(size, Point((20,20))))
        pos = get_default(pos, Point((0,0)))
        start_pos = get_default(start_pos, Point((0,0)))
        self.pos = MovingValue(start_pos, pos, step=0.3)

        self.widgets = [] # sub-widgets
        self.focused_widgets = []
        self.hovered_widget = None
        self.trigger_lists = {'pre' : TriggerList(),
                              'post' : TriggerList(),}
                                  
        
        self.text = text
        self._font_size = font_size
        self.reset()
        
        self._init_params()
        self._init_event_triggers()

        self.max_undo = 25

        # TODO kick this outta here
        #from Ellipse import Ellipse
        from Shapes.Rectangle import Rectangle
        self.shape = Rectangle(pygame.Rect(self.get_current_rect()))
Exemplo n.º 2
0
    def __init__(self, node, *args, **kw):
        self.node = node
        self.style = style._make_style()
        self.text_widget = TextEdit(self.style, self._get_text, self._set_text, [all_printable], {'\r':'\n'})
        
        self.node.obs.add_observer(self, '_node_')
        Widget.__init__(self, *args, **kw)
        
        self._size = MovingValue(Point)
        self._pos = MovingValue(Point)
        from Shapes.Ellipse import Ellipse
        self.shape = Ellipse(pygame.Rect(0,0,1,1))

        self.obs_loc = Observable()
        self._register_keys()

        self.reset_max_text_size()
        self.mouse_area = MouseArea(self.in_bounds)
Exemplo n.º 3
0
    def __init__(self, size, *args, **kw):
        Widget.__init__(self, *args, **kw)
        self._size = MovingValue(Point, final=Point(size))
        self.nodes = set()
        self.edges = set()
        self.node_widgets = Dict()
        self.edge_widgets = Dict()
        self.sorted_widgets = SortedItems(self.node_widgets)

        self.layout = Layout()
        
        self.selected_widget_index = None
        self._update_index()

        self._node_link_start_pos = None
        self._node_link_source_node = None
        self._node_link_color = None
        
        self._register_keys()
        self._save_next_display_update = False
Exemplo n.º 4
0
class GraphWidget(Widget):
    bg_color=(0,0,0)
    activated_bg_color=(10,10,20)

    key_create_node = Key(pygame.KMOD_CTRL, pygame.K_a)
    key_delete_node = Key(pygame.KMOD_CTRL, pygame.K_d)
    key_next_node = Key(0, pygame.K_TAB)
    key_prev_node = Key(pygame.KMOD_SHIFT, pygame.K_TAB)
    key_select_node_right = Key(0, pygame.K_RIGHT)
    key_select_node_left = Key(0, pygame.K_LEFT)
    key_select_node_up = Key(0, pygame.K_UP)
    key_select_node_down = Key(0, pygame.K_DOWN)
    key_connect = Key(pygame.KMOD_CTRL, pygame.K_RETURN)
    key_disconnect = Key(pygame.KMOD_CTRL, pygame.K_BACKSPACE)
    key_cycle_layout = Key(pygame.KMOD_CTRL, pygame.K_k)
    #key_export_dot = Key(pygame.KMOD_CTRL, pygame.K_)
    key_export_snapshot = Key(pygame.KMOD_CTRL, pygame.K_x)

    key_save = Key(pygame.KMOD_CTRL, pygame.K_s)
    key_load = Key(pygame.KMOD_CTRL, pygame.K_l)
    
    def __init__(self, size, *args, **kw):
        Widget.__init__(self, *args, **kw)
        self._size = MovingValue(Point, final=Point(size))
        self.nodes = set()
        self.edges = set()
        self.node_widgets = Dict()
        self.edge_widgets = Dict()
        self.sorted_widgets = SortedItems(self.node_widgets)

        self.layout = Layout()
        
        self.selected_widget_index = None
        self._update_index()

        self._node_link_start_pos = None
        self._node_link_source_node = None
        self._node_link_color = None
        
        self._register_keys()
        self._save_next_display_update = False
        
    def __getstate__(self):
        d = Widget.__getstate__(self)
        del d['parenting_keymap']
        return d
    def __setstate__(self, d):
        Widget.__setstate__(self, d)
        self._register_keys()

    def _register_keys(self):
        self.parenting_keymap = Keymap()
        r = self.keymap.register_key
        r(self.key_create_node, keydown_noarg(self._create_new_node))
        r(self.key_cycle_layout, keydown_noarg(self._cycle_layout_engine))
        r(self.key_save, keydown_noarg(self._save))
        r(self.key_load, keydown_noarg(self._load))
        r(self.key_export_snapshot, keydown_noarg(self._export_snapshot))
        self._set_next_keymap()

    def get_size(self):
        return tuple(self._size.current)
    def set_size(self, p):
        self._size.final = p 
    size = property(get_size, set_size)
    
    def _node_connect(self, e):
        for node in e.source, e.target:
            if node not in self.nodes:
                self.add_node(node)
        if e not in self.edges:
            self.add_edge(e)
    def _node_disconnect(self, e):
        self.remove_edge(e)

    def update_edges_lines(self, widget, node):
        center = Point(widget.final_rect().center)
        for edge in node.connections['in']:
            edge_w = self.edge_widgets[edge]
            edge_w.line.final[-1] = center.copy()
            edge_w.line.reset()
        for edge in node.connections['out']:
            edge_w = self.edge_widgets[edge]
            edge_w.line.final[0] = center.copy()
            edge_w.line.reset()
        
    def _node_widget_loc_pos_set(self, widget, node, new_pos):
        self.update_edges_lines(widget, node)
    def _node_widget_loc_size_set(self, widget, node, new_size):
        self.update_edges_lines(widget, node)
        
    def add_edge(self, edge):
        edge.obs.add_observer(self, '_edge_')
        self.edges.add(edge)
        if edge.source not in self.node_widgets:
            self.add_node(edge.source)
            
        source = self.node_widgets[edge.source].final_rect().center
        if edge.target not in self.node_widgets:
            self.add_node(edge.target)
            
        target = self.node_widgets[edge.target].final_rect().center
        w = EdgeWidget(edge, partial(self.node_widgets.get),
                       MovingLine(list, [Point(source), Point(target)]))
        self.edge_widgets[edge] = w
        self.update_layout()
    def remove_edge(self, edge):
        edge.obs.remove_observer(self)
        del self.edge_widgets[edge]
        self.edges.remove(edge)
        self.update_layout()

    def add_node(self, node):
        if node in self.nodes:
            return
        self.nodes.add(node)
        w = NodeWidget(node)
        self.node_widgets[node] = w
        node.obs.add_observer(self, '_node_')
        w.obs_loc.add_observer(self, '_node_widget_loc_', w, node)
        loop.loop.mouse_map.push_area(w.mouse_area, partial(self._widget_mouse_event, node, w))
        # They might be adding a node that is connected to some other,
        # yet-to-be added nodes
        for edge in node.iter_all_connections():
            self.add_edge(edge)
        self.update_layout()
        return w
    def remove_node(self, node):
        node.disconnect_all()
        w = self.node_widgets[node]
        loop.loop.mouse_map.remove_area(w.mouse_area)
        w.obs_loc.remove_observer(self)
        node.obs.remove_observer(self)
        del self.node_widgets[node]
        self.nodes.remove(node)
        self._update_index()
        if self._node_link_source_node:
            self._node_link_source_node = None
        self.update_layout()
        return w

    def generate_groups(self):
        groups = {'0':[]}
        for node in self.nodes:
            groups['0'].append(node)
        return groups

    def update_layout(self):
        groups = self.generate_groups()
        self.layout.update(groups, self.size, self.node_widgets, self.edge_widgets)
        
    def update(self):
        if self.node_widgets:
            import math
            # todo rewrite this shitty code
            sizes = {}
            slots = {}
            slot_places = {}
            average = 0
            num = len(self.node_widgets)
            for w in self.node_widgets.values():
                size = w.reset_max_text_size()
                sizes[w] = size
            for w, size in sizes.iteritems():
                dist = int(round(math.log(size + 1)/2.))
                slot = dist 
                slots.setdefault(slot, size)
                slots[slot] = min(slots[slot],size)
                
                slot_places[w] = slot

            for w,slot in slot_places.iteritems():
                size = sizes[w]
                new_max_size = slots[slot]
                if new_max_size < size:
                    w.reset_max_text_size(new_max_size)
                w.update()

##             for w in self.node_widgets.values():
##                 w.reset_max_text_size()
##                 w.update()
        for w in self.edge_widgets.itervalues():
            w.update()
        self._size.update()
        
        
    def _draw(self, surface, pos):
        for w in self.edge_widgets.values():
            p = Point(pos)
            w._draw(surface, pos)
        for w in self.node_widgets.values():
            # for our children, pos is the parent's pos offset
            # because of how NodeWidget works.
            w._draw(surface, pos)

        if self._node_link_start_pos is not None:
            n,w = self.selected()
            if w is not None:
                start_pos = self._node_link_start_pos()
                end_pos = self._node_link_end_pos()
                draw.line(surface, self._node_link_color, start_pos, end_pos)

        if self._save_next_display_update:
            self._save_next_display_update = None
            draw.save(surface.subsurface(pygame.Rect(pos, self.size)), self._save_next_display_update)

    def selected(self):
        if self.selected_widget_index is None:
            return None, None
        return self.sorted_widgets[self.selected_widget_index]
    
    def _set_next_keymap(self):
        self.keymap.set_next_keymap(self.parenting_keymap)
        if self.selected_widget_index is not None:
            self.parenting_keymap.set_next_keymap(self.selected()[1].keymap)
            r = self.parenting_keymap.register_key
            r(self.key_delete_node, keydown_noarg(self._delete_selected_node))
            r(self.key_next_node, keydown_noarg(self._next_node))
            r(self.key_prev_node, keydown_noarg(self._prev_node))
            r(self.key_select_node_right, keydown_noarg(self._select_node_right))
            r(self.key_select_node_left, keydown_noarg(self._select_node_left))
            r(self.key_select_node_up, keydown_noarg(self._select_node_up))
            r(self.key_select_node_down, keydown_noarg(self._select_node_down))
            if self.key_connect not in self.parenting_keymap:
                r(self.key_connect, keydown_noarg(self._start_connect))
            if self.key_disconnect not in self.parenting_keymap:
                r(self.key_disconnect, keydown_noarg(self._start_disconnect))
        else:
            ur = self.parenting_keymap.unregister_key
            ur(self.key_next_node)
            ur(self.key_prev_node)
            ur(self.key_select_node_right)
            ur(self.key_select_node_left)
            ur(self.key_select_node_up)
            ur(self.key_select_node_down)
            ur(self.key_delete_node)
            ur(self.key_connect)
            ur(self.key_disconnect)
            self.parenting_keymap.set_next_keymap(self.focus_keymap)
    
    def _set_index(self, index):
        if self.selected_widget_index != index:
            self.selected_widget_index = index
        self._update_index()

    def _update_index(self):
        if not self.node_widgets:
            self.selected_widget_index = None
        elif self.selected_widget_index is not None:
            self.selected_widget_index %= len(self.sorted_widgets)
            self._set_next_keymap()
            
    def _find_widget_index(self, w):
        for i, (node, widget) in enumerate(self.sorted_widgets):
            if w == widget:
                return i
            
    def _add_index(self, amount):
        if self.selected_widget_index is None:
            index = 0
        else:
            index = self.selected_widget_index
            l = len(self.sorted_widgets)
            index += amount
            index %= l
        self._set_index(index)


    def _next_node(self):
        '''Next node'''
        self._add_index(1)
    def _prev_node(self):
        '''Previous node'''
        self._add_index(-1)

    def _select_node_right(self):
        '''Select the next node to the right'''
        def dist(pos1, pos2):
            if pos1[0] < pos2[0]:
                return None
            return pos1[0] - pos2[0]
        self._select_node_dir(dist)
        
    def _select_node_left(self):
        '''Select the next node to the left'''
        def dist(pos1, pos2):
            if pos1[0] > pos2[0]:
                return None
            return pos2[0] - pos1[0]
        self._select_node_dir(dist)

    def _select_node_up(self):
        '''Select the next node above'''
        def dist(pos1, pos2):
            if pos1[1] > pos2[1]:
                return None
            return pos2[1] - pos1[1]
        self._select_node_dir(dist)

    def _select_node_down(self):
        '''Select the next node below'''
        def dist(pos1, pos2):
            if pos1[1] < pos2[1]:
                return None
            return pos1[1] - pos2[1]
        self._select_node_dir(dist)

    def _select_node_dir(self, distance_between):
        closest_right = None
        min_dist = None
        n, w = self.selected()
        if not w:
            return
        for widget in self.node_widgets.itervalues():
            if widget == w:
                continue
            dist = distance_between(widget.pos, w.pos)
            if dist is None:
                continue
            if closest_right is None or dist < min_dist:
                closest_right = widget
                min_dist = dist
                
        if closest_right is not None:
            i = self._find_widget_index(closest_right)
            self._set_index(i)
            
    def _create_new_node(self):
        '''Create new node'''
        n = Graph.Node()
        w = self.add_node(n)
        self._set_index(self._find_widget_index(w))
        
    def _delete_selected_node(self):
        '''Delete selected node'''
        n, w = self.sorted_widgets[self.selected_widget_index]
        self._add_index(1)
        self.remove_node(n)

    def _node_linking_started(self, key, start_func, connect_func, color, doc):
        r = self.parenting_keymap.register_key
        ur = self.parenting_keymap.unregister_key
        start_node, start_node_widget = self.selected()
        assert start_node is not None
        def _end_connect():
            ur(key)
            r(key, keydown_noarg(start_func))

            end_node, end_node_widget = self.sorted_widgets[self.selected_widget_index]
            self._node_link_func(start_node, end_node)
            self._node_link_start_pos = None
            self._node_link_end_pos = None
        _end_connect.__doc__ = doc
        
        def _start_pos():
            return start_node_widget.rect().center
        def _end_pos():
            n, w = self.selected()
            if w is None:
                return None
            return w.rect().center

        ur(key)
        r(key, keydown_noarg(_end_connect))
        self._node_link_start_pos = _start_pos
        self._node_link_end_pos = _end_pos
        self._node_link_color = color
        self._node_link_func = connect_func
        
    def _connect_func(self, node1, node2):
        node1.connect_node(node2)
    def _disconnect_func(self, node1, node2):
        node1.disconnect_node(node2)
        
    def _start_connect(self):
        '''Sets the source node to connect'''
        self._node_linking_started(self.key_connect, self._start_connect, self._connect_func, (0,255,0), "Sets the target node to connect")

    def _start_disconnect(self):
        '''Sets the source node to disconnect'''
        self._node_linking_started(self.key_disconnect, self._start_disconnect, self._disconnect_func, (255,0,0), "Sets the target node to disconnect")

    def _widget_mouse_event(self, node, widget, event):
        #print event
        connect_mod = pygame.key.get_mods() & pygame.KMOD_SHIFT 
        #disconnect_mod = pygame.key.get_modes() & pygame.KMOD_SHIFT 
        if event.type == pygame.MOUSEBUTTONDOWN:
            i = self._find_widget_index(widget)
            self._set_index(i)
            if connect_mod and self._node_link_start_pos is None:
                def _start_pos():
                    return widget.rect().center
                self._node_link_source_node = node
                self._node_link_start_pos = _start_pos
                self._node_link_end_pos = pygame.mouse.get_pos
                self._node_link_color = (0,255,0)
                self._node_link_func = self._connect_func
        elif event.type == pygame.MOUSEBUTTONUP:
            if self._node_link_source_node is not None:
                self._node_link_start_pos = None
                self._node_link_end_pos = None
                self._node_link_func(self._node_link_source_node, node)
                self._node_link_source_node = None
                self._node_link_color = None
                
        
    def _cycle_layout_engine(self):
        '''Change to the next layout engine'''
        self.layout.cycle_layout_engines()
        self.update_layout()

    def _save(self):
        '''Save'''
        import pickle
        f=open('save.pkl', 'wb')
        pickle.dump(self.nodes,f,2)

    def _load(self):
        '''Load'''
        import pickle
        f=open('save.pkl', 'rb')
        nodes = pickle.load(f)
        for node in tuple(self.nodes):
            self.remove_node(node)
        self._set_next_keymap()
        for node in nodes:
            # TODO if we are loading nodes in addition to existing nodes,
            # make sure ID's are re-allocated to prevent collisions.
            node.id = Graph.persistent_id(node)
            for edge in node.iter_all_connections():
                edge.id = Graph.persistent_id(edge)
        for node in nodes:
            self.add_node(node)
            

    def _export_dot(self):
        '''Export the graph to a .dot file'''
        d = Graph.generate_dot(self.generate_groups())
        print d
        open('save.dot', 'wb').write(d)

    def _export_snapshot(self):
        '''Export the graph to a snapshot image'''
        self._save_next_display_update = 'snapshot.bmp'
Exemplo n.º 5
0
class NodeWidget(Widget):
    bg_color=(10,10,100)
    activated_bg_color=(40,40,160)
    fg_color=(70,70,150)
    activated_fg_color=(100,100,250)

    key_stop_edit=Key(0, pygame.K_ESCAPE)
    
    def __init__(self, node, *args, **kw):
        self.node = node
        self.style = style._make_style()
        self.text_widget = TextEdit(self.style, self._get_text, self._set_text, [all_printable], {'\r':'\n'})
        
        self.node.obs.add_observer(self, '_node_')
        Widget.__init__(self, *args, **kw)
        
        self._size = MovingValue(Point)
        self._pos = MovingValue(Point)
        from Shapes.Ellipse import Ellipse
        self.shape = Ellipse(pygame.Rect(0,0,1,1))

        self.obs_loc = Observable()
        self._register_keys()

        self.reset_max_text_size()
        self.mouse_area = MouseArea(self.in_bounds)

    def __getstate__(self):
        d= self.__dict__.copy()
        del d['focus_keymap']
        del d['keymap']
        return d
    def __setstate__(self, d):
        for k,v in d.iteritems():
            self.__dict__[k] = v
        self.focus_keymap = Keymap()
        self.keymap = Keymap()
        self._register_keys()

    def _register_keys(self):
        r = self.focus_keymap.register_key
        r(Key(pygame.KMOD_CTRL, pygame.K_RIGHT), keydown_noarg(self._move_right))
        r(Key(pygame.KMOD_CTRL, pygame.K_LEFT), keydown_noarg(self._move_left))
        r(Key(pygame.KMOD_CTRL, pygame.K_UP), keydown_noarg(self._move_up))
        r(Key(pygame.KMOD_CTRL, pygame.K_DOWN), keydown_noarg(self._move_down))

        r(Key(0, pygame.K_RETURN), keydown_noarg(self._edit_value))
        

    def in_bounds(self, p):
        return self.rect().collidepoint(tuple(p))
        
    def _get_text(self):
        if self.node.value is None:
            return ''
        return str(self.node.value)
    def _set_text(self, text):
        self.node.value = text
        self._update_text_size()

    def reset_max_text_size(self, value=None):
        # returns the calculated actual size
        if value is None:
            value = FONT_SIZE_ABSOLUTE_MAX
        self.max_font_size = value
        return self.calc_text_size()
        
    def calc_text_size(self):
        min_font_size = 2
        ratio = 0.7
        yratio = 1
        size = self.style.font_size
        self.text_widget.update()
        while ((self.text_widget.size[0] < self._size.final[0]*ratio
               or self.text_widget.size[1] < self._size.final[1]*yratio)
               and self.style.font_size < min(self.max_font_size, FONT_SIZE_ABSOLUTE_MAX)):
            self.style.font_size = min(self.style.font_size + 1, FONT_SIZE_ABSOLUTE_MAX)
            self._update_text_size()
            
        while ((self.text_widget.size[0] > self._size.final[0]*ratio
               or self.text_widget.size[1] > self._size.final[1]*yratio)
               and self.style.font_size > min_font_size or self.style.font_size > self.max_font_size):
            self.style.font_size -= 1
            self._update_text_size()
            
        return self.style.font_size
    
    def _update_text_size(self, new_size=None):
        if new_size is not None:
            self.style.font_size = new_size
        self.text_widget.set_style(self.style)
        self.text_widget.update(force=True)
        
    def get_size(self):
        return tuple(self._size.current)
    def set_size(self, p):
        p = Point(p)
        self._size.final = p
        self._update_text_size()
        self.obs_loc.notify.size_set(p)
    size = property(get_size, set_size)
    
    def get_pos(self):
        return tuple(self._pos.current)
    def set_pos(self, p):
        p = Point(p)
        self._pos.final = p
        self.obs_loc.notify.pos_set(p)
    pos = property(get_pos, set_pos)

    def rect(self):
        s = self.size
        return pygame.Rect(self.pos[0],self.pos[1],s[0],s[1])
    def final_rect(self):
        s = self._size.final
        p = self._pos.final
        return pygame.Rect(p[0],p[1],s[0],s[1])
        
    def _node_connect(self, e):
        pass
    def _node_disconnect(self, e):
        pass
    
        
    def update(self):
        self.text_widget.update()
        self._update_text_size()
        self._size.update()
        self._pos.update()
        self.shape.rect = self.rect()
        
    def _draw(self, surface, pos_offset):
        self.shape.paint(pos_offset, surface, self.fg_color, self.bg_color)
        self.text_widget.draw(surface, tuple(Point(pos_offset) + Point(self.rect().center) - Point(self.text_widget.size)/2))
        

    def _move_right(self):
        '''move right'''
        self.pos = Point(self.pos) + Point((5,0))
    def _move_left(self):
        '''move left'''
        self.pos = Point(self.pos) + Point((-5,0))
    def _move_up(self):
        '''move up'''
        self.pos = Point(self.pos) + Point((0,-5))
    def _move_down(self):
        '''move down'''
        self.pos = Point(self.pos) + Point((0,5))
        
    def _edit_value(self):
        '''Edit value'''
        self.keymap.set_next_keymap(self.text_widget.focus_keymap)
        self.text_widget._start_editing()
        self.keymap.register_key(self.key_stop_edit, keydown_noarg(self._stop_edit_value))

    def _stop_edit_value(self):
        '''Stop editing value'''
        self.text_widget._stop_editing()
        self.keymap.set_next_keymap(self.focus_keymap)
        self.keymap.unregister_key(self.key_stop_edit)
Exemplo n.º 6
0
class Widget(object):
    painting_z_order = 0
    font_size = 40
    
    def __init__(self, text = '', pos=None, size=None, font_size=40, start_pos=None):
        self.size = MovingValue(Point((20,20)), get_default(size, Point((20,20))))
        pos = get_default(pos, Point((0,0)))
        start_pos = get_default(start_pos, Point((0,0)))
        self.pos = MovingValue(start_pos, pos, step=0.3)

        self.widgets = [] # sub-widgets
        self.focused_widgets = []
        self.hovered_widget = None
        self.trigger_lists = {'pre' : TriggerList(),
                              'post' : TriggerList(),}
                                  
        
        self.text = text
        self._font_size = font_size
        self.reset()
        
        self._init_params()
        self._init_event_triggers()

        self.max_undo = 25

        # TODO kick this outta here
        #from Ellipse import Ellipse
        from Shapes.Rectangle import Rectangle
        self.shape = Rectangle(pygame.Rect(self.get_current_rect()))


    def get_font_size(self):
        return self._font_size
    def set_font_size(self, value):
        self._font_size = value
        self.update_default_font()
    font_size = property(fget=get_font_size,fset=set_font_size)
    
    def reset(self):
        self.record = False
        self.font=None
        self.default_font=None
        self.rendered_params = None
        self.rendered_text = None
        self.update_default_font()
        self.save_next_paint = None
        
        self.drag_start_pos = None
        self.dragged_widget = None
        
        self.history = []
        self.history_redo = []
        self.undoing = False

        self.shape_image = None
        self.focused_shape_image = None
        self.hovered_shape_image = None
        
    def __getstate__(self):
        d = self.__dict__.copy()
        del d['font']
        del d['default_font']
        del d['rendered_text']
        del d['rendered_params']
        return d
    def __setstate__(self, d):
        self.__dict__ = d
        self.reset()

    def update_default_font(self):
        self.default_font = get_font(self.font_size)

    def _init_params(self):
        self.params = ParamHolder(["visible", "enabled",
                                   "fore_color",
                                   "back_color",
                                   "text_color",
                                   "in_focus",
                                   "in_hover",
                                   "focus_back_color",
                                   "focus_fore_color",
                                   "focus_text_color",
                                   "hover_back_color",
                                   "hover_text_color",
                                   "autosize",
                                   "in_drag_mode",
                                   "user"], "WidgetParams")
        self.params.enabled = True
        self.params.visible = True

        # In drag mode, you can drag the widget with the mouse to move
        # it
        self.params.in_drag_mode = False
        
        self.params.fore_color = (100,100,200)
        self.params.back_color = (10,  10,15)
        self.params.text_color = (150,150,150)
        
        # If the widget is NOT really in focus, it will not receive
        # events. But this parameter specifies how to DISPLAY the
        # widget
        self.params.in_focus = False
        
        self.params.in_hover = False
        self.params.focus_back_color = (50,50,100)
        self.params.focus_fore_color = (150,150,250)
        self.params.focus_text_color = (230,230,255)
        self.params.hover_back_color = (20,20,60)
        self.params.hover_text_color = (200,200,215)
        self.params.user = None
        self.params.autosize = "by size"

        
    def update_moving(self):
        self.render_text()
        self.size.update()
        self.pos.update()

    def in_bounds(self, pos):
        # Pos is relative to PARENTs origin
        p = self.pos.current
        s = self.size.current
        if ((pos.x > p.x)
            and (pos.y > p.y)
            and (pos.x < p.x + s.x)
            and (pos.y < p.y + s.y)):
            return True
        return False
        
    def center_pos(self, current=True):
        if current:
            return self.pos.current+self.size.current*0.5
        return self.pos.final+self.size.final*0.5
        
    ######################################################################
    def add_widget(self, widget, z = None):
        if widget in self.widgets:
            raise ValueError("Widget already exists! %r" % (widget,))

        if z is None:
            self.widgets.append(widget)
        else:
            self.widgets.insert(z, widget)

    def remove_widget(self, widget):
        self.widgets.remove(widget)
        
    def _z_ordered_widgets(self):
        return sorted((w.painting_z_order, w) for w in self.widgets)

    ######################################################################

    def pos_relative_to_widget(self, pos, widget):
        return pos - widget.pos.current
    
    def handle_event(self, event):
        if self.trigger_lists['pre'].handle_event(event):
            return True

        if 'mouse' in event.type:
            orig_pos = event.pos.copy()
        handled = False
        if event.to_all:
            for widget in self.widgets:
                if 'mouse' in event.type:
                    event.pos = self.pos_relative_to_widget(orig_pos, widget)
                handled = widget.handle_event(event)
        elif event.to_focused and self.focused_widgets:
            for widget in self.focused_widgets:
                if 'mouse' in event.type:
                    event.pos = self.pos_relative_to_widget(orig_pos, widget)
                handled = widget.handle_event(event)
        if 'mouse' in event.type:
            event.pos = orig_pos
        if handled:
            self.trigger_lists['post'].handle_event(event, only_forced=True)
            return True

        return self.trigger_lists['post'].handle_event(event)
            
    def _init_event_triggers(self):
        # force_handling = True means that the handler will be called
        # even if the event was handled by someone lower in the
        # hierarchy
        self.trigger_lists['pre'].register_event_type('paint', self.paint)
        for event_type, handler, force_handling in (('mouse motion', self.mouse_motion, False),
                                                    ('mouse down', self.mouse_down, False),
                                                    ('mouse up', self.mouse_up, False),
                                                    ('key up', self.key_up, False),
                                                    ('key down', self.key_down, False),
                                                    ('enter down', self.enter_down, False),
                                                    ):
            for when in ('pre', 'post'):
                # This saves for the common event handlers the need to
                # implement two methods
                self.trigger_lists[when].register_event_type(event_type, partial(handler, when), forced=force_handling)
            
    def mouse_motion(self, when, event):
        if when == 'post':
            return

        # This is expected to be relative to our origin, not global origin.
        p = event.pos
            
        self.params.in_hover = True
        self.hovered_widget = None

        if self.dragged_widget:
            new_event = event.copy()
            new_event.pos -= self.dragged_widget.pos.current
            self.dragged_widget.mouse_motion(when, new_event)
        else:
            for z, widget in reversed(self._z_ordered_widgets()):
                if self.hovered_widget or not widget.in_bounds(p):
                    # todo call some widget.?? method?
                    widget.params.in_hover = False
                    continue

                new_event = event.copy()
                # make it relative
                new_event.pos = self.pos_relative_to_widget(event.pos, widget)
                if widget.mouse_motion(when, new_event):
                    self.hovered_widget = widget

        if self.params.in_drag_mode and self.drag_start_pos:
             # if we use event.pos, the widget will not be dragged beyond its own borders
            self.pos.final = self.drag_start_pos + mouse_pos()

        return True

    def _modkey_used(self, key, mods=None):
        if not mods:
            mods = pygame.key.get_mods()
        return mods & key
    multiselect_modifier = pygame.KMOD_CTRL
    def _multiselect_modifier_used(self, mods=None):
        return self._modkey_used(self.multiselect_modifier)

    def mouse_down(self, when, event):
        if when == 'post':
            return
        p = event.pos
        self.params.in_focus = True
        if not self._multiselect_modifier_used():
            self.unset_focus()

        self.dragged_widget = None
        for z, widget in reversed(self._z_ordered_widgets()):
            if not widget.in_bounds(p):
                continue

            self.set_focus(widget)
            new_event = event.copy()
            # make it relative
            new_event.pos = self.pos_relative_to_widget(event.pos, widget)
            if widget.params.in_drag_mode:
                self.dragged_widget = widget
            if widget.mouse_down(when, new_event):
                return True

        if self.params.in_drag_mode:
            self.drag_start_pos = self.pos.current - mouse_pos()
            self.pos.final = self.pos.current
            
        return True
        
    def mouse_up(self, when, event):
        self.dragged_widget = None
        if self.params.in_drag_mode and self.drag_start_pos:
            self.drag_start_pos = None
            return True
        return False

    def set_focus(self, widget):
        if widget not in self.widgets:
            for subwidget in self.widgets:
                if subwidget.set_focus(widget):
                    self.set_focus(subwidget)
                    return True
            return False
        elif widget not in self.focused_widgets:
            self.focused_widgets.append(widget)
            widget.params.in_focus = True
            return True
        
    def unset_focus(self):
        for w in self.focused_widgets:
            w.params.in_focus = False
        self.focused_widgets = []

            
    def key_up(self, when, event):
        pass
    
    def key_down(self, when, event):
        if when=='pre':
            return False
        if not self.params.enabled:
            return False
        self.entered_text(event)
        return True

    def entered_text(self, event):
        import string
        event = event.pygame_event
        if event.key == pygame.K_BACKSPACE:
            self.text = self.text[:-1]
        elif event.unicode in (string.letters + string.digits + string.hexdigits + ' \r' + string.punctuation):
            ch = event.unicode.replace('\r', '\n')
            self.text += ch

    def enter_down(self, when, event):
        pass
    
    ########################################################
    def get_current_rect(self):
        return self.pos.current.x, self.pos.current.y, self.size.current.x, self.size.current.y

    def paint(self, event):
        if not self.params.visible:
            return

        surface = event.surface
        
        self.update_moving()
        if self.params.in_focus:
            back_color = self.params.focus_back_color
            fore_color = self.params.focus_fore_color
        elif self.params.in_hover:
            back_color = self.params.hover_back_color
            fore_color = self.params.fore_color
        else:
            back_color = self.params.back_color
            fore_color = self.params.fore_color

        self.paint_shape(event.parent_offset, surface, fore_color, back_color)
        self.paint_text(event.parent_offset, surface)

        self.paint_widgets(event)

        if self.save_next_paint:
            # TODO rewrite this to use the Trigger for 'paint'
            args, kw = self.save_next_paint
            self.save_next_paint = None # reset before calling to prevent hypothetical recursions
            self.save_snapshot_image(event, *args, **kw)
            
        if self.record:
            pygame.draw.circle(surface, (255,100,100), (self.size.current.x-7, 7), 5, 0)
            self.save_snapshot_image(event, self.record_dir + '/img%4.4d.BMP' % (self._frame_counter))
            self._frame_counter+=1

        return True # since we are painting them explicitly, the lower widgets don't need to

    def paint_widgets(self, event):
        new_event = event.copy()
        new_event.parent_offset = new_event.parent_offset + self.pos.current
        for z, widget in self._z_ordered_widgets():
            # We want to control the specific order of painting, so
            # don't let the standard event passing through the
            # hierarchy. just do it here
            widget.handle_event(new_event)

    #############################################################################
            
    def undo(self):
        if not self.history:
            return
        doer, undoer, args, kw = self.history.pop()
        self.undoing = True
        undoer()
        self.undoing = False

    def redo(self):
        if not self.history_redo:
            return
        doer, undoer, args, kw = self.history_redo.pop()
        self.undoing = False
        undoer()
        
    def save(self, filename):
        import pickle
        f=open(filename, 'wb')
        pickle.dump(self.widgets,f,2)
    def load(self, filename):
        import pickle
        f=open(filename, 'rb')
        self.widgets = pickle.load(f)


    def save_snapshot_on_next_paint(self, filename, width=None, height=None, callback_when_done=None):
        self.save_next_paint = (filename,), dict(width=width, height=height, callback_when_done=callback_when_done)
        
    def save_snapshot_image(self, event, filename, width=None, height=None, callback_when_done=None, **kw):
        if width and not height:
            height = self.size.current.y/(self.size.current.x/float(width))
        elif height and not width:
            width = self.size.current.x/(self.size.current.y/float(height))
            
        width = get_default(width, self.size.current.x)
        height = get_default(height, self.size.current.y)
        
        rect = pygame.Rect(event.parent_offset.x, event.parent_offset.y, self.size.current.x, self.size.current.y)
        rect = rect.clip(event.surface.get_rect())
        subsurface = event.surface.subsurface(rect)
        pygame.image.save(pygame.transform.scale(subsurface, (width,height)), filename)
        if callback_when_done:
            callback_when_done(filename, width, height)


    #############################################################################
        

    # TODO move this to some subclass
    # :
    def get_current_text_color(self):
        if self.params.in_focus:
            text_color = self.params.focus_text_color
        elif self.params.in_hover:
            text_color = self.params.hover_text_color
        else:
            text_color = self.params.text_color
        return text_color
        
    def render_text(self):
        text_color = self.get_current_text_color()

        params = (self.params.autosize, tuple(self.size.final), self.text, text_color)
        if self.params.autosize == "by text":
            params += (self.default_font,)
        if self.rendered_params == params:
            return
            
        lines = self.text.split('\n')
        if self.params.autosize == "by size":
            does_fit, self.font = find_font(lines, tuple(self.size.final*(3./4)))
        else:
            self.font = self.default_font
            if self.params.autosize == "by text":
                width, height = lines_size(self.font, lines)
                self.size.final.x = self.size.current.x = width
                self.size.final.y = self.size.current.y = height

        rendered_lines = [self.font.render(line, True, text_color)
                          for line in lines]
        size = (max(t.get_width() for t in rendered_lines),
                sum(t.get_height() for t in rendered_lines))
        self.rendered_text = pygame.Surface(size, pygame.SWSURFACE|pygame.SRCALPHA|pygame.SRCCOLORKEY, 32)
        self.rendered_params = params
        
        # TODO: Support centering and stuff?
        y = 0
        for rline in rendered_lines:
            self.rendered_text.blit(rline, (0, y))
            y += rline.get_height()

    def get_shape(self):
        # TODO make the shape a mutable attribute of self?
        if not self.shape:
            return None
        self.shape.rect = pygame.Rect(self.get_current_rect())
        return self.shape
    
    def paint_shape(self, parent_offset, surface, fore_color, back_color):
        # TODO use subsurfaces instead of parent_offset (problematic beacuse of EdgeWidget right now)

        self.get_shape()
        if not self.shape:
            return

        #rect = tuple(self.pos.current + parent_offset) + tuple(self.size.current)
        if self.params.in_focus:
            image = self.focused_shape_image
        elif self.params.in_hover:
            image = self.hovered_shape_image
        else:
            image = self.shape_image
        self.shape.paint(parent_offset, surface, fore_color, back_color, image)

    def paint_text(self, parent_offset, surface):
        lines = self.text.split('\n')
        text_size = Point(lines_size(self.font, lines))
        surface.blit(self.rendered_text, tuple(parent_offset + self.center_pos()-text_size*0.5))

    def change_font_size(self, add = 0, mul = 1):
        self._font_size *= mul
        self._font_size += add
        self.update_default_font()

        
    def start_record(self):
        self._frame_counter = 0
        import os, time
        self.record_dir = '/tmp/record_%s' % (time.time())
        os.makedirs(self.record_dir)
        self.record = True
    def stop_record(self):
        self.record = False