Added a copy of SGC

This commit is contained in:
Anna Rose 2012-04-14 18:38:47 -04:00
parent 90ff7ceba8
commit 52eb18994d
22 changed files with 3283 additions and 0 deletions

22
sgc/LICENSE Normal file
View File

@ -0,0 +1,22 @@
Copyright (c) 2010-2012, Sam Bull
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

14
sgc/__init__.py Normal file
View File

@ -0,0 +1,14 @@
"""
Module Packages:
:py:mod:`widgets<sgc.widgets>`: All the widgets available for use in this toolkit.
Modules:
:py:mod:`locals<sgc.locals>`: Constants to be imported into the local namespace for convenience.
:py:mod:`surface<sgc.surface>`: Extended pygame.surface classes.
"""
import surface
import locals
import widgets
from widgets._locals import Font

16
sgc/example/menu Normal file
View File

@ -0,0 +1,16 @@
("m:Main Menu",
("m:Sub-menu",
("w:input_box","name=input","label=Input","default=start typing"),
("c:Category/divider",),
("w:button","name=btn","label=Click\nhere","func=print_input")
),
("m:Settings",
("m:Graphic Settings",
("c:Graphic stuff",)
),
("m:Sound Settings",
("c:Sound stuff",)
)
),
("f:remove", "Quit")
)

151
sgc/example/test.py Normal file
View File

@ -0,0 +1,151 @@
#!/usr/bin/env python
# Copyright (C) 2010-2012 Sam Bull
"""
An example file demonstrating proper use of widgets.
"""
import os
import sys
import random
import pygame
from pygame.locals import *
try:
from OpenGL.GL import *
except: pass
pygame.display.init()
pygame.font.init()
sys.path.insert(0, "../..")
import sgc
from sgc.locals import *
ver_no = "0.1.3"
screen = sgc.surface.Screen((640,480)) #, flags=OPENGL
clock = pygame.time.Clock()
pygame.scrap.init()
def clear():
"""Clear input box when enter key is pressed."""
input_box.text = ""
class MainMenu(sgc.widgets.Menu):
"""Create a subclass for custom functions."""
func_dict = lambda self: {"print_input": self.print_input,
"remove": self.remove}
def print_input(self):
print self["input"].text
sgc.Font.col = (150,150,150) # TODO Button font colour
# Title
title = sgc.widgets.Label(text="Simple Game Code " + ver_no,
font=sgc.Font["title"], col=sgc.Font.col)
title.rect.center = (screen.rect.w/2, 40)
title.add()
# Create input_box
input_box = sgc.widgets.InputBox(label="Input Box", default="default text...")
input_box.config(pos=(30,120))
input_box.add(0)
# Change colour button, activate event caught in event loop
button = sgc.widgets.Button(label="Change\ncolour", pos=(40,200))
# Create FPS counter
fps = sgc.widgets.FPSCounter(clock=clock)
fps.rect.midbottom = (screen.rect.w/2, screen.rect.h)
fps.add()
# Pass config file as argument, to have Menu parse file
with open("menu") as menu_file:
menu = MainMenu(menu=menu_file)
# Display menu on button click, activate replaced through assignment
btn_menu = sgc.widgets.Button(label="Menu", pos=(250,200))
btn_menu.activate = menu.add
# Input_box for dialog window
password_box = sgc.widgets.InputBox(label="Password", default="Enter password...")
password_box.pos = (0,10)
# Button for dialog window
def print_pass():
print password_box.text
dialogs[-1].remove()
btn_ok = sgc.widgets.Button(label="OK", pos=(30,60))
btn_ok.activate = print_pass
# Place widgets into a container
dialog_container = sgc.widgets.Container(widgets=(password_box, btn_ok),
border=10)
# Display dialog window, activate replaced through inheritance
dialogs = []
class BtnDialog(sgc.widgets.Button):
def activate(self):
dialogs.append(sgc.widgets.Dialog(widget=dialog_container,
title="Window title here..."))
dialogs[-1].rect.center = screen.rect.center
dialogs[-1].add()
btn_dialog = BtnDialog(label="Dialog", pos=(460,200))
box_btn = sgc.widgets.HBox(widgets=[button, btn_menu, btn_dialog], spacing=70)
scroll_box = sgc.widgets.ScrollBox((300, box_btn.rect.h), widget=box_btn)
scroll_box.rect.center = screen.rect.center
scroll_box.add(1)
# Radio Buttons
radio1 = sgc.widgets.Radio(group="group1", label="Option 1", active=True)
radio2 = sgc.widgets.Radio(group="group1", label="Option 2")
radio3 = sgc.widgets.Radio(group="group1", label="Option 3")
radio_box = sgc.widgets.VBox(widgets=(radio1, radio2, radio3), pos=(40,320))
radio_box.add(2)
# Toggle Button
toggle = sgc.widgets.Toggle(label="Toggle", pos=(200,320))
toggle.add(3)
# Selectable Label
label = sgc.widgets.Label(text="This is a selectable label", selectable=True)
label.rect.midtop = title.rect.midbottom
label.add()
while True:
time = clock.tick(30)
for event in pygame.event.get():
# Send event to widgets
sgc.widgets.event(event)
if event.type == GUI:
if event.widget_type is sgc.widgets.Button:
print "Button event"
if event.widget is button and event.gui_type == "activate":
button.config(col=[random.randrange(1,200) for x in range(3)])
elif event.widget is input_box:
clear()
elif event.type == KEYDOWN:
if event.key == K_f:
fps.toggle()
elif event.type == QUIT:
exit()
# Cleanup removed windows
for widget in dialogs:
if not widget.active():
dialogs.remove(widget)
if not screen._opengl:
screen.fill((0,0,255))
else:
glClearColor(0,0,1,1)
glClear(GL_COLOR_BUFFER_BIT)
# Update the widgets once for each frame
sgc.widgets.update(time)
pygame.display.flip()

13
sgc/locals.py Normal file
View File

@ -0,0 +1,13 @@
#!/usr/bin/env python
# Copyright (C) 2012 Sam Bull
"""
Imports useful objects into the local namespace.
Constants:
GUI: Event type for any event emitted by this toolkit.
"""
from widgets._locals import GUI

47
sgc/surface.py Normal file
View File

@ -0,0 +1,47 @@
#!/usr/bin/env python
#Copyright (C) 2010-2012 Sam Bull
"""
Screen class to store rect information with the screen and setup the toolkit.
"""
import pygame.display
from pygame.locals import *
import widgets._locals
class Screen():
"""
Class for the screen.
This must be used instead of ``pygame.display.set_mode()``.
Attributes:
image: The pygame.display screen.
rect: ``pygame.Rect`` containing screen size.
"""
__slots__ = ("_a", "rect", "image", "_opengl")
_a = 1 # Base alpha for OpenGLImage's
_opengl = False
def __init__(self, size, flags=0, depth=0):
"""
Args:
size, flags, depth: Arguments for pygame.display.set_mode()
"""
self.rect = Rect((0,0), size)
self.image = pygame.display.set_mode(size, flags, depth)
if flags & OPENGL:
self._opengl = True
widgets._locals.SCREEN = self
widgets._locals.Font.set_fonts()
def __getattr__(self, atr):
return getattr(self.image, atr)

40
sgc/widgets/__init__.py Normal file
View File

@ -0,0 +1,40 @@
"""
All widgets are imported into widget's namespace. This means you can access
widgets without their modules, such as ``sgc.widgets.Button``.
Widgets:
:py:class:`Simple<base_widget.Simple>`: Simple widget that does nothing. May be useful for images etc.
:py:class:`Button<button.Button>`: Clickable button.
:py:class:`FPSCounter<fps_counter.FPSCounter>`: FPS counter.
:py:class:`InputBox<input_box.InputBox>`: Input box.
:py:class:`Label<label.Label>`: Label.
:py:class:`Menu<menu.Menu>`: Game menu.
:py:class:`Radio<radio_button.Radio>`: Radio button.
:py:class:`settings`: TODO (Stay away). Common user settings (keymap etc.)
:py:class:`Toggle<toggle.Toggle>`: Toggle button.
Container widgets:
:py:class:`Container<container.Container>`: Basic container, holds a group of other widgets and handles
focus between them.
:py:class:`VBox<boxes.VBox>`: Automatically aligns widgets into a vertical column.
:py:class:`HBox<boxes.HBox>`: Automatically aligns widgets into a horizontal row.
:py:class:`Dialog<dialog.Dialog>`: Dialog window.
:py:class:`ScrollBox<scroll_box.ScrollBox>`: Allows another widget to be scrollable.
"""
from . import *
from _locals import update, event
from base_widget import Simple
from boxes import VBox, HBox
from button import Button
from container import Container
from dialog import Dialog
from fps_counter import FPSCounter
from input_box import InputBox
from label import Label
from menu import Menu
from radio_button import Radio
from scroll_box import ScrollBox
from settings import Keys
from toggle import Toggle

368
sgc/widgets/_locals.py Normal file
View File

