Source code for peng3d.gui.widgets

#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
#  widgets.py
#  
#  Copyright 2016 notna <notna@apparat.org>
#  
#  This program is free software; you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation; either version 2 of the License, or
#  (at your option) any later version.
#  
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#  
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software
#  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
#  MA 02110-1301, USA.
#  
#  

__all__ = [
    "mouse_aabb",
    "BasicWidget","Background",
    "Widget","EmptyBackground",
    ]

import time

try:
    import pyglet
    from pyglet.gl import *
except ImportError:
    pass # Headless mode

[docs]def mouse_aabb(mpos,size,pos): """ Simple AABB collision algorithm used for checking if a mouse click hit a widget. """ return pos[0]<=mpos[0]<=pos[0]+size[0] and pos[1]<=mpos[1]<=pos[1]+size[1]
class _WatchingList(list): def __init__(self,l,callback=lambda:None): self.callback = callback super(_WatchingList,self).__init__(l) def __setitem__(self,*args): super(_WatchingList,self).__setitem__(*args) self.callback(self)
[docs]class Background(object): """ Class representing the background of a widget. This base class does not do anything. """ def __init__(self,widget): self.widget = widget self.initialized = False
[docs] def init_bg(self): """ Called just before the background will be drawn the first time. Commonly used to initialize vertex lists. It is recommended to add all vertex lists to the ``submenu.batch2d`` Batch to speed up rendering and preventing glitches with grouping. """ pass
[docs] def redraw_bg(self): """ Method called by the parent widget every time its :py:meth:`Widget.redraw()` method is called. """ pass
@property def submenu(self): """ Property for accessing the parent widget's submenu. """ return self.widget.submenu @property def window(self): """ Property for accessing the parent widget's window. """ return self.widget.window @property def peng(self): """ Property for accessing the parent widget's instance of :py:class:`peng3d.peng.Peng`\ . """ return self.widget.peng
[docs]class EmptyBackground(Background): """ Background that draws simply nothing. Can be used as a placeholder. """ pass
[docs]class BasicWidget(object): """ Basic Widget class. Every widget must be registered with their appropriate sub-menus to work properly. ``pos`` may be either a list or 2-tuple of ``(x,y)`` for static positions or a function with the signature ``window_width,window_height,widget_width,widget_height`` returning a tuple. ``size`` is similar to ``pos`` but will only get ``window_width,window_height`` as its arguments. Commonly, the lambda ``lambda sw,sh,bw,bh: (sw/2.-bw/2.,sh/2.-bh/2.)`` is used to center the widget. """ def __init__(self,name,submenu,window,peng, pos=None,size=None): self.name = name self.submenu = submenu self.window = window self.peng = peng self.actions = {} self.vlists = [] # :deprecated: VBOs should be added to the batch2d of the submenu # This feature will be removed in future versions self._pos = pos self._size = size self.is_hovering = False self.pressed = False self._enabled = True self._is_scrollbar = False self.registerEventHandlers()
[docs] def registerEventHandlers(self): """ Registers event handlers used by this widget, e.g. mouse click/motion and window resize. This will allow the widget to redraw itself upon resizing of the window in case the position needs to be adjusted. """ self.peng.registerEventHandler("on_mouse_press",self.on_mouse_press) self.peng.registerEventHandler("on_mouse_release",self.on_mouse_release) self.peng.registerEventHandler("on_mouse_drag",self.on_mouse_drag) self.peng.registerEventHandler("on_mouse_motion",self.on_mouse_motion) self.peng.registerEventHandler("on_resize",self.on_resize)
@property def pos(self): """ Property that will always be a 2-tuple representing the position of the widget. Note that this method may call the method given as ``pos`` in the initializer. The returned object will actually be an instance of a helper class to allow for setting only the x/y coordinate. This property also respects any :py:class:`Container` set as its parent, any offset will be added automatically. Note that setting this property will override any callable set permanently. """ if isinstance(self._pos,list) or isinstance(self._pos,tuple): r = self._pos elif callable(self._pos): r = self._pos(self.window.width,self.window.height,*self.size) else: raise TypeError("Invalid position type") if isinstance(self.submenu,Container): # Widget is inside container r = r[0]+self.submenu.pos[0],r[1]+self.submenu.pos[1] if isinstance(self.submenu,ScrollableContainer) and not self._is_scrollbar:# and self.name != "__scrollbar_%s"%self.submenu.name: # Widget inside scrollable container and not the scrollbar r = r[0],r[1]+self.submenu.offset_y return _WatchingList(r,self._wlredraw_pos) @pos.setter def pos(self,value): self._pos = value self.redraw() @property def size(self): """ Similar to :py:attr:`pos` but for the size instead. """ if isinstance(self._size,list) or isinstance(self._size,tuple): return _WatchingList(self._size,self._wlredraw_size) elif callable(self._size): return _WatchingList(self._size(self.window.width,self.window.height),self._wlredraw_size) else: raise TypeError("Invalid size type") @size.setter def size(self,value): self._size = value self.redraw() @property def clickable(self): """ Property used for determining if the widget should be clickable by the user. This is only true if the submenu of this widget is active and this widget is enabled. The widget may be either disabled by setting this property or the :py:attr:`enabled` attribute. """ if not isinstance(self.submenu,Container): return self.submenu.name == self.submenu.menu.activeSubMenu and self.submenu.menu.name == self.window.activeMenu and self.enabled else: return self.submenu.clickable and self.enabled @clickable.setter def clickable(self,value): self._enabled=value self.redraw() @property def enabled(self): """ Property used for storing whether or not this widget is enabled. May influence rendering and behavior. Note that the widget will be immediately redrawn if this property is changed. """ return self._enabled @enabled.setter def enabled(self,value): self._enabled=value self.redraw()
[docs] def addAction(self,action,func,*args,**kwargs): """ Adds the a callback to the specified action. All other positional and keyword arguments will be stored and passed to the function upon activation. The actions available may differ from widget to widget, by default these are used: - ``press`` is called upon starting to click on the widget - ``click`` is called if the mouse is released on the widget while also having been pressed on it before, recommended for typical button callbacks - ``context`` is called upon right-clicking on the widget and may be used to display a context menu - ``hover_start`` signals that the cursor is now hovering over the widget - ``hover`` is called every time the cursor moves while still being over the widget - ``hover_end`` is called after the cursor leaves the widget """ if action not in self.actions: self.actions[action] = [] self.actions[action].append((func,args,kwargs))
[docs] def doAction(self,action): """ Helper method that calls all callbacks registered for the given action. """ for f,args,kwargs in self.actions.get(action,[]): f(*args,**kwargs)
[docs] def draw(self): """ Draws all vertex lists associated with this widget. :deprecated: Add vertex lists to the submenu instead """ for vlist,t in self.vlists: vlist.draw(t)
[docs] def redraw(self): """ Callback to be overridden by subclasses called if redrawing the widget seems necessary. """ pass
def _wlredraw_pos(self,wl): self._pos = wl[:] self.redraw() def _wlredraw_size(self,wl): self._size = wl[:] self.redraw() def on_mouse_press(self,x,y,button,modifiers): if not self.clickable: return elif mouse_aabb([x,y],self.size,self.pos): if button == pyglet.window.mouse.LEFT: self.doAction("press") self.pressed = True elif button == pyglet.window.mouse.RIGHT: self.doAction("context") self.redraw() def on_mouse_release(self,x,y,button,modifiers): if not self.clickable: return if self.pressed: if mouse_aabb([x,y],self.size,self.pos): self.doAction("click") self.pressed = False self.redraw() def on_mouse_drag(self,x,y,dx,dy,button,modifiers): self.on_mouse_motion(x,y,dx,dy) def on_mouse_motion(self,x,y,dx,dy): if not self.clickable: return if mouse_aabb([x,y],self.size,self.pos): if not self.is_hovering: self.is_hovering = True self.doAction("hover_start") self.redraw() else: self.doAction("hover") else: if self.is_hovering: self.is_hovering = False self.doAction("hover_end") self.redraw() def on_resize(self,width,height): self.redraw()
[docs]class Widget(BasicWidget): """ Subclass of :py:class:`BasicWidget` adding support for changing the :py:class:`Background`\ . If no background is given, an :py:class:`EmptyBackground` will be used instead. """ def __init__(self,name,submenu,window,peng, pos=None,size=None, bg=None ): if bg is None: bg = EmptyBackground(self) self.bg = bg super(Widget,self).__init__(name,submenu,window,peng,pos,size)
[docs] def setBackground(self,bg): """ Sets the background of the widget. This may cause the background to be initialized. """ self.bg = bg self.redraw()
[docs] def redraw(self): """ Draws the background and the widget itself. Subclasses should use ``super()`` to call this method, or rendering may glitch out. """ if self.bg is not None: if not self.bg.initialized: self.bg.init_bg() self.bg.initialized=True self.bg.redraw_bg() super(Widget,self).redraw()