369 lines
11 KiB
Python
369 lines
11 KiB
Python
|
#!/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[:]
|