@ -0,0 +1,368 @@
#!/usr/bin/env python
# Copyright (C) 2012 Sam Bull
"""
A collection of things for widgets to use.
Constants:
GUI: Widgets should use this for the event type of any events emitted.
get_screen(): Returns the screen object.
"""
import pygame.sprite
from pygame.locals import *
try:
import opengl
except ImportError: pass
# Things for widgets to import
__all__ = ["GUI", "get_screen", "Font"]
# Event type
GUI = USEREVENT
SCREEN = None
get_screen = lambda: SCREEN
# Cursor queue for set_cursor() and remove_cursor()
cursors = []
# ----- EXTERNAL FUNCTIONS -----
def update(time):
"""Updates all active widgets or modal widgets each frame."""
def _fade(widget):
"""Fade widget."""
if widget._fade is not None:
widget.image.set_alpha(widget._fade)
if widget._fade_up:
widget._fade += time / 3.
else:
widget._fade -= time / 4.
if widget._fade <= 0:
# Remove after fading
widget.kill()
# Reset widget to be added again
widget._fade = None
elif widget._fade >= 255:
widget._fade = None
widget.image.set_alpha(255)
if SCREEN._opengl:
glMatrixMode(GL_PROJECTION)
glLoadIdentity()
w,h = SCREEN.get_size()
glOrtho(0, w, h, 0, 0, 1)
glMatrixMode(GL_MODELVIEW)
glPushMatrix()
glLoadIdentity()
glDisable(GL_LIGHTING)
glDisable(GL_DEPTH_TEST)
glEnable(GL_SCISSOR_TEST)
glEnable(GL_BLEND)
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
# Fade widgets
for widget in layer_widgets:
_fade(widget)
for widget in active_widgets:
_fade(widget)
# Update widgets
active_widgets.update(time)
if not SCREEN._opengl:
active_widgets.draw(SCREEN)
else:
for widget in active_widgets:
widget.image.draw()
# Update layered widgets
layer_widgets.update(time)
if not SCREEN._opengl:
layer_widgets.draw(SCREEN)
else:
for widget in layer_widgets:
widget.image.draw()
if SCREEN._opengl:
glDisable(GL_SCISSOR_TEST)
glPopMatrix()
def event(event):
"""Send event to focused widget and handle widget focus."""
if modal_widgets and not focus:
modal_widgets.sprites()[-1].add(0)
# Mouse focus
if event.type == MOUSEBUTTONDOWN:
if not modal_widgets:
hit = False
for widget_list in (reversed(layer_widgets.sprites()),
active_widgets):
for widget in widget_list:
# Check if user clicked a widget
if widget._can_focus and \
widget.rect.collidepoint(event.pos):
focus.add(2, widget)
hit = True
if widget in layer_widgets:
layer_widgets.move_to_front(widget)
break
if hit: break
# Lose focus if clicking away from widgets
if not hit:
focus.empty()
# Keyboard focus
elif event.type == KEYDOWN and event.key == K_TAB:
if not modal_widgets and focus_order:
# Flattened focus_order
order = sum(focus_order,())
if focus.sprite not in order:
curr_num = None
else:
# Focus number for current focused widget
curr_num = order[order.index(focus.sprite)-1]
# Sorted list of the focus numbers being used
list_num = sorted(order[::2])
if not event.mod & KMOD_SHIFT: # Move focus to next widget
if curr_num is None:
# If nothing focused, focus first widget
new_num = list_num[0]
elif not focus.sprite._change_focus(True):
# Don't change when not at end of container widget
new_num = curr_num
elif list_num.index(curr_num) == len(list_num)-1:
# Jump back to first widget
new_num = list_num[0]
else:
# Next focus number in the list
new_num = list_num[list_num.index(curr_num)+1]
else: # Shift key - move focus to previous widget
if curr_num is None:
new_num = list_num[-1]
elif not focus.sprite._change_focus(False):
new_num = curr_num
elif list_num.index(curr_num) == 0:
# Jump back to last widget
new_num = list_num[len(list_num)-1]
else:
new_num = list_num[list_num.index(curr_num)-1]
if curr_num != new_num:
# Set widget at new focus number
focus.add(1, order[order.index(new_num)+1])
# Send event to focused widget
if focus:
focus.sprite._event(event)
# ----- FONTS -----
class _Font():
"""Wrapper class for font objects."""
__slots__ = ("_font",)
_font = None
def replace(self, font):
"""Replace the font in-place."""
self._font = font
def __getattr__(self, atr):
return getattr(self._font, atr)
def __nonzero__(self):
return True if self._font else False
class FontMetaclass(type):
"""Font metaclass to allow indexing of class."""
def __getitem__(cls, item):
return cls._fonts[item]
class Font():
"""
Class containing fonts available for use.
Index class to get fonts, such as ``Font["widget"]`` for the widget font.
The default fonts are:
widget: The default font for widgets.
title: A larger title font.
mono: A monospaced font.
Attributes:
col: (r,g,b) tuple, containing the default font colour.
"""
__metaclass__ = FontMetaclass
__slots__ = ("_fonts", "col")
_fonts = {"widget": _Font(), "title": _Font(), "mono": _Font()}
col = (255,255,255)
@classmethod
def set_fonts(cls, fonts={}):
"""
Set fonts to a specific font. If a font exists, it will be replaced,
otherwise it will be newly created.
Args:
fonts: Dictionary containing fonts to use.
Key should be name of font. Value should be string
naming either custom FreeType or a system font.
"""
for font in fonts:
if font not in cls._fonts:
cls._fonts[font] = _Font()
cls._fonts[font].replace(cls._create_font(fonts[font], 16))
if not cls._fonts["widget"]:
cls._fonts["widget"].replace(cls._create_font("Arial", 16))
if not cls._fonts["title"]:
name = fonts["widget"] if ("widget" in fonts) else "Arial"
cls._fonts["title"].replace(cls._create_font(name, 30))
if not cls._fonts["mono"]:
cls._fonts["mono"].replace(cls._create_font(
"FreeMono, Monospace", 16))
if SCREEN._opengl:
cls.mono_w = cls["mono"].font.Advance("e")
else:
cls.mono_w = cls["mono"].render("e", False, (0,0,0)).get_size()[0]
@classmethod
def _create_font(cls, font, size):
"""
Returns the correct font object for FreeType or system font, and
for OpenGL or Pygame.
"""
if font[-4:] in (".ttf", ".otf"):
if not SCREEN._opengl:
return pygame.font.Font(font, size)
else:
return opengl.OpenGLFont(font, size)
else:
if not SCREEN._opengl:
return pygame.font.SysFont(font, size)
else:
font = str(pygame.font.match_font(font))
return opengl.OpenGLFont(font, size)
# ----- WIDGET GROUPS -----
class Focus(pygame.sprite.GroupSingle):
"""
Contains currently focused widget.
"""
def add(self, focus=0, *sprites):
"""Extend add to call _focus_exit and _focus_enter methods."""
if self.sprite: self.sprite._focus_exit()
pygame.sprite.GroupSingle.add(self, *sprites)
self.sprite._focus_enter(focus)
def empty(self):
"""Extend empty to call _focus_exit method."""
if self.sprite: self.sprite._focus_exit()
pygame.sprite.GroupSingle.empty(self)
# Widget groups
active_widgets = pygame.sprite.Group()
modal_widgets = pygame.sprite.OrderedUpdates()
layer_widgets = pygame.sprite.LayeredUpdates()
# The widget that currently has focus
focus = Focus()
# Order the widgets should receive focus through TAB
focus_order = []
# ----- WIDGET FUNCTIONS -----
def add_widget(widget, order):
"""
Add widget to screen. Used by the base widget.
Returns:
True if widget has been added. False if already added.
"""
added = False
# Add to group of active widgets
if widget not in active_widgets and not widget._layered:
active_widgets.add(widget)
added = True
if order is not None and widget._can_focus:
focus_order.append((order,widget))
# Add to layered group
elif widget._layered and widget not in layer_widgets:
layer_widgets.add(widget)
added = True
# Add to group of modal widgets
if widget._modal and widget not in modal_widgets:
modal_widgets.add(widget)
added = True
# Focus newly added modal widgets
if widget._modal:
focus.add(0, widget)
return added
def remove_widget_order(widget):
"""Remove widget from focus order. Called by the base widget."""
order = sum(focus_order,())
if widget in order:
# Remove from focus_order
num = (order.index(widget)-1)/2
del focus_order[num]
def has_focus(widget):
"""Checks if a widget currently has focus."""
for group in widget.groups():
if isinstance(group, Focus):
return True
return False
def is_active(widget):
"""Checks if widget is onscreen."""
return widget in active_widgets or widget in layer_widgets
def set_cursor(widget, size, hotspot, xormasks, andmasks):
"""
Sets a cursor and adds to a queue.
Args:
widget: The widget that set the cursor, used as an ID in the queue.
size,hotspot,xormasks,andmasks: Arguments for pygame.mouse.set_cursor().
"""
if not cursors:
cursors.append((None, pygame.mouse.get_cursor()))
cursors.append((widget, (size, hotspot, xormasks, andmasks)))
pygame.mouse.set_cursor(size, hotspot, xormasks, andmasks)
def remove_cursor(widget):
"""
Removes the cursor set by widget and sets cursor to whichever cursor
is now at the end of the queue.
"""
for w, c in cursors:
if w == widget:
cursors.remove((w, c))
pygame.mouse.set_cursor(*cursors[-1][1])
if len(cursors) <= 1:
del cursors[:]

299
sgc/widgets/base_widget.py Normal file
View File

@ -0,0 +1,299 @@
#!/usr/bin/env python
# Copyright (C) 2010-2012 Sam Bull
"""
Base widget, all widgets inherit from this.
"""
import pygame
from pygame.locals import Rect
from _locals import *
from _locals import (has_focus, is_active, add_widget, remove_widget_order,
set_cursor, remove_cursor)
try:
import opengl
except ImportError:
print "OpenGL widgets disabled."
class Simple(pygame.sprite.Sprite):
"""
Widget foundations all widgets should inherit from.
This can also be used as a simple widget that does nothing, such as
displaying an image.
Attributes:
image: The current surface that will be drawn to the screen.
rect: The ``pygame.Rect`` used for the widget's position and size.
rect_abs: A ``pygame.Rect`` using the absolute screen position.
pos: The widget's position. Can be retrieved or assigned as a shortcut
for rect.topleft. Also a shortcut for setting pos through config().
pos_abs: The widget's absolute screen position.
"""
# Widget settings
_can_focus = False
_modal = False
_layered = False # Layered updates for dialog windows etc.
_default_size = None
_parent = None
_available_images = ()
_extra_images = {}
_settings_default = {}
_fade = None # Alpha level when fading
_fade_up = True
_custom_image = False
def __init__(self, surf=None, **kwargs):
"""
Args:
surf: The surface that should be drawn to screen, of type:
pygame.Surface: Use an existing surface.
tuple,list: Contains size as (width,height), creates a new surface.
str: Contains file name to load an image.
dict: Contains multiple images to be loaded. The documentation will
specify if a widget uses multiple images and what names to use.
kwargs: Any number of keyword arguments matching those for config().
"""
pygame.sprite.Sprite.__init__(self)
# Implicitly pass attributes into _draw()
# Used to reduce complexity for widget developers
draw = self._draw
self._draw = lambda d=self.get_draw(): draw(d)
# Initialise attributes
self._images = {}
self._available_images = ("image",) + self._available_images
self._settings = self._settings_default.copy()
self.rect = Rect((0,0), (0,0))
# Use default size if none specified
if surf is None:
surf = self._default_size
# Create base surfaces if not None.
# If None, widget is expected to call this function later.
if surf is not None:
self._create_base_images(surf)
self.config(init=None, **kwargs)
def config(self, **kwargs):
"""
Update widget configuration and redraw the widget.
Keyword Args:
pos: ``tuple`` (x,y) Position to set widget to.
"""
if "pos" in kwargs:
self.rect.topleft = kwargs["pos"]
self._config(**kwargs)
self._draw()
def _config(self, **kwargs):
"""Widgets should overload for custom widget configuration."""
pass
def add(self, order=None, fade=True):
"""
Add widget to screen.
Args:
order: Integer representing the order widget should receive focus
when user presses TAB. The widget with the lowest order will
receive focus first, then moving up with increasing values.
fade: True if widget should fade in, False if not.
"""
added = add_widget(self, order)
# Fade widget in
if fade:
self._fade_up = True
if added and self._fade is None: self._fade = 1
self.image.set_alpha(self._fade)
else:
self._fade = None
self.image.set_alpha(255)
def remove(self, fade=True):
"""
Remove widget from screen.
Args:
fade: True if widget should fade out.
"""
if fade: # Fade widget out
self._fade_up = False
if self._fade == None: self._fade = 250
else: # Remove widget immediately
self.kill()
remove_widget_order(self)
def active(self):
"""Return True if widget is active (onscreen)."""
return is_active(self)
def has_focus(self):
"""Return True if this widget has focus."""
return has_focus(self)
def get_draw(self):
"""
Return appropriate draw module for pygame or OpenGL.
Use like:
``draw = self.get_draw()``
``draw.rect(self.image, ...)``
"""
if not get_screen()._opengl:
return pygame.draw
else:
return opengl.draw
def update(self, time):
"""Placeholder for function that updates the widget per frame."""
pass
def _event(self, event):
"""Placeholder for function that receives event for the widget."""
pass
def _draw(self, draw):
"""Widgets should overload to draw default images."""
pass
def _create_base_images(self, surf, parent=None):
"""
Creates the base surfaces to draw on, or uses existing images.
If self._default_size is None, widget is expected to call this
function manually when no size is given.
"""
Image = opengl.OpenGLImage if get_screen()._opengl else pygame.Surface
def create_image(surf):
"""Return a created surface."""
if isinstance(surf, pygame.Surface):
surf.set_colorkey(0)
self._custom_image = True
return surf
elif isinstance(surf, (tuple,list)):
self._custom_image = False
if isinstance(surf[0], (tuple,list)):
surf = (self.rect.w * surf[0][0] + surf[0][1],
self.rect.h * surf[1][0] + surf[1][1])
surf = Image(surf)
surf.set_colorkey(0)
return surf
elif isinstance(surf, str):
self._custom_image = True
return pygame.image.load(surf).convert_alpha()
# Create base images
if isinstance(surf, dict):
for img in surf:
assert (img in self._available_images or
img in self._extra_images), "Incorrect image."
self._images[img] = create_image(surf[img])
else:
self._images["image"] = create_image(surf)
# Copy other images, if any have not been supplied.
assert "image" in self._images, "Must supply 'image'"
for count, name in enumerate(self._available_images):
if name not in self._images:
img = self._images[self._available_images[count-1]]
self._images[name] = img.copy()
self.image = self._images["image"].copy()
self.rect.size = self.image.get_size()
# Set up extra images
for name in self._extra_images:
if name not in self._images:
self._images[name] = create_image(self._extra_images[name])
def _change_focus(self, forward=True):
"""
Called when focus should be changed. Container widget
should override this function.
Args:
forward: True if toggling focus forwards, False if backwards.
Returns:
True if widget should change focus from this widget.
"""
return True
def _focus_enter(self, focus=0):
"""
Called when the widget gains focus.
Args:
focus: 1 if focused by keyboard, 2 if by mouse.
"""
pass
def _focus_exit(self):
"""Called when the widget loses focus."""
pass
def _dotted_rect(self, col=(255,255,255)):
"""Draw a dotted rectangle to show keyboard focus."""
self.image.lock()
for i in range(0, self.rect.w, 3):
# Draw horizontal lines
self.image.set_at((i, 0), col)
self.image.set_at((i, self.rect.h-1), col)
for i in range(0, self.rect.h, 2):
# Draw vertical lines
self.image.set_at((0, i), col)
self.image.set_at((self.rect.w-1, i), col)
self.image.unlock()
def _set_cursor(self, size, hotspot, xormasks, andmasks):
set_cursor(self, size, hotspot, xormasks, andmasks)
def _remove_cursor(self):
remove_cursor(self)
# --PROPERTIES--
@property
def rect_abs(self):
if self._parent is None:
return self.rect
else:
p_abs = self._parent.pos_abs
p = (self.rect.x + p_abs[0], self.rect.y + p_abs[1])
return Rect(p, self.rect.size)
@property
def pos(self):
return self.rect.topleft
@pos.setter
def pos(self, value):
self.rect.topleft = value
@property
def pos_abs(self):
if self._parent is None:
return self.rect.topleft
else:
p_abs = self._parent.pos_abs
return (self.rect.x + p_abs[0], self.rect.y + p_abs[1])

