#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# layout.py
#
# Copyright 2020 notna <notna@apparat.org>
#
# This file is part of peng3d.
#
# peng3d 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 3 of the License, or
# (at your option) any later version.
#
# peng3d 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 peng3d. If not, see <http://www.gnu.org/licenses/>.
#
__all__ = [
"Layout", "GridLayout",
"LayoutCell",
]
import peng3d
from peng3d import util
from peng3d.util import WatchingList
try:
import pyglet
from pyglet.gl import *
except ImportError:
pass # Headless mode
[docs]class Layout(util.ActionDispatcher):
"""
Base Layout class.
This class does not serve any purpose directly other than to be a common base class
for all layouts.
Note that layouts can be nested, e.g. usually the first layouts parent is a SubMenu
and sub-layouts get a LayoutCell of their parent layout as their parent.
"""
def __init__(self, peng, parent):
self.peng = peng
self.parent = parent
@property
def pos(self):
return self.parent.pos
@property
def size(self):
return self.parent.size
[docs]class GridLayout(Layout):
"""
Grid-based layout helper class.
This class provides a grid-like layout to its sub-widgets. A border between widgets
can be defined. Additionally, all widgets using this layout should automatically scale
with screen size.
"""
def __init__(self, peng, parent, res, border):
super().__init__(peng, parent)
self.res = res
self.bordersize = border
@property
def cell_size(self):
"""
Helper property defining the current size of cells in both x and y axis.
:return: 2-tuple of float
"""
return self.size[0]/self.res[0], self.size[1]/self.res[1]
[docs] def get_cell(self, pos, size, anchor_x="left", anchor_y="bottom", border=1):
"""
Returns a grid cell suitable for use as the ``pos`` parameter of any widget.
The ``size`` parameter of the widget will automatically be overwritten.
:param pos: Grid position, in cell
:param size: Size, in cells
:param anchor_x: either ``left``\\ , ``center`` or ``right``
:param anchor_y: either ``bottom``\\ , ``center`` or ``top``
:return: LayoutCell subclass
"""
return _GridCell(self.peng, self, pos, size, anchor_x, anchor_y, border)
[docs]class LayoutCell(object):
"""
Base Layout Cell.
Not to be used directly. Usually subclasses of this class are returned by layouts.
Instances can be passed to Widgets as the ``pos`` argument. The ``size`` argument will
be automatically overridden.
Note that manually setting ``size`` will override the size set by the layout cell,
though the position will be kept.
"""
@property
def pos(self):
"""
Property accessing the position of the cell.
This usually refers to the bottom-left corner, but may change depending on arguments
passed during creation.
Note that results can be floats.
:return: 2-tuple of ``(x,y)``
"""
raise NotImplementedError("pos property has to be overridden")
@property
def size(self):
"""
Property accessing the size of the cell.
Note that results can be floats.
:return: 2-tuple of ``(width, height)``
"""
raise NotImplementedError("size property has to be overridden")
class DumbLayoutCell(LayoutCell):
"""
Dumb layout cell that behaves like a widget.
Note that this class is not actually widget and should only be used as the ``pos``
argument to a widget or the ``parent`` to another Layout.
It can be used to create, for example, a :py:class:`GridLayout()` over only a portion
of the screen.
Even though setting the :py:attr:`pos` and :py:attr:`size` attributes is possible,
sometimes a redraw cannot be triggered correctly if e.g. the parent is not submenu.
"""
def __init__(self, parent, pos, size):
self.parent = parent
self._pos = pos
self._size = size
@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):
w, h = self.parent.size[:]
r = self._pos(w, h, *self.size)
elif isinstance(self._pos, LayoutCell):
r = self._pos.pos
else:
raise TypeError("Invalid position type")
ox, oy = self.parent.pos
r = r[0] + ox, r[1] + oy
# 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
if hasattr(self.parent, "redraw"):
self.parent.redraw()
@property
def size(self):
"""
Similar to :py:attr:`pos` but for the size instead.
"""
if isinstance(getattr(self, "_pos", None), LayoutCell):
s = self._pos.size
elif isinstance(self._size, list) or isinstance(self._size, tuple):
s = self._size
elif callable(self._size):
w, h = self.parent.size[:]
s = self._size(w, h)
else:
raise TypeError("Invalid size type")
s = s[:]
if s[0] == -1 or s[1] == -1:
raise ValueError("Cannot set size to -1 in DumbLayoutCell")
# Prevents crashes with negative size
s = [max(s[0], 0), max(s[1], 0)]
return WatchingList(s, self._wlredraw_size)
@size.setter
def size(self, value):
self._size = value
if hasattr(self.parent, "redraw"):
self.parent.redraw()
def _wlredraw_pos(self,wl):
self._pos = wl[:]
if hasattr(self.parent, "redraw"):
self.parent.redraw()
def _wlredraw_size(self,wl):
self._size = wl[:]
if hasattr(self.parent, "redraw"):
self.parent.redraw()
class _GridCell(LayoutCell):
def __init__(self, peng, parent, offset, size, anchor_x, anchor_y, border=1):
self.peng = peng
self.parent = parent
self.offset = offset
self._size = size
self.anchor_x = anchor_x
self.anchor_y = anchor_y
self.border = border
@property
def pos(self):
dx, dy = self.parent.bordersize
dx *= self.border
dy *= self.border
px, py = self.parent.pos # Parent position in px
oxc, oyc = self.offset # Offset in cells
csx, csy = self.parent.cell_size # Cell size in px
ox, oy = oxc*csx, oyc*csy # Offset in px
sxc, sxy = self._size # Size in cells
sx, sy = sxc*csx, sxy*csy # Size in px
if self.anchor_x == "left":
x = px+ox+dx/2
elif self.anchor_x == "center":
x = px+ox+sx/2
elif self.anchor_x == "right":
x = px+ox+sx-dx/2
else:
raise ValueError(f"Invalid anchor_x of {self.anchor_x}")
if self.anchor_y == "bottom":
y = py+oy+dy/2
elif self.anchor_y == "center":
y = py+oy+sy/2
elif self.anchor_y == "top":
y = py+oy+sy-dy/2
else:
raise ValueError(f"Invalid anchor_y of {self.anchor_y}")
return x, y
@property
def size(self):
dx, dy = self.parent.bordersize
csx, csy = self.parent.cell_size # Cell size in px
sxc, sxy = self._size # Size in cells
sx, sy = sxc * csx-dx*self.border, sxy * csy-dy*self.border
return sx, sy