Source code for peng3d.gui.slider

#!/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