74
sgc/widgets/boxes.py Normal file
View File

@ -0,0 +1,74 @@
#!/usr/bin/env python
# Copyright (C) 2011-2012 Sam Bull
"""
Boxes are container widgets with automatic positioning/padding of widgets.
"""
from container import Container
class VBox(Container):
"""
VBox is a container widget which sorts widgets into a vertical
structure.
If ``surf`` is not given, container will be the right size to fit all
widgets.
"""
_settings_default = {"border": 5, "spacing": 5, "col": 0, "widgets": None}
def _config(self, **kwargs):
"""
widgets: ``list`` Contains widgets to pack into box.
The order of widgets in the list denotes order they are packed.
border: ``int`` Number of pixels to space around edges when ``surf``
is not given.
col: ``tuple`` (r,g,b) Colour for background, 0 is transparent.
spacing: ``int`` Number of pixels to space between widgets.
"""
if "spacing" in kwargs:
self._settings["spacing"] = kwargs["spacing"]
if "widgets" in kwargs:
pos = 0
for w in kwargs["widgets"]:
w.pos = (0, pos)
pos += w.rect.h + self._settings["spacing"]
Container._config(self, **kwargs)
class HBox(Container):
"""
HBox is a container widget which sorts widgets into a horizontal
structure.
If ``surf`` is not given, container will be the right size to fit all
widgets.
"""
_settings_default = {"border": 5, "spacing": 5, "col": 0, "widgets": None}
def _config(self, **kwargs):
"""
widgets: ``list`` Contains widgets to pack into box.
The order of widgets in the list denotes order they are packed.
border: ``int`` Number of pixels to space around edges when ``surf``
is not given.
col: ``tuple`` (r,g,b) Colour for background, 0 is transparent.
spacing: ``int`` Number of pixels to space between widgets.
"""
if "spacing" in kwargs:
self._settings["spacing"] = kwargs["spacing"]
if "widgets" in kwargs:
pos = 0
for w in kwargs["widgets"]:
w.pos = (pos, 0)
pos += w.rect.w + self._settings["spacing"]
Container._config(self, **kwargs)

162
sgc/widgets/button.py Normal file
View File

@ -0,0 +1,162 @@
#!/usr/bin/env python
# Copyright (C) 2010-2012 Sam Bull
"""
Button widget, allows input from the user clicking the button.
"""
import pygame
from pygame.locals import *
from _locals import *
from base_widget import Simple
class Button(Simple):
"""
A clickable button.
Images:
'image': The default button state.
'over': The image used when the cursor is hovering over the button.
'down': The image used when the user is clicking down on the button.
"""
_can_focus = True
_default_size = (110, 50)
_available_images = ("over", "down")
_settings_default = {"label": ("",), "col": (127, 127, 169),
"label_col": Font.col}
_state = None
_draw_rect = False
def _config(self, **kwargs):
"""
label: ``str`` Text to display on the button.
col: ``tuple`` (r,g,b) The central colour used if no image is
provided. If you want to avoid the colours saturating keep the
RGB values below 200.
label_col: ``tuple`` (r,g,b) The text colour for the button's label.
"""
# Label in middle of button
if "label" in kwargs:
# Save string as first argument
self._settings["label"] = [kwargs["label"]]
self._draw_label()
if "col" in kwargs:
self._settings["col"] = kwargs["col"]
if "label_col" in kwargs:
self._settings["label_col"] = kwargs["label_col"]
self._draw_label()
def _draw_label(self):
# Clear previous renderings
del self._settings["label"][1:]
label = self._settings["label"][0].split("\n")
for count, line in enumerate(label):
lbl = Simple(Font["widget"].render(line, True,
self._settings["label_col"]))
self._settings["label"].append(lbl)
y = (self.rect.h - (lbl.rect.h * len(label))) / 2 + \
(lbl.rect.h * count)
lbl.rect.midtop = (self.rect.w/2, y)
def _draw(self, draw):
# Frames around edge of button
x = min(self.image.get_size()) / 8
self._frame_lt = ((0,0), (self.rect.w,0), (self.rect.w-x,x),
(x,x), (x,self.rect.h-x), (0,self.rect.h))
self._frame_rb = ((self.rect.w,self.rect.h),
(0,self.rect.h), (x,self.rect.h-x),
(self.rect.w-x,self.rect.h-x),
(self.rect.w-x,x), (self.rect.w,0))
cols = {}
cols["image"] = self._settings["col"]
cols["over"] = [min(c*1.1, 255) for c in self._settings["col"]]
cols["down"] = [c*0.8 for c in self._settings["col"]]
for img in cols:
if not self._custom_image:
self._images[img].fill(cols[img])
# Draw a frame around the edges of the button
frame_lt_c = [min(c*1.3,255) for c in cols[img]]
frame_rb_c = [c*0.8 for c in cols[img]]
draw.polygon(self._images[img], frame_lt_c, self._frame_lt)
draw.polygon(self._images[img], frame_rb_c, self._frame_rb)
# Blit label onto button
for line in self._settings["label"][1:]:
self._images[img].blit(line.image, line.pos)
self._draw_button()
def activate(self):
"""
Called when the button is activated.
Emits an event with attribute 'gui_type' == "activate".
Override this function to use as a callback handler.
"""
ev = pygame.event.Event(GUI, {"gui_type": "activate",
"widget_type": self.__class__,
"widget":self})
pygame.event.post(ev)
def update(self, time):
"""Update the button each frame."""
if self.rect_abs.collidepoint(pygame.mouse.get_pos()):
if self._state not in ("over","down"):
# Draw over state
self._state = "over"
self._draw_button()
elif self._state not in ("off","down"):
# Draw normal state
self._state = "off"
self._draw_button()
def _event(self, event):
"""Respond to events."""
if event.type == MOUSEBUTTONDOWN and event.button == 1:
# Draw down state
self._state = "down"
self._draw_button()
elif event.type == MOUSEBUTTONUP and event.button == 1:
self._state = None
# If releasing mouse on button, call function
if self.rect_abs.collidepoint(event.pos):
self.activate()
elif event.type == KEYDOWN:
if event.key in (K_SPACE, K_RETURN):
self._state = "down"
self._draw_button()
self.activate()
elif event.type == KEYUP:
if event.key in (K_SPACE, K_RETURN):
self._state = None
def _focus_enter(self, focus):
"""Draw rectangle when focus is gained from keyboard."""
if focus == 1:
self._draw_rect = True
self._draw_button()
def _focus_exit(self):
"""Stop drawing rectangle when focus is lost."""
self._draw_rect = False
self._draw_button()
def _draw_button(self):
"""Draw the button."""
if self._state == "off":
self.image = self._images["image"].copy()
elif self._state == "over":
self.image = self._images["over"].copy()
elif self._state == "down":
self.image = self._images["down"].copy()
# Draw dotted rectangle to show keyboard focus
if self._draw_rect:
self._dotted_rect()

132
sgc/widgets/container.py Normal file
View File

@ -0,0 +1,132 @@
#!/usr/bin/env python
# Copyright (C) 2010-2012 Sam Bull
"""
Container widget, can be inherited to implement more complex behaviour.
"""
import pygame.sprite
from pygame.locals import *
from _locals import *
from _locals import Focus
from base_widget import Simple
class Container(Simple):
"""
Container widget. Handles focus and events of a group
of widgets packed into a single container.
If ``surf`` is not given, container will be the right size to fit all
widgets.
"""
_can_focus = True
_settings_default = {"border": 0, "col": 0, "widgets": None}
_focus = None
_order = None
def _config(self, **kwargs):
"""
widgets: ``list`` Contains widgets to be added at creation time.
The order of widgets in the list denotes order they receive
focus when user hits :kbd:`TAB`.
border: ``int`` Number of pixels to space around edges when ``surf``
is not given.
col: ``tuple`` (r,g,b) Colour for background, 0 is transparent.
"""
if "border" in kwargs:
self._settings["border"] = kwargs["border"]
if "col" in kwargs:
self._settings["col"] = kwargs["col"]
if "widgets" in kwargs:
self._settings["widgets"] = pygame.sprite.Group()
self._focus = Focus()
self._order = []
pad = self._settings["border"]
for w in kwargs["widgets"]:
w._parent = self
w.pos = (w.rect.x + pad, w.rect.y + pad)
self._settings["widgets"].add(w)
if w._can_focus: self._order.append(w)
if not hasattr(self, "image"):
width = max(kwargs["widgets"], key=lambda w: w.rect.right)
width = width.rect.right + pad
height = max(kwargs["widgets"], key=lambda w: w.rect.bottom)
height = height.rect.bottom + pad
self._create_base_images((width, height))
def update(self, time):
"""Update widgets each frame."""
self.image.fill(self._settings["col"])
self._settings["widgets"].update(time)
for w in self._settings["widgets"]:
self.image.blit(w.image, w.pos)
def _event(self, event):
"""Handle focus and send events to sub-widgets."""
if event.type == MOUSEBUTTONDOWN:
hit = False
for widget in self._settings["widgets"]:
# Check if user clicked a widget
if widget._can_focus:
if widget.rect_abs.collidepoint(event.pos):
self._focus.add(2, widget)
hit = True
break
# Lose focus if clicking away from widgets
if not hit:
self._focus.empty()
elif event.type == KEYDOWN and event.key == K_TAB:
# Focus number for current focused widget
if self._focus.sprite not in self._order:
curr_num = None
else:
curr_num = self._order.index(self._focus.sprite)
if not event.mod & KMOD_SHIFT: # Move focus to next widget
# Next focus number in the list
if curr_num is None:
# If nothing focused, focus first widget
new_num = 0
elif not self._focus.sprite._change_focus(True):
# Test for container widgets
new_num = curr_num
elif curr_num >= len(self._order)-1:
new_num = 0
else:
new_num = curr_num + 1
else: # Shift key - move focus to previous widget
if curr_num is None:
new_num = -1
elif not self._focus.sprite._change_focus(False):
new_num = curr_num
elif curr_num <= 0:
new_num = -1
else:
new_num = curr_num - 1
if curr_num != new_num:
self._focus.add(1, self._order[new_num])
if self._focus:
self._focus.sprite._event(event)
def _change_focus(self, forward=True):
"""Override Simple and check if focus should leave yet."""
if self._focus and not self._focus.sprite._change_focus(forward):
return False
if not self._focus:
return False
num = self._order.index(self._focus.sprite)
if forward and num < len(self._order)-1:
return False
if not forward and num > 0:
return False
return True
def _focus_exit(self):
self._focus.empty()

