#!/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 .widgets import Background,Widget
from .button import ButtonBackground
basestring = str # for py2 compat, may get dropped at a later release
[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.)
# 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,submenu,window,peng,
pos=None,size=None,
bg=None,
nmin=0,nmax=100,n=0,
border=[4,4],
borderstyle=None,
colors=[[240,119,70],[240,119,70]],
):
borderstyle = borderstyle if borderstyle is not None else submenu.borderstyle
self._nmin = nmin
self._nmax = nmax
self._n = n
if bg is None:
bg = ProgressbarBackground(self,border,borderstyle,colors)
super(Progressbar,self).__init__(name,submenu,window,peng,pos,size,bg)
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
[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,submenu,window,peng,
pos=None,size=None,
bg=None,
categories={},
offset_nmin=0,offset_nmax=0,offset_n=0,
border=[4,4],
borderstyle=None,
colors=[[240,119,70],[240,119,70]],
):
super(AdvancedProgressbar,self).__init__(name,submenu,window,peng,pos,size,bg,offset_nmin,offset_nmax,offset_n,border,borderstyle,colors)
self.categories = 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,basestring) # py2 compat is done at the top
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'"%name)
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,basestring) # py2 compat is done at the top
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,submenu,window,peng,
pos=None, size=[100,24],bg=None,
border=[4,4], borderstyle=None,
nmin=0,nmax=100,n=0,
handlesize=[16,24],
):
borderstyle = borderstyle if borderstyle is not None else submenu.borderstyle
self.handlesize = handlesize
if bg is None:
bg = SliderBackground(self,border,borderstyle)
super(Slider,self).__init__(name,submenu,window,peng,pos,size,bg,nmin,nmax,n)
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))+.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,submenu,window,peng,
pos=None, size=[24,100], bg=None,
border=[4,4], borderstyle=None,
nmin=0,nmax=100,n=0,
handlesize=[24,16],
):
borderstyle = borderstyle if borderstyle is not None else submenu.borderstyle
if bg is None:
bg = VerticalSliderBackground(self,border,borderstyle)
super(VerticalSlider,self).__init__(name,submenu,window,peng,pos,size,bg,border,borderstyle,nmin,nmax,n,handlesize)
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))+.5)+self.nmin
n = min(max(n,self.nmin),self.nmax)
self.n = n