Source code for peng3d.gui.container
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# container.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__ = ["Container", "ScrollableContainer", "ContainerButtonBackground"]
import collections
from typing import Optional, Any, TYPE_CHECKING
if TYPE_CHECKING:
from . import SubMenu
import peng3d
import pyglet
from pyglet.gl import *
from .widgets import Widget, Background, _WatchingList
from .slider import VerticalSlider
from .button import ButtonBackground
from ..layer import Layer
from ..util.types import *
from ..util import default_property
[docs]class ContainerButtonBackground(ButtonBackground):
"""
Background class used to render the background of containers using a button style.
Mostly identical with :py:class:`ButtonBackground` with added compatibility for containers.
"""
change_on_press = False
[docs] def getColors(self):
bg, o, i, s, h = super(ContainerButtonBackground, self).getColors()
i = bg
return bg, o, i, s, h
getColors.__noautodoc__ = True
[docs]class Container(Widget):
"""
Main class of the container system.
This widget may contain other widgets, limiting the childs to only draw within the defined bounds.
Additionally, the given position will also act as a offset, making the child coordinates relative to the parent.
The :py:attr:`visible` attribute may be set to control whether or not this container is visible.
This Class is a subclass of :py:class:`peng3d.gui.widgets.Widget` but also exhibits part of the API of :py:class:`peng3d.gui.SubMenu`\\ .
"""
IS_CLICKABLE = True
def __init__(
self,
name: Optional[str],
submenu: "SubMenu",
window: Any = None,
peng: Any = None,
*,
pos: DynPosition,
size: DynSize = None,
_skip_draw=False,
font=None,
font_size=None,
font_color=None,
borderstyle=None,
):
self.borderstyle = (
borderstyle if borderstyle is not None else submenu.borderstyle
)
super(Container, self).__init__(name, submenu, window, peng, pos=pos, size=size)
self.font = font
self.font_size = font_size
self.font_color = font_color
self.menu = submenu
self.widgets = collections.OrderedDict()
self.widget_order = {}
self.bg = [242, 241, 240, 255]
self.bg_vlist = pyglet.graphics.vertex_list(
4,
"v2f",
"c4B",
)
self.stencil_vlist = pyglet.graphics.vertex_list(
4,
"v2f",
("c4B", [0, 0, 0, 0] * 4),
)
self.peng.registerEventHandler("on_resize", self.on_resize)
if not _skip_draw:
self.on_resize(*self.submenu.size)
self.batch2d = pyglet.graphics.Batch()
self.visible = True
[docs] def setBackground(self, bg):
"""
Sets the background of the Container.
Similar to :py:meth:`peng3d.gui.SubMenu.setBackground()`\\ , but only effects the region covered by the Container.
"""
self.bg = bg
if isinstance(bg, list) or isinstance(bg, tuple):
if len(bg) == 3 and isinstance(bg, list):
bg.append(255)
self.bg_vlist.colors = bg * 4
elif bg in ["flat", "gradient", "oldshadow", "material"]:
self.bg = ContainerButtonBackground(
self, borderstyle=bg, batch=self.batch2d
)
self.redraw()
def on_resize(self, width, height):
# Stencil/BG Vlist are updated by redraw()
self.redraw()
@property
def clickable(self):
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
and self.visible
)
else:
return self.submenu.clickable and self.enabled and self.visible
@clickable.setter
def clickable(self, value):
self._enabled = value
self.redraw()
borderstyle = default_property("style")
[docs] def addWidget(self, widget, order_key=0):
"""
Adds a widget to this container.
Note that trying to add the Container to itself will be ignored.
"""
if (
self is widget
): # Prevents being able to add the container to itself, causing a recursion loop on redraw
return
self.widgets[widget.name] = widget
if order_key not in self.widget_order:
self.widget_order[order_key] = []
self.widget_order[order_key].append(widget)
[docs] def getWidget(self, name):
"""
Returns the widget with the given name.
"""
return self.widgets[name]
[docs] def draw(self):
"""
Draws the submenu and its background.
Note that this leaves the OpenGL state set to 2d drawing and may modify the scissor settings.
"""
if not self.visible:
# Simple visibility check, has to be tested to see if it works properly
return
if not isinstance(self.submenu, Container):
glEnable(GL_SCISSOR_TEST)
glScissor(*[int(i) for i in self.pos + self.size])
SubMenu.draw(self)
if not isinstance(self.submenu, Container):
glDisable(GL_SCISSOR_TEST)
[docs] def on_redraw(self):
"""
Redraws the background and any child widgets.
"""
x, y = self.pos
sx, sy = self.size
self.bg_vlist.vertices = [x, y, x + sx, y, x + sx, y + sy, x, y + sy]
self.stencil_vlist.vertices = [x, y, x + sx, y, x + sx, y + sy, x, y + sy]
if isinstance(self.bg, Background):
if not self.bg.initialized:
self.bg.init_bg()
self.bg.initialized = True
self.bg.redraw_bg()
[docs] def redraw(self):
super(Container, self).redraw()
# Also schedules redraws for sub-widgets
for widget in self.widgets.values():
widget.redraw()
redraw.__noautodoc__ = True
[docs] def on_enter(self, old):
"""
Dummy method defined for compatibility with :py:class:`peng3d.gui.SubMenu`, simply does nothing.
"""
pass
[docs] def on_exit(self, new):
"""
Dummy method defined for compatibility with :py:class:`peng3d.gui.SubMenu`, simply does nothing.
"""
pass
[docs]class ScrollableContainer(Container):
"""
Subclass of :py:class:`Container` allowing for scrolling its content.
The scrollbar currently is always on the right side and simply consists of a :py:class:`peng3d.gui.slider.VerticalSlider`\\ .
``scrollbar_width`` and ``borderstyle`` will be passed to the scrollbar.
``content_height`` refers to the maximum offset the user can scroll to.
The content height may be changed, but manually calling :py:meth:`redraw()` will be necessary.
"""
def __init__(
self,
name: Optional[str],
submenu: "SubMenu",
window: Any = None,
peng: Any = None,
*,
pos: DynPosition,
size: DynSize = None,
scrollbar_width=12,
font=None,
font_size=None,
font_color=None,
borderstyle=None,
content_height=100,
):
self.offset_y = 0
self.content_height = content_height
super(ScrollableContainer, self).__init__(
name,
submenu,
window,
peng,
pos=pos,
size=size,
borderstyle=borderstyle,
font=font,
font_size=font_size,
font_color=font_color,
_skip_draw=True,
)
self._scrollbar = VerticalSlider(
"__scrollbar_%s" % name,
self,
pos=[0, 0],
size=[24, 0],
borderstyle=borderstyle,
border=[3, 3],
n=self.content_height,
nmax=self.content_height,
)
self._scrollbar._is_scrollbar = True
self._scrollbar.addAction("progresschange", self.redraw)
self.addWidget(self._scrollbar)
self.redraw()
self.peng.registerEventHandler("on_mouse_scroll", self.on_mouse_scroll)
[docs] def on_redraw(self):
"""
Redraws the background and contents, including scrollbar.
This method will also check the scrollbar for any movement and will be automatically called on movement of the slider.
"""
n = self._scrollbar.n
self.offset_y = (
-n
) # Causes the content to move in the opposite direction of the slider
# Size of scrollbar
sx = 24 # Currently constant, TODO: add dynamic sx of scrollbar
sy = self.size[1]
# Pos of scrollbar
x = self.size[0] - sx
y = 0 # Currently constant, TODO: add dynamic y-pos of scrollbar
# Dynamic pos/size may be added via align/lambda/etc.
# Note that the values are written to the _* variant of the attribute to avoid 3 uneccessary redraws
self._scrollbar._size = sx, sy
self._scrollbar._pos = x, y
self._scrollbar._nmax = self.content_height
super(
ScrollableContainer, self
).on_redraw() # Re-draws everything, including child widgets
def on_mouse_scroll(self, x, y, scroll_x, scroll_y):
if not self.is_hovering:
return
dx = scroll_x * self.window.cfg["controls.scroll.mult_x"]
dy = scroll_y * self.window.cfg["controls.scroll.mult_y"]
# TODO: implement x-scrolling
self._scrollbar.n += dy
# Hack to allow BasicWidget to do isinstance of Container/ScrollableContainer for offset code
from ..gui import widgets
widgets.Container = Container
widgets.ScrollableContainer = ScrollableContainer