125
sgc/widgets/dialog.py Normal file
View File

@ -0,0 +1,125 @@
#!/usr/bin/env python
# Copyright (C) 2010-2012 Sam Bull
"""
Dialog window, creates a popup window.
"""
import pygame.mouse
from pygame.locals import *
from _locals import *
from base_widget import Simple
class Dialog(Simple):
"""
Dialog Window
If ``surf`` is not given, window will be large enough to fit the
given widget.
"""
_can_focus = True
_modal = True
_layered = True
_settings_default = {"title": None, "widget": None, "col_bg": (240,240,240),
"col_border": (50,40,90)}
_drag = _over = False
def _config(self, **kwargs):
"""
widget: Widget that should be displayed in the dialog window.
title: ``str`` Text to display in the title bar.
col_border: ``tuple`` (r,g,b) Window decoration colour.
col_bg: ``tuple`` (r,g,b) Background colour.
modal: ``bool`` ``True`` if window should be modal.
Defaults to ``True``.
"""
if "widget" in kwargs:
self._settings["widget"] = kwargs["widget"]
self._settings["widget"]._parent = self
self._settings["widget"].pos = (2, 20)
if not hasattr(self, "image"):
r = self._settings["widget"].rect
self._create_base_images((r.w + 4, r.h + 22))
if "title" in kwargs:
self._settings["title"] = kwargs["title"]
if "col_border" in kwargs:
self._settings["col_border"] = kwargs["col_border"]
if "col_bg" in kwargs:
self._settings["col_bg"] = kwargs["col_bg"]
if "modal" in kwargs:
self._modal = kwargs["modal"]
def _draw(self, draw):
# Draw window
inner_rect = Rect((2,20), (self.rect.w-4,self.rect.h-22))
self._images["image"].fill(self._settings["col_border"])
self._images["image"].fill(self._settings["col_bg"], inner_rect)
if self._settings["title"]:
t = Simple(Font["widget"].render(
self._settings["title"], True, Font.col), pos = (22,0))
self._images["image"].blit(t.image, t.pos)
# Close button
self._close_off = Simple((16,16), parent=self)
self._close_off.image.fill(self._settings["col_border"])
draw.circle(self._close_off.image, (140,6,15), (8,8), 8)
draw.line(self._close_off.image, (0,0,1), (5,5), (11,11), 3)
draw.line(self._close_off.image, (0,0,1), (5,11), (11,5), 3)
self._close_over = Simple((16,16), parent=self)
self._close_over.image.fill(self._settings["col_border"])
draw.circle(self._close_over.image, (234,14,50), (8,8), 8)
draw.line(self._close_over.image, (0,0,1), (5,5), (11,11), 5)
draw.line(self._close_over.image, (0,0,1), (5,11), (11,5), 5)
self._close_off.pos = self._close_over.pos = (1,1)
self.image = self._images["image"].copy()
self.image.blit(self._close_off.image, self._close_off.pos)
def update(self, time):
"""Update dialog window each frame."""
r = self._close_off.rect_abs
if not self._over and r.collidepoint(pygame.mouse.get_pos()):
# Display over button
self.image = self._images["image"].copy()
self.image.blit(self._close_over.image, self._close_over.pos)
self._over = True
elif self._over and not r.collidepoint(pygame.mouse.get_pos()):
# Display normal button
self.image = self._images["image"].copy()
self.image.blit(self._close_off.image, self._close_off.pos)
self._over = False
self._settings["widget"].update(time)
self.image.blit(self._settings["widget"].image,
self._settings["widget"].pos)
def _event(self, event):
"""Respond to events."""
minus_pos = lambda p1, p2: (p1[0] - p2[0], p1[1] - p2[1])
if event.type == MOUSEBUTTONDOWN and event.button == 1 and \
self.rect.collidepoint(event.pos) and event.pos[1] < self.rect.y + 20:
# Clicking title bar of window
if self._close_off.rect_abs.collidepoint(event.pos):
# Close button
self.remove()
else:
# Initialise window drag
self._offset = minus_pos(event.pos, self.pos)
self._drag = True
elif event.type == MOUSEMOTION and self._drag:
# Move window
self.pos = minus_pos(event.pos, self._offset)
elif event.type == MOUSEBUTTONUP and event.button == 1 and self._drag:
# Stop moving window
self.pos = minus_pos(event.pos, self._offset)
self._drag = False
else:
self._settings["widget"]._event(event)

View File

@ -0,0 +1,55 @@
#!/usr/bin/env python
# Copyright (C) 2010-2012 Sam Bull
"""
FPS counter, display current FPS performance to the user.
"""
from _locals import *
from base_widget import Simple
class FPSCounter(Simple):
"""
FPS counter
"""
_default_size = (80, 30)
_settings_default = {"label": "", "clock": None}
def _config(self, **kwargs):
"""
clock: ``pygame.time.Clock`` Clock used to time the game loop.
label: ``str`` Text to display in front of the value.
"""
if "clock" in kwargs:
self._settings["clock"] = kwargs["clock"]
if "label" in kwargs:
self._settings["label"] = kwargs["label"]
def toggle(self):
"""Toggle the FPS counter, adding or removing this widget."""
if self.active():
if self._fade is not None:
if self._fade_up:
self.remove()
else:
self.add()
else:
self.remove()
else:
self.add()
def update(self, time):
"""Update counter each frame."""
text = Simple(Font["widget"].render(
self._settings["label"] +
str(round(self._settings["clock"].get_fps(), 1)),
True, Font.col))
text.rect.center = (self.rect.w/2, self.rect.h/2)
self.image.fill(0)
self.image.blit(text.image, text.pos)

325
sgc/widgets/input_box.py Normal file
View File

@ -0,0 +1,325 @@
#!/usr/bin/env python
# Copyright (C) 2010-2012 Sam Bull
"""
Input Box for receiving text input.
"""
import pygame
from pygame.locals import *
from _locals import *
from base_widget import Simple
class InputBox(Simple):
"""
Input box
Attributes:
text: Text entered in input box. Can be set or retrieved directly.
Images:
'image': The background of the input box when focused.
'inactive': The background of the input box when not focused.
"""
_can_focus = True # Override Simple
_default_size = (240, 30)
_available_images = ("inactive",)
_settings_default = {"label": "", "default": "", "blink_interval": 600,
"col_selection": (118, 45, 215),
"col_focus": (255,255,255),
"col_focus_not": (200,200,200), "max_chars": 80,
"repeat_begin": 300, "repeat_interval": 30}
_blink_time = 0
_blink = True
__cursor_pos = 0
_r = None # Rect for position and size of input box
_repeat_key = None
_repeat_time = 0
_select = None # Starting point of selection
_offset = 6 # Offset to render text in the input box
def _config(self, **kwargs):
"""
default: ``str`` Contains the default text displayed when nothing
has been entered and input box does not have focus.
label: ``str`` Contains text to be rendered to left of widget.
blink_interval: ``int`` Milliseconds between cursor blink.
col_focus: ``tuple`` (r,g,b) Background colour when focused.
col_focus_not: ``tuple`` (r,g,b) Background colour when not focused.
col_selection: ``tuple`` (r,g,b) Colour of selection rectangle.
max_chars: ``int`` Maximum number of characters.
repeat_begin: ``int`` Milliseconds key is held down before repeating.
repeat_interval: ``int`` Milliseconds between key repeats.
text: ``str`` Set the text entered in input box.
"""
if "init" in kwargs:
self._text = []
if "default" in kwargs:
self._settings["default"] = kwargs["default"]
if "label" in kwargs:
self._settings["label"] = kwargs["label"]
if hasattr(self, "_label"):
self._images["image"].fill(0, self._label.rect)
# Label to left of input box
self._label = Simple(Font["widget"].render(self._settings["label"],
True, Font.col))
self._label.rect.centery = self.rect.h/2
# Update input rect
self._r = Rect((self._label.rect.w+5, 0),
(self.rect.w-(self._label.rect.w+5), self.rect.h))
if "blink_interval" in kwargs:
self._settings["blink_interval"] = kwargs["blink_interval"]
if "col_focus" in kwargs:
self._settings["col_focus"] = kwargs["col_focus"]
if "col_focus_not" in kwargs:
self._settings["col_focus_not"] = kwargs["col_focus_not"]
if "col_selection" in kwargs:
self._settings["col_selection"] = kwargs["col_selection"]
if "max_chars" in kwargs:
self._settings["max_chars"] = kwargs["max_chars"]
if "repeat_begin" in kwargs:
self._settings["repeat_begin"] = kwargs["repeat_begin"]
if "repeat_interval" in kwargs:
self._settings["repeat_interval"] = kwargs["repeat_interval"]
if "text" in kwargs:
self._text = [unicode(char) for char in kwargs["text"]]
def _draw(self, draw):
# Active state background
self._images["image"].fill(self._settings["col_focus"], self._r)
draw.rect(self._images["image"], (0,0,1), self._r, 4)
self._images["image"].blit(self._label.image, self._label.pos)
# Inactive state background
self._images["inactive"].fill(self._settings["col_focus_not"], self._r)
draw.rect(self._images["inactive"], (0,0,1), self._r, 4)
self._images["inactive"].blit(self._label.image, self._label.pos)
# Draw image in non-focus state
self._focus_exit()
# Store the input text as a list
@property
def text(self):
return "".join(self._text)
@text.setter
def text(self, txt):
self._text = [unicode(char) for char in txt]
# Re-evaluate cursor position.
self._cursor_pos = self._cursor_pos
def activate(self):
"""
Called when the user hits the enter key.
Emits an event with attribute 'gui_type' == "activate".
Override this function to use as a callback handler.
"""
ev = pygame.event.Event(GUI, {"gui_type": "activate",
"widget_type": self.__class__,
"widget": self})
pygame.event.post(ev)
def update(self, time):
"""Update the input box each frame."""
if self.has_focus():
draw = self.get_draw()
# Repeat key if held down
if self._repeat_key:
self._repeat_time += time
while self._repeat_time > self._settings["repeat_begin"]:
self._repeat_time -= self._settings["repeat_interval"]
self._event(self._repeat_key)
# Draw input box
text = Font["mono"].render(self.text, True, (0,0,0))
y = (self._r.h - text.get_height()) / 2
self.image = self._images["image"].copy()
area = ((6-self._offset,0), (self._r.w-8, self._r.h))
self.image.blit(text, (self._r.x+6, y), area)
# If enough time has passed, blink cursor
self._blink_time += time
if self._blink_time > self._settings["blink_interval"]:
self._blink_time -= self._settings["blink_interval"]
self._blink = not self._blink
# Draw cursor in box
if self._blink and self._select is None:
x = self._cursor_pos*Font.mono_w + self._r.x + self._offset
draw.line(self.image, (0,0,1), (x, 6), (x, self._r.h-6))
# Draw selection highlighting
if self._select is not None:
select = self._select_fix()
# Semi-transparent selection rectangle
w = ((select[1]*Font.mono_w + self._offset) -
max(4, (select[0]*Font.mono_w + self._offset)))
selection = Simple((w, self._r.h - 11))
selection.pos = (self._r.x + select[0] * Font.mono_w +
self._offset, 6)
selection.image.fill(self._settings["col_selection"])
selection.image.set_alpha(100)
# Border around selection rectangle
selection_b = Simple((selection.rect.w+2, selection.rect.h+2))
draw.rect(selection_b.image, self._settings["col_selection"],
selection_b.rect, 1)
pos = (max(self._r.x+4, selection.rect.x), selection.rect.y)
self.image.blit(selection.image, pos)
self.image.blit(selection_b.image, (pos[0]-1, pos[1]-1))
def _event(self, event):
"""Update text field based on input."""
if event.type == KEYDOWN:
# Reset cursor blink when typing
self._blink_time = 0
self._blink = True
# Save last key press for repeat
if self._repeat_key != event:
self._repeat_key = event
self._repeat_time = 0
if event.key in (9,): # Keys to ignore
pass
elif event.key == K_ESCAPE:
self._select = None
elif event.key == K_RETURN:
self.activate()
elif event.key == K_BACKSPACE:
if self._select is not None:
self._delete_selection()
elif self._cursor_pos > 0:
self._cursor_pos -= 1
self._text.pop(self._cursor_pos)
elif event.key == K_DELETE:
if self._select is not None:
self._delete_selection()
elif self._cursor_pos < len(self._text):
self._text.pop(self._cursor_pos)
elif event.key == K_LEFT:
if not event.mod & KMOD_SHIFT:
self._select = None # Break selection
elif self._select is None:
# Reset selection if not selecting
self._select = self._cursor_pos
self._cursor_pos -= 1
# Remove selection when cursor is at same position
if self._select == self._cursor_pos:
self._select = None
elif event.key == K_RIGHT:
if not event.mod & KMOD_SHIFT:
self._select = None # Break selection
elif self._select is None:
self._select = self._cursor_pos
self._cursor_pos += 1
if self._select == self._cursor_pos:
self._select = None
elif event.unicode:
if event.mod & KMOD_CTRL:
if event.key == K_a: # Select all
self._select = 0
self._cursor_pos = len(self._text)
elif event.key == K_c and self._select is not None: # Copy
select = self._select_fix()
string = "".join(self._text[select[0]:select[1]])
try:
pygame.scrap.put(SCRAP_TEXT, string)
except pygame.error:
print "Please run 'pygame.scrap.init()'" \
" to use the clipboard."
elif event.key == K_v: # Paste
text = pygame.scrap.get(SCRAP_TEXT)
if text:
if self._select is not None:
self._delete_selection()
# Get list of text to insert into input_text
text = [unicode(char) for char in text]
self._text[self._cursor_pos:self._cursor_pos] = text
self._cursor_pos += len(text)
elif event.key == K_x and self._select is not None: # Cut
select = self._select_fix()
string = "".join(self._text[select[0]:select[1]])
try:
pygame.scrap.put(SCRAP_TEXT, string)
except pygame.error:
print "Please run 'pygame.scrap.init()'" \
" to use the clipboard"
self._delete_selection()
else:
# Delete selection
if self._select is not None:
self._delete_selection()
# Insert new character
if len(self._text) < self._settings["max_chars"]:
self._text.insert(self._cursor_pos, event.unicode)
self._cursor_pos += 1
elif event.type == KEYUP:
if self._repeat_key and self._repeat_key.key == event.key:
self._repeat_key = None # Stop repeat
elif event.type == MOUSEBUTTONDOWN:
# Begin drawing selection
if pygame.key.get_mods() & KMOD_SHIFT and self._select is None:
self._select = self._cursor_pos
self._cursor_pos = self._mouse_cursor(event.pos)
if not pygame.key.get_mods() & KMOD_SHIFT:
self._select = self._cursor_pos
elif event.type == MOUSEMOTION and event.buttons[0]:
# Continue drawing selection while mouse held down
self._cursor_pos = self._mouse_cursor(event.pos)
elif event.type == MOUSEBUTTONUP:
# Set cursor position with mouse click
self._cursor_pos = self._mouse_cursor(event.pos)
if self._select == self._cursor_pos:
self._select = None
def _focus_exit(self):
"""Draw non-focused input box when focus is lost."""
self.image = self._images["inactive"].copy()
if self._text: # Blit input text into box...
text = Simple(Font["mono"].render(self.text, True, (70,70,70)))
else: # ...or default text if empty.
text = Simple(Font["mono"].render(self._settings["default"], True,
(70,70,70)))
text.rect.midleft = (self._r.x + self._offset, self._r.h / 2)
self.image.blit(text.image, text.pos)
# Stop repeat key
self._repeat_key = None
def _mouse_cursor(self, mouse_pos):
"""Return the text cursor position of the mouse."""
pos = mouse_pos[0] - self.rect_abs.x - self._offset - self._r.x
pos = int(round(float(pos) / Font.mono_w))
return max(min(pos, len(self._text)), 0)
def _select_fix(self):
"""If selection is right-to-left then reverse positions."""
if self._select > self._cursor_pos:
return (self._cursor_pos, self._select)
else:
return (self._select, self._cursor_pos)
def _delete_selection(self):
"""Delete the current selection of text."""
select = self._select_fix()
del self._text[select[0]:select[1]]
self._select = None
self._cursor_pos = select[0]
@property
def _cursor_pos(self):
return self.__cursor_pos
@_cursor_pos.setter
def _cursor_pos(self, value):
# Keep cursor position within text
self.__cursor_pos = min(max(value, 0), len(self._text))
# Scroll text in input box when it's too long
pos = self._cursor_pos * Font.mono_w
if pos > (self._r.w - self._offset):
self._offset = -(pos - self._r.w + 6)
elif pos < (6 - self._offset):
self._offset = 6 - pos

