#!/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[:]