Source code for peng3d.gui.layered
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# layered.py
#
# Copyright 2017 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.
#
#
import inspect
import weakref
__all__ = [
"LayeredWidget",
"BasicWidgetLayer","WidgetLayer",
"GroupWidgetLayer",
"ImageWidgetLayer","DynImageWidgetLayer",
"FramedImageWidgetLayer",
"ImageButtonWidgetLayer",
"LabelWidgetLayer",
"FormattedLabelWidgetLayer","HTMLLabelWidgetLayer",
"BaseBorderWidgetLayer","ButtonBorderWidgetLayer",
]
import warnings
import pyglet
from pyglet.gl import *
from .widgets import Background, Widget
from .. import util
[docs]class LayeredWidget(Widget):
"""
Layered Widget allowing for easy creation of custom widgets.
A Layered Widget consists of (nearly) any amount of layers in a specific order.
All Layers should be subclasses of :py:class:`BasicWidgetLayer` or :py:class:`WidgetLayer`\ .
``layers`` must be a list of 2-tuples of ``(layer,z_index)``\ .
"""
IS_CLICKABLE = True
def __init__(self,name,submenu,window,peng,
pos=None,size=None,
bg=None,layers=[],
):
super(LayeredWidget,self).__init__(name,submenu,window,peng,pos,size,bg)
self.layers = []
self._layers = {}
for l,z in layers:
# Makes sure that all layers are sorted
# Does not use sort() to keep original order
self.addLayer(l,z)
[docs] def addLayer(self,layer,z_index=None):
"""
Adds the given layer at the given Z Index.
If ``z_index`` is not given, the Z Index specified by the layer will be used.
"""
if z_index is None:
z_index = layer.z_index
i = 0
for l,z in self.layers:
if z>z_index:
break
i+=1
self._layers[layer.name]=layer
self.layers.insert(i,[layer,z_index])
[docs] def getLayer(self,name):
"""
Returns the layer corresponding to the given name.
:raises KeyError: If there is no Layer with the given name.
"""
return self._layers[name]
[docs] def on_redraw(self):
super(LayeredWidget,self).on_redraw()
for layer,_ in self.layers:
layer.on_redraw()
[docs] def redraw_layer(self,name):
"""
Redraws the given layer.
:raises ValueError: If there is no Layer with the given name.
"""
if name not in self._layers:
raise ValueError("Layer %s not part of widget, cannot redraw")
self._layers[name].on_redraw()
[docs] def draw(self):
"""
Draws all layers of this LayeredWidget.
This should normally be unneccessary, since it is recommended that layers use Vertex Lists instead of OpenGL Immediate Mode.
"""
super(LayeredWidget,self).draw()
for layer,_ in self.layers:
layer._draw()
[docs] def delete(self):
"""
Deletes all layers within this LayeredWidget before deleting itself.
Recommended to call if you are removing the widget, but not yet exiting the interpreter.
"""
for layer,_ in self.layers:
layer.delete()
self.layers = []
self._layers = {}
super(LayeredWidget,self).delete()
[docs]class BasicWidgetLayer(object):
"""
Base class for all Layers to be used with :py:class:`LayeredWidget()`\ .
Not to be confused with :py:class:`peng3d.layer.Layer()`\ , these classes are not compatible.
It is recommended to use :py:class:`WidgetLayer()` instead, since functionality is limited in this basic class.
Note that the ``z_index`` will default to a reasonable value for most subclasses and thus is not required to be given explicitly.
The ``z_index`` for this Layer defaults to ``0``\ .
"""
z_index = 0
def __init__(self,name,widget,
z_index=None,
):
self.name = name
self.widget = widget
if z_index is not None:
self.z_index = z_index
self.group = pyglet.graphics.OrderedGroup(self.z_index)
self._vlists = []
[docs] def on_redraw(self):
"""
Called by the parent widget if this Layer should be redrawn.
Note that it is recommended to call the Baseclass Variant of this Method first when overwriting it.
See :py:meth:`WidgetLayer.on_redraw` for more information.
"""
pass
def _draw(self):
self.predraw()
self.draw()
self.postdraw()
[docs] def predraw(self):
"""
Called before calling the :py:meth:`draw()` Method.
Useful for setting up OpenGL state.
"""
pass
[docs] def draw(self):
"""
Called to draw the layer.
Note that using this function is discouraged, use Pyglet Vertex Lists instead.
If you want to call this method manually, call :py:meth:`_draw()` instead.
This will make sure that :py:meth:`predraw()` and :py:meth:`postdraw()` are called.
"""
pass
[docs] def postdraw(self):
"""
Called after calling the :py:meth:`draw()` Method.
Useful for unsetting OpenGL state.
"""
pass
[docs] def regVList(self,vlist):
"""
Registers a vertex list for proper deletion once this Layer gets destroyed.
This prevents visual artifacts from forming during deletion of a layer.
"""
self._vlists.append(vlist)
[docs] def delete(self):
"""
Deletes this Layer.
Currently only deletes VertexLists registered with :py:meth:`regVList()`\ .
"""
for vlist in self._vlists:
vlist.delete()
self._vlists = []
for e_type,e_handlers in self.widget.peng.eventHandlers.items():
if True or e_type in eh:
to_del = []
for e_handler in e_handlers:
# Weird workaround due to implementation details of WeakMethod
if isinstance(e_handler,weakref.ref):
if super(weakref.WeakMethod,e_handler).__call__() is self:
to_del.append(e_handler)
elif e_handler is self:
to_del.append(e_handler)
for d in to_del:
try:
#print("Deleting handler %s of type %s"%(d,e_type))
del e_handlers[e_handlers.index(d)]
except Exception:
#print("Could not delete handler %s, memory leak may occur"%d)
import traceback;traceback.print_exc()
for eframe in self.widget.peng.window._event_stack:
for e_t, e_m in eframe.items():
if inspect.ismethod(e_m) and dict(inspect.getmembers(e_m))["__self__"] == self:
self.widget.peng.window.remove_handler(e_t, e_m)
[docs]class WidgetLayer(BasicWidgetLayer):
"""
Subclass of :py:class:`WidgetLayer()` adding commonly used utility features.
This subclass adds a border and offset system.
The ``border`` is a 2-tuple of ``(x_border,y_border)``\ . The border is applied to all sides, resulting in the size being decreased by two pixel per pixel border width.
``offset`` is relative to the bottom left corner of the screen.
"""
def __init__(self,name,widget,
z_index=None,
border=[0,0],offset=[0,0],
):
super(WidgetLayer,self).__init__(name,widget,z_index)
self._initialized = False
self._border = border
self._offset = offset
[docs] def on_redraw(self):
"""
Called when the Layer should be redrawn.
If a subclass uses the :py:meth:`initialize()` Method, it is very important to also call the Super Class Method to prevent crashes.
"""
super(WidgetLayer,self).on_redraw()
if not self._initialized:
self.initialize()
self._initialized = True
[docs] def initialize(self):
"""
Called just before :py:meth:`on_redraw()` is called the first time.
"""
pass
@property
def border(self):
"""
Property to be used for setting and getting the border of the layer.
Note that setting this property causes an immediate redraw.
"""
if callable(self._border):
return util.WatchingList(self._border(*(self.widget.pos+self.widget.size)),self._wlredraw_border)
else:
return util.WatchingList(self._border,self._wlredraw_border)
@border.setter
def border(self,value):
self._border = value
self.on_redraw()
@property
def offset(self):
"""
Property to be used for setting and getting the offset of the layer.
Note that setting this property causes an immediate redraw.
"""
if callable(self._offset):
return util.WatchingList(self._offset(*(self.widget.pos+self.widget.size)),self._wlredraw_offset)
else:
return util.WatchingList(self._offset,self._wlredraw_offset)
@offset.setter
def offset(self,value):
self._offset = value
self.on_redraw()
def _wlredraw_border(self,wl):
self.border = wl[:]
def _wlredraw_offset(self,wl):
self.offset = wl[:]
[docs] def getPos(self):
"""
Returns the absolute position and size of the layer.
This method is intended for use in vertex position calculation, as the border and offset have already been applied.
The returned value is a 4-tuple of ``(sx,sy,ex,ey)``\ .
The two values starting with an s are the "start" position, or the lower-left corner.
The second pair of values signify the "end" position, or upper-right corner.
"""
# Returns sx,sy,ex,ey
# sx,sy are bottom-left/lowest
# ex,ey are top-right/highest
sx,sy = self.widget.pos[0]+self.border[0]+self.offset[0], self.widget.pos[1]+self.border[1]+self.offset[1]
ex,ey = self.widget.pos[0]+self.widget.size[0]-self.border[0]+self.offset[0], self.widget.pos[1]+self.widget.size[1]-self.border[1]+self.offset[1]
return sx,sy,ex,ey
[docs] def getSize(self):
"""
Returns the size of the layer, with the border size already subtracted.
"""
return self.widget.size[0]-self.border[0]*2,self.widget.size[1]-self.border[1]*2
[docs]class GroupWidgetLayer(WidgetLayer):
"""
Subclass of :py:class:`WidgetLayer()` allowing for using a pyglet group to manage OpenGL state.
If no pyglet group is given, :py:class:`pyglet.graphics.NullGroup()` will be used.
"""
def __init__(self,name,widget,
group=None,z_index=None,
border=[0,0],offset=[0,0],
):
super(GroupWidgetLayer,self).__init__(name,widget,z_index,border,offset)
self.group = group if group is not None else pyglet.graphics.NullGroup()
predraw.__noautodoc__ = True
postdraw.__noautodoc__ = True
[docs]class ImageWidgetLayer(WidgetLayer):
"""
Subclass of :py:class:`WidgetLayer()` implementing a simple static image view.
This layer can display any resource representable by the :py:class:`ResourceManager()`\ .
``img`` is a 2-tuple of ``(resource_name,category)``\ .
The ``z_index`` for this Layer defaults to ``1``\ .
"""
z_index = 1
def __init__(self,name,widget,
z_index=None,
border=[0,0],offset=[0,0],
img=[None,None],
):
super(ImageWidgetLayer,self).__init__(name,widget,z_index,border,offset)
self.img = widget.peng.resourceMgr.getTex(*img)
self.img_group = util.ResourceGroup(self.img,self.group)
[docs] def initialize(self):
self.img_vlist = self.widget.submenu.batch2d.add(4,GL_QUADS,self.img_group,
"v2f",
("t3f",self.img[2]),
)
self.regVList(self.img_vlist)
initialize.__noautodoc__ = True
[docs] def on_redraw(self):
super(ImageWidgetLayer,self).on_redraw()
sx,sy, ex,ey = self.getPos()
# A simple rectangle
self.img_vlist.vertices = sx,sy, ex,sy, ex,ey, sx,ey
class _DynImageGroup(pyglet.graphics.Group):
def __init__(self,layer,parent=None):
super(_DynImageGroup,self).__init__(parent)
self.layer = layer
def set_state(self):
tex_info = self.layer.imgs[self.layer.cur_img]
glEnable(tex_info[0])
glBindTexture(tex_info[0],tex_info[1])
def unset_state(self):
tex_info = self.layer.imgs[self.layer.cur_img]
glDisable(tex_info[0])
[docs]class DynImageWidgetLayer(WidgetLayer):
"""
Subclass of :py:class:`WidgetLayer` allowing for dynamic images.
``imgs`` is a dictionary of names to 2-tuples of ``(resource_name,category)``\ .
If no default image name is given, a semi-random one will be selected.
The ``z_index`` for this Layer defaults to ``1``\ .
"""
z_index = 1
def __init__(self,name,widget,
z_index=None,
border=[0,0],offset=[0,0],
imgs={},
default=None,
):
super(DynImageWidgetLayer,self).__init__(name,widget,z_index,border,offset)
self.imgs = {}
self.img_group = _DynImageGroup(self,self.group)
for name,rsrc in imgs.items():
self.addImage(name,rsrc)
self.cur_img = None
self.default_img = default
[docs] def addImage(self,name,rsrc):
"""
Adds an image to the internal registry.
``rsrc`` should be a 2-tuple of ``(resource_name,category)``\ .
"""
self.imgs[name]=self.widget.peng.resourceMgr.normTex(rsrc)
[docs] def switchImage(self,name):
"""
Switches the active image to the given name.
:raises ValueError: If there is no such image
"""
if name not in self.imgs:
raise ValueError("No image of name '%s'"%name)
elif self.cur_img==name:
return
self.cur_img = name
self.on_redraw()
[docs] def initialize(self):
self.img_vlist = self.widget.submenu.batch2d.add(4,GL_QUADS,self.img_group,
"v2f",
"t3f",
)
self.regVList(self.img_vlist)
self.cur_img = self.cur_img if self.cur_img is not None else (self.default_img if self.default_img is not None else list(self.imgs.keys())[0])
initialize.__noautodoc__ = True
[docs] def on_redraw(self):
super(DynImageWidgetLayer,self).on_redraw()
sx,sy, ex,ey = self.getPos()
# A simple rectangle
self.img_vlist.vertices = sx,sy, ex,sy, ex,ey, sx,ey
self.img_vlist.tex_coords = self.imgs[self.cur_img][2]
[docs]class FramedImageWidgetLayer(DynImageWidgetLayer):
"""
Subclass of :py:class:`DynImageWidgetLayer` allowing for dynamically smart scaled images.
Similar to :py:class:`FramedImageButton`\\ . Allows for scaling and/or repeating
the borders, corners and center independently.
Note that the ``tex_size`` parameter, if not given, will be derived from a random texture
that has been given in ``imgs``\\ . Also note that the ``frame``\\ , ``scale``\\ ,
``repeat_edge`` and ``repeat_center`` parameters are identical for all images.
"""
def __init__(self,name, widget,
z_index=None,
border=[0, 0], offset=[0, 0],
imgs={},
default=None,
frame=[[2,10,2],[2,10,2]],
scale=(0, 0),
repeat_edge=False,
repeat_center=False,
tex_size=None,
):
super().__init__(name, widget, z_index, border, offset, imgs, default)
if tex_size is None:
t = list(imgs.values())[0]
self.tsx, self.tsy = self.widget.peng.resourceMgr.getTexSize(*t) # Texture Size in pixels
else:
self.tsx, self.tsy = tex_size
self.frame_x = list(
map(lambda x: x * (self.tsx / sum(frame[0])), frame[0])) # Frame Size in the texture, in pixels
self.frame_y = list(map(lambda y: y * (self.tsy / sum(frame[1])), frame[1]))
self._scale = scale
self.repeat_edge = repeat_edge
self.repeat_center = repeat_center
for i in frame:
if (self.repeat_edge or self.repeat_center) and i[1] == 0:
raise ValueError("Cannot repeat edges or center with the middle frame being 0")
@property
def scale(self):
if self._scale == (None, None):
raise ValueError(f"Scale cannot be {self._scale}")
scale = self._scale
if scale[0] == 0: # 0 makes the resulting frames similar to the texture frames but scaled up to the widget size
scale = self.size[0] / sum(self.frame_x), scale[1]
if scale[1] == 0:
scale = scale[0], self.size[1] / sum(self.frame_y)
if scale[0] == None: # None makes the scale similar to the second scale
scale = scale[1], scale[1]
if scale[1] == None:
scale = scale[0], scale[0]
return scale
@property
def size(self):
return self.getSize()
[docs] def initialize(self):
self.cur_img = self.cur_img if self.cur_img is not None else (
self.default_img if self.default_img is not None else list(self.imgs.keys())[0])
if (self.frame_x[0] + self.frame_x[2]) * self.scale[0] > self.widget.size[0] or \
(self.frame_y[0] + self.frame_y[2]) * self.scale[1] > self.widget.size[1]:
raise ValueError(f"Scale {self.scale} is too large for this widget")
self.bg_group = _DynImageGroup(self, self.group)
self.vlist_corners = self.widget.submenu.batch2d.add(16, GL_QUADS, self.bg_group, "v2f", "t3f")
self.vlist_edges = self.widget.submenu.batch2d.add(16, GL_QUADS, self.bg_group, "v2f", "t3f")
self.vlist_center = self.widget.submenu.batch2d.add(4, GL_QUADS, self.bg_group, "v2f", "t3f")
self.regVList(self.vlist_corners)
self.regVList(self.vlist_edges)
self.regVList(self.vlist_center)
[docs] def on_redraw(self):
WidgetLayer.on_redraw(self)
# Convenience Variables
sx, sy = self.size
x, y, _, _ = self.getPos()
# Frame length in the result, in pixels
flx, fcx, frx = map(lambda x: self.scale[0] * x, self.frame_x)
sfcx = sx - (flx + frx) # Stretched center frame length
fdy, fcy, fuy = map(lambda y: self.scale[1] * y, self.frame_y)
sfcy = sy - (fdy + fuy)
amx, amy, rx, ry = 0, 0, 0, 0
if self.repeat_center or self.repeat_edge:
amx, amy = int(sfcx / fcx), int(sfcy / fcy) # Amount of complete textures in an edge
rx, ry = sfcx % fcx, sfcy % fcy # Length of the rest tile in pixels
# Vertices
# 11-10---15-14
# | | | |
# 8---9---12-13
# | | | |
# 3---2---7---6
# | | | |
# 0---1---4---5
# Corners
# x y
v0 = x, y
v1 = x + flx, y
v2 = x + flx, y + fdy
v3 = x, y + fdy
v4 = x + sx - frx, y
v5 = x + sx, y
v6 = x + sx, y + fdy
v7 = x + sx - frx, y + fdy
v8 = x, y + sy - fuy
v9 = x + flx, y + sy - fuy
v10 = x + flx, y + sy
v11 = x, y + sy
v12 = x + sx - frx, y + sy - fuy
v13 = x + sx, y + sy - fuy
v14 = x + sx, y + sy
v15 = x + sx - frx, y + sy
self.vlist_corners.vertices = (
v0 + v1 + v2 + v3 +
v4 + v5 + v6 + v7 +
v8 + v9 + v10 + v11 +
v12 + v13 + v14 + v15
)
if self.repeat_edge:
self.vlist_edges.resize(8 * (amx + amy + 2))
vd, vu, vl, vr = [], [], [], []
for i in range(amx):
vd += x + flx + i * fcx, y
vd += x + flx + (i + 1) * fcx, y
vd += x + flx + (i + 1) * fcx, y + fdy
vd += x + flx + i * fcx, y + fdy
vu += x + flx + i * fcx, y + sy - fuy
vu += x + flx + (i + 1) * fcx, y + sy - fuy
vu += x + flx + (i + 1) * fcx, y + sy
vu += x + flx + i * fcx, y + sy
vd += x + sx - frx - rx, y
vd += x + sx - frx, y
vd += x + sx - frx, y + fdy
vd += x + sx - frx - rx, y + fdy
vu += x + sx - frx - rx, y + sy - fuy
vu += x + sx - frx, y + sy - fuy
vu += x + sx - frx, y + sy
vu += x + sx - frx - rx, y + sy
for j in range(amy):
vl += x, y + fdy + j * fcy
vl += x + flx, y + fdy + j * fcy
vl += x + flx, y + fdy + (j + 1) * fcy
vl += x, y + fdy + (j + 1) * fcy
vr += x + sx - frx, y + fdy + j * fcy
vr += x + sx, y + fdy + j * fcy
vr += x + sx, y + fdy + (j + 1) * fcy
vr += x + sx - frx, y + fdy + (j + 1) * fcy
vl += x, y + sy - fuy - ry
vl += x + flx, y + sy - fuy - ry
vl += x + flx, y + sy - fuy
vl += x, y + sy - fuy
vr += x + sx - frx, y + sy - fuy - ry
vr += x + sx, y + sy - fuy - ry
vr += x + sx, y + sy - fuy
vr += x + sx - frx, y + sy - fuy
self.vlist_edges.vertices = vd + vl + vr + vu
else:
self.vlist_edges.vertices = (
v1 + v4 + v7 + v2 +
v3 + v2 + v9 + v8 +
v7 + v6 + v13 + v12 +
v9 + v12 + v15 + v10
)
if self.repeat_center:
self.vlist_center.resize(4 * (amx + 1) * (amy + 1))
v = []
# Completed tiles
for j in range(amy):
for i in range(amx):
v += x + flx + i * fcx, y + fdy + j * fcy
v += x + flx + (i + 1) * fcx, y + fdy + j * fcy
v += x + flx + (i + 1) * fcx, y + fdy + (j + 1) * fcy
v += x + flx + i * fcx, y + fdy + (j + 1) * fcy
# X-shortened tiles
for j in range(amy):
v += x + sx - frx - rx, y + fdy + j * fcy
v += x + sx - frx, y + fdy + j * fcy
v += x + sx - frx, y + fdy + (j + 1) * fcy
v += x + sx - frx - rx, y + fdy + (j + 1) * fcy
# Y-shortened tiles
for i in range(amx):
v += x + flx + i * fcx, y + sy - fuy - ry
v += x + flx + (i + 1) * fcx, y + sy - fuy - ry
v += x + flx + (i + 1) * fcx, y + sy - fuy
v += x + flx + i * fcx, y + sy - fuy
# X-Y-shortened tile
v += x + sx - frx - rx, y + sy - fuy - ry
v += x + sx - frx, y + sy - fuy - ry
v += x + sx - frx, y + sy - fuy
v += x + sx - frx - rx, y + sy - fuy
self.vlist_center.vertices = v
else:
self.vlist_center.vertices = v2 + v7 + v12 + v9
t = self.transform_texture(self.imgs[self.cur_img], amx, amy, rx, ry)
self.vlist_corners.tex_coords, self.vlist_edges.tex_coords, self.vlist_center.tex_coords = t
def transform_texture(self, texture, amx, amy, rx, ry):
t = texture[2]
sx, sy = self.size
tx, ty = t[3] - t[0], t[10] - t[1] # Texture Size on texture level
# Frame length on texture level
flx, fcx, frx = map(lambda x: x * tx / self.tsx, self.frame_x)
fdy, fcy, fuy = map(lambda y: y * ty / self.tsy, self.frame_y)
if self.repeat_center or self.repeat_edge:
rx = (rx * tx) / (self.tsx * self.scale[0])
ry *= ty / self.tsy / self.scale[1]
t0 = t[0], t[1], t[2]
t1 = t[0] + flx, t[1], t[2]
t2 = t[0] + flx, t[1] + fdy, t[2]
t3 = t[0], t[1] + fdy, t[2]
t4 = t[3] - frx, t[4], t[5]
t5 = t[3], t[4], t[5]
t6 = t[3], t[4] + fdy, t[5]
t7 = t[3] - frx, t[4] + fdy, t[5]
t8 = t[9], t[10] - fuy, t[11]
t9 = t[9] + flx, t[10] - fuy, t[11]
t10 = t[9] + flx, t[10], t[11]
t11 = t[9], t[10], t[11]
t12 = t[6] - frx, t[7] - fuy, t[8]
t13 = t[6], t[7] - fuy, t[8]
t14 = t[6], t[7], t[8]
t15 = t[6] - frx, t[7], t[8]
corner_tex = t0 + t1 + t2 + t3 + t4 + t5 + t6 + t7 + t8 + t9 + t10 + t11 + t12 + t13 + t14 + t15
if self.repeat_edge:
td, tl, tr, tu = [], [], [], []
td += (t1 + t4 + t7 + t2) * amx
tu += (t9 + t12 + t15 + t10) * amx
td += t1
td += t[0] + flx + rx, t[1], t[2]
td += t[0] + flx + rx, t[1] + fdy, t[2]
td += t2
tu += t9
tu += t[9] + flx + rx, t[10] - fuy, t[11]
tu += t[9] + flx + rx, t[10], t[11]
tu += t10
tl += (t3 + t2 + t9 + t8) * amy
tr += (t7 + t6 + t13 + t12) * amy
tl += t3 + t2
tl += t[0] + flx, t[1] + fdy + ry, t[2]
tl += t[0], t[1] + fdy + ry, t[2]
tr += t7 + t6
tr += t[3], t[4] + fdy + ry, t[5]
tr += t[3] - frx, t[4] + fdy + ry, t[5]
edge_tex = td + tl + tr + tu
else:
edge_tex = t1 + t4 + t7 + t2 + t3 + t2 + t9 + t8 + t7 + t6 + t13 + t12 + t9 + t12 + t15 + t10
if self.repeat_center:
tc = []
tc += (t2 + t7 + t12 + t9) * amx * amy
for i in range(amy):
tc += t2
tc += t[0] + flx + rx, t[1] + fdy, t[2]
tc += t[9] + flx + rx, t[10] - fuy, t[11]
tc += t9
for i in range(amx):
tc += t2 + t7
tc += t[3] - frx, t[4] + fdy + ry, t[5]
tc += t[0] + flx, t[1] + fdy + ry, t[2]
tc += t2
tc += t[0] + flx + rx, t[1] + fdy, t[2]
tc += t[0] + flx + rx, t[1] + fdy + ry, t[8]
tc += t[0] + flx, t[1] + fdy + ry, t[2]
center_tex = tc
else:
center_tex = t2 + t7 + t12 + t9
return corner_tex, edge_tex, center_tex
[docs]class ImageButtonWidgetLayer(DynImageWidgetLayer):
"""
Subclass of :py:class:`DynImageWidgetLayer()` that acts like an :py:class:`ImageButton()`\ .
The ``img_*`` arguments are of the same format as in :py:class:`DynImageWidgetLayer()`\ .
This class internally uses the :py:meth:`BasicWidget.getState()` method for getting the state of the widget.
"""
def __init__(self,name,widget,
z_index=None,
border=[0,0],offset=[0,0],
img_idle=None,img_pressed=None,img_hover=None,img_disabled=None,
):
super(ImageButtonWidgetLayer,self).__init__(name,widget,z_index,border,
imgs={"idle":img_idle,
"pressed":img_pressed,
"hover":img_hover,
"disabled":img_disabled,
},
default="idle",
)
self.widget.addAction("statechanged",lambda:self.switchImage(self.widget.getState()))
[docs]class LabelWidgetLayer(WidgetLayer):
"""
Subclass of :py:class:`WidgetLayer()` displaying arbitrary plain text.
Note that this method internally uses a pyglet Label that is centered on the Layer.
The ``z_index`` for this Layer defaults to ``2``\ .
"""
z_index = 2
def __init__(self,name,widget,
z_index=None,
border=[0,0],offset=[0,0],
label="",
font_size=None,font=None,
font_color=None,
multiline=False,
):
font = font if font is not None else widget.submenu.font
font_size = font_size if font_size is not None else widget.submenu.font_size
font_color = font_color if font_color is not None else widget.submenu.font_color
super(LabelWidgetLayer,self).__init__(name,widget,z_index,border,offset)
self._label = pyglet.text.Label(str(label),
font_name=font,
font_size=font_size,
color=font_color,
x=0,y=0,
batch=self.widget.submenu.batch2d,
group=self.group,
anchor_x="center", anchor_y="center",
width=self.getSize()[0],#height=self.getSize()[1],
multiline=multiline,
)
if getattr(label,"_dynamic",False):
def f():
self.label = str(label)
self.widget.peng.i18n.addAction("setlang",f)
self.on_redraw()
[docs] def redraw_label(self):
"""
Re-draws the text by calculating its position.
Currently, the text will always be centered on the position of the layer.
"""
# Convenience variables
x,y,_,_ = self.getPos()
sx,sy = self.getSize()
self._label.x = x+sx/2.
self._label.y = y+sy/2.
self._label.width = sx
# Height is not set, would look weird otherwise
#self._label.height = sx
self._label._update() # Needed to prevent the label from drifting to the top-left after resizing by odd amounts
@property
def label(self):
"""
Property for accessing the text of the label.
"""
return self._label.text
@label.setter
def label(self,label):
self._label.text = str(label)
[docs]class FormattedLabelWidgetLayer(WidgetLayer):
"""
Subclass of :py:class:`WidgetLayer()` serving as a base class for other formatted label layers.
The Label Type can be set via the class attribute ``cls``\ , it should be set to any class that is compatible with :py:class:`pyglet.text.Label`\ .
It is recommended to use one of the subclasses of this class instead of this class directly.
The ``z_index`` for this Layer defaults to ``2``\ .
"""
z_index = 2
cls = pyglet.text.HTMLLabel
def __init__(self,name,widget,
z_index=None,
border=[0,0],offset=[0,0],
label="",
font_size=None,font="Arial",
font_color=None,
multiline=False,
):
super(FormattedLabelWidgetLayer,self).__init__(name,widget,z_index,border,offset)
self.font_size = font_size
self.font_name = font
self.font_color = font_color
self._label = self.cls(str(label),
x=0,y=0,
batch=self.widget.submenu.batch2d,
group=self.group,
anchor_x="center", anchor_y="center",
width=self.getSize()[0],#height=self.getSize()[1],
multiline=multiline,
)
if getattr(label,"_dynamic",False):
def f():
self.label = str(label)
self.peng.i18n.addAction("setlang",f)
self.on_redraw()
[docs] def on_redraw(self,dt=None):
super(FormattedLabelWidgetLayer,self).on_redraw()
self.redraw_label()
[docs] def redraw_label(self):
"""
Re-draws the text by calculating its position.
Currently, the text will always be centered on the position of the layer.
"""
# Convenience variables
x,y,_,_ = self.getPos()
sx,sy = self.getSize()
if self.font_name is not None:
self._label.font_name = self.font_name
if self.font_size is not None:
self._label.font_size = self.font_size
if self.font_color is not None:
self._label.color = self.font_color
self._label.x = x+sx/2.
self._label.y = y+sy/2.
self._label.width = sx
# Height is not set, would look weird otherwise
#self._label.height = sx
self._label._update() # Needed to prevent the label from drifting to the top-left after resizing by odd amounts
@property
def label(self):
"""
Property for accessing the text of the label.
Note that depending on the type of format, this property may not exactly represent the original text as it is converted internally.
"""
return self._label.text
@label.setter
def label(self,label):
self._label.text = label
[docs]class HTMLLabelWidgetLayer(FormattedLabelWidgetLayer):
"""
Subclass of :py:class:`FormattedLabelWidgetLayer` implementing a basic HTML Label.
Note that not all tags are supported, see the docs for :py:class:`pyglet.text.HTMLLabel` for details.
"""
cls = pyglet.text.HTMLLabel
# TODO: support other formats (Markdown, Pyglet-style Attributed Text, Maybe others?)
[docs]class BaseBorderWidgetLayer(WidgetLayer):
"""
Subclass of :py:class:`WidgetLayer` that displays a basic border around the layer.
Note that not all styles will look good with this class, see :py:class:`ButtonBorderWidgetLayer()` for more information.
Note that the ``border`` and ``offset`` arguments have been renamed to ``base_border`` and ``base_offset`` to prevent naming conflicts.
Subclasses may set the :py:attr:`n_vertices` value to change the number of
vertices or :py:attr:`change_on_press` to change the default value for the
argument of the same name.
By default, 36 vertices are used and ``changed_on_press`` is set to ``True``\ .
The ``z_index`` for this Layer defaults to ``0.5``\ .
"""
z_index = 0.5 # between default and image layer
n_vertices = 36
change_on_press = True
color_bg = None
color_o = None
color_i = None
color_s = None
color_h = None
def __init__(self,name,widget,
z_index=None,
base_border=[0,0],base_offset=[0,0],
border=[4,4, 4,4, 4,4, 4,4],
style="flat",
batch=None, change_on_press=None,
):
super(BaseBorderWidgetLayer,self).__init__(name,widget,z_index,base_border,base_offset)
self.bborder = border
self.style = style
if style=="material" and self.__class__==BaseBorderWidgetLayer:
warnings.warn("Material style may have visual artifacts if used with BaseBorderWidgetLayer, use ButtonBorderWidgetLayer instead",stacklevel=2)
elif style=="gradient" and self.__class__==BaseBorderWidgetLayer:
warnings.warn("Gradient style may have visual artifacts if used with BaseBorderWidgetLayer, use ButtonBorderWidgetLayer instead",stacklevel=2)
elif style=="oldshadow" and self.__class__==BaseBorderWidgetLayer:
warnings.warn("Oldshadow style may have visual artifacts if used with BaseBorderWidgetLayer, use ButtonBorderWidgetLayer instead",stacklevel=2)
self.batch = batch
self.change_on_press = change_on_press if change_on_press is not None else self.change_on_press
self.styles = {}
self.addStyle("flat",self.s_flat)
self.addStyle("gradient",self.s_gradient)
self.addStyle("oldshadow",self.s_oldshadow)
self.addStyle("material",self.s_material)
[docs] def addStyle(self,name,func):
"""
Adds a style to the layer.
Note that styles must be registered seperately for each layer.
``name`` is the (string) name of the style.
``func`` will be called with its arguments as ``(bg,o,i,s,h)``\ , see :py:meth:`getColors()` for more information.
"""
self.styles[name]=func
[docs] def getColors(self):
"""
Overrideable function that generates the colors to be used by various styles.
Should return a 5-tuple of ``(bg,o,i,s,h)``\ .
``bg`` is the base color of the background.
``o`` is the outer color, it is usually the same as the background color.
``i`` is the inner color, it is usually lighter than the background color.
``s`` is the shadow color, it is usually quite a bit darker than the background.
``h`` is the highlight color, it is usually quite a bit lighter than the background.
The returned values may also be statically overridden by setting the :py:attr:`color_<var>` attribute to anything but ``None``\ .
"""
bg = self.widget.submenu.bg[:3] if isinstance(self.widget.submenu.bg,list) or isinstance(self.widget.submenu.bg,tuple) else [242,241,240]
bg = bg if self.color_bg is None else self.color_bg
o,i = bg, [min(bg[0]+8,255),min(bg[1]+8,255),min(bg[2]+8,255)]
s,h = [max(bg[0]-40,0),max(bg[1]-40,0),max(bg[2]-40,0)], [min(bg[0]+12,255),min(bg[1]+12,255),min(bg[2]+12,255)]
o = o if self.color_o is None else self.color_o
i = i if self.color_i is None else self.color_i
s = s if self.color_s is None else self.color_s
h = h if self.color_h is None else self.color_h
# Outer,Inner,Shadow,Highlight
return bg,o,i,s,h
[docs] def initialize(self):
self.batch = self.batch if self.batch is not None else self.widget.submenu.batch2d
self.vlist = self.batch.add(self.n_vertices,GL_QUADS,self.group,
"v2f",
"c3B",
)
self.regVList(self.vlist)
initialize.__noautodoc__ = True
[docs] def on_redraw(self):
super(BaseBorderWidgetLayer,self).on_redraw()
self.vlist.vertices = self.genVertices()
if self.style not in self.styles:
raise ValueError("Invalid Style")
c = self.styles[self.style](*self.getColors())
if len(c)==self.n_vertices*3: # one color per vertex
# No need to change anything
pass
elif len(c)==20*3: # old button-style coloring
c = self.stretchColors(c)
else:
raise ValueError("Style produced %s colors, but %s required"%(len(c)/3,self.n_vertices))
self.vlist.colors = c
[docs] def genVertices(self):
"""
Called to generate the vertices used by this layer.
The length of the output of this method should be three times the :py:attr:`n_vertices` attribute.
See the source code of this method for more information about the order of the vertices.
"""
sx,sy,ex,ey = self.getPos()
b = self.bborder
# Vertex Naming
# Y
# |1 2 3 4
# |5 6 7 8
# |9 10 11 12
# |13 14 15 16
# +------> X
# Border order
# 4 2-tuples
# Each marks x,y offset from the respective corner
# tuples are in order topleft,topright,bottomleft,bottomright
# indices:
# 0,1:topleft; 2,3:topright; 4,5:bottomleft; 6,7:bottomright
# For a simple border that is even, just repeat the first tuple three more times
v1 = sx, ey
v2 = sx+b[0], ey
v3 = ex-b[2], ey
v4 = ex, ey
v5 = sx, ey-b[1]
v6 = sx+b[0], ey-b[1]
v7 = ex-b[2], ey-b[3]
v8 = ex, ey-b[3]
v9 = sx, sy+b[5]
v10= sx+b[4], sy+b[5]
v11= ex-b[6], sy+b[7]
v12= ex, sy+b[7]
v13= sx, sy
v14= sx+b[4], sy
v15= ex-b[6], sy
v16= ex, sy
# Layer is separated into 9 sections, naming:
# 1 2 3
# 4 5 6
# 7 8 9
# Within each section, vertices are given counter-clockwise, starting with the bottom-left
# 4 3
# 1 2
# This is important when assigning colors
q1 = v5 +v6 +v2 +v1
q2 = v6 +v7 +v3 +v2
q3 = v7 +v8 +v4 +v3
q4 = v9 +v10+v6 +v5
q5 = v10+v11+v7 +v6
q6 = v11+v12+v8 +v7
q7 = v13+v14+v10+v9
q8 = v14+v15+v11+v10
q9 = v15+v16+v12+v11
return q1+q2+q3+q4+q5+q6+q7+q8+q9
[docs] def stretchColors(self,c):
"""
Method that is called to stretch the colors.
Note that this should be implemented by subclasses if plausible and reasonable.
"""
# Subclasses should override this method to implement color stretching
# Not possible here since the corners are implemented differently
raise NotImplementedError("Color stretching is not supported on %s"%self.__class__.__name__)
@property
def pressed(self):
"""
Read-only helper property to be used by styles for determining if the layer should be rendered as pressed or not.
Note that this property may not represent the actual pressed state, it will always be False if ``change_on_press`` is disabled.
"""
return self.change_on_press and self.widget.pressed
@property
def is_hovering(self):
"""
Read-only helper property to be used by styles for determining if the layer should be rendered as hovered or not.
Note that this property may not represent the actual hovering state, it will always be False if ``change_on_press`` is disabled.
"""
return self.change_on_press and self.widget.is_hovering
def s_flat(self,bg,o,i,s,h):
# Flat style makes no difference between normal,hover and pressed
return i*36
s_flat.__noautodoc__ = True
def s_gradient(self,bg,o,i,s,h):
if self.pressed:
i = s
elif self.is_hovering:
i = [min(i[0]+6,255),min(i[1]+6,255),min(i[2]+6,255)]
return (
o+i+o+o+
i+i+o+o+
i+o+o+o+
o+i+i+o+
i+i+i+i+
i+o+o+i+
o+o+i+o+
o+o+i+i+
o+o+o+i
)
s_gradient.__noautodoc__ = True
def s_oldshadow(self,bg,o,i,s,h):
if self.pressed:
i = s
s,h = h,s
elif self.is_hovering:
i = [min(i[0]+6,255),min(i[1]+6,255),min(i[2]+6,255)]
s = [min(s[0]+6,255),min(s[1]+6,255),min(s[2]+6,255)]
return (
h+h+h+h+
h+h+h+h+
h+s+h+h+
h+h+h+h+
i+i+i+i+
s+s+s+s+
h+s+h+h+
s+s+s+s+
s+s+s+s
)
s_oldshadow.__noautodoc__ = True
def s_material(self,bg,o,i,s,h):
if self.pressed:
i = [max(bg[0]-20,0),max(bg[1]-20,0),max(bg[2]-20,0)]
elif self.is_hovering:
i = [max(bg[0]-10,0),max(bg[1]-10,0),max(bg[2]-10,0)]
return (
o+s+o+o+
s+s+o+o+
s+o+o+o+
o+s+s+o+
i+i+i+i+
s+o+o+s+
o+o+s+o+
o+o+s+s+
o+o+o+s
)
s_material.__noautodoc__ = True
[docs]class ButtonBorderWidgetLayer(BaseBorderWidgetLayer):
"""
Subclass of :py:class:`BaseBorderWidgetLayer()` implementing Button-Style borders.
This class is based on the :py:class:`ButtonBackground` class.
This means that most styles are also available here and should look identical.
Note that this class uses only 20 vertices and is thus not compatible with styles
created for use with :py:class:`BaseBorderWidgetLayer`\ .
Also note that the ``border`` argument also only receives two values instead of eight.
"""
n_vertices = 20
def __init__(self,name,widget,
z_index=None,
base_border=[0,0],base_offset=[0,0],
border=[4,4],
style="flat",
batch=None, change_on_press=None,
):
super(ButtonBorderWidgetLayer,self).__init__(name,widget,z_index,base_border,base_offset,border[0:2],style,batch,change_on_press)
[docs] def genVertices(self):
sx,sy, ex,ey = self.getPos()
bx,by = self.bborder
# Vertex Naming
# Y
# | 1 2
# | 5 6
# | 7 8
# | 3 4
# +-------> X
# Quad order
# 1
# 4 5 2
# 3
# Within each quad, vertices are ordered from the bottom-left counter-clockwise
v1 = sx, ey
v2 = ex, ey
v3 = sx, sy
v4 = ex, sy
v5 = sx+bx, ey-by
v6 = ex-bx, ey-by
v7 = sx+bx, sy+by
v8 = ex-bx, sy+by
q1 = v5+v6+v2+v1
q2 = v8+v4+v2+v6
q3 = v3+v4+v8+v7
q4 = v3+v7+v5+v1
q5 = v7+v8+v6+v5
return q1+q2+q3+q4+q5
genVertices.__noautodoc__ = True
def s_flat(self,bg,o,i,s,h):
# Flat style makes no difference between normal,hover and pressed
cb1 = i+i+i+i
cb2 = i+i+i+i
cb3 = i+i+i+i
cb4 = i+i+i+i
cc = i+i+i+i
return cb1+cb2+cb3+cb4+cc
s_flat.__noautodoc__ = True
def s_gradient(self,bg,o,i,s,h):
if self.pressed:
i = s
elif self.is_hovering:
i = [min(i[0]+6,255),min(i[1]+6,255),min(i[2]+6,255)]
cb1 = i+i+o+o
cb2 = i+o+o+i
cb3 = o+o+i+i
cb4 = o+i+i+o
cc = i+i+i+i
return cb1+cb2+cb3+cb4+cc
s_gradient.__noautodoc__ = True
def s_oldshadow(self,bg,o,i,s,h):
if self.pressed:
i = s
s,h = h,s
elif self.is_hovering:
i = [min(i[0]+6,255),min(i[1]+6,255),min(i[2]+6,255)]
s = [min(s[0]+6,255),min(s[1]+6,255),min(s[2]+6,255)]
cb1 = h+h+h+h
cb2 = s+s+s+s
cb3 = s+s+s+s
cb4 = h+h+h+h
cc = i+i+i+i
return cb1+cb2+cb3+cb4+cc
s_oldshadow.__noautodoc__ = True
def s_material(self,bg,o,i,s,h):
if self.pressed:
i = [max(bg[0]-20,0),max(bg[1]-20,0),max(bg[2]-20,0)]
elif self.is_hovering:
i = [max(bg[0]-10,0),max(bg[1]-10,0),max(bg[2]-10,0)]
cb1 = s+s+o+o
cb2 = s+o+o+s
cb3 = o+o+s+s
cb4 = o+s+s+o
cc = i+i+i+i
return cb1+cb2+cb3+cb4+cc
s_material.__noautodoc__ = True
stretchColors.__noautodoc__ = True