229
sgc/widgets/label.py Normal file
View File

@ -0,0 +1,229 @@
#!/usr/bin/env python
# Copyright (C) 2010-2012 Sam Bull, Michael Rochester
"""
Label to display information to the user.
"""
import pygame.mouse
from pygame.locals import *
from _locals import *
from base_widget import Simple
class Label(Simple):
"""
Label
Attributes:
text: ``str`` displayed in label. Can be assigned as a shortcut for
``config(label=)`` with no second paramenter.
"""
_settings_default = {"text": "", "col": Font.col, "font": Font["widget"],
"col_selection": (118, 45, 215)}
_over = False
_select = None # Starting point of selection
__cursor_pos = 0
def _config(self, **kwargs):
"""
text: Either ``str`` containing text to be displayed or
``tuple`` containing two strings. First string is text to
be displayed, second string is rect attribute to be used
for position. Defaults to 'topleft' if not passing a tuple.
col: ``tuple`` (r,g,b) Text colour.
font: Font object the label will render with.
selectable: ``bool`` True if the text should be selectable.
col_selection: ``tuple`` (r,g,b) Colour of selection rectangle.
"""
if "init" in kwargs:
strings = pygame.cursors.textmarker_strings
cursor = pygame.cursors.compile(strings)
size = (len(strings[0]), len(strings))
hotspot = (size[0]/2, size[1]/2)
self._cursor = (size, hotspot) + cursor
if "text" in kwargs:
if isinstance(kwargs["text"], str):
self._settings["text"] = kwargs["text"]
else:
self._settings["text"] = kwargs["text"][0]
self._temp_pos = kwargs["text"][1]
if "col" in kwargs:
self._settings["col"] = kwargs["col"]
if "font" in kwargs:
self._settings["font"] = kwargs["font"]
if "selectable" in kwargs:
self._can_focus = kwargs["selectable"]
if "col_selection" in kwargs:
self._settings["col_selection"] = kwargs["col_selection"]
def _draw(self, draw):
if hasattr(self, "_temp_pos"):
pos = getattr(self.rect, self._temp_pos)
# Split into lines
text = []
for line in self._settings["text"].split("\n"):
text.append(self._settings["font"].render(line, True,
self._settings["col"]))
# Dynamically set size
h = 0
for line in text:
h += line.get_size()[1]
w = max(text, key=lambda x: x.get_size()[0])
self._create_base_images((w.get_size()[0], h))
# Blit each line
y = 0
for line in text:
self._images["image"].blit(line, (0,y))
y += line.get_size()[1]
# Copy position attribute over
if hasattr(self, "_temp_pos"):
setattr(self.rect, self._temp_pos, pos)
del self._temp_pos
self.image = self._images["image"].copy()
# Store as tuple of (pos, width) tuples.
if self._can_focus:
chars = []
p = 0
for c in range(1, len(self._settings["text"])+1):
char = self._settings["font"].render(self._settings["text"][:c],
True, (0,0,0))
chars.append((p, char.get_size()[0] - p))
p = char.get_size()[0]
chars.append((p, 0))
self._chars = tuple(chars)
def _event(self, event):
if event.type == MOUSEBUTTONDOWN and event.button == 1:
# Begin drawing selection
if pygame.key.get_mods() & KMOD_SHIFT and self._select is None:
self._select = self._cursor_pos
self._cursor_pos = self._mouse_cursor(event.pos)
if not pygame.key.get_mods() & KMOD_SHIFT:
self._select = self._cursor_pos
elif event.type == MOUSEMOTION and event.buttons[0]:
# Continue drawing selection while mouse held down
self._cursor_pos = self._mouse_cursor(event.pos)
elif event.type == MOUSEBUTTONUP:
# Set cursor position with mouse click
self._cursor_pos = self._mouse_cursor(event.pos)
if self._select == self._cursor_pos:
self._select = None
elif event.type == KEYDOWN:
if event.key == K_ESCAPE:
self._select = None
elif event.key == K_LEFT:
if not event.mod & KMOD_SHIFT:
self._select = None # Break selection
elif self._select is None:
# Reset selection if not selecting
self._select = self._cursor_pos
self._cursor_pos -= 1
# Remove selection when cursor is at same position
if self._select == self._cursor_pos:
self._select = None
elif event.key == K_RIGHT:
if not event.mod & KMOD_SHIFT:
self._select = None # Break selection
elif self._select is None:
self._select = self._cursor_pos
self._cursor_pos += 1
if self._select == self._cursor_pos:
self._select = None
elif event.mod & KMOD_CTRL:
if event.key == K_a: # Select all
self._select = 0
self._cursor_pos = len(self._settings["text"])
elif event.key == K_c and self._select is not None: # Copy
select = self._select_fix()
string = "".join(
self._settings["text"][select[0]:select[1]])
try:
pygame.scrap.put(SCRAP_TEXT, string)
except pygame.error:
print "Please run 'pygame.scrap.init()'" \
" to use the clipboard."
def update(self, time):
if self._can_focus:
# Change cursor when mouse not held down
if not pygame.mouse.get_pressed()[0]:
if not self._over and \
self.rect_abs.collidepoint(pygame.mouse.get_pos()):
self._over = True
self._set_cursor(*self._cursor)
elif self._over and \
not self.rect_abs.collidepoint(pygame.mouse.get_pos()):
self._over = False
self._remove_cursor()
if self.has_focus():
self.image = self._images["image"].copy()
draw = self.get_draw()
if self._select is None:
# Draw cursor in box
x = self._chars[self._cursor_pos][0] - 1
draw.line(self.image, (0,0,1), (x, 2), (x, self.rect.h-2))
else:
select = self._select_fix()
# Semi-transparent selection rectangle
w = (self._chars[select[1]][0] - self._chars[select[0]][0])
selection = Simple((w, self.rect.h - 2))
selection.pos = (self._chars[select[0]][0], 1)
selection.image.fill(self._settings["col_selection"])
selection.image.set_alpha(100)
# Border around selection rectangle
selection_b = Simple((selection.rect.w+2,
selection.rect.h+2))
draw.rect(selection_b.image,
self._settings["col_selection"],
selection_b.rect, 1)
self.image.blit(selection.image, selection.pos)
self.image.blit(selection_b.image, (selection.rect.x-1,
selection.rect.y-1))
@property
def text(self):
return self._settings["text"]
@text.setter
def text(self, value):
self._settings["text"] = value
self._draw()
def _mouse_cursor(self, mouse_pos):
"""Return the text cursor position of the mouse."""
pos = mouse_pos[0] - self.rect_abs.x
for index, (p,w) in enumerate(self._chars):
if pos <= p + w/2:
break
return index
def _select_fix(self):
"""If selection is right-to-left then reverse positions."""
if self._select > self._cursor_pos:
return (self._cursor_pos, self._select)
else:
return (self._select, self._cursor_pos)
def _focus_exit(self):
"""Cancel any selection when focus is lost."""
self.image = self._images["image"].copy()
@property
def _cursor_pos(self):
return self.__cursor_pos
@_cursor_pos.setter
def _cursor_pos(self, value):
# Keep cursor position within text
self.__cursor_pos = min(max(value, 0), len(self._settings["text"]))

217
sgc/widgets/menu.py Normal file
View File

