#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# slider.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__ = [
"Progressbar",
"ProgressbarBackground",
"AdvancedProgressbar",
"Slider",
"SliderBackground",
"VerticalSlider",
"VerticalSliderBackground",
]
import pyglet
from pyglet.gl import *
from typing import Optional, Any, TYPE_CHECKING
if TYPE_CHECKING:
from . import SubMenu
import peng3d
from .widgets import Background, Widget, DEFER_BG
from .button import ButtonBackground
from ..util import default, default_property
from ..util.types import *
[docs]class ProgressbarBackground(Background):
"""
Background for the :py:class:`Progressbar` Widget.
This background displays a bar with a border similar to :py:class:`ButtonBackground`\\ .
Note that two colors may be given, one for the left and one for the right.
"""
def __init__(self, widget, border, borderstyle, colors):
super(ProgressbarBackground, self).__init__(widget)
self.border = border
self.borderstyle = borderstyle
self.colors = colors
[docs] def init_bg(self):
self.vlist = self.submenu.batch2d.add(
24,
GL_QUADS,
pyglet.graphics.OrderedGroup(1),
"v2f",
"c3B",
)
self.reg_vlist(self.vlist)
[docs] def redraw_bg(self):
x, y = self.widget.pos
sx, sy = self.widget.size
bx, by = self.border
nmin, nmax, n = self.widget.nmin, self.widget.nmax, float(self.widget.n)
if (nmax - nmin) == 0:
p = 0 # prevents ZeroDivisionError
else:
p = min((n - nmin) / (nmax - nmin), 1.0)
# Outer vertices
# x y
v1 = x, y + sy
v2 = x + sx, y + sy
v3 = x, y
v4 = x + sx, y
# Inner vertices
# x y
v5 = x + bx, y + sy - by
v6 = x + sx - bx, y + sy - by
v7 = x + bx, y + by
v8 = x + sx - bx, y + by
v9 = x + (sx - bx) * p, y + by
v10 = x + (sx - bx) * p, y + sy - by
if p <= 0:
v9, v10 = v7, v5
# 5 Quads, for edges and the center
qb1 = v5 + v6 + v2 + v1
qb2 = v8 + v4 + v2 + v6
qb3 = v3 + v4 + v8 + v7
qb4 = v3 + v7 + v5 + v1
qc1 = v7 + v8 + v6 + v5
qc2 = v7 + v9 + v10 + v5
v = qb1 + qb2 + qb3 + qb4 + qc1 + qc2
self.vlist.vertices = v
bg = (
self.submenu.bg[:3]
if isinstance(self.submenu.bg, list) or isinstance(self.submenu.bg, tuple)
else [242, 241, 240]
)
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),
]
# Outer,Inner,Shadow,Highlight
j, k = self.colors # Other progress color
if self.borderstyle == "flat":
# 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
cc1 = i + i + i + i
cc2 = j + k + k + j
elif self.borderstyle == "gradient":
cb1 = i + i + o + o
cb2 = i + o + o + i
cb3 = o + o + i + i
cb4 = o + i + i + o
cc1 = i + i + i + i
cc2 = j + k + k + j
elif self.borderstyle == "oldshadow":
cb1 = h + h + h + h
cb2 = s + s + s + s
cb3 = s + s + s + s
cb4 = h + h + h + h
cc1 = i + i + i + i
cc2 = j + k + k + j
elif self.borderstyle == "material":
cb1 = s + s + o + o
cb2 = s + o + o + s
cb3 = o + o + s + s
cb4 = o + s + s + o
cc1 = i + i + i + i
cc2 = j + k + k + j
else:
raise ValueError("Invalid Border style")
c = cb1 + cb2 + cb3 + cb4 + cc1 + cc2
self.vlist.colors = c
[docs]class Progressbar(Widget):
"""
Progressbar displaying a progress of any action to the user.
By default, this Widget uses :py:class:`ProgressbarBackground` as its Background class.
The border and borderstyle options are the same as for the :py:class:`peng3d.gui.button.Button` Widget.
The two colors given are for left and right, respectively. This may be used to create gradients.
``nmin``\\ , ``nmax`` and ``n`` represent the minimal value, maximal value and current value, respectively.
Unexpected behavior may occur if the minimal value is bigger then the maximum value.
"""
IS_CLICKABLE = True
def __init__(
self,
name: Optional[str],
submenu: "SubMenu",
window: Any = None,
peng: Any = None,
*,
pos: DynPosition,
size: DynSize = None,
bg=None,
nmin=0,
nmax=100,
n=0,
border=None,
borderstyle=None,
colors=[[240, 119, 70], [240, 119, 70]],
):
super(Progressbar, self).__init__(
name, submenu, window, peng, pos=pos, size=size, bg=default(bg, DEFER_BG)
)
self.borderstyle = borderstyle
self.style.override_if_not_none("border", border)
self._nmin = nmin
self._nmax = nmax
self._n = n
if bg is None:
self.setBackground(
ProgressbarBackground(self, self.style.border, self.borderstyle, colors)
)
self.redraw()
@property
def nmin(self):
"""
Property representing the minimal value of the progressbar. Typically ``0``\\ .
"""
return self._nmin
@nmin.setter
def nmin(self, value):
self._nmin = value
self.redraw()
@property
def nmax(self):
"""
Property representing the maximum value of the progressbar. Typically ``100`` to represent percentages easily.
"""
return self._nmax
@nmax.setter
def nmax(self, value):
self._nmax = value
self.redraw()
@property
def n(self):
"""
Property representing the current value of the progressbar.
Changing this property will activate the ``progresschange`` action.
"""
return self._n
@n.setter
def n(self, value):
value = min(max(value, self.nmin), self.nmax)
if self._n != value:
self.doAction("progresschange")
self._n = value
self.redraw()
@property
def value(self):
"""
Alias to the :py:attr:`n` property.
"""
return self.n
@value.setter
def value(self, value):
# May be a tiny bit slower, but safer if n is changed
self.n = value
borderstyle = default_property("style")
[docs]class AdvancedProgressbar(Progressbar):
"""
Advanced Progressbar displaying the combined progress through multiple actions.
Visually, this widget is identical to :py:class:`Progressbar` with the only difference
being the way the progress percentage is calculated.
The ``offset_nmin``\\ , ``offset_n`` and ``offset_nmax`` parameters are equivalent
to the parameters of the same name minus the ``offset_`` prefix.
``categories`` may be any dictionary mapping category names to 3-tuples of
format ``(nmin,n,nmax)``\\ .
It is possible to read, write and delete categories through the ``widget[cat]`` syntax.
Note however, that modifying categories in-place, e.g. like ``widget[cat][1]=100``\\ ,
requires a manual call to :py:meth:`redraw()`\\ .
When setting the :py:attr:`nmin`\\ , :py:attr:`n` or :py:attr:`nmax` properties, only
an internal offset value will be modified. This may result in otherwise unexpected behavior
if setting e.g. ``n`` to ``nmax`` because the categories may influence the total percentage calculation.
"""
def __init__(
self,
name: Optional[str],
submenu: "SubMenu",
window: Any = None,
peng: Any = None,
*,
pos: DynPosition,
size: DynSize = None,
bg=None,
categories=None,
offset_nmin=0,
offset_nmax=0,
offset_n=0,
border=None,
borderstyle=None,
colors=[[240, 119, 70], [240, 119, 70]],
):
super(AdvancedProgressbar, self).__init__(
name,
submenu,
window,
peng,
pos=pos,
size=size,
bg=bg,
nmin=offset_nmin,
nmax=offset_nmax,
n=offset_n,
border=border,
borderstyle=borderstyle,
colors=colors,
)
self.categories = default(categories, {})
for cname, cdat in self.categories.items():
assert len(cdat) == 3 # nmin,n,nmax
@property
def nmin(self):
return self._nmin + sum(map(lambda cdat: cdat[0], self.categories.values()))
@nmin.setter
def nmin(self, value):
# may confuse users if base_nmax is very low but the categories nmax is high
# may also prevent the expected behavior that if n is set to nmin, p is equal to 0%
self._nmin = value
self.redraw()
@property
def n(self):
return self._n + sum(map(lambda cdat: cdat[1], self.categories.values()))
@n.setter
def n(self, value):
# see nmin for information about some of the implications of this behavior
self._n = value
self.redraw()
@property
def nmax(self):
return self._nmax + sum(map(lambda cdat: cdat[2], self.categories.values()))
@nmax.setter
def nmax(self, value):
# see nmin for information about some of the implications of this behavior
self._nmax = value
self.redraw()
def __getitem__(self, key):
# returns the 3-tuple associated with the category
if key not in self.categories:
raise KeyError("No Category with name '%s'" % key)
return self.categories[key]
# TODO: automatically redraw if list returned here is modified
def __setitem__(self, key, value):
# sets the 3-tuple associated with the category
# mostly used for category creation, since expressions of the form
# widget[category][0]=1 will only use __getitem__ and modify data in-place
assert isinstance(key, str)
assert len(value) == 3 # nmin,n,nmax
self.categories[key] = list(
value
) # conversion to list allows in-place modification if a tuple was passed
self.redraw()
def __delitem__(self, key):
if key not in self.categories:
raise KeyError("No Category with name '%s'" % key)
del self.categories[key]
self.redraw()
[docs] def addCategory(self, name, nmin=0, n=0, nmax=100):
"""
Adds a category with the given name.
If the category already exists, a :py:exc:`KeyError` will be thrown. Use
:py:meth:`updateCategory()` instead if you want to update a category.
"""
assert isinstance(name, str)
if name in self.categories:
raise KeyError("Category with name '%s' already exists" % name)
self.categories[name] = [nmin, n, nmax]
self.redraw()
[docs] def updateCategory(self, name, nmin=None, n=None, nmax=None):
"""
Smartly updates the given category.
Only values that are given will be updated, others will be left unchanged.
If the category does not exist, a :py:exc:`KeyError` will be thrown. Use
:py:meth:`addCategory()` instead if you want to add a category.
"""
# smart update, only stuff that was given
if name not in self.categories:
raise KeyError("No Category with name '%s'" % name)
if nmin is not None:
self.categories[name][0] = nmin
if n is not None:
self.categories[name][1] = n
if nmax is not None:
self.categories[name][2] = nmax
self.redraw()
self.doAction("progresschange")
[docs] def deleteCategory(self, name):
"""
Deletes the category with the given name.
If the category does not exist, a :py:exc:`KeyError` will be thrown.
"""
if name not in self.categories:
raise KeyError("No Category with name '%s'" % name)
del self.categories[name]
self.redraw()
[docs]class SliderBackground(ButtonBackground):
"""
Background for the :py:class:`Slider` Widget.
This background displays a button-like handle on top of a bar representing the selectable range.
All given parameters will affect the handle.
"""
[docs] def init_bg(self):
self.vlist_bg = self.submenu.batch2d.add(
4,
GL_QUADS,
pyglet.graphics.OrderedGroup(0),
"v2f",
"c3B",
)
self.reg_vlist(self.vlist_bg)
super(SliderBackground, self).init_bg()
[docs] def redraw_bg(self):
super(SliderBackground, self).redraw_bg()
sx, sy, x, y, bx, by = super(SliderBackground, self).getPosSize()
# x y
v5 = x + bx, y + sy - by
v6 = x + sx - bx, y + sy - by
v7 = x + bx, y + by
v8 = x + sx - bx, y + by
qbg = v7 + v8 + v6 + v5
bg, _, _, _, _ = self.getColors()
cbg = [min(bg[0] + 8, 255), min(bg[1] + 8, 255), min(bg[2] + 8, 255)] * 4
self.vlist_bg.vertices = qbg
self.vlist_bg.colors = cbg
[docs] def getPosSize(self):
sx, sy = self.widget.handlesize
bx, by = self.border
x, y = (
self.widget.pos[0] + (self.widget.size[0] - bx * 2) * self.widget.p,
self.widget.pos[1],
)
return sx, sy, x, y, bx, by
getPosSize.__noautodoc__ = True
[docs]class Slider(Progressbar):
"""
Slider that can be used to get a number from the user.
By default, this Widget uses :py:class:`SliderBackground` as its Background class.
Most options are the same as for :py:class:`Progressbar`\\ .
``handlesize`` simply determines the size of the handle.
Note that scaling this widget on the y-axis will not do much, scale the handlesize instead.
"""
def __init__(
self,
name: Optional[str],
submenu: "SubMenu",
window: Any = None,
peng: Any = None,
*,
pos: DynPosition,
size: DynSize = None,
bg=None,
border=None,
borderstyle=None,
nmin=0,
nmax=100,
n=0,
handlesize=None,
):
super(Slider, self).__init__(
name,
submenu,
window,
peng,
pos=pos,
size=default(size, [100, 24]),
bg=default(bg, DEFER_BG),
nmin=nmin,
nmax=nmax,
n=n,
border=border,
borderstyle=borderstyle,
)
self.handlesize = default(handlesize, [16, 24])
if bg is None:
self.setBackground(SliderBackground(self, self.style.border, borderstyle))
def on_mouse_drag(self, x, y, dx, dy, button, modifiers):
if not self.pressed:
return
totx = self.size[0] - self.bg.border[0] * 2
x = x - self.pos[0] - self.bg.border[0]
x = min(max(x, 0), totx)
n = int(((x / totx) * (self.nmax - self.nmin)) + 0.5) + self.nmin
n = min(max(n, self.nmin), self.nmax)
self.n = n
@property
def p(self):
"""
Helper property containing the percentage this slider is "filled".
This property is read-only.
"""
return (self.n - self.nmin) / max((self.nmax - self.nmin), 1)
[docs]class VerticalSliderBackground(SliderBackground):
"""
Background for the :py:class:`VerticalSlider` Widget.
This background uses the same technique as :py:class:`SliderBackground`\\ , simply turned by 90 Degrees.
"""
[docs] def getPosSize(self):
sx, sy = self.widget.handlesize
bx, by = self.border
x, y = (
self.widget.pos[0],
self.widget.pos[1] + (self.widget.size[1] - by * 2) * self.widget.p,
)
return sx, sy, x, y, bx, by
getPosSize.__noautodoc__ = True
[docs]class VerticalSlider(Slider):
"""
Vertical slider that can be used as a scrollbar or getting other input.
By default, this Widget uses :py:class:`VerticalSliderBackground` as its Background class.
This widget is essentially the same as :py:class:`Slider`\\ , only vertical.
Note that you may need to flip the x and y values of ``size``\\ , ``handlesize`` and ``border`` compared to :py:class:`Slider`\\ .
"""
def __init__(
self,
name: Optional[str],
submenu: "SubMenu",
window: Any = None,
peng: Any = None,
*,
pos: DynPosition,
size: DynSize = None,
bg=None,
border=None,
borderstyle=None,
**kwargs,
):
super(VerticalSlider, self).__init__(
name,
submenu,
window,
peng,
pos=pos,
size=default(size, [24, 100]),
bg=default(bg, DEFER_BG),
border=border,
borderstyle=borderstyle,
**kwargs,
)
if bg is None:
self.setBackground(
VerticalSliderBackground(self, self.style.border, self.borderstyle)
)
def on_mouse_drag(self, x, y, dx, dy, button, modifiers):
if not self.pressed:
return
toty = self.size[1] - self.bg.border[1] * 2
y = y - self.pos[1] - self.bg.border[1]
y = min(max(y, 0), toty)
n = int(((y / toty) * (self.nmax - self.nmin)) + 0.5) + self.nmin
n = min(max(n, self.nmin), self.nmax)
self.n = n