changeset 555:c0474fe18b96 pyntnclick

Copy in widgets from mamba (currently unused)
author Stefano Rivera <stefano@rivera.za.net>
date Sat, 11 Feb 2012 14:09:46 +0200
parents 99a1420097df
children a4f28da12720
files pyntnclick/widgets.py pyntnclick/widgets/__init__.py pyntnclick/widgets/base.py pyntnclick/widgets/editlevel.py pyntnclick/widgets/editsprite.py pyntnclick/widgets/entrybox.py pyntnclick/widgets/game.py pyntnclick/widgets/imagebutton.py pyntnclick/widgets/level.py pyntnclick/widgets/levelbutton.py pyntnclick/widgets/listbox.py pyntnclick/widgets/messagebox.py pyntnclick/widgets/overlay.py pyntnclick/widgets/text.py pyntnclick/widgets/toollist.py
diffstat 15 files changed, 1517 insertions(+), 215 deletions(-) [+]
line wrap: on
line diff
--- a/pyntnclick/widgets.py	Sat Feb 11 14:04:48 2012 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,215 +0,0 @@
-# widgets.py
-# Copyright Boomslang team, 2010 (see COPYING File)
-
-"""Custom Albow widgets"""
-
-import textwrap
-
-import albow.controls
-import albow.menu
-from albow.resource import get_font, get_image
-from pygame.color import Color
-from pygame.rect import Rect
-from pygame.draw import lines as draw_lines
-from pygame import mouse
-
-from pyntnclick.constants import BUTTON_SIZE
-from pyntnclick.cursor import CursorWidget
-
-
-class BoomLabel(albow.controls.Label):
-
-    trim_line_top = 0
-
-    def __init__(self, text, width=None, **kwds):
-        albow.controls.Label.__init__(self, text, width, **kwds)
-        w, h = self.size
-        h -= self.trim_line_top * len(self.text.split('\n'))
-        self.size = (w, h)
-
-    def set_margin(self, margin):
-        """Add a set_margin method that recalculates the label size"""
-        old_margin = self.margin
-        w, h = self.size
-        d = margin - old_margin
-        self.margin = margin
-        self.size = (w + 2 * d, h + 2 * d)
-
-    def draw_all(self, surface):
-        bg_color = self.bg_color
-        self.bg_color = None
-        if bg_color is not None:
-            new_surface = surface.convert_alpha()
-            new_surface.fill(bg_color)
-            surface.blit(new_surface, surface.get_rect())
-        albow.controls.Label.draw_all(self, surface)
-        self._draw_all_no_bg(surface)
-        self.bg_color = bg_color
-
-    def _draw_all_no_bg(self, surface):
-        pass
-
-    def draw_with(self, surface, fg, _bg=None):
-        m = self.margin
-        align = self.align
-        width = surface.get_width()
-        y = m
-        lines = self.text.split("\n")
-        font = self.font
-        dy = font.get_linesize() - self.trim_line_top
-        for line in lines:
-            image = font.render(line, True, fg)
-            r = image.get_rect()
-            image = image.subsurface(r.clip(r.move(0, self.trim_line_top)))
-            r.top = y
-            if align == 'l':
-                r.left = m
-            elif align == 'r':
-                r.right = width - m
-            else:
-                r.centerx = width // 2
-            surface.blit(image, r)
-            y += dy
-
-
-class BoomButton(BoomLabel):
-
-    def __init__(self, text, action, screen):
-        super(BoomButton, self).__init__(text, font=get_font(20, 'Vera.ttf'),
-                margin=4)
-        self.bg_color = (0, 0, 0)
-        self._frame_color = Color(50, 50, 50)
-        self.action = action
-        self.screen = screen
-
-    def mouse_down(self, event):
-        self.action()
-        self.screen.state_widget.mouse_move(event)
-
-    def mouse_move(self, event):
-        self.screen.state.highlight_override = True
-
-    def draw(self, surface):
-        super(BoomButton, self).draw(surface)
-        r = surface.get_rect()
-        w = 2
-        top, bottom, left, right = r.top, r.bottom, r.left, r.right
-        draw_lines(surface, self._frame_color, False, [
-            (left, bottom), (left, top), (right - w, top), (right - w, bottom)
-            ], w)
-
-
-class MessageDialog(BoomLabel, CursorWidget):
-
-    def __init__(self, screen, text, wrap_width, style=None, **kwds):
-        CursorWidget.__init__(self, screen)
-        self.set_style(style)
-        paras = text.split("\n\n")
-        text = "\n".join([textwrap.fill(para, wrap_width) for para in paras])
-        BoomLabel.__init__(self, text, **kwds)
-
-    def set_style(self, style):
-        self.set_margin(5)
-        self.border_width = 1
-        self.border_color = (0, 0, 0)
-        self.bg_color = (127, 127, 127)
-        self.fg_color = (0, 0, 0)
-        if style == "JIM":
-            self.set(font=get_font(20, "Monospace.ttf"))
-            self.trim_line_top = 10
-            self.bg_color = Color(255, 175, 127, 191)
-            self.fg_color = (0, 0, 0)
-            self.border_color = (127, 15, 0)
-
-    def draw_all(self, surface):
-        root_surface = self.get_root().surface
-        overlay = root_surface.convert_alpha()
-        overlay.fill(Color(0, 0, 0, 191))
-        root_surface.blit(overlay, (0, 0))
-        BoomLabel.draw_all(self, surface)
-
-    def _draw_all_no_bg(self, surface):
-        CursorWidget.draw_all(self, surface)
-
-    def mouse_down(self, event):
-        self.dismiss()
-        self.screen.state_widget._mouse_move(mouse.get_pos())
-        for widget in self.screen.state_widget.subwidgets:
-            widget._mouse_move(mouse.get_pos())
-
-    def cursor_highlight(self):
-        return False
-
-
-class HandButton(albow.controls.Image):
-    """The fancy hand button for the widget"""
-
-    def __init__(self, action):
-        # FIXME: Yes, please.
-        this_image = get_image('items', 'hand.png')
-        albow.controls.Image.__init__(self, image=this_image)
-        self.action = action
-        self.set_rect(Rect(0, 0, BUTTON_SIZE, BUTTON_SIZE))
-
-    def mouse_down(self, event):
-        self.action()
-
-
-class PopupMenuButton(albow.controls.Button):
-
-    def __init__(self, text, action):
-        albow.controls.Button.__init__(self, text, action)
-
-        self.font = get_font(16, 'Vera.ttf')
-        self.set_rect(Rect(0, 0, BUTTON_SIZE, BUTTON_SIZE))
-        self.margin = (BUTTON_SIZE - self.font.get_linesize()) / 2
-
-
-class PopupMenu(albow.menu.Menu, CursorWidget):
-
-    def __init__(self, screen):
-        CursorWidget.__init__(self, screen)
-        self.screen = screen
-        self.shell = screen.shell
-        items = [
-                ('Quit Game', 'quit'),
-                ('Exit to Main Menu', 'main_menu'),
-                ]
-        # albow.menu.Menu ignores title string
-        albow.menu.Menu.__init__(self, None, items)
-        self.font = get_font(16, 'Vera.ttf')
-
-    def show_menu(self):
-        """Call present, with the correct position"""
-        item_height = self.font.get_linesize()
-        menu_top = 600 - (len(self.items) * item_height + BUTTON_SIZE)
-        item = self.present(self.shell, (0, menu_top))
-        if item > -1:
-            # A menu item needs to be invoked
-            self.invoke_item(item)
-
-
-class BoomImageButton(albow.controls.Image):
-    """The fancy image button for the screens"""
-
-    FOLDER = None
-
-    def __init__(self, filename, x, y, action, enable=None):
-        this_image = get_image(self.FOLDER, filename)
-        albow.controls.Image.__init__(self, image=this_image)
-        self.action = action
-        self.set_rect(Rect((x, y), this_image.get_size()))
-        self.enable = enable
-
-    def draw(self, surface):
-        if self.is_enabled():
-            surface.blit(self.get_image(), self.get_rect())
-
-    def mouse_down(self, event):
-        if self.is_enabled():
-            self.action()
-
-    def is_enabled(self):
-        if self.enable:
-            return self.enable()
-        return True
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pyntnclick/widgets/__init__.py	Sat Feb 11 14:09:46 2012 +0200
@@ -0,0 +1,217 @@
+# widgets.py
+# Copyright Boomslang team, 2010 (see COPYING File)
+
+# XXX: This should be deleted when albow is gone
+
+"""Custom Albow widgets"""
+
+import textwrap
+
+import albow.controls
+import albow.menu
+from albow.resource import get_font, get_image
+from pygame.color import Color
+from pygame.rect import Rect
+from pygame.draw import lines as draw_lines
+from pygame import mouse
+
+from pyntnclick.constants import BUTTON_SIZE
+from pyntnclick.cursor import CursorWidget
+
+
+class BoomLabel(albow.controls.Label):
+
+    trim_line_top = 0
+
+    def __init__(self, text, width=None, **kwds):
+        albow.controls.Label.__init__(self, text, width, **kwds)
+        w, h = self.size
+        h -= self.trim_line_top * len(self.text.split('\n'))
+        self.size = (w, h)
+
+    def set_margin(self, margin):
+        """Add a set_margin method that recalculates the label size"""
+        old_margin = self.margin
+        w, h = self.size
+        d = margin - old_margin
+        self.margin = margin
+        self.size = (w + 2 * d, h + 2 * d)
+
+    def draw_all(self, surface):
+        bg_color = self.bg_color
+        self.bg_color = None
+        if bg_color is not None:
+            new_surface = surface.convert_alpha()
+            new_surface.fill(bg_color)
+            surface.blit(new_surface, surface.get_rect())
+        albow.controls.Label.draw_all(self, surface)
+        self._draw_all_no_bg(surface)
+        self.bg_color = bg_color
+
+    def _draw_all_no_bg(self, surface):
+        pass
+
+    def draw_with(self, surface, fg, _bg=None):
+        m = self.margin
+        align = self.align
+        width = surface.get_width()
+        y = m
+        lines = self.text.split("\n")
+        font = self.font
+        dy = font.get_linesize() - self.trim_line_top
+        for line in lines:
+            image = font.render(line, True, fg)
+            r = image.get_rect()
+            image = image.subsurface(r.clip(r.move(0, self.trim_line_top)))
+            r.top = y
+            if align == 'l':
+                r.left = m
+            elif align == 'r':
+                r.right = width - m
+            else:
+                r.centerx = width // 2
+            surface.blit(image, r)
+            y += dy
+
+
+class BoomButton(BoomLabel):
+
+    def __init__(self, text, action, screen):
+        super(BoomButton, self).__init__(text, font=get_font(20, 'Vera.ttf'),
+                margin=4)
+        self.bg_color = (0, 0, 0)
+        self._frame_color = Color(50, 50, 50)
+        self.action = action
+        self.screen = screen
+
+    def mouse_down(self, event):
+        self.action()
+        self.screen.state_widget.mouse_move(event)
+
+    def mouse_move(self, event):
+        self.screen.state.highlight_override = True
+
+    def draw(self, surface):
+        super(BoomButton, self).draw(surface)
+        r = surface.get_rect()
+        w = 2
+        top, bottom, left, right = r.top, r.bottom, r.left, r.right
+        draw_lines(surface, self._frame_color, False, [
+            (left, bottom), (left, top), (right - w, top), (right - w, bottom)
+            ], w)
+
+
+class MessageDialog(BoomLabel, CursorWidget):
+
+    def __init__(self, screen, text, wrap_width, style=None, **kwds):
+        CursorWidget.__init__(self, screen)
+        self.set_style(style)
+        paras = text.split("\n\n")
+        text = "\n".join([textwrap.fill(para, wrap_width) for para in paras])
+        BoomLabel.__init__(self, text, **kwds)
+
+    def set_style(self, style):
+        self.set_margin(5)
+        self.border_width = 1
+        self.border_color = (0, 0, 0)
+        self.bg_color = (127, 127, 127)
+        self.fg_color = (0, 0, 0)
+        if style == "JIM":
+            self.set(font=get_font(20, "Monospace.ttf"))
+            self.trim_line_top = 10
+            self.bg_color = Color(255, 175, 127, 191)
+            self.fg_color = (0, 0, 0)
+            self.border_color = (127, 15, 0)
+
+    def draw_all(self, surface):
+        root_surface = self.get_root().surface
+        overlay = root_surface.convert_alpha()
+        overlay.fill(Color(0, 0, 0, 191))
+        root_surface.blit(overlay, (0, 0))
+        BoomLabel.draw_all(self, surface)
+
+    def _draw_all_no_bg(self, surface):
+        CursorWidget.draw_all(self, surface)
+
+    def mouse_down(self, event):
+        self.dismiss()
+        self.screen.state_widget._mouse_move(mouse.get_pos())
+        for widget in self.screen.state_widget.subwidgets:
+            widget._mouse_move(mouse.get_pos())
+
+    def cursor_highlight(self):
+        return False
+
+
+class HandButton(albow.controls.Image):
+    """The fancy hand button for the widget"""
+
+    def __init__(self, action):
+        # FIXME: Yes, please.
+        this_image = get_image('items', 'hand.png')
+        albow.controls.Image.__init__(self, image=this_image)
+        self.action = action
+        self.set_rect(Rect(0, 0, BUTTON_SIZE, BUTTON_SIZE))
+
+    def mouse_down(self, event):
+        self.action()
+
+
+class PopupMenuButton(albow.controls.Button):
+
+    def __init__(self, text, action):
+        albow.controls.Button.__init__(self, text, action)
+
+        self.font = get_font(16, 'Vera.ttf')
+        self.set_rect(Rect(0, 0, BUTTON_SIZE, BUTTON_SIZE))
+        self.margin = (BUTTON_SIZE - self.font.get_linesize()) / 2
+
+
+class PopupMenu(albow.menu.Menu, CursorWidget):
+
+    def __init__(self, screen):
+        CursorWidget.__init__(self, screen)
+        self.screen = screen
+        self.shell = screen.shell
+        items = [
+                ('Quit Game', 'quit'),
+                ('Exit to Main Menu', 'main_menu'),
+                ]
+        # albow.menu.Menu ignores title string
+        albow.menu.Menu.__init__(self, None, items)
+        self.font = get_font(16, 'Vera.ttf')
+
+    def show_menu(self):
+        """Call present, with the correct position"""
+        item_height = self.font.get_linesize()
+        menu_top = 600 - (len(self.items) * item_height + BUTTON_SIZE)
+        item = self.present(self.shell, (0, menu_top))
+        if item > -1:
+            # A menu item needs to be invoked
+            self.invoke_item(item)
+
+
+class BoomImageButton(albow.controls.Image):
+    """The fancy image button for the screens"""
+
+    FOLDER = None
+
+    def __init__(self, filename, x, y, action, enable=None):
+        this_image = get_image(self.FOLDER, filename)
+        albow.controls.Image.__init__(self, image=this_image)
+        self.action = action
+        self.set_rect(Rect((x, y), this_image.get_size()))
+        self.enable = enable
+
+    def draw(self, surface):
+        if self.is_enabled():
+            surface.blit(self.get_image(), self.get_rect())
+
+    def mouse_down(self, event):
+        if self.is_enabled():
+            self.action()
+
+    def is_enabled(self):
+        if self.enable:
+            return self.enable()
+        return True
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pyntnclick/widgets/base.py	Sat Feb 11 14:09:46 2012 +0200
@@ -0,0 +1,310 @@
+import collections
+
+import pygame
+from pygame.locals import (KEYDOWN, K_DOWN, K_LEFT, K_RETURN, K_RIGHT, K_UP,
+                           K_KP_ENTER, MOUSEBUTTONDOWN, MOUSEBUTTONUP,
+                           MOUSEMOTION, SRCALPHA, USEREVENT)
+
+from mamba.constants import UP, DOWN, LEFT, RIGHT
+from mamba.engine import UserEvent
+
+
+class Widget(object):
+
+    def __init__(self, rect):
+        if not isinstance(rect, pygame.Rect):
+            rect = pygame.Rect(rect, (0, 0))
+        self.rect = rect
+        self.focussable = False
+        self.focussed = False
+        self.modal = False
+        self.parent = None
+        self.disabled = False
+        self.callbacks = collections.defaultdict(list)
+
+    def add_callback(self, eventtype, callback, *args):
+        self.callbacks[eventtype].append((callback, args))
+
+    def event(self, ev):
+        "Don't override this without damn good reason"
+        if self.disabled:
+            return True
+
+        type_ = ev.type
+        if type_ == USEREVENT:
+            for k in self.callbacks.iterkeys():
+                if (isinstance(k, type) and issubclass(k, UserEvent)
+                        and k.matches(ev)):
+                    type_ = k
+                    break
+
+        for callback, args in self.callbacks[type_]:
+            if callback(ev, self, *args):
+                return True
+        return False
+
+    def draw(self, surface):
+        "Override me"
+        pass
+
+    def grab_focus(self):
+        if self.focussable:
+            # Find the root and current focus
+            root = self
+            while root.parent is not None:
+                root = root.parent
+            focus = root
+            focus_modal_base = None
+            while (isinstance(focus, Container)
+                    and focus.focussed_child is not None):
+                if focus.modal:
+                    focus_modal_base = focus
+                focus = focus.children[focus.focussed_child]
+
+            # Don't leave a modal heirarchy
+            if focus_modal_base:
+                widget = self
+                while widget.parent is not None:
+                    if widget == focus_modal_base:
+                        break
+                    widget = widget.parent
+                else:
+                    return False
+
+            root.defocus()
+            widget = self
+            while widget.parent is not None:
+                parent = widget.parent
+                if isinstance(parent, Container):
+                    idx = parent.children.index(widget)
+                    parent.focussed_child = idx
+                widget = parent
+            self.focussed = True
+            return True
+        return False
+
+    def disable(self):
+        if not self.disabled:
+            self.disabled = True
+            self._focussable_when_enabled = self.focussable
+            self.focussable = False
+            if hasattr(self, 'prepare'):
+                self.prepare()
+
+    def enable(self):
+        if self.disabled:
+            self.disabled = False
+            self.focussable = self._focussable_when_enabled
+            if hasattr(self, 'prepare'):
+                self.prepare()
+
+
+class Button(Widget):
+
+    def event(self, ev):
+        if super(Button, self).event(ev):
+            return True
+        if (ev.type == MOUSEBUTTONDOWN
+                or (ev.type == KEYDOWN and ev.key in (K_RETURN, K_KP_ENTER))):
+            for callback, args in self.callbacks['clicked']:
+                if callback(ev, self, *args):
+                    return True
+            return False
+
+    def forced_click(self):
+        """Force calling the clicked handler"""
+        self.grab_focus()
+        for callback, args in self.callbacks['clicked']:
+            if callback(None, self, *args):
+                return True
+        return False
+
+
+class Container(Widget):
+
+    def __init__(self, rect=None):
+        if rect is None:
+            rect = pygame.Rect(0, 0, 0, 0)
+        super(Container, self).__init__(rect)
+        self.children = []
+        self.focussed_child = None
+
+    def event(self, ev):
+        """Push an event down through the tree, and fire our own event as a
+        last resort
+        """
+        if ev.type in (MOUSEMOTION, MOUSEBUTTONUP, MOUSEBUTTONDOWN):
+            for child in self.children[:]:
+                if child.rect.collidepoint(ev.pos):
+                    if ev.type == MOUSEBUTTONDOWN and child.focussable:
+                        if not child.grab_focus():
+                            continue
+                    if child.event(ev):
+                        return True
+
+        elif ev.type == KEYDOWN:
+            for i, child in enumerate(self.children):
+                if child.focussed or i == self.focussed_child:
+                    if child.event(ev):
+                        return True
+        else:
+            # Other events go to all children first
+            for child in self.children[:]:
+                if child.event(ev):
+                    return True
+        if super(Container, self).event(ev):
+            return True
+        if (self.parent is None and ev.type == KEYDOWN
+                and ev.key in (K_UP, K_DOWN)):
+            return self.adjust_focus(1 if ev.key == K_DOWN else -1)
+
+    def add(self, widget):
+        widget.parent = self
+        self.children.append(widget)
+        self.rect = self.rect.union(widget.rect)
+
+    def remove(self, widget):
+        widget.parent = None
+        if self.focussed_child is not None:
+            child = self.children[self.focussed_child]
+        self.children.remove(widget)
+        # We don't update the rect, for reasons of simplificty and laziness
+        if self.focussed_child is not None and child in self.children:
+            # Fix focus index
+            self.focussed_child = self.children.index(child)
+        else:
+            self.focussed_child = None
+
+    def defocus(self):
+        if self.focussed_child is not None:
+            child = self.children[self.focussed_child]
+            if isinstance(child, Container):
+                if not child.defocus():
+                    return False
+            child.focussed = False
+            self.focussed_child = None
+            return True
+
+    def adjust_focus(self, direction):
+        """Try and adjust focus in direction (integer)
+        """
+        if self.focussed_child is not None:
+            child = self.children[self.focussed_child]
+            if isinstance(child, Container):
+                if child.adjust_focus(direction):
+                    return True
+                elif child.modal:
+                    # We're modal, go back
+                    if child.adjust_focus(-direction):
+                        return True
+            else:
+                child.focussed = False
+
+        current = self.focussed_child
+        if current is None:
+            current = -1 if direction > 0 else len(self.children)
+        if direction > 0:
+            possibles = list(enumerate(self.children))[current + 1:]
+        else:
+            possibles = list(enumerate(self.children))[:current]
+            possibles.reverse()
+        for i, child in possibles:
+            if child.focussable:
+                child.focussed = True
+                self.focussed_child = i
+                return True
+            if isinstance(child, Container):
+                if child.adjust_focus(direction):
+                    self.focussed_child = i
+                    return True
+        else:
+            if self.parent is None:
+                if self.focussed_child is not None:
+                    # At the end, mark the last one as focussed, again
+                    child = self.children[self.focussed_child]
+                    if isinstance(child, Container):
+                        if child.adjust_focus(-direction):
+                            return True
+                    else:
+                        child.focussed = True
+                        return True
+            else:
+                self.focussed_child = None
+            return False
+
+    def draw(self, surface):
+        if self.parent is None and not self.focussed:
+            self.focussed = True
+            self.adjust_focus(1)
+        for child in self.children:
+            child.draw(surface)
+
+
+class GridContainer(Container):
+    """Hacky container that only supports grids, won't work with Container
+    children, or modal children.
+    """
+
+    def __init__(self, width, rect=None):
+        super(GridContainer, self).__init__(rect)
+        self.width = width
+
+    def event(self, ev):
+        if (ev.type == KEYDOWN and ev.key in (K_UP, K_DOWN, K_LEFT, K_RIGHT)):
+            direction = None
+            if ev.key == K_UP:
+                direction = UP
+            elif ev.key == K_DOWN:
+                direction = DOWN
+            elif ev.key == K_LEFT:
+                direction = LEFT
+            elif ev.key == K_RIGHT:
+                direction = RIGHT
+            return self.adjust_focus(direction)
+        super(GridContainer, self).event(ev)
+
+    def add(self, widget):
+        assert not isinstance(widget, Container)
+        assert not widget.modal
+        super(GridContainer, self).add(widget)
+
+    def adjust_focus(self, direction):
+        if isinstance(direction, int):
+            direction = (direction, 0)
+
+        if len(self.children) == 0:
+            return False
+
+        if self.focussed_child is None:
+            if sum(direction) > 0:
+                self.focussed_child = 0
+            else:
+                self.focussed_child = len(self.children) - 1
+        else:
+            self.children[self.focussed_child].focussed = False
+            if direction[0] != 0:
+                self.focussed_child += direction[0]
+            if direction[1] != 0:
+                self.focussed_child += self.width * direction[1]
+        if not 0 <= self.focussed_child < len(self.children):
+            self.focussed_child = None
+            return False
+        self.children[self.focussed_child].focussed = True
+        return True
+
+
+class Box(Container):
+    """A container that draws a filled background with a border"""
+    padding = 4
+
+    def draw(self, surface):
+        expandrect = self.rect.move((-self.padding, -self.padding))
+        expandrect.width = self.rect.width + 2 * self.padding
+        expandrect.height = self.rect.height + 2 * self.padding
+        border = pygame.Surface(expandrect.size, SRCALPHA)
+        border.fill(pygame.Color('black'))
+        surface.blit(border, expandrect)
+        background = pygame.Surface(self.rect.size, SRCALPHA)
+        background.fill(pygame.Color('gray'))
+        surface.blit(background, self.rect)
+        super(Box, self).draw(surface)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pyntnclick/widgets/editlevel.py	Sat Feb 11 14:09:46 2012 +0200
@@ -0,0 +1,108 @@
+from mamba.data import get_tileset_list, get_track_list
+from mamba.widgets.base import Box
+from mamba.widgets.text import TextWidget, TextButton, EntryTextWidget
+from mamba.widgets.listbox import ListBox
+
+
+class EditLevelBox(Box):
+    """Edit details for a level map"""
+
+    button_padding = 2
+
+    def __init__(self, rect, level, post_callback=None):
+        super(EditLevelBox, self).__init__(rect)
+        self.level = level
+        self.level_tileset = self.level.tileset.name
+        self.level_track = self.level.background_track
+        self.post_callback = post_callback
+        self.prepare()
+        self.modal = True
+
+    def add_widget(self, cls, *args, **kw):
+        clicked = kw.pop('clicked', None)
+        offset = kw.pop('offset', (0, 0))
+        pos = (self.widget_left + offset[0],
+               self.widget_top + offset[1])
+        widget = cls(pos, *args, **kw)
+        if clicked:
+            widget.add_callback('clicked', *clicked)
+        self.add(widget)
+        self.widget_top += widget.rect.height + self.button_padding
+        return widget
+
+    def prepare(self):
+        self.widget_left = self.rect.left
+        self.widget_top = self.rect.top
+
+        self.add_widget(TextWidget, "Specify Level Details")
+
+        self.filename = self.add_widget(
+            EntryTextWidget, self.level.level_name, prompt="File:")
+
+        self.levelname = self.add_widget(
+            EntryTextWidget, self.level.name, prompt='Level Title:')
+
+        self.authorname = self.add_widget(
+            EntryTextWidget, self.level.author, prompt='Author:')
+
+        # self.tileset = self.add_widget(
+        #     TextButton, 'Tileset: %s' % self.level_tileset,
+        #     color='white', clicked=(self.list_tilesets,))
+
+        self.trackname = self.add_widget(
+            TextButton, 'Music: %s' % self.level_track,
+            color='white', clicked=(self.list_tracks,))
+
+        self.ok_button = self.add_widget(
+            TextButton, 'OK', offset=(10, 0), clicked=(self.close, True))
+
+        self.cancel_button = self.add_widget(
+            TextButton, 'Cancel', offset=(10, 0), clicked=(self.close, False))
+
+        self.rect.width = max(self.rect.width, 400)
+        self.rect.height += 5
+
+    def change_tileset(self, ev, widget, name):
+        self.level_tileset = name
+        self.tileset.text = 'Tileset: %s' % name
+        self.tileset.prepare()
+
+    def change_track(self, ev, widget, name):
+        self.level_track = name
+        self.trackname.text = 'Music: %s' % name
+        self.trackname.prepare()
+
+    def mk_loadlist(self, title, items, callback):
+        load_list = []
+        for name in items:
+            load_button = TextButton((0, 0), name)
+            load_button.add_callback('clicked', callback, name)
+            load_list.append(load_button)
+        lb = ListBox((200, 200), title, load_list, 6)
+        lb.parent_modal = self.modal
+        self.modal = False
+        self.parent.add(lb)
+        lb.grab_focus()
+
+    def list_tilesets(self, ev, widget):
+        tilesets = [i for i in get_tileset_list() if i != 'common']
+        self.mk_loadlist('Select Tileset', tilesets, self.change_tileset)
+
+    def list_tracks(self, ev, widget):
+        tracks = get_track_list()
+        self.mk_loadlist('Select Music', tracks, self.change_track)
+
+    def close(self, ev, widget, do_update):
+        self.modal = False
+        self.parent.remove(self)
+        if do_update:
+            self.post_callback(
+                self.filename.value,
+                self.levelname.value,
+                self.authorname.value,
+                self.level_tileset,
+                self.level_track)
+        return True
+
+    def grab_focus(self):
+        return self.ok_button.grab_focus()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pyntnclick/widgets/editsprite.py	Sat Feb 11 14:09:46 2012 +0200
@@ -0,0 +1,148 @@
+from mamba.widgets.base import Box
+from mamba.widgets.text import TextWidget, TextButton, EntryTextWidget
+
+
+class EditSpriteBox(Box):
+    """Edit details for a special sprite on the level map"""
+
+    def __init__(self, rect, sprite_pos, sprite_info, post_callback=None):
+        super(EditSpriteBox, self).__init__(rect)
+        self.sprite_pos = sprite_pos
+        sprite_cls_name, sprite_cls, sprite_id, args = sprite_info
+        self.sprite_cls_name = sprite_cls_name
+        self.sprite_cls = sprite_cls
+        if sprite_id:
+            self.sprite_id = sprite_id
+        else:
+            self.sprite_id = ''
+        self.sprite_parameters = args
+        self.new_sprite_parameters = []
+        self.post_callback = post_callback
+        self.parameter_widgets = []
+        self.prepare()
+        self.modal = True
+
+    def prepare(self):
+        title = TextWidget(self.rect, "Specify Sprite Details")
+        self.add(title)
+        height = self.rect.top + title.rect.height + 2
+        self.edit_sprite_name = TextWidget((self.rect.left, height),
+                'Sprite Class: %s' % self.sprite_cls.name)
+        self.add(self.edit_sprite_name)
+        height += self.edit_sprite_name.rect.height + 2
+        self.edit_sprite_id = EntryTextWidget((self.rect.left + 20, height),
+                self.sprite_id, prompt='Sprite Id (required):')
+        self.add(self.edit_sprite_id)
+        height += self.edit_sprite_id.rect.height + 2
+        poss_params = self.sprite_cls.get_sprite_args()
+        if not poss_params:
+            self.sprite_param = TextWidget((self.rect.left, height),
+                    'No Parameters')
+            self.add(self.sprite_param)
+            height += self.sprite_param.rect.height + 2
+        else:
+            self.sprite_param = TextWidget((self.rect.left, height),
+                    'Parameters')
+            self.add(self.sprite_param)
+            height += self.sprite_param.rect.height + 2
+            for i, param_tuple in enumerate(poss_params):
+                if len(self.sprite_parameters) > i:
+                    value = self.sprite_parameters[i]
+                else:
+                    value = None
+                if param_tuple[1] is None:
+                    # Text Entry Parameter
+                    if value is None:
+                        value = ''
+                    edit_widget = EntryTextWidget(
+                            (self.rect.left + 20, height),
+                            value, prompt=param_tuple[0])
+                    self.parameter_widgets.append(edit_widget)
+                    self.add(edit_widget)
+                    height += edit_widget.rect.height
+                elif isinstance(param_tuple[1], tuple):
+                    # We have a list of possible values
+                    if value is None:
+                        value = param_tuple[1][0]  # Take the first
+                    mylist = []
+                    list_width = 0
+                    list_height = 0
+                    for choice in param_tuple[1]:
+                        list_parameter = TextWidget(
+                                (self.rect.left + 20, height),
+                                '%s: %s' % (param_tuple[0], choice))
+                        # So we can pull it out of this later
+                        list_parameter.choice = choice
+                        list_width = max(list_width, list_parameter.rect.width)
+                        change_list = TextButton(
+                                (list_parameter.rect.right + 5, height),
+                                'Next Option')
+                        change_list.add_callback('clicked', self.change_list,
+                                choice, param_tuple[1], mylist)
+                        mylist.append((list_parameter, change_list))
+                        list_height = max(list_height, change_list.rect.height,
+                                list_parameter.rect.height)
+                        if choice == value:
+                            self.add(list_parameter)
+                            self.add(change_list)
+                    for x in mylist:
+                        x[1].rect.left = self.rect.left + list_width + 25
+                        if x[0].rect.height < list_height:
+                            x[0].rect.top += (list_height -
+                                    x[0].rect.height) / 2
+                    height += max(list_parameter.rect.height,
+                            change_list.rect.height)
+                    self.parameter_widgets.append(mylist)
+                # FIXME: Other cases
+        height += 20
+        self.ok_button = TextButton((self.rect.left + 10, height), 'OK')
+        self.ok_button.add_callback('clicked', self.close, True)
+        self.add(self.ok_button)
+        cancel_button = TextButton((self.ok_button.rect.right + 10, height),
+                'Cancel')
+        cancel_button.add_callback('clicked', self.close, False)
+        self.add(cancel_button)
+        self.rect.width = max(self.rect.width, 400)
+        self.rect.height += 5
+
+    def change_list(self, ev, widget, cur_choice, all_choices, widget_list):
+        pos = all_choices.index(cur_choice)
+        if pos == len(all_choices) - 1:
+            next_pos = 0
+        else:
+            next_pos = pos + 1
+        self.remove(widget_list[pos][0])
+        self.remove(widget_list[pos][1])
+        self.add(widget_list[next_pos][0])
+        self.add(widget_list[next_pos][1])
+
+    def close(self, ev, widget, do_update):
+        if do_update:
+            self.new_sprite_parameters = []
+            for param in self.parameter_widgets:
+                if hasattr(param, 'value'):
+                    self.new_sprite_parameters.append(param.value)
+                elif isinstance(param, list):
+                    # Find the selected one
+                    for choice, _ in param:
+                        if choice in self.children:
+                            # Is selected, so we grab this choice
+                            self.new_sprite_parameters.append(choice.choice)
+                            break
+            self.sprite_id = self.edit_sprite_id.value
+            sprite = self.make_sprite()
+            if not self.post_callback(sprite):
+                return  # Call-back failed, so don't remove
+        self.parent.paused = False
+        self.parent.remove(self)
+        return True
+
+    def make_sprite(self):
+        """Convert values to a sprite line"""
+        pos = "%s, %s" % self.sprite_pos
+        sprite_string = "%s: %s %s %s" % (pos, self.sprite_cls_name,
+                self.sprite_id, " ".join(self.new_sprite_parameters))
+        return sprite_string
+
+    def grab_focus(self):
+        return self.ok_button.grab_focus()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pyntnclick/widgets/entrybox.py	Sat Feb 11 14:09:46 2012 +0200
@@ -0,0 +1,66 @@
+from pygame.constants import K_ESCAPE, K_RETURN, K_KP_ENTER, KEYDOWN
+
+from mamba.widgets.base import Box
+from mamba.widgets.text import TextWidget, TextButton, EntryTextWidget
+
+
+class EntryBox(Box):
+
+    def __init__(self, rect, text, init_value, accept_callback=None,
+            color='white', entry_color='red'):
+        super(EntryBox, self).__init__(rect)
+        self.text = text
+        self.accept_callback = accept_callback
+        self.color = color
+        self.entry_color = entry_color
+        self.value = init_value
+        self.prepare()
+        self.modal = True
+
+    def prepare(self):
+        message = TextWidget((self.rect.left + 50, self.rect.top + 2),
+                self.text, color=self.color)
+        self.rect.width = message.rect.width + 100
+        self.add(message)
+        self.entry_text = EntryTextWidget((self.rect.left + 5,
+            self.rect.top + message.rect.height + 5), self.value,
+            focus_color=self.entry_color)
+        self.add_callback(KEYDOWN, self.edit)
+        self.add(self.entry_text)
+        ok_button = TextButton((self.rect.left + 50,
+            self.entry_text.rect.bottom), 'Accept')
+        ok_button.add_callback('clicked', self.close, True)
+        self.add(ok_button)
+        cancel_button = ok_button = TextButton(
+                (ok_button.rect.right + 10, self.entry_text.rect.bottom),
+                'Cancel')
+        cancel_button.add_callback('clicked', self.close, False)
+        self.add(cancel_button)
+        self.rect.height += 5
+
+    def close(self, ev, widget, ok):
+        if self.accept_callback and ok:
+            self.value = self.entry_text.value
+            if self.accept_callback(self.value):
+                if self.parent:
+                    if hasattr(self.parent, 'paused'):
+                        self.parent.paused = False
+                    self.parent.remove(self)
+            # Don't remove if the accept callback failed
+            return
+        if hasattr(self.parent, 'paused'):
+            self.parent.paused = False
+        self.parent.remove(self)
+        return True
+
+    def edit(self, ev, widget):
+        if ev.key == K_ESCAPE:
+            self.close(ev, widget, False)
+            return True
+        elif ev.key in (K_RETURN, K_KP_ENTER):
+            self.close(ev, widget, True)
+            return True
+        return False  # pass this up to parent
+
+    def grab_focus(self):
+        self.entry_text.grab_focus()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pyntnclick/widgets/game.py	Sat Feb 11 14:09:46 2012 +0200
@@ -0,0 +1,50 @@
+"""Display the game area."""
+
+from pygame.rect import Rect
+from pygame.locals import (KEYDOWN, K_LEFT, K_RIGHT, K_DOWN, K_UP, K_p,
+                           K_SPACE, K_PAUSE)
+
+from mamba.constants import UP, DOWN, LEFT, RIGHT
+from mamba.widgets.base import Widget
+from mamba.engine import FlipArrowsEvent
+
+
+class GameWidget(Widget):
+    def __init__(self, world, offset=(0, 0)):
+        self.world = world
+        self.actions = self.create_action_map()
+        rect = Rect(offset, world.get_size())
+        super(GameWidget, self).__init__(rect)
+        self.focussable = True
+        self.add_callback(KEYDOWN, self.action_callback)
+        self.add_callback(FlipArrowsEvent, self.flip_arrows)
+
+    def create_action_map(self):
+        actions = {}
+        pause = (self.world.toggle_pause, ())
+        actions[K_LEFT] = (self.world.snake.send_new_direction, (LEFT,))
+        actions[K_RIGHT] = (self.world.snake.send_new_direction, (RIGHT,))
+        actions[K_DOWN] = (self.world.snake.send_new_direction, (DOWN,))
+        actions[K_UP] = (self.world.snake.send_new_direction, (UP,))
+        actions[K_p] = pause
+        actions[K_SPACE] = pause
+        actions[K_PAUSE] = pause
+        return actions
+
+    def action_callback(self, ev, widget):
+        if ev.key in self.actions:
+            func, args = self.actions[ev.key]
+            func(*args)
+            return True
+
+    def flip_arrows(self, ev, widget):
+        self.world.level.flip_arrows()
+
+    def draw(self, surface):
+        self.world.update()
+        self.world.draw(surface)
+
+    def restart(self):
+        self.world.restart()
+        self.actions = self.create_action_map()
+        self.grab_focus()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pyntnclick/widgets/imagebutton.py	Sat Feb 11 14:09:46 2012 +0200
@@ -0,0 +1,44 @@
+import pygame
+from pygame.locals import SRCALPHA
+
+from mamba.constants import COLOR, FONT_SIZE, FOCUS_COLOR
+from mamba.widgets.base import Button
+from mamba.widgets.text import TextWidget
+
+
+class ImageButtonWidget(Button, TextWidget):
+    """Text label with image on the left"""
+
+    def __init__(self, rect, image, text, fontsize=FONT_SIZE, color=COLOR):
+        self.image = image
+        self.focus_color = pygame.Color(FOCUS_COLOR)
+        self.padding = 5
+        self.border = 3
+        super(ImageButtonWidget, self).__init__(rect, text, fontsize, color)
+        self.focussable = True
+
+    def prepare(self):
+        super(ImageButtonWidget, self).prepare()
+        text_surface = self.surface
+        # Image is already a surface
+        self._focussed = self.focussed
+        color = self.focus_color if self.focussed else self.color
+
+        width = (text_surface.get_width() + self.image.get_width()
+                + 5 + self.padding * 2)
+        height = max(text_surface.get_height(),
+                self.image.get_height()) + self.padding * 2
+        self.rect.width = max(self.rect.width, width)
+        self.rect.height = max(self.rect.height, height)
+        self.surface = pygame.Surface(self.rect.size, SRCALPHA)
+        self.surface.fill(0)
+        self.surface.blit(self.image, (self.padding, self.padding))
+        self.surface.blit(text_surface,
+                (self.image.get_width() + 5 + self.padding, self.padding))
+        pygame.draw.rect(self.surface, color, self.surface.get_rect(),
+                         self.border)
+
+    def draw(self, surface):
+        if self._focussed != self.focussed:
+            self.prepare()
+        super(ImageButtonWidget, self).draw(surface)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pyntnclick/widgets/level.py	Sat Feb 11 14:09:46 2012 +0200
@@ -0,0 +1,144 @@
+from pygame.rect import Rect
+from pygame.locals import MOUSEBUTTONDOWN, MOUSEBUTTONUP, MOUSEMOTION
+from pygame.key import set_repeat
+
+from mamba.widgets.base import Widget
+from mamba.constants import TILE_SIZE
+from mamba.snake import Snake
+from mamba.engine import FlipArrowsEvent, ReplayEvent
+
+
+class EditLevelWidget(Widget):
+    def __init__(self, level, offset=(0, 0)):
+        self.level = level
+        level_rect = Rect(offset, level.get_size())
+        self.main_tool = None
+        self.tool = None
+        self.drawing = False
+        self.last_click_pos = None
+        self.tile_mode = True  # Flag for sprite interactions
+        super(EditLevelWidget, self).__init__(level_rect)
+        self.add_callback(MOUSEBUTTONDOWN, self.place_tile)
+        self.add_callback(MOUSEBUTTONUP, self.end_draw)
+        self.add_callback(MOUSEMOTION, self.draw_tiles)
+        self.add_callback(FlipArrowsEvent, self.flip_arrows)
+        self.add_callback(ReplayEvent, self.handle_replay)
+        self.snake = None
+        self.snake_alive = False
+        self.replay_data = []
+        self.last_run = []
+        self.replay_pos = 0
+
+    def get_replay(self):
+        return self.replay_data[:]
+
+    def replay(self, run=None):
+        if run is None:
+            # We exclude the last couple of steps, so we don't redo
+            # the final crash in this run
+            run = self.last_run[:-4]
+        if len(run) > 0:
+            self.start_test()
+            ReplayEvent.post(run, 0)
+
+    def handle_replay(self, ev, widget):
+        self.apply_action(ev.run[ev.replay_pos])
+        if ev.replay_pos < len(ev.run) - 1:
+            ReplayEvent.post(ev.run, ev.replay_pos + 1)
+
+    def start_test(self):
+        self.level.update_tiles_ascii()
+        self.last_run = self.replay_data
+        self.replay_data = []
+        self.level.restart()
+        tile_pos, orientation = self.level.get_entry()
+        self.snake = Snake(tile_pos, orientation)
+        set_repeat(40, 100)
+        self.snake_alive = True
+
+    def stop_test(self):
+        self.snake = None
+        self.snake_alive = False
+        self.level.restart()
+        set_repeat(0, 0)
+
+    def draw(self, surface):
+        self.level.draw(surface)
+        if self.snake:
+            self.snake.draw(surface)
+
+    def kill_snake(self):
+        """Prevent user interaction while snake is dead"""
+        self.snake_alive = False
+
+    def restart(self):
+        self.start_test()
+
+    def interact(self, segment):
+        if not self.snake or not self.snake_alive:
+            return
+        tiles = segment.filter_collisions(self.level.sprites)
+        for tile in tiles:
+            tile.interact(self, segment)
+
+    def get_sprite(self, sprite_id):
+        return self.level.extra_sprites[sprite_id]
+
+    def apply_action(self, orientation):
+        self.replay_data.append(orientation)
+        if not self.snake or not self.snake_alive:
+            return
+        # We choose numbers that are close to, but not exactly, move 0.5 tiles
+        # to avoid a couple of rounding corner cases in the snake code
+        if orientation is None or orientation == self.snake.orientation:
+            self.snake.update(9.99 / self.snake.speed, self)
+        else:
+            self.snake.send_new_direction(orientation)
+            self.snake.update(9.99 / self.snake.speed, self)
+
+    def set_tool(self, new_tool):
+        self.main_tool = new_tool
+        self.tool = new_tool
+
+    def end_draw(self, event, widget):
+        self.drawing = False
+
+    def draw_tiles(self, event, widget):
+        if self.drawing and self.tool:
+            # FIXME: Need to consider leaving and re-entering the widget
+            self.update_tile(self.convert_pos(event.pos))
+
+    def flip_arrows(self, ev, widget):
+        self.level.flip_arrows()
+
+    def place_tile(self, event, widget):
+        if self.tile_mode:
+            if event.button == 1:  # Left button
+                self.tool = self.main_tool
+            else:
+                self.tool = '.'
+            self.drawing = True
+            if self.tool:
+                self.update_tile(self.convert_pos(event.pos))
+        else:
+            self.last_click_pos = self.convert_pos(event.pos)
+            self.drawing = False
+
+    def convert_pos(self, pos):
+        return (pos[0] / TILE_SIZE[0], pos[1] / TILE_SIZE[1])
+
+    def update_tile(self, tile_pos):
+        """Update the tile at the current mouse position"""
+        if self.check_paused():
+            return  # Do nothing if dialogs showing
+        # We convert our current position into a tile position
+        # and replace the tile with the current tool
+        old_tile = self.level.get_tile(tile_pos)
+        if self.tool == '.' and old_tile is None:
+            return
+        elif old_tile is not None and old_tile.tile_char == self.tool:
+            return
+        self.level.replace_tile(tile_pos, self.tool)
+
+    def check_paused(self):
+        return hasattr(self.parent, 'paused') and self.parent.paused
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pyntnclick/widgets/levelbutton.py	Sat Feb 11 14:09:46 2012 +0200
@@ -0,0 +1,86 @@
+import pygame
+from pygame.locals import SRCALPHA
+
+from mamba.constants import FOCUS_COLOR
+from mamba.data import load_image
+from mamba.widgets.base import Button
+from mamba.widgets.text import TextWidget
+
+
+class LevelButton(Button):
+
+    def __init__(self, rect, level, done=False):
+        super(LevelButton, self).__init__(rect)
+        self.level = level
+        self.text = level.name
+        self.done = done
+        self.focussable = True
+        self.border = 2
+        self.rect.width = 100
+        self.rect.height = 120
+        self.prepare()
+
+    def make_thumbnail(self, dest_rect):
+        level_surface = pygame.Surface(self.level.get_size(), SRCALPHA)
+        self.level.draw(level_surface)
+        size = level_surface.get_rect().fit(dest_rect).size
+        level_thumbnail = pygame.transform.scale(level_surface, size)
+        return level_thumbnail
+
+    def prepare(self):
+        self.surface = pygame.Surface(self.rect.size, SRCALPHA)
+        self.surface.fill(0)
+
+        dest_rect = pygame.Rect((self.border, self.border),
+                                (self.rect.width - self.border,
+                                 self.rect.height - self.border))
+        if not hasattr(self.level, 'button_thumbnail'):
+            self.level.button_thumbnail = self.make_thumbnail(dest_rect)
+        self.surface.blit(self.level.button_thumbnail, dest_rect)
+
+        if self.done:
+            image = load_image('menus/tick.png')
+            self.surface.blit(image, image.get_rect())
+
+        # We only have space for two lines
+        self._text_lines = self.wrap_text(self.text)[:2]
+
+        text_height = sum(line.rect.height for line in self._text_lines)
+        text_pos = self.level.button_thumbnail.get_rect().height
+        text_pos += (self.rect.height - text_height - text_pos) // 2
+        for line in self._text_lines:
+            text_rect = pygame.Rect(((self.rect.width - line.rect.width) // 2,
+                                     text_pos),
+                                    line.rect.size)
+            text_pos = text_rect.bottom
+            self.surface.blit(line.surface, text_rect)
+
+        color = pygame.Color(FOCUS_COLOR if self.focussed else '#444444')
+        pygame.draw.rect(self.surface, color, self.surface.get_rect(),
+                         self.border + 1)
+        self._state = (self.done, self.focussed)
+
+    def wrap_text(self, text):
+        args = {'rect': (0, 0),
+                'text': text,
+                'fontsize': 12,
+                'color': 'white',
+               }
+        w = TextWidget(**args)
+        w.prepare()
+        splitpoint = len(text)
+        max_width = self.rect.width - (self.border * 3)
+        while w.rect.width > max_width and ' ' in text[:splitpoint]:
+            splitpoint = text.rfind(' ', 0, splitpoint)
+            args['text'] = text[:splitpoint]
+            w = TextWidget(**args)
+            w.prepare()
+        if splitpoint < len(text):
+            return [w] + self.wrap_text(text[splitpoint + 1:])
+        else:
+            return [w]
+
+    def draw(self, surface):
+        if self._state != (self.done, self.focussed):
+            self.prepare()
+        surface.blit(self.surface, self.rect)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pyntnclick/widgets/listbox.py	Sat Feb 11 14:09:46 2012 +0200
@@ -0,0 +1,45 @@
+from mamba.widgets.base import Box
+from mamba.widgets.toollist import ToolListWidget
+from mamba.widgets.text import TextWidget, TextButton
+
+
+class ListBox(Box):
+
+    def __init__(self, rect, text, widget_list, page_length=8):
+        super(ListBox, self).__init__(rect)
+        self.message = TextWidget(rect, text)
+        self.toolbar = ToolListWidget(rect, widget_list, page_length,
+                start_key=None)
+        self.prepare()
+        self.modal = True
+
+    def prepare(self):
+        width = max(self.toolbar.rect.width, self.message.rect.width)
+        if width > self.message.rect.width:
+            message_pos = (self.rect.left + width / 2
+                    - self.message.rect.width / 2, self.rect.top)
+        else:
+            message_pos = (self.rect.left, self.rect.top + 5)
+        tool_pos = (self.rect.left,
+                self.rect.top + self.message.rect.height + 2)
+        self.message.rect.topleft = message_pos
+        self.toolbar.rect.topleft = tool_pos
+        self.toolbar.fill_page()  # Fix alignment
+        self.add(self.message)
+        self.add(self.toolbar)
+        self.ok_button = ok_button = TextButton((0, 0), 'OK')
+        ok_pos = (self.rect.left + width / 2 - ok_button.rect.width / 2,
+                tool_pos[1] + 2 + self.toolbar.rect.height)
+        ok_button.rect.topleft = ok_pos
+        ok_button.add_callback('clicked', self.close)
+        self.add(ok_button)
+        self.rect.height += 5
+
+    def close(self, ev, widget):
+        if hasattr(self.parent, 'paused'):
+            self.parent.paused = False
+        self.parent.remove(self)
+        return True
+
+    def grab_focus(self):
+        return self.ok_button.grab_focus()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pyntnclick/widgets/messagebox.py	Sat Feb 11 14:09:46 2012 +0200
@@ -0,0 +1,54 @@
+from mamba.constants import FONT_SIZE
+from mamba.widgets.base import Box
+from mamba.widgets.text import TextWidget, TextButton
+
+
+class MessageBox(Box):
+
+    def __init__(self, rect, text, post_callback=None, color='red',
+            fontsize=FONT_SIZE):
+        super(MessageBox, self).__init__(rect)
+        self.text = text
+        self.font_size = fontsize
+        self.post_callback = post_callback
+        self.color = color
+        self.prepare()
+        self.modal = True
+
+    def prepare(self):
+        cont = TextWidget((0, 0), "Press [OK] or Enter to continue",
+                fontsize=self.font_size)
+        widgets = []
+        width = cont.rect.width
+        for line in self.text.split('\n'):
+            message = TextWidget((0, 0), line, color=self.color,
+                    fontsize=self.font_size)
+            widgets.append(message)
+            width = max(width, message.rect.width)
+        widgets.append(cont)
+        top = self.rect.top + 10
+        left = self.rect.left + 5
+        for widget in widgets:
+            pos = (left + width / 2 - widget.rect.width / 2, top)
+            widget.rect.topleft = pos
+            top += widget.rect.height + 5
+            self.add(widget)
+        self.ok_button = ok_button = TextButton((0, 0), 'OK')
+        ok_pos = (self.rect.left + 5 + width / 2 - ok_button.rect.width / 2,
+                top + 5)
+        ok_button.rect.topleft = ok_pos
+        ok_button.add_callback('clicked', self.close)
+        self.add(ok_button)
+        self.rect.height += 5
+
+    def close(self, ev, widget):
+        if hasattr(self.parent, 'paused'):
+            self.parent.paused = False
+        self.parent.remove(self)
+        if self.post_callback:
+            self.post_callback()
+        if getattr(self, 'parent_modal', False):
+            self.parent.modal = True
+
+    def grab_focus(self):
+        return self.ok_button.grab_focus()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pyntnclick/widgets/overlay.py	Sat Feb 11 14:09:46 2012 +0200
@@ -0,0 +1,29 @@
+from mamba.widgets.base import Button
+
+
+class OverlayOnFocusButton(Button):
+    """A non-visiable clickable area, that causes an overlay to be
+       displayed when focussed"""
+
+    def __init__(self, rect, image):
+        self.image = image
+        super(OverlayOnFocusButton, self).__init__(rect)
+        self.focussable = True
+
+    def draw(self, surface):
+        if self.focussed:
+            surface.blit(self.image, surface.get_rect())
+
+
+class OverlayButton(Button):
+    """A non-visiable clickable area, that causes an overlay to be
+    displayed. Doesn't really understand this focus thing."""
+
+    def __init__(self, rect, image):
+        self.image = image
+        super(OverlayButton, self).__init__(rect)
+        self.focussable = True
+
+    def draw(self, surface):
+        if not self.disabled:
+            surface.blit(self.image, surface.get_rect())
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pyntnclick/widgets/text.py	Sat Feb 11 14:09:46 2012 +0200
@@ -0,0 +1,131 @@
+import pygame
+from pygame.constants import (SRCALPHA, KEYDOWN, K_ESCAPE, K_RETURN, K_UP,
+        K_DOWN, K_SPACE, K_KP_ENTER)
+
+from mamba.constants import COLOR, FONT_SIZE, FOCUS_COLOR, DELETE_KEYS
+from mamba.widgets.base import Widget, Button
+from mamba.data import filepath
+from mamba.constants import DEFAULT_FONT
+
+
+class TextWidget(Widget):
+    fontcache = {}
+
+    def __init__(self, rect, text, fontsize=FONT_SIZE, color=COLOR):
+        super(TextWidget, self).__init__(rect)
+        self.text = text
+        self.fontsize = fontsize
+        self.color = color
+        self.prepare()
+
+    def prepare(self):
+        self.fontname = DEFAULT_FONT
+        font = (self.fontname, self.fontsize)
+        if font not in TextWidget.fontcache:
+            fontfn = filepath('fonts/' + self.fontname)
+            TextWidget.fontcache[font] = pygame.font.Font(fontfn,
+                    self.fontsize)
+        self.font = TextWidget.fontcache[font]
+        if not isinstance(self.color, pygame.Color):
+            self.color = pygame.Color(self.color)
+        self.surface = self.font.render(self.text, True, self.color)
+        self.text_rect = self.surface.get_rect()
+        width, height = self.surface.get_rect().size
+        self.rect.width = max(self.rect.width, width)
+        self.rect.height = max(self.rect.height, height)
+
+    def draw(self, surface):
+        surface.blit(self.surface, self.rect)
+
+
+class TextButton(Button, TextWidget):
+    def __init__(self, *args, **kwargs):
+        self.focus_color = kwargs.pop('focus_color', FOCUS_COLOR)
+        self.padding = kwargs.pop('padding', 10)
+        self.border = kwargs.pop('border', 3)
+        super(TextButton, self).__init__(*args, **kwargs)
+        if not isinstance(self.focus_color, pygame.Color):
+            self.focus_color = pygame.Color(self.focus_color)
+        self.focussable = True
+
+    def prepare(self):
+        super(TextButton, self).prepare()
+        text = self.surface
+        text_rect = self.text_rect
+        self._focussed = self.focussed
+        if self.disabled:
+            color = pygame.Color('#666666')
+        elif self.focussed:
+            color = self.focus_color
+        else:
+            color = self.color
+
+        width = text_rect.width + self.padding * 2
+        height = text_rect.height + self.padding * 2
+        self.rect.width = max(self.rect.width, width)
+        self.rect.height = max(self.rect.height, height)
+        self.surface = pygame.Surface(self.rect.size, SRCALPHA)
+        self.surface.fill(0)
+        self.surface.blit(text, text.get_rect().move(self.padding,
+                                                     self.padding))
+        pygame.draw.rect(self.surface, color, self.surface.get_rect(),
+                         self.border)
+
+    def draw(self, surface):
+        if self._focussed != self.focussed:
+            self.prepare()
+        super(TextButton, self).draw(surface)
+
+    def event(self, ev):
+        if ev.type == KEYDOWN and ev.key == K_SPACE:
+            return self.forced_click()
+        return super(TextButton, self).event(ev)
+
+
+class EntryTextWidget(TextWidget):
+    def __init__(self, rect, text, **kwargs):
+        self.focus_color = kwargs.pop('focus_color', FOCUS_COLOR)
+        self.prompt = kwargs.pop('prompt', 'Entry:')
+        self.value = text
+        text = '%s %s' % (self.prompt, text)
+        self.base_color = COLOR
+        self.update_func = kwargs.pop('update', None)
+        super(EntryTextWidget, self).__init__(rect, text, **kwargs)
+        if not isinstance(self.focus_color, pygame.Color):
+            self.focus_color = pygame.Color(self.focus_color)
+        self.focussable = True
+        self.base_color = self.color
+        self.add_callback(KEYDOWN, self.update)
+
+    def update(self, ev, widget):
+        old_value = self.value
+        if ev.key in DELETE_KEYS:
+            if self.value:
+                self.value = self.value[:-1]
+        elif ev.key in (K_ESCAPE, K_RETURN, K_KP_ENTER, K_UP, K_DOWN):
+            return False  # ignore these
+        else:
+            self.value += ev.unicode
+        if old_value != self.value:
+            self.text = '%s %s' % (self.prompt, self.value)
+            self.prepare()
+        return True
+
+    def prepare(self):
+        self.color = self.focus_color if self.focussed else self.base_color
+        super(EntryTextWidget, self).prepare()
+        self._focussed = self.focussed
+
+    def draw(self, surface):
+        if self._focussed != self.focussed:
+            self.prepare()
+        super(EntryTextWidget, self).draw(surface)
+        if self.focussed:
+            text_rect = self.text_rect
+            # Warning, 00:30 AM code on the last Saturday
+            cur_start = text_rect.move(
+                    (self.rect.left + 2, self.rect.top + 3)).topright
+            cur_end = text_rect.move(
+                    (self.rect.left + 2, self.rect.top - 3)).bottomright
+            pygame.draw.line(surface, self.focus_color,
+                    cur_start, cur_end, 2)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pyntnclick/widgets/toollist.py	Sat Feb 11 14:09:46 2012 +0200
@@ -0,0 +1,85 @@
+from pygame.constants import KEYUP, K_1, K_PAGEDOWN, K_PAGEUP
+
+from mamba.widgets.base import Container
+from mamba.widgets.text import TextButton
+
+
+class ToolListWidget(Container):
+    """List of other widgets, with some paging trickery"""
+
+    def __init__(self, rect, widget_list, page_length, start_key=K_1,
+            padding=2):
+        widget_list.sort(key=lambda w: w.text)
+        self.widget_list = widget_list
+        self.page_length = page_length
+        self.padding = padding
+        self.page = 0
+        self.start_key = start_key
+        super(ToolListWidget, self).__init__(rect)
+        self.prev_but = None
+        self.next_but = None
+        self.fill_page()
+        # We do this to avoid needing to worry about focus too much
+        self.add_callback(KEYUP, self.handle_key)
+        self.focussable = True
+
+    def fill_page(self):
+        for widget in self.children[:]:
+            self.remove(widget)
+        self.hot_keys = {}
+        start_page = self.page * self.page_length
+        end_page = start_page + self.page_length
+        button_height = self.rect.top + self.padding
+        button_left = self.rect.left + self.padding
+        key = self.start_key
+        for widget in self.widget_list[start_page:end_page]:
+            widget.rect.topleft = (button_left, button_height)
+            self.add(widget)
+            if key:
+                self.hot_keys[key] = widget
+                key += 1
+            button_height += widget.rect.height + self.padding
+        if not self.prev_but:
+            self.prev_but = TextButton((button_left, button_height),
+                                       u'\N{LEFTWARDS ARROW}')
+            self.prev_but.add_callback('clicked', self.change_page, -1)
+        else:
+            self.prev_but.rect.top = max(button_height, self.prev_but.rect.top)
+        if not self.next_but:
+            self.next_but = TextButton((button_left + 100, button_height),
+                    u'\N{RIGHTWARDS ARROW}')
+            self.next_but.add_callback('clicked', self.change_page, 1)
+        else:
+            self.next_but.rect.top = max(button_height, self.next_but.rect.top)
+        if start_page > 0:
+            self.prev_but.enable()
+        else:
+            self.prev_but.disable()
+        if end_page < len(self.widget_list):
+            self.next_but.enable()
+        else:
+            self.next_but.disable()
+        self.add(self.prev_but)
+        self.add(self.next_but)
+        for widget in self.children[:]:
+            if widget in self.widget_list:
+                # Standardise widdths
+                widget.rect.width = self.rect.width - 2
+                widget.prepare()
+
+    def handle_key(self, ev, widget):
+        if hasattr(self.parent, 'paused') and self.parent.paused:
+            # No hotjets when pasued
+            return False
+        if ev.key in self.hot_keys:
+            widget = self.hot_keys[ev.key]
+            return widget.forced_click()
+        elif ev.key == K_PAGEDOWN and self.prev_but:
+            return self.prev_but.forced_click()
+        elif ev.key == K_PAGEUP and self.next_but:
+            return self.next_but.forced_click()
+
+    def change_page(self, ev, widget, change):
+        self.page += change
+        self.fill_page()
+        return True