@ -0,0 +1,217 @@
#!/usr/bin/env python
# Copyright (C) 2010-2012 Sam Bull
"""
Menu widget. Creates a menu for a game.
"""
from _locals import *
from boxes import VBox
from scroll_box import ScrollBox
from . import *
class Menu(Simple):
"""
Menu
Can be indexed to access widgets by name.
Attributes:
func_dict: Assign a lambda to return a dictionary of functions for
config file to utilise.
"""
_modal = True
_layered = True
_settings_default = {"offset": (100, 50), "col": (0,0,1), "apply": False}
_menus = []
_dict = {} # Dict for indexing widgets
_old_menu = None
_curr_menu = 0
func_dict = lambda self: {}
def _config(self, **kwargs):
"""
menu: Either tuple containing menu data, to be documented.
Or file object to read config data from, in the same format.
apply: TODO ``bool`` True if an apply button should be added.
col: ``tuple`` (r,g,b), Colour used for the background.
offset: ``tuple`` (x,y) Contains position of menu widgets. y is
added to bottom of title.
"""
if "apply" in kwargs:
self._settings["apply"] = kwargs["apply"]
if "col" in kwargs:
self._settings["col"] = kwargs["col"]
if "offset" in kwargs:
self._settings["offset"] = kwargs["offset"]
if "menu" in kwargs:
self._menus = [] # Remove previous menus
self._dict = {}
# Grab function dictionary
self._funcs = self.func_dict()
# Set size to screen size
if not hasattr(self, "image"):
self._create_base_images(get_screen().rect.size)
menu = kwargs["menu"]
# If file, read config tuple
if isinstance(menu, file):
menu_data = "".join(menu.readlines())
menu = eval(menu_data)
assert isinstance(menu, tuple)
menu_data = [(menu, None)] # (data, parent)
# Create each submenu, by iterating through the menu data
while menu_data:
self._menus.append(_SubMenu(self.rect.size,
col=self._settings["col"]))
self._config_menu(menu_data, self._menus[-1])
def _config_menu(self, data_queue, menu):
"""
Configure the passed in menu, using the information from the first
item in data_queue.
New sub-menus discovered in the data will be appended to the data_queue
for later processing.
"""
data, parent = data_queue.pop(0)
widgets = []
# Parse menu data
for item in data:
# Title
if isinstance(item, str):
menu._title = Simple(Font["title"].render(item[2:], True,
Font.col))
menu._title.rect.midtop = (self.rect.centerx, 40)
# Sub-menu
elif item[0].startswith("m:"):
data_queue.append((item, len(self._menus)-1))
surf = Font["widget"].render(item[0][2:], True, Font.col)
widgets.append(Button(surf))
# Change menu on button activate
num = len(self._menus)-1 + len(data_queue)
widgets[-1].activate = lambda n=num: self.change_menu(n)
# Category/divider
elif item[0].startswith("c:"):
div = Simple(Font["widget"].render(item[0][2:], True,
Font.col))
self.get_draw().line(div.image, Font.col, (0, div.rect.h-1),
(div.rect.w, div.rect.h-1))
widgets.append(div)
# Widget
elif item[0].startswith("w:"):
args = self._get_args(item[1:])
name = args.pop("name")
f = args.pop("func") if ("func" in args) else None
if item[0].endswith("input_box"):
widget = input_box.InputBox(**args)
elif item[0].endswith("button"):
for key in args:
if key == "surf":
args[key] = eval(args[key])
widget = button.Button(**args)
elif item[0].endswith("label"):
widget = label.Label(**args)
self._dict[name] = widget
if f: widget.activate = self._funcs[f]
widgets.append(widget)
# Function
elif item[0].startswith("f:"):
surf = Font["widget"].render(item[1], True, Font.col)
widgets.append(button.Button(surf=surf))
widgets[-1].activate = self._funcs[item[0][2:]]
# Draw a back menu item
if parent is not None:
surf = Font["widget"].render("Back", True, Font.col)
widgets.append(button.Button(surf=surf))
widgets[-1].activate = lambda n=parent: self.change_menu(n)
menu._widgets = tuple(widgets)
def _draw(self, draw):
for menu in self._menus:
# Pack all widgets into a VBox
box = VBox(widgets=menu._widgets, spacing=15)
pos = (self._settings["offset"][0],
self._settings["offset"][1] + menu._title.rect.bottom)
box = ScrollBox((self.rect.w - pos[0], self.rect.h - pos[1]),
widget=box, pos=pos)
menu.config(col=self._settings["col"], menu=box)
def change_menu(self, menu_num):
"""
Change the currently displayed menu.
Args:
menu_num: ``int`` The number representing the menu.
"""
self._old_menu = self._curr_menu
self._curr_menu = menu_num
self._menus[self._curr_menu]._fade = 0
def update(self, time):
menu = self._menus[self._curr_menu]
menu.update(time)
if self._old_menu is not None:
self.image.blit(self._menus[self._old_menu].image, (0,0))
menu.image.set_alpha(menu._fade)
menu._fade += time / 3.
if menu._fade >= 255:
menu._fade = None
self._old_menu = None
menu.image.set_alpha(255)
self.image.blit(menu.image, (0,0))
def _event(self, event):
self._menus[self._curr_menu]._event(event)
def _get_args(self, args):
"""Get the arguments passed in, saving them into a dictionary."""
return {arg.split("=")[0]: arg.split("=")[1] for arg in args}
def __getitem__(self, key):
"""Return widgets by name."""
return self._dict[key]
class _SubMenu(Simple):
"""
A single menu object to be created and managed by the Menu class.
"""
_settings_default = {"col": (0,0,1), "menu": None}
_title = None
_widgets = ()
def _config(self, **kwargs):
if "col" in kwargs:
self._settings["col"] = kwargs["col"]
if "menu" in kwargs:
self._settings["menu"] = kwargs["menu"]
def _draw(self, draw):
self._images["image"].fill(self._settings["col"])
if self._title:
self._images["image"].blit(self._title.image, self._title.pos)
def update(self, time):
self.image = self._images["image"].copy()
self._settings["menu"].update(time)
self.image.blit(self._settings["menu"].image,self._settings["menu"].pos)
def _event(self, event):
"""Send events to container."""
self._settings["menu"]._event(event)

255
sgc/widgets/opengl.py Normal file
View File

@ -0,0 +1,255 @@
#!/usr/bin/env python
# Copyright (C) 2011 Sam Bull
"""
Base widget, all widgets inherit from this.
"""
from pygame.locals import Rect
from OpenGL.GL import *
import FTGL
from ..surface import SurfaceBase
class OpenGLImage(SurfaceBase):
"""
Class used to emulate the interface of Surface for OpenGL drawing.
Functions should be used in the same manner as pygame.Surface.
Differences are shown in documentation, otherwise assume it to
function the same as the equivalent pygame.Surface function.
"""
def __init__(self, surf, parent=None, **kwargs):
self._a = None
self._lock = False
self.display_list = []
self._children = []
if parent:
self._parent = parent # Parent surface used for _abs
else:
self._parent = self._default_screen
self._rect = Rect((0,0), (0,0))
if isinstance(surf, (tuple,list)):
self._rect.size = surf
elif isinstance(surf, OpenGLFont):
self._rect.size = (surf.size)
self.display_list.extend(surf.display_list)
elif isinstance(surf, OpenGLImage):
self._rect = surf._rect
self._a = surf._a
self.display_list = surf.display_list
self._children = surf._children
def blit(self, surf, pos=None):
assert isinstance(surf, OpenGLImage)
if surf not in self._children:
if pos is not None: surf.pos = pos
surf._parent = self
self._children.append(surf)
def draw(self):
glLoadIdentity()
if self.rect.w <= self._parent.rect.w and \
self.rect.h <= self._parent.rect.h:
# Mask the area, so nothing is drawn outside surface area.
bottom = self._default_screen.h - self.rect_abs.bottom
glScissor(self.rect_abs.x, bottom,
self.rect.w+1, self.rect.h+1)
glTranslatef(self.pos_abs[0], self.pos_abs[1], 0)
for dl,col in self.display_list:
if col is not None: glColor(col[0], col[1], col[2], self.a)
glCallList(dl)
for child in self._children:
child.draw()
def fill(self, col=0, rect=None, special_flags=0):
"""If col == 0 and rect is None, clears image with no fill."""
if rect is None:
# Clear the surface
self.display_list = []
self._children = []
rect = Rect((0,0), self.size)
if col != 0:
col = [c/255. for c in col]
dl = glGenLists(1)
glNewList(dl, GL_COMPILE)
glRectfv(rect.topleft, rect.bottomright)
glEndList()
self.display_list.append((dl, col))
def copy(self):
return OpenGLImage(self)
def get_size(self):
return self.size
def set_alpha(self, alpha):
self._a = alpha/255.
def set_at(self, vertex, col):
col = [c/255. for c in col]
if self._lock:
glColor(*col)
else:
dl = glGenLists(1)
glNewList(dl, GL_COMPILE)
glBegin(GL_POINTS)
glVertex(vertex)
if not self._lock:
glEnd()
glEndList()
surf.display_list.append((dl, col))
def lock(self):
"""
Should only be used for multiple set_at() calls.
Should not be used in conjuction with the draw functions.
"""
self._lock = True
self._lock_dl = glGenLists(1)
glNewList(self._lock_dl, GL_COMPILE)
glBegin(GL_POINTS)
def unlock(self):
glEnd()
glEndList()
self.display_list.append((self._lock_dl, None))
del self._lock_dl
self._lock = False
def replace(self, surf, **kwargs):
self._rect.size = surf.size
self.display_list = surf.display_list[:]
self._children = surf._children[:]
@property
def a(self):
if self._a is not None:
return self._a
else:
return self._parent.a
# --- Dummy methods. Ignore. ---
def __call__(self):
return self
def set_colorkey(self, col):
pass
class OpenGLFont():
"""
Wraps the FTGL.TextureFont to allow it to be added
to an OpenGLImage object in the same manner as pygame.Font.
"""
def __init__(self, font, size):
self._children = []
self.font = FTGL.TextureFont(font)
self.font.FaceSize(16)
self.y_offset = self.font.line_height * .75
def render(self, text, antialias, color, background=None):
text = text.encode()
col = [c/255. for c in color]
dl = glGenLists(1)
self.size = (self.font.Advance(text), self.font.line_height)
glNewList(dl, GL_COMPILE)
glPushMatrix()
# Flip text right way up
glMultMatrixf((1, 0, 0, 0,
0,-1, 0, 0,
0, 0, 1, 0,
0, self.y_offset, 0, 1))
self.font.Render(text)
glPopMatrix()
glEndList()
self.display_list = [(dl, col)]
return self
class Draw():
"""
Class to emulate the pygame.draw module.
Functions should work in the same manner.
"""
def rect(self, surf, col, rect, width=0):
col = [c/255. for c in col]
dl = glGenLists(1)
glNewList(dl, GL_COMPILE)
if not width:
glRectfv(rect.topleft, rect.bottomright)
else:
if width > 1: width -= 1
hw = width/2.
glLineWidth(width)
glBegin(GL_LINES)
glVertex(rect.x, rect.y + hw)
glVertex(rect.right - width, rect.y + hw)
# (hw%1) fixes line rendering off by a pixel
glVertex(rect.right - hw, rect.y + (hw%1))
glVertex(rect.right - hw, rect.bottom - width + (hw%1))
glVertex(rect.right, rect.bottom - hw)
glVertex(rect.x + width, rect.bottom - hw)
glVertex(rect.x + hw, rect.bottom)
glVertex(rect.x + hw, rect.y + width)
glEnd()
glEndList()
surf.display_list.append((dl, col))
def polygon(self, surf, col, pointlist, width=0):
"""
With width == 0, can only draw convex polygons. Be careful of
the order of vertices if it draws differently than pygame.
"""
col = [c/255. for c in col]
dl = glGenLists(1)
glNewList(dl, GL_COMPILE)
if not width:
glBegin(GL_POLYGON)
for v in pointlist:
glVertex(v)
glEnd()
glEndList()
surf.display_list.append((dl, col))
def circle(self, surf, col, pos, radius, width=0):
col = [c/255. for c in col]
dl = glGenLists(1)
glNewList(dl, GL_COMPILE)
if not width:
glEnable(GL_POINT_SMOOTH)
glHint(GL_POINT_SMOOTH_HINT, GL_NICEST)
glPointSize(radius*2)
glBegin(GL_POINTS)
glVertex(pos)
glEnd()
glDisable(GL_POINT_SMOOTH)
glPointSize()
glEndList()
surf.display_list.append((dl, col))
def line(self, surf, col, start_pos, end_pos, width=1):
col = [c/255. for c in col]
dl = glGenLists(1)
glNewList(dl, GL_COMPILE)
glLineWidth(width)
glBegin(GL_LINES)
glVertex(start_pos)
glVertex(end_pos)
glEnd()
glEndList()
surf.display_list.append((dl, col))
# Export Draw functions
draw = Draw()

180
sgc/widgets/radio_button.py Normal file
View File

@ -0,0 +1,180 @@
#!/usr/bin/env python
# Copyright (C) 2012 Michael Rochester, Sam Bull
"""
Radio Button, allows the user to select a single option from a group.
"""
import pygame.mouse
from pygame.locals import *
from _locals import *
from _locals import focus
from base_widget import Simple
class Radio(Simple):
"""
A selectable radio button.
Attributes:
groups: A dictionary containing the active radio button or ``None`` for
each radio group. Key is ``str`` containing the name of the group.
selected: True if widget is the currently selected radio button in
it's group.
Images:
'image': The default, inactive button state.
'over': The image used when the cursor is hovering over the button.
'active': The image used for the active button in a group
(if applicable).
"""
_can_focus = True
_available_images = ("over", "active")
_settings_default = {"group": None, "label": "", "col": (118, 45, 215),
"label_col": Font.col, "radius": 7}
_over_state = False
_draw_rect = False
groups = {}
_order = {}
def _config(self, **kwargs):
"""
group: ``str`` Name of the group for widget to be added to.
label: ``str`` Text to be displayed to the right of the widget.
active: ``True`` Makes this the active radio button for it's group.
col: ``tuple`` (r,g,b) The colour to be used for the 'over' image
if not using a custom image.
label_col: ``tuple`` (r,g,b) The colour for the label text.
radius: ``int`` Radius of the button if not using a custom image.
"""
if "group" in kwargs:
if kwargs["group"] not in self.groups:
self.groups[kwargs["group"]] = None
self._order[kwargs["group"]] = []
self._settings["group"] = kwargs["group"]
self._order[self._settings["group"]].append(self)
if "label" in kwargs:
self._settings["label"] = kwargs["label"]
if "col" in kwargs:
self._settings["col"] = kwargs["col"]
if "label_col" in kwargs:
self._settings["label_col"] = kwargs["label_col"]
if "radius" in kwargs:
self._settings["radius"] = kwargs["radius"]
assert self._settings["group"] is not None
if "active" in kwargs:
self._draw()
self._activate()
def _draw(self, draw):
r = self._settings["radius"]
# Render text
label = Simple(Font["widget"].render(self._settings["label"], True,
self._settings["label_col"]))
if not hasattr(self, "image"):
self._create_base_images((r*2 + 10 + label.rect.w,
max(label.rect.height, r*2)))
pos = (r, self.rect.h/2)
# Background circles
draw.circle(self._images["image"], (255,255,255), pos, r)
draw.circle(self._images["over"], self._settings["col"], pos, r)
# Border circles
draw.circle(self._images["image"], (0,0,1), pos, r, 1)
draw.circle(self._images["over"], (0,0,1), pos, r, 1)
# Central dot for 'active' state
draw.circle(self._images["active"],(0,0,1), pos, int(r/1.5))
label.rect.midleft = (r*2 + 10, pos[1])
self._images["image"].blit(label.image, label.pos)
self._images["over"].blit(label.image, label.pos)
self._draw_button()
def update(self, time):
"""Update the radio button each frame."""
if self.rect_abs.collidepoint(pygame.mouse.get_pos()):
if not self._over_state:
# Draw over state
self._over_state = True
self._draw_button()
elif self._over_state:
# Draw normal state
self._over_state = False
self._draw_button()
def _event(self, event):
if event.type == MOUSEBUTTONUP and event.button == 1:
if self.rect_abs.collidepoint(event.pos):
self._activate()
elif event.type == KEYDOWN:
def focus_change(diff):
next_widget = order[order.index(widget) + diff]
next_widget._activate()
if self._parent:
self._parent._focus.add(1, next_widget)
else:
focus.add(1, next_widget)
order = self._order[self._settings["group"]]
widget = self.groups[self._settings["group"]]
if event.key == K_UP and order.index(widget) > 0:
focus_change(-1)
elif event.key == K_DOWN and order.index(widget) < len(order)-1:
focus_change(1)
elif event.type == KEYUP:
if event.key in (K_SPACE, K_RETURN):
self._activate()
def _focus_enter(self, focus):
"""Draw rectangle when focus is gained from keyboard."""
if focus == 1:
self._draw_rect = True
self._draw_button()
def _focus_exit(self):
"""Stop drawing rectangle when focus is lost."""
self._draw_rect = False
self._draw_button()
def _draw_button(self):
"""Draw the button."""
if not self._over_state:
self.image = self._images["image"].copy()
else:
self.image = self._images["over"].copy()
if self.groups[self._settings["group"]] is self:
self.image.blit(self._images["active"], (0,0))
# Draw dotted rectangle to show keyboard focus
if self._draw_rect:
self._dotted_rect()
def _activate(self):
"""Reset drawing of new and previous widget."""
old = self.groups[self._settings["group"]]
self.groups[self._settings["group"]] = self
if old is not None: old._draw_button()
self._draw_button()
def clear(self, group=None):
"""
Clear a group so no radio button is selected.
Args:
group: ``str`` Group name to clear. Clear this widget's group if None.
"""
if group is None: group = self._settings["group"]
old = self.groups[group]
self.groups[group] = None
if old is not None: old._draw_button()
@property
def selected(self):
return self is self.groups[self._settings["group"]]

245
sgc/widgets/scroll_box.py Normal file
View File

@ -0,0 +1,245 @@
#!/usr/bin/env python
# Copyright (C) 2011-2012 Sam Bull
"""
Scroll box. A container widget that provides scroll bars to be able to
view a larger widget.
BUG: Scrolling gives focus. Can't scroll without focus.
BUG: Scroll bar pops up over a modal widget.
BUG: Scroll bars don't work in a modal widget.
"""
import pygame.mouse
from pygame.locals import *
from _locals import *
from base_widget import Simple
class ScrollBox(Simple):
"""
Scroll Box
"""
_can_focus = True
_default_size = (300, 200)
_settings_default = {"widget": None, "col": (118, 45, 215)}
_scroll_x = _scroll_y = None
_handle_x = _handle_y = None
def _config(self, **kwargs):
"""
widget: Widget that should be displayed in scroll box.
col: ``tuple`` (r,g,b) Colour used for scroll bars and handles.
"""
if "widget" in kwargs:
self._settings["widget"] = kwargs["widget"]
self._settings["widget"]._parent = self
self._settings["widget"].pos = (0,0)
if "col" in kwargs:
self._settings["col"] = kwargs["col"]
def _draw(self, draw):
# Create scroll bars and handles
if self._settings["widget"].rect.w > self.rect.w:
ratio = float(self.rect.w) / self._settings["widget"].rect.w
self._scroll_x = Simple((self.rect.w * ratio, 3))
self._scroll_x._parent = self
self._scroll_x.image.fill(self._settings["col"])
self._scroll_x.pos = (0, self.rect.h - 3)
self._handle_x = _ScrollHandleH(widget=self)
if self._settings["widget"].rect.h > self.rect.h:
ratio = float(self.rect.h) / self._settings["widget"].rect.h
self._scroll_y = Simple((3, self.rect.h * ratio))
self._scroll_y._parent = self
self._scroll_y.image.fill(self._settings["col"])
self._scroll_y.pos = (self.rect.w - 3, 0)
self._handle_y = _ScrollHandleV(widget=self)
def update(self, time):
"""Update scroll box each frame."""
self._settings["widget"].update(time)
self.image.fill(0)
self.image.blit(self._settings["widget"].image,
self._settings["widget"].pos)
pos = pygame.mouse.get_pos()
if self._scroll_y is not None:
self.image.blit(self._scroll_y.image, self._scroll_y.pos)
r = self._scroll_y.rect_abs
# Add scroll handles when cursor moves near scroll bar
if not self._handle_y.active() and \
r.inflate(20, 5).collidepoint(pos):
# Position to left if handle would be off-screen.
edge = (r.right + self._handle_y.rect.w)
if edge < get_screen().rect.w:
self._handle_y.rect.x = r.right
else:
self._handle_y.rect.right = r.left
self._handle_y.update_pos(pos[1])
self._handle_y.add()
if self._scroll_x is not None:
self.image.blit(self._scroll_x.image, self._scroll_x.pos)
r = self._scroll_x.rect_abs
if not self._handle_x.active() and \
r.inflate(5, 20).collidepoint(pos):
edge = (r.bottom + self._handle_x.rect.h)
if edge < get_screen().rect.h:
self._handle_x.rect.y = r.bottom
else:
self._handle_x.rect.bottom = r.top
self._handle_x.update_pos(pos[0])
self._handle_x.add()
def _event(self, event):
"""Respond to events."""
self._settings["widget"]._event(event)
if event.type == MOUSEBUTTONDOWN:
if event.button == 4: # Scroll up
self.scroll(y=-10)
elif event.button == 5: # Scroll down
self.scroll(y=10)
elif event.button == 6: # Scroll left
self.scroll(x=-10)
elif event.button == 7: # Scroll right
self.scroll(x=10)
def scroll(self, x=None, y=None):
"""Scroll by x and y coordinates."""
if x is not None and self._scroll_x is not None:
# Set scroll bar position
r = self._scroll_x.rect
r.x = max(min(r.x + x, self.rect.w - r.w), 0)
# Set widget's position
ratio = r.x / float(self.rect.w - r.w)
max_w = self._settings["widget"].rect.w - self.rect.w
self._settings["widget"].rect.x = -max_w * ratio
if y is not None and self._scroll_y is not None:
r = self._scroll_y.rect
r.y = max(min(r.y + y, self.rect.h - r.h), 0)
ratio = r.y / float(self.rect.h - r.h)
max_h = self._settings["widget"].rect.h - self.rect.h
self._settings["widget"].rect.y = -max_h * ratio
def _change_focus(self, forward=True):
return self._settings["widget"]._change_focus(forward)
def _focus_exit(self):
self._settings["widget"]._focus_exit()
class _ScrollHandle(Simple):
"""
Scroll bar to manipulate scroll box.
To be inherited from by _ScrollHandle[V/H], not to be used directly.
Uses lots of getattr() and other tricks to provide inheritable functions.
"""
_can_focus = True
_layered = True
_settings_default = {"widget": None}
_drag = None
def _config(self, **kwargs):
"""
widget: Scroll box that this handle should be synced to.
"""
if "init" in kwargs:
self._rect2 = self.rect_abs.inflate(20, 20)
if "widget" in kwargs:
self._settings["widget"] = kwargs["widget"]
def _draw(self, draw):
self.image.fill(self._settings["widget"]._settings["col"])
self.image.fill((200,200,200), self.rect.inflate(-4, -4))
# Draw line in center
r = self.rect
start_pos = (3, r.centery) if self.xy == "y" else (r.centerx, 3)
end_pos = (r.w-4, r.centery) if self.xy == "y" else (r.centerx, r.h-4)
draw.line(self.image, (100,100,100), start_pos, end_pos)
# Draw arrows
if self.xy == "y":
points1 = ((3, r.h/4), (r.centerx, r.h/5-1), (r.w-3, r.h/4))
points2 = ((3, r.h*.75), (r.centerx, r.h*.8), (r.w-3, r.h*.75))
else:
points1 = ((r.w/4, 3), (r.w/5-1, r.centery), (r.w/4, r.h-3))
points2 = ((r.w*.75, 3), (r.w*.8, r.centery), (r.w*.75, r.h-3))
draw.polygon(self.image, (50,50,50), points1)
draw.polygon(self.image, (50,50,50), points2)
def update_pos(self, xy):
"""
Change position of scroll handle.
Args:
xy: Integer to move the scroll handle to, along the correct axis.
"""
scroll_bar = getattr(self._settings["widget"], "_scroll_%s" % self.xy)
if scroll_bar is not None:
r = scroll_bar.rect_abs
a,b = (r.bottom, r.top) if self.xy == "y" else (r.right, r.left)
xy = min(a, max(xy, b))
setattr(self.rect, "center%s" % self.xy, xy)
self._rect2.center = self.rect.center
def update(self, time):
# Move handle to cursor when cursor not hovering over.
if not self.rect.collidepoint(pygame.mouse.get_pos()):
self.update_pos(pygame.mouse.get_pos()[0 if self.xy == "x" else 1])
# Hide handle when cursor moves too far.
if self._drag is None and \
not self._rect2.collidepoint(pygame.mouse.get_pos()):
self.remove()
def _event(self, event):
index = 1 if self.xy == "y" else 0
if event.type == MOUSEBUTTONDOWN and event.button == 1 and \
self.rect.collidepoint(event.pos):
# Initialise drag
center = getattr(self.rect_abs, "center%s" % self.xy)
self._offset = event.pos[index] - center
self._drag = event.pos[index]
elif self._drag is not None:
if event.type == MOUSEMOTION:
# Move scroll handle and bar
self.update_pos(event.pos[index] - self._offset)
kwarg = {self.xy: event.rel[index]}
self._settings["widget"].scroll(**kwarg)
elif event.type == MOUSEBUTTONUP and event.button == 1:
# Move scroll box up when clicked
if -5 < (self._drag - event.pos[index]) < 5:
center = getattr(self.rect_abs, "center%s" % self.xy)
if event.pos[index] < center:
kwarg = {self.xy: -40}
self._settings["widget"].scroll(**kwarg)
else:
kwarg = {self.xy: 40}
self._settings["widget"].scroll(**kwarg)
# Or stop moving and set final position after drag
else:
self.update_pos(event.pos[index] - self._offset)
self._drag = None
class _ScrollHandleV(_ScrollHandle):
_default_size = (12,50)
xy = "y"
class _ScrollHandleH(_ScrollHandle):
_default_size = (50,12)
xy = "x"

131
sgc/widgets/settings.py Normal file
View File

@ -0,0 +1,131 @@
#!/usr/bin/env python
# Copyright (C) 2010 Sam Bull
"""
Settings for games, these include:
CONTROLS
Keymap
Mouse Sensitivity (speed) TODO
DISPLAY
Resolution (width, height) TODO
Fullscreen (bool) TODO
"""
import pygame
from pygame.locals import *
from ..locals import *
from _locals import *
from base_widget import Simple
class Keys(Simple):
_can_focus = True # Override Simple
"""
Screen used to change keymap settings.
Keys is a special widget that will fill the screen and take
over the game loop for maximum effiencency.
"""
def __init__(self, keymap_file, parent=None, **kwargs):
"""
Extend Simple and prepare the key order.
keymap_file -- String containing filename containing keymap.
Key order should be on second line.
parent,kwargs -- Pass through to Simple
"""
size = self._default_screen.size
Simple.__init__(self, size, parent, **kwargs)
# Load key order
with open(keymap_file) as f:
f.readline()
self._key_order = eval(f.readline())
assert isinstance(self._key_order, list)
def add(self):
"""
Display the settings for the keymap to the player.
"""
# Display title
message = Surface(font_title.render("Keymap Settings",
True, font_col))
message.y = 30
message.x = (self._parent.w - message.w)/2
self._parent().blit(message(), message.pos)
# Display settings
positions = {}
temp_y = 100
row = 0
for key in self._key_order:
if temp_y > (self._parent.h - message.h*2):
row += 1
temp_y = 100
# Render name
message = Surface(font_widget.render(key.title(),
True, font_col))
message.y = temp_y
message.x = 30 + ((self._parent.w-30)/3) * row
self._parent().blit(message(), message.pos)
# Render keymap
message = Surface(font_widget.render(
pygame.key.name(keymap[key]),
True, font_col))
message.y = temp_y
message.x = ((((self._parent.w-30)/3) * (row+1) - 30) -
message.w/2)
self._parent().blit(message(), message.pos)
positions[key] = message
temp_y += message.h*2
# Event loop
keypress_wait = False
while True:
event = pygame.event.wait()
if event.type == QUIT:
exit() #TODO exit to menu
elif event.type == MOUSEBUTTONDOWN and not keypress_wait:
# If clicking a key, then prepare to change keymap
for key in positions:
if pygame.mouse.get_pos()[0] >= positions[key].x and \
pygame.mouse.get_pos()[1] >= positions[key].y and \
pygame.mouse.get_pos()[0] <= positions[key].x + \
positions[key].w and \
pygame.mouse.get_pos()[1] <= positions[key].y + \
positions[key].h:
# Replace key with 'press key...' message
self._parent().fill((0,0,0), positions[key].rect)
message = Surface(font_widget.render(
"press key...", True, font_col))
message.y = positions[key].y
message.x = positions[key].x - \
(message.w - positions[key].w)/2
positions[key] = message
self._parent().blit(message(), message.pos)
keypress_wait = key
elif event.type == KEYDOWN and keypress_wait:
# When waiting for new key, replace text with new key
if event.key != K_ESCAPE:
keymap[keypress_wait] = event.key
self._parent().fill((0,0,0), positions[keypress_wait].rect)
message = Surface(font_widget.render(
pygame.key.name(keymap[keypress_wait]),
True, font_col))
message.y = positions[keypress_wait].y
message.x = positions[keypress_wait].x - \
(message.w - positions[keypress_wait].w)/2
positions[keypress_wait] = message
self._parent().blit(message(), message.pos)
keypress_wait = None
pygame.display.update()

183
sgc/widgets/toggle.py Normal file
View File

@ -0,0 +1,183 @@
#!/usr/bin/env python
# Copyright (C) 2012 Michael Rochester, Sam Bull
"""
Toggle button, allows the user to change a boolean setting.
"""
import pygame
from pygame.locals import *
from _locals import *
from _locals import focus
from base_widget import Simple
class Toggle(Simple):
"""
A toggle button, allowing the user to select between two states.
Attributes:
state: True if toggle button is switched on.
Images:
'image': The background when the button is set to off.
'active': The background when the button is set to on.
'handle': The image used for the slider.
"""
_can_focus = True
_default_size = (130,30)
_available_images = ("active", "handle")
_settings_default = {"state": False, "label": "", "label_col": Font.col,
"on_col": (88, 158, 232), "off_col": (191, 191, 186),
"on_label_col": (255,255,255),
"off_label_col": (93,82,80)}
_draw_rect = False
_drag = None
_handle_rect = None
def _config(self, **kwargs):
"""
state: ``bool`` Sets the state of the button (False by default).
label: ``str`` Text to be displayed next to the button.
label_col: ``tuple`` (r,g,b) The colour for the label.
on_col: ``tuple`` (r,g,b) The background colour when the button is
set to the 'on' state.
off_col: ``tuple`` (r,g,b) The background colour when the button is
set to the 'off' state.
on_label_col: ``tuple`` (r,g,b) The on/off text colour when the
button is set to the 'on' state.
off_label_col: ``tuple`` (r,g,b) The on/off text colour when the
button is set to the 'off' state.
"""
if "init" in kwargs:
self._handle_rect = Rect(0,0,0,0)
if "state" in kwargs:
self._settings["state"] = kwargs["state"]
if "label" in kwargs:
self._settings["label"] = kwargs["label"]
if "label_col" in kwargs:
self._settings["label_col"] = kwargs["label_col"]
if "on_col" in kwargs:
self._settings["on_col"] = kwargs["on_col"]
if "off_col" in kwargs:
self._settings["off_col"] = kwargs["off_col"]
if "on_label_col" in kwargs:
self._settings["on_label_col"] = kwargs["on_label_col"]
if "off_label_col" in kwargs:
self._settings["off_label_col"] = kwargs["off_label_col"]
def _draw(self, draw):
label = Simple(Font["widget"].render(self._settings["label"], True,
self._settings["label_col"]))
label.rect.centery = self.rect.h/2
# Calculate widget and handle rects
self.box = Rect((label.rect.w + 10, 0),
(self.rect.w - label.rect.w - 10, self.rect.h))
self._handle_rect.size = ((self.box.w/2) - 4, self.rect.h - 4)
# Draw handle
draw.rect(self._images["handle"], (245,245,244),
((0,2), self._handle_rect.size))
for x in range(2,5): # Grips
draw.line(self._images["handle"], (232,232,229),
((self._handle_rect.w/6)*x, 10),
((self._handle_rect.w/6)*x, self.rect.h-10), 3)
# Draw main images
for img in ("off", "on"):
image = "image" if (img == "off") else "active"
draw.rect(self._images[image], self._settings[img+"_col"], self.box)
# Render the labels
col = self._settings[img+"_label_col"]
on = Simple(Font["widget"].render("ON", True, col))
off = Simple(Font["widget"].render("OFF", True, col))
on.rect.center = (label.rect.w + 10 + self.box.w*.25 - 1,
self.rect.h/2)
off.rect.center = (label.rect.w + 10 + self.box.w*.75 + 1,
self.rect.h/2)
# Blit all text
self._images[image].blit(label.image, label.pos)
self._images[image].blit(on.image, on.pos)
self._images[image].blit(off.image, off.pos)
self._draw_button()
def _event(self, event):
if event.type == MOUSEBUTTONDOWN and event.button == 1:
# If clicking handle
if self._handle_rect.collidepoint(
(event.pos[0]-self.pos_abs[0],
event.pos[1]-self.pos_abs[1])):
self._drag = (event.pos[0], event.pos[0] - self._handle_rect.x)
elif event.type == MOUSEMOTION and event.buttons[0]:
if self._drag is not None:
# Move handle
self._handle_rect.x = max(min(self.box.centerx + 3,
event.pos[0] - self._drag[1]),
self.box.x + 2)
self._draw_button()
elif event.type == MOUSEBUTTONUP and event.button == 1:
if self._drag is not None:
if abs(self._drag[0] - event.pos[0]) < 5: # Clicked
self._settings["state"] = not self._settings["state"]
else: # Dragged
# Determine if dropped in on/off position
if self._handle_rect.centerx < self.box.centerx:
self._settings["state"] = False
else:
self._settings["state"] = True
self._drag = None
self._draw_button()
elif self.rect_abs.collidepoint(event.pos):
# Clicked outside of handle
self._settings["state"] = not self._settings["state"]
self._draw_button()
elif event.type == KEYUP:
if event.key in (K_RETURN, K_SPACE):
self._settings["state"] = not self._settings["state"]
self._draw_button()
def _focus_enter(self, focus):
"""Draw dotted rect when focus is gained from keyboard."""
if focus == 1:
self._draw_rect = True
self._draw_button()
def _focus_exit(self):
"""Stop drawing dotted rect when focus is lost."""
self._draw_rect = False
self._draw_button()
def _draw_button(self):
"""Render the widget and blit the handle in the correct place."""
if self._settings["state"] is False:
self.image = self._images["image"].copy()
else:
self.image = self._images["active"].copy()
if self._drag is None:
# Fix handle in place when not dragging
if self._settings["state"] is False:
self._handle_rect.x = self.box.x + 2
else:
self._handle_rect.x = self.box.centerx + 3
self.image.blit(self._images["handle"], self._handle_rect)
# Draw dotted rectangle to show keyboard focus
if self._draw_rect:
self._dotted_rect()
@property
def state(self):
return self._settings["state"]