#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
#  menus.py
#
#  Copyright 2017 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.
#
#

"""
Menus are special submenus that act like modal dialogs.

They include glue code that automatically switches back to the previous submenu
after they are left. Note that this will cuase the :py:meth:`SubMenu.on_enter()`
method to be called again.

Since these menus are internally implemented as submenus, they are specific to their
:py:class:`Menu`\ , which must be active to be able to use the dialog.

Customization
-------------

Menus are customizable via several different means.

If you just want to change the appearance or label of a part of the menu, you can
use keyword arguments while initializing the class.
For example, setting the ``label_main`` argument to the string ``Hello World!`` the
main label or title of the dialog will now display ``Hello World!`` instead of its default
value.
What exact arguments are supported differs from dialog to dialog.

Note that sometimes specific labels are supported, but not used by default.
Just setting these to anything may cause GUI components to be rendered that
should not be there.

It is also possible for most of these values to be set on-the-fly via properties
on the object they belong to.

For example, the :py:attr:`DialogSubMenu.label_main` property may be set to change
the main label even while the dialog is active.

Note that the values accessible via keyword arguments and properties may differ.
This depends on the dialog implementing them.

For clarity, these keyword arguments will from now on be called "labels". This also
includes labels that are not strictly text, like the maximum value of a progressbar.
"""

__all__ = [
    "DialogSubMenu",
    "ConfirmSubMenu","TextSubMenu",
    "ProgressSubMenu","AdvancedProgressSubMenu",
    ]

import pyglet

from . import SubMenu
from . import text
from . import button
from . import slider

class DialogSubMenu(SubMenu):
    """
    Base Dialog Class.

    This class acts as a base class for all other dialog submenus.

    When the dialog is entered, the :py:attr:`prev_submenu` attribute will be set
    to the name of the previous submenu. This attribute is later used when exiting
    the dialog.

    Dialog submenus also support the basic actions used by all submenus, e.g.
    ``enter`` and ``exit``\ . Additionally, many dialogs also add actions for whenever
    a label is changed or the dialog is exited through a special means, e.g. pressing
    a specific button of multiple presented.

    If used by itself, it will display a text centered on the screen with a button
    below it. Clicking the button will cause the dialog to exit and also the
    additional ``click_ok`` action to be fired.

    The labels supported by default are ``label_main``\ , which defaults to ``Default Text``
    and is recommended to always be customized, and ``label_ok``\, which defaults to ``OK``
    and may be left as-is.

    Subclasses may override these defaults by setting the keys of the same name in the
    ``DEFAULT_LABELS`` class attribute. Note that any unchanged labels must also be declared
    when overwriting any labels, or they may not be displayed.

    Widgets and their initializers are stored in the :py:data:`WIDGETS` class attribute,
    see :py:meth:`add_widgets()` for more information.
    """
    DEFAULT_LABELS = {
        "label_main":"Default Text",
        "label_ok":"OK",
        }
    WIDGETS = {
        "label_main":"add_label_main",
        "label_ok":"add_btn_ok",
    }
    def __init__(self,name,menu,window,peng,
                borderstyle="oldshadow",
                **kwargs # for label_main etc.
                ):
        super(DialogSubMenu,self).__init__(name,menu,window,peng)

        self.prev_submenu = None
        self.borderstyle = borderstyle

        labels = {}
        labels.update(self.DEFAULT_LABELS)
        labels.update(kwargs)
        self.labels = kwargs
        self.kwargs = kwargs

        self.add_widgets(**labels)

    def add_widgets(self,**kwargs):
        """
        Called by the initializer to add all widgets.

        Widgets are discovered by searching through the :py:attr:`WIDGETS` class attribute.
        If a key in :py:attr:`WIDGETS` is also found in the keyword arguments and
        not none, the function with the name given in the value of the key will
        be called with its only argument being the value of the keyword argument.

        For more complex usage scenarios, it is also possible to override this method
        in a subclass, but the original method should always be called to ensure
        compatibility with classes relying on this feature.
        """
        for name,fname in self.WIDGETS.items():
            if name in kwargs and kwargs[name] is not None:
                assert hasattr(self,fname)
                assert callable(getattr(self,fname))
                getattr(self,fname)(kwargs[name])

    def add_label_main(self,label_main):
        """
        Adds the main label of the dialog.

        This widget can be triggered by setting the label ``label_main`` to a string.

        This widget will be centered on the screen.
        """
        # Main Label
        self.wlabel_main = text.Label("label_main",self,self.window,self.peng,
                        pos=lambda sw,sh, bw,bh: (sw/2-bw/2,sh/2-bh/2),
                        size=[0,0],
                        label=label_main,
                        #multiline=True, # TODO: implement multine dialog
                        )
        self.wlabel_main.size = lambda sw,sh: (sw,self.wlabel_main._label.font_size)
        self.addWidget(self.wlabel_main)

    def add_btn_ok(self,label_ok):
        """
        Adds an OK button to allow the user to exit the dialog.

        This widget can be triggered by setting the label ``label_ok`` to a string.

        This widget will be mostly centered on the screen, but below the main label
        by the double of its height.
        """
        # OK Button
        self.wbtn_ok = button.Button("btn_ok",self,self.window,self.peng,
                        pos=lambda sw,sh, bw,bh: (sw/2-bw/2,sh/2-bh/2-bh*2),
                        size=[0,0],
                        label=label_ok,
                        borderstyle=self.borderstyle
                        )
        self.wbtn_ok.size = lambda sw,sh: (self.wbtn_ok._label.font_size*8,self.wbtn_ok._label.font_size*2)
        self.addWidget(self.wbtn_ok)

        def f():
            self.doAction("click_ok")
            self.exitDialog()
        self.wbtn_ok.addAction("click",f)

    @property
    def label_main(self):
        """
        Property that proxies the ``label_main`` label.

        Setting this property will cause the ``label_main_change`` action to trigger.

        Note that trying to access this property if the widget is not used may cause
        an error.
        """
        # no check for initialized label, NameError should be good enough to debug
        return self.wlabel_main.label
    @label_main.setter
    def label_main(self,value):
        self.wlabel_main.label = value
        self.doAction("label_main_change")

    @property
    def label_ok(self):
        """
        Property that proxies the ``label_ok`` label.

        Setting this property will cause the ``label_ok_change`` action to trigger.

        Note that trying to access this property if the widget is not used may cause
        an error.
        """
        return self.wbtn_ok.label
    @label_ok.setter
    def label_ok(self,value):
        self.wbtn_ok.label = value
        self.doAction("label_ok_change")

    def on_enter(self,old):
        if self.menu.activeSubMenu==self.menu:
            raise RuntimeError("Cannot open a dialog twice")
        self.prev_submenu = old # name or None

    def exitDialog(self):
        """
        Helper method that exits the dialog.

        This method will cause the previously active submenu to activate.
        """
        if self.prev_submenu is not None:
            # change back to the previous submenu
            # could in theory form a stack if one dialog opens another
            self.menu.changeSubMenu(self.prev_submenu)
        self.prev_submenu = None

    def activate(self):
        """
        Helper method to enter the dialog.

        Calling this method will simply cause the dialog to become the active submenu.

        Note that is not necessary to call this method over :py:meth:`changeSubMenu()`\ ,
        as the storing of the previous submenu is done elsewhere.
        """
        # error checking done indirectly by on_enter
        # on_enter will be called automatically to store previous submenu
        self.menu.changeSubMenu(self.name)

class ConfirmSubMenu(DialogSubMenu):
    """
    Dialog that allows the user to confirm or cancel an action.

    By default, the OK button will be hidden and the ``label_main`` will be set
    to ``Are you sure?``\ .

    Clicking the confirm button will cause the ``confirm`` action to trigger, while
    the cancel button will cause the ``cancel`` action to trigger.
    """
    DEFAULT_LABELS = {
        "label_main":"Are you sure?",
        "label_confirm":"Confirm",
        "label_cancel":"Cancel",
        }
    WIDGETS = {
        **DialogSubMenu.WIDGETS,
        "label_confirm":"add_btn_confirm",
        "label_cancel":"add_btn_cancel",
        }

    def add_btn_confirm(self,label_confirm):
        """
        Adds a confirm button to let the user confirm whatever action they were presented with.

        This widget can be triggered by setting the label ``label_confirm`` to a string.

        This widget will be positioned slightly below the main label and to the left
        of the cancel button.
        """
        # Confirm Button
        self.wbtn_confirm = button.Button("btn_confirm",self,self.window,self.peng,
                        pos=lambda sw,sh, bw,bh: (sw/2-bw-4,sh/2-bh/2-bh*2),
                        size=[0,0],
                        label=label_confirm,
                        borderstyle=self.borderstyle
                        )
        self.wbtn_confirm.size = lambda sw,sh: (self.wbtn_confirm._label.font_size*8,self.wbtn_confirm._label.font_size*2)
        self.addWidget(self.wbtn_confirm)

        def f():
            self.doAction("confirm")
            self.exitDialog()
        self.wbtn_confirm.addAction("click",f)

    def add_btn_cancel(self,label_cancel):
        """
        Adds a cancel button to let the user cancel whatever choice they were given.

        This widget can be triggered by setting the label ``label_cancel`` to a string.

        This widget will be positioned slightly below the main label and to the right
        of the confirm button.
        """
        # Cancel Button
        self.wbtn_cancel = button.Button("btn_cancel",self,self.window,self.peng,
                        pos=lambda sw,sh, bw,bh: (sw/2+4,sh/2-bh/2-bh*2),
                        size=[0,0],
                        label=label_cancel,
                        borderstyle=self.borderstyle
                        )
        self.wbtn_cancel.size = lambda sw,sh: (self.wbtn_cancel._label.font_size*8,self.wbtn_cancel._label.font_size*2)
        self.addWidget(self.wbtn_cancel)

        def f():
            self.doAction("cancel")
            self.exitDialog()
        self.wbtn_cancel.addAction("click",f)

    @property
    def label_confirm(self):
        """
        Property that proxies the ``label_confirm`` label.

        Setting this property will cause the ``label_confirm_change`` action to trigger.

        Note that trying to access this property if the widget is not used may cause
        an error.
        """
        return self.wbtn_confirm.label
    @label_confirm.setter
    def label_confirm(self,value):
        self.wbtn_confirm.label = value
        self.doAction("label_confirm_change")

    @property
    def label_cancel(self):
        """
        Property that proxies the ``label_cancel`` label.

        Setting this property will cause the ``label_cancel_change`` action to trigger.

        Note that trying to access this property if the widget is not used may cause
        an error.
        """
        return self.wbtn_cancel.label
    @label_cancel.setter
    def label_cancel(self,value):
        self.wbtn_cancel.label = value
        self.doAction("label_cancel_change")

class TextSubMenu(DialogSubMenu):
    """
    Dialog without user interaction that can automatically exit after a certain amount of time.

    This dialog accepts the ``timeout`` keyword argument, which may be set to any
    time in seconds to delay before exiting the dialog. A value of ``-1`` will cause
    the dialog to never exit on its own.

    Note that the user will not be able to exit this dialog and may believe the program
    is hanging if not assured otherwise. It is thus recommended to use the :py:class:`ProgressSubMenu`
    dialog instead, especially for long-running operations.
    """
    DEFAULT_LABELS = {
        "label_main":"Default Text",
        # no button needed, timer does the rest
        }
    def __init__(self,name,menu,window,peng,
                borderstyle="oldshadow",
                timeout=10,
                **kwargs
                ):
        super(TextSubMenu,self).__init__(name,menu,window,peng,borderstyle,**kwargs)
        self.timeout = timeout

    def on_enter(self,old):
        super(TextSubMenu,self).on_enter(old)

        if self.timeout!=-1:
            pyglet.clock.schedule_once(lambda dt:self.exitDialog(),self.timeout)

class ProgressSubMenu(DialogSubMenu):
    """
    Dialog without user interaction displaying a progressbar.

    By default, the progressbar will range from 0-100, effectively a percentage.

    The :py:attr:`auto_exit` attribute may be set to control whether or not the dialog
    will exit automatically when the maximum value is reached.
    """
    DEFAULT_LABELS = {
        "label_main":"Loading...",
        "label_progressbar":"{percent:.1}%",
        # TODO: actually implement the progress_* labels
        "progress_n":0, # should be updated on-the-fly through property progress_n
        "progress_nmin":0,
        "progress_nmax":100, # basically equal to percentages
        }
    WIDGETS = {
        **DialogSubMenu.WIDGETS,
        "label_progressbar":"add_progressbar",
        }
    auto_exit = False
    """
    Controls whether or not the dialog will exit automatically after the maximum
    value has been reached.
    """

    def add_progressbar(self,label_progressbar):
        """
        Adds a progressbar and label displaying the progress within a certain task.

        This widget can be triggered by setting the label ``label_progressbar`` to
        a string.

        The progressbar will be displayed centered and below the main label.
        The progress label will be displayed within the progressbar.

        The label of the progressbar may be a string containing formatting codes
        which will be resolved via the ``format()`` method.

        Currently, there are six keys available:

        ``n`` and ``value`` are the current progress rounded to 4 decimal places.

        ``nmin`` is the minimum progress value rounded to 4 decimal places.

        ``nmax`` is the maximum progress value rounded to 4 decimal places.

        ``p`` and ``percent`` are the percentage value that the progressbar is completed
        rounded to 4 decimal places.

        By default, the progressbar label will be ``{percent}%`` displaying the percentage
        the progressbar is complete.
        """
        # Progressbar
        self.wprogressbar = slider.Progressbar("progressbar",self,self.window,self.peng,
                        pos=lambda sw,sh, bw,bh: (sw/2-bw/2,self.wlabel_main.pos[1]-bh*1.5),
                        size=[0,0],
                        #label=label_progressbar # TODO: add label
                        borderstyle=self.borderstyle
                        )
        self.addWidget(self.wprogressbar)

        # Progress Label
        self.wprogresslabel = text.Label("progresslabel",self,self.window,self.peng,
                        pos=lambda sw,sh, bw,bh: (sw/2-bw/2,self.wprogressbar.pos[1]+8),
                        size=[0,0],
                        label="", # set by update_progressbar()
                        )
        self.wprogresslabel.size = lambda sw,sh: (sw,self.wprogresslabel._label.font_size)
        self.addWidget(self.wprogresslabel)

        self.wprogressbar.size = lambda sw,sh: (sw*0.8,self.wprogresslabel._label.font_size+10)

        self._label_progressbar = label_progressbar

        if getattr(label_progressbar,"_dynamic",False):
            def f():
                self.label_progressbar = str(label_progressbar)
            self.peng.i18n.addAction("setlang",f)

        self.wprogressbar.addAction("progresschange",self.update_progressbar)

        self.update_progressbar()

    def update_progressbar(self):
        """
        Updates the progressbar by re-calculating the label.

        It is not required to manually call this method since setting any of the
        properties of this class will automatically trigger a re-calculation.
        """
        n,nmin,nmax = self.wprogressbar.n,self.wprogressbar.nmin,self.wprogressbar.nmax
        if (nmax-nmin)==0:
            percent = 0 # prevents ZeroDivisionError
        else:
            percent = max(min((n-nmin)/(nmax-nmin),1.),0.)*100
        dat = {"value":round(n,4),"n":round(n,4),"nmin":round(nmin,4),"nmax":round(nmax,4),"percent":round(percent,4),"p":round(percent,4)}
        txt = self._label_progressbar.format(**dat)
        self.wprogresslabel.label = txt

    @property
    def progress_n(self):
        """
        Property that proxies the ``progress_n`` label.

        Setting this property will cause the progressbar label to be recalculated.

        Additionally, if the supplied value is higher than the maximum value and
        :py:attr:`auto_exit` is true, the dialog will exit.
        """
        return self.wprogressbar.n
    @progress_n.setter
    def progress_n(self,value):
        self.wprogressbar.n = value
        self.update_progressbar()
        if self.auto_exit:
            if self.wprogressbar.n>=self.wprogressbar.nmax:
                self.exitDialog()

    @property
    def progress_nmin(self):
        """
        Property that proxies the ``progress_nmin`` label.

        Setting this property will cause the progressbar label to be recalculated.

        Note that setting this property if the widget has not been initialized may
        cause various errors to occur.
        """
        return self.wprogressbar.nmin
    @progress_nmin.setter
    def progress_nmin(self,value):
        self.wprogressbar.nmin = value
        self.update_progressbar()

    @property
    def progress_nmax(self):
        """
        Property that proxies the ``progress_nmax`` label.

        Setting this property will cause the progressbar label to be recalculated.

        Note that setting this property if the widget has not been initialized may
        cause various errors to occur.
        """
        return self.wprogressbar.nmax
    @progress_nmax.setter
    def progress_nmax(self,value):
        self.wprogressbar.nmax = value
        self.update_progressbar()

    @property
    def label_progressbar(self):
        """
        Property that proxies the ``label_progressbar`` label.

        Setting this property will cause the progressbar label to be recalculated.

        Note that setting this property if the widget has not been initialized may
        cause various errors to occur.
        """
        return self.wprogresslabel.label
    @label_progressbar.setter
    def label_progressbar(self,value):
        self._label_progressbar = value
        self.update_progressbar()

class AdvancedProgressSubMenu(ProgressSubMenu):
    def add_progressbar(self,label_progressbar):
        """
        Adds a progressbar and label displaying the progress within a certain task.

        This widget can be triggered by setting the label ``label_progressbar`` to
        a string.

        The progressbar will be displayed centered and below the main label.
        The progress label will be displayed within the progressbar.

        The label of the progressbar may be a string containing formatting codes
        which will be resolved via the ``format()`` method.

        Currently, there are six keys available:

        ``n`` and ``value`` are the current progress rounded to 4 decimal places.

        ``nmin`` is the minimum progress value rounded to 4 decimal places.

        ``nmax`` is the maximum progress value rounded to 4 decimal places.

        ``p`` and ``percent`` are the percentage value that the progressbar is completed
        rounded to 4 decimal places.

        By default, the progressbar label will be ``{percent}%`` displaying the percentage
        the progressbar is complete.
        """
        # Progressbar
        self.wprogressbar = slider.AdvancedProgressbar("progressbar",self,self.window,self.peng,
                        pos=lambda sw,sh, bw,bh: (sw/2-bw/2,self.wlabel_main.pos[1]-bh*1.5),
                        size=[0,0],
                        #label=label_progressbar # TODO: add label
                        borderstyle=self.borderstyle
                        )
        self.addWidget(self.wprogressbar)

        # Progress Label
        self.wprogresslabel = text.Label("progresslabel",self,self.window,self.peng,
                        pos=lambda sw,sh, bw,bh: (sw/2-bw/2,self.wprogressbar.pos[1]+8),
                        size=[0,0],
                        label="", # set by update_progressbar()
                        )
        self.wprogresslabel.size = lambda sw,sh: (sw,self.wprogresslabel._label.font_size)
        self.addWidget(self.wprogresslabel)

        self.wprogressbar.size = lambda sw,sh: (sw*0.8,self.wprogresslabel._label.font_size+10)

        self._label_progressbar = label_progressbar

        if getattr(label_progressbar,"_dynamic",False):
            def f():
                self.label_progressbar = str(label_progressbar)
            self.peng.i18n.addAction("setlang",f)

        self.wprogressbar.addAction("progresschange",self.update_progressbar)

        self.update_progressbar()

    def addCategory(self,*args,**kwargs):
        """
        Proxy for :py:meth:`~peng3d.gui.slider.AdvancedProgressbar.addCategory()`\ .
        """
        return self.wprogressbar.addCategory(*args,**kwargs)
    def updateCategory(self,*args,**kwargs):
        """
        Proxy for :py:meth:`~peng3d.gui.slider.AdvancedProgressbar.updateCategory()`\ .
        """
        return self.wprogressbar.updateCategory(*args,**kwargs)
    def deleteCategory(self,*args,**kwargs):
        """
        Proxy for :py:meth:`~peng3d.gui.slider.AdvancedProgressbar.deleteCategory()`\ .
        """
        return self.wprogressbar.deleteCategory(*args,**kwargs)


#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
#  peng.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__ = ["Peng","HeadlessPeng"]

import sys

import weakref
import inspect

#from . import window, config, keybind, pyglet_patch
from . import config, world, resource, i18n

_pyglet_patched = sys.version_info.major == 2 or not world._have_pyglet

class Peng(object):
    """
    This Class should only be instantiated once per application, if you want to use multiple windows, see :py:meth:`createWindow()`\ .

    An Instance of this class represents the whole Engine, with all accompanying state and window/world objects.

    Be sure to keep your instance accessible, as it will be needed to create most other classes.
    """

    def __init__(self,cfg={}):
        global _pyglet_patched
        if world._have_pyglet:
            from . import pyglet_patch, keybind # Local import for compat with headless machines
        self.window = None

        self.pygletEventHandlers = {}

        self.eventHandlers = {}

        self.events_ignored = {}
        self.event_list = set()

        if cfg == {}:
            cfg = {} # To avoid bugs with default arguments
        self.cfg = config.Config(cfg,defaults=config.DEFAULT_CONFIG)
        if world._have_pyglet:
            self.keybinds = keybind.KeybindHandler(self)

        self.resourceMgr = None
        self.i18n = None

        self.t = lambda *args,**kwargs:self._t(*args,**kwargs)
        self.tl = lambda *args,**kwargs:self._tl(*args,**kwargs)

        self.addEventListener("peng3d:peng.exit",self.handler_exit)

        if not _pyglet_patched and self.cfg["pyglet.patch.patch_float2int"]:
            _pyglet_patched = True
            pyglet_patch.patch_float2int()

    def createWindow(self,cls=None,caption_t=None,*args,**kwargs):
        """
        createWindow(cls=window.PengWindow, *args, **kwargs)

        Creates a new window using the supplied ``cls``\ .

        If ``cls`` is not given, :py:class:`peng3d.window.PengWindow()` will be used.

        Any other positional or keyword arguments are passed to the class constructor.

        Note that this method currently does not support using multiple windows.

        .. todo::

           Implement having multiple windows.
        """
        if cls is None:
            from . import window
            cls = window.PengWindow
        if self.window is not None:
            raise RuntimeError("Window already created!")
        self.sendEvent("peng3d:window.create.pre",{"peng":self,"cls":cls})
        if caption_t is not None:
            kwargs["caption"] = "Peng3d Application"
        self.window = cls(self,*args,**kwargs)
        self.sendEvent("peng3d:window.create.post",{"peng":self,"window":self.window})
        if self.cfg["rsrc.enable"] and self.resourceMgr is None:
            self.sendEvent("peng3d:rsrc.init.pre",{"peng":self,"basepath":self.cfg["rsrc.basepath"]})
            self.resourceMgr = resource.ResourceManager(self,self.cfg["rsrc.basepath"])
            self.rsrcMgr = self.resourceMgr
            self.sendEvent("peng3d:rsrc.init.post",{"peng":self,"rsrcMgr":self.resourceMgr})
            if self.cfg["i18n.enable"] and self.i18n is None:
                self.sendEvent("peng3d:i18n.init.pre",{"peng":self})
                self.i18n = i18n.TranslationManager(self)
                self._t = self.i18n.t
                self._tl = self.i18n.tl
                self.sendEvent("peng3d:i18n.init.post",{"peng":self,"i18n":self.i18n})
        if caption_t is not None:
            self.window.set_caption(self.t(caption_t))
            def f():
                self.window.set_caption(self.t(caption_t))
            self.i18n.addAction("setlang",f)
        return self.window

    def run(self,evloop=None):
        """
        Runs the application main loop.

        This method is blocking and needs to be called from the main thread to avoid OpenGL bugs that can occur.

        ``evloop`` may optionally be a subclass of :py:class:`pyglet.app.base.EventLoop` to replace the default event loop.
        """
        self.sendEvent("peng3d:peng.run",{"peng":self,"window":self.window,"evloop":evloop})
        self.window.run(evloop)
        self.sendEvent("peng3d:peng.exit",{"peng":self})

    def sendPygletEvent(self,event_type,args,window=None):
        """
        Handles a pyglet event.

        This method is called by :py:meth:`PengWindow.dispatch_event()` and handles all events.

        See :py:meth:`registerEventHandler()` for how to listen to these events.

        This method should be used to send pyglet events.
        For new code, it is recommended to use :py:meth:`sendEvent()` instead.
        For "tunneling" pyglet events, use event names of the format ``pyglet:<event>``
        and for the data use ``{"args":<args as list>,"window":<window object or none>,"src":<event source>,"event_type":<event type>}``

        Note that you should send pyglet events only via this method, the above event will be sent automatically.

        Do not use this method to send custom events, use :py:meth:`sendEvent` instead.
        """
        args = list(args)

        self.sendEvent("pyglet:%s"%event_type,{"peng":self,"args":args,"window":window,"src":self,"event_type":event_type})
        self.sendEvent("peng3d:pyglet",{"peng":self,"args":args,"window":window,"src":self,"event_type":event_type})

        if event_type not in ["on_draw","on_mouse_motion"] and self.cfg["debug.events.dump"]:
            print("Event %s with args %s"%(event_type,args))
        if event_type in self.pygletEventHandlers:
            for whandler in self.pygletEventHandlers[event_type]:
                # This allows for proper collection of deleted handler methods by using weak references
                handler = whandler()
                if handler is None:
                    del self.pygletEventHandlers[event_type][self.pygletEventHandlers[event_type].index(whandler)]
                handler(*args)
    def addPygletListener(self,event_type,handler):
        """
        Registers an event handler.

        The specified callable handler will be called every time an event with the same ``event_type`` is encountered.

        All event arguments are passed as positional arguments.

        This method should be used to listen for pyglet events.
        For new code, it is recommended to use :py:meth:`addEventListener()` instead.

        See :py:meth:`handleEvent()` for information about tunneled pyglet events.

        For custom events, use :py:meth:`addEventListener()` instead.
        """
        if self.cfg["debug.events.register"]:
            print("Registered Event: %s Handler: %s"%(event_type,handler))
        if event_type not in self.pygletEventHandlers:
            self.pygletEventHandlers[event_type]=[]
        # Only a weak reference is kept
        if inspect.ismethod(handler):
            handler = weakref.WeakMethod(handler)
        else:
            handler = weakref.ref(handler)
        self.pygletEventHandlers[event_type].append(handler)

    # For compatibility
    handleEvent = sendPygletEvent
    registerEventHandler = addPygletListener

    def sendEvent(self,event,data=None):
        """
        Sends an event with attached data.

        ``event`` should be a string of format ``<namespace>:<category1>.<subcategory2>.<name>``\ .
        There may be an arbitrary amount of subcategories. Also note that this
        format is not strictly enforced, but rather recommended by convention.

        ``data`` may be any Python Object, but it usually is a dictionary containing relevant parameters.
        For example, most built-in events use a dictionary containing at least the ``peng`` key set to an instance of this class.

        If there are no handlers for the event, a corresponding message will be printed to the log file.
        To prevent spam, the maximum amount of ignored messages can be configured via :confval:`events.maxignore` and defaults to 3.

        If the config value :confval:`debug.events.dumpfile` is a file path, the event type will be added to an internal list and be saved to the given file during program exit.
        """
        if self.cfg["debug.events.dumpfile"]!="" and event not in self.event_list:
            self.event_list.add(event)
        if event not in self.eventHandlers:
            if event not in self.events_ignored or self.events_ignored[event]<=self.cfg["events.maxignore"]: # Prevents spamming logfile with ignored event messages
                # TODO: write to logfile
                # Needs a logging module first...
                self.events_ignored[event] = self.events_ignored.get(event,0)+1
            return

        for handler in self.eventHandlers[event]:
            f = handler[0]
            try:
                f(event,data)
            except Exception:
                if not handler[1]:
                    raise
                else:
                    # TODO: write to logfile
                    if self.cfg["events.removeonerror"]:
                        self.delEventListener(event,f)

    def addEventListener(self,event,func,raiseErrors=False):
        """
        Adds a handler to the given event.

        A event may have an arbitrary amount of handlers, though assigning too
        many handlers may slow down event processing.

        For the format of ``event``\ , see :py:meth:`sendEvent()`\ .

        ``func`` is the handler which will be executed with two arguments, ``event_type`` and ``data``\ , as supplied to :py:meth:`sendEvent()`\ .

        If ``raiseErrors`` is True, exceptions caused by the handler will be re-raised.
        Defaults to ``False``\ .
        """
        if not isinstance(event,str):
            raise TypeError("Event types must always be strings")

        if event not in self.eventHandlers:
            self.eventHandlers[event]=[]
        self.eventHandlers[event].append([func,raiseErrors])
    def delEventListener(self,event,func):
        """
        Removes the given handler from the given event.

        If the event does not exist, a :py:exc:`NameError` is thrown.

        If the handler has not been registered previously, also a :py:exc:`NameError` will be thrown.
        """
        if event not in self.eventHandlers:
            raise NameError("No handlers exist for event %s"%event)
        if [func,True] in self.eventHandlers[event]:
            del self.eventHandlers[event][self.eventHandlers[event].index(func)]
        elif [func,False] in self.eventHandler[event]:
            del self.eventHandlers[event][self.eventHandlers[event].index(func)]
        else:
            raise NameError("This handler is not registered for event %s"%event)
        if self.eventHandlers[event] == []:
            del self.eventHandlers[event]

    def handler_exit(self,event,data):
        if self.cfg["debug.events.dumpfile"]!="":
            with open(self.cfg["debug.events.dumpfile"],"w") as f:
                f.write("\n".join(sorted(list(self.event_list))))
    handler_exit.__noautodoc__ = True

class HeadlessPeng(object):
    """
    Variant of peng that should work without having pyglet installed.

    This class is intended for use in servers as a drop-in replacement for the normal engine class.

    Note that this class is only in its beginnings and should not be used yet.
    """
    def __init__(self,cfg={}):
        if "rsrc.enable" not in cfg:
            cfg["rsrc.enable"]=False
        super(HeadlessPeng,self).__init__(cfg)


#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
#  i18n.py
#
#  Copyright 2018 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__ = [
    "TranslationManager",
]

import glob
import re

from .util import ActionDispatcher

# TODO: add test cases for translations
class TranslationManager(ActionDispatcher):
    """
    Manages sets of translation files in multiple languages.

    This Translation System uses language codes to identify languages, there is
    no requirement to follow a specific standard, but it is recommended to use
    simple 2-digit codes like ``en`` and ``de``\ , adding an underscore to
    define sub-languages like ``en_gb`` and ``en_us``\ .

    Whenever a new translation file is needed, it will be parsed and then cached.
    This speeds up access times and also practically eliminates load times when
    switching languages.

    Several events are sent by this class, see :ref:`events-i18n`\ .

    Most of these events are also sent as actions, these actions are described
    in the methods that cause them.

    There are also severale config options that determine the behaviour of this class.
    See :ref:`cfg-i18n` for more information.

    This Manager requires the :py:class:`~peng3d.resource.ResourceManager()` to
    be already initialized.
    """
    def __init__(self,peng):
        if not peng.cfg["rsrc.enable"]:
            raise RuntimeError("ResourceManager needs to be enabled to use Translations")
        elif peng.rsrcMgr is None:
            raise RuntimeError("ResourceManager needs to be initialized before TranslationManager")

        self.peng = peng

        self.lang = self.peng.cfg["i18n.lang"]

        self.cache = {} # dict of dicts, first lang, then domain

        self.peng.sendEvent("peng3d:i18n.init",{"lang":self.lang,"i18n":self})
        self.setLang(self.peng.cfg["i18n.lang"])

    def setLang(self,lang):
        """
        Sets the default language for all domains.

        For recommendations regarding the format of the language code, see
        :py:class:`TranslationManager`\ .

        Note that the ``lang`` parameter of both :py:meth:`translate()` and
        :py:meth:`translate_lazy()` will override this setting.

        Also note that the code won't be checked for existence or plausibility.
        This may cause the fallback strings to be displayed instead if the language
        does not exist.

        Calling this method will cause the ``setlang`` action and the
        :peng3d:event`peng3d:i18n.set_lang` event to be triggered. Note that both
        action and event will be triggered even if the language did not actually change.

        This method also automatically updates the :confval:`i18n.lang` config value.
        """
        self.lang = lang
        self.peng.cfg["i18n.lang"] = lang

        if lang not in self.cache:
            self.cache[lang]={}

        self.doAction("setlang")
        self.peng.sendEvent("peng3d:i18n.set_lang",{"lang":self.lang,"i18n":self})

    def discoverLangs(self,domain="*"):
        """
        Generates a list of languages based on files found on disk.

        The optional ``domain`` argument may specify a domain to use when checking
        for files. By default, all domains are checked.

        This internally uses the :py:mod:`glob` built-in module and the
        :confval:`i18n.lang.format` config option to find suitable filenames.
        It then applies the regex in :confval:`i18n.discover_regex` to extract the
        language code.
        """
        rsrc = self.peng.cfg["i18n.lang.format"].format(domain=domain,lang="*")
        pattern = self.peng.rsrcMgr.resourceNameToPath(rsrc,self.peng.cfg["i18n.lang.ext"])
        files = glob.iglob(pattern)

        langs = set()
        r = re.compile(self.peng.cfg["i18n.discover_regex"])

        for f in files:
            m = r.fullmatch(f)
            if m is not None:
                langs.add(m.group("lang"))

        return list(langs)

    def translate(self,key,translate=True,lang=None):
        if lang is None:
            lang = self.lang

        if not translate or key.count(":")!=1:
            return key
        domain,name = key.split(":")

        if domain not in self.cache[lang]:
            self.loadDomain(domain,lang)
        if lang not in self.cache or domain not in self.cache[lang] or name not in self.cache[lang][domain]:
            return key # would just display the key to the user, good enough to be understood
        return self.cache[lang][domain][name]
    t = translate
    def translate_lazy(self,key,translate=True,lang=None):
        return _LazyTranslator(self,key,translate,lang)
    tl = translate_lazy

    def loadDomain(self,domain,lang=None):
        if lang is None:
            lang = self.lang

        if lang not in self.cache:
            self.cache[lang][domain]={} # prevents errors if function aborts prematurely

        rsrc = self.peng.cfg["i18n.lang.format"].format(domain=domain,lang=lang)
        if not self.peng.rsrcMgr.resourceExists(rsrc,self.peng.cfg["i18n.lang.ext"]):
            return # prevents errors
        fname = self.peng.rsrcMgr.resourceNameToPath(rsrc,self.peng.cfg["i18n.lang.ext"])
        try:
            with open(fname,"r") as f:
                data = f.readlines()
        except Exception:
            return # prevents errors

        d = {}
        for line in data:
            line.strip()
            if line=="" or line.startswith("#"):
                continue
            ls = line.split("=")
            k = ls.pop(0) # first ever useful application of pop
            v = "=".join(ls).strip()
            d[k]=v
        self.cache[lang][domain]=d

        self.doAction("loaddomain")

    def __getitem__(self,key):
        return self.translate(key)

class _LazyTranslator(object):
    _dynamic=True
    def __init__(self,i18n,key,translate=True,lang=None):
        self.i18n = i18n
        self.key = key
        self.translate = translate
        self.lang = lang
    def __str__(self):
        return self.i18n.translate(self.key,self.translate,self.lang)
    def __repr__(self):
        return self.i18n.translate(self.key,self.translate,self.lang)
    def __mod__(self,other):
        try:
            return str(self)%other
        except Exception:
            return str(self)
    def format(self,*args,**kwargs):
        try:
            return str(self).format(*args,**kwargs)
        except Exception:
            return str(self)


#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
#  world.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__ = ["World","StaticWorld",
           "WorldView", "WorldViewMouseRotatable"]

import weakref
import inspect

from .camera import Camera

try:
    import pyglet
    from pyglet.gl import *
    from pyglet.window import key
    _have_pyglet = True
except ImportError:
    _have_pyglet = False

class World(object):
    """
    World containing terrain, actors, cameras and views.

    See the docs about :py:class:`Camera()`\ , :py:class:`WorldView()`\ , :py:class:`Actor()` for more information about each class.

    This class does not draw anything, see :py:class:`StaticWorld()` for drawing simple terrain.
    """
    def __init__(self,peng):
        self.peng = peng
        self.cameras = {}
        self.actors = {}
        self.views = {}

        self.eventHandlers = {}
        self.recvEvents = True

    def addCamera(self,camera):
        """
        Add the camera to the internal registry.

        Each camera name must be unique, or else only the most recent version will be used. This behavior should not be relied on because some objects may cache objects.

        Additionally, only instances of :py:class:`Camera() <peng3d.camera.Camera>` may be used, everything else raises a :py:exc:`TypeError`\ .
        """
        if not isinstance(camera,Camera):
            raise TypeError("camera is not of type Camera!")
        self.cameras[camera.name]=camera
    def addView(self,view):
        """
        Adds the supplied :py:class:`WorldView()` object to the internal registry.

        The same restrictions as for cameras apply, e.g. no duplicate names.

        Additionally, only instances of :py:class:`WorldView()` may be used, everything else raises a :py:exc:`TypeError`\ .
        """
        if not isinstance(view,WorldView):
            raise TypeError("view is not of type WorldView!")
        self.views[view.name]=view
    def addActor(self,actor):
        """
        Adds the given actor to the internal registry.

        Note that this actors :py:attr:`uuid` attribute must be unique, else it will override any actors previously registered with its UUID.
        """
        self.actors[actor.uuid]=actor

    def getView(self,name):
        """
        Returns the view with name ``name``\ .

        Raises a :py:exc:`ValueError` if the view does not exist.
        """
        if name not in self.views:
            raise ValueError("Unknown world view")
        return self.views[name]

    def render3d(self,view=None):
        """
        Renders the world in 3d-mode.

        If you want to render custom terrain, you may override this method. Be careful that you still call the original method or else actors may not be rendered.
        """
        for actor in self.actors.values():
            actor.render(view)

    # Event Handlers
    def handle_event(self,event_type,args,window=None):
        if not self.recvEvents:
            if self.peng.cfg["debug.events.dump"]:
                print("World skyps event type %s"%event_type)
            return
        args = list(args)
        #if window is not None:
        #    args.append(window)
        if event_type in self.eventHandlers:
            for whandler in self.eventHandlers[event_type]:
                # This allows for proper collection of deleted handler methods by using weak references
                handler = whandler()
                if handler is None:
                    del self.eventHandlers[event_type][self.eventHandlers[event_type].index(whandler)]
                handler(*args)
    handle_event.__noautodoc__ = True
    def registerEventHandler(self,event_type,handler):
        if self.peng.cfg["debug.events.register"]:
            print("Registered Event: %s Handler: %s"%(event_type,handler))
        if event_type not in self.eventHandlers:
            self.eventHandlers[event_type]=[]
        # Only a weak reference is kept
        if inspect.ismethod(handler):
            handler = weakref.WeakMethod(handler)
        else:
            handler = weakref.ref(handler)
        self.eventHandlers[event_type].append(handler)

class StaticWorld(World):
    """
    Subclass of :py:class:`StaticWorld()`\ , allows for semi-static terrain to be rendered.

    This class is not suitable for highly complex or user-modifiable terrain.

    ``quads`` is a list of 3d vertices, e.g. a single quad may be ``[-1,-1,-1, 1,-1,-1, 1,-1,1, -1,-1,1]``\ , which represents a rectangle of size 2x2 centered around 0,0.
    It should also be noted that all quads have to be in a single list.

    ``colors`` is a list of RGB Colors in a similar format to ``quads`` but with colors instead. Note that there must be a color for every vertex in the vertex list.
    Every color is an integer between 0 and 255 using the internal pyglet scheme ``c3B/static``\ .

    You can modify the terrain via the ``terrain`` attribute, note that it is a pyglet vertex list, and not a python list.
    """
    def __init__(self,peng,quads,colors):
        super(StaticWorld,self).__init__(peng)
        assert _have_pyglet
        self.batch3d = pyglet.graphics.Batch()
        self.terrain = self.batch3d.add(len(quads)/3, GL_QUADS, None,
            ("v3f/static",quads),
            ("c3B/static",colors),
            )
    def render3d(self,view=None):
        """
        Renders the world.
        """
        super(StaticWorld,self).render3d(view)
        self.batch3d.draw()

class WorldView(object):
    """
    Object representing a view on the world.

    A :py:class:`WorldView()` object references a camera and has a name.

    ``cam`` is a valid camera name known to the world object supplied.
    """
    def __init__(self,world,name,cam):
        self.world = world
        self.name = name
        self.activeCamera = cam
        self.cam.on_activate(None)

    def setActiveCamera(self,name):
        """
        Sets the active camera.

        This method also calls the :py:meth:`Camera.on_activate() <peng3d.camera.Camera.on_activate>` event handler if the camera is not already active.
        """
        if name == self.activeCamera:
            return # Cam is already active
        if name not in self.world.cameras:
            raise ValueError("Unknown camera name")
        old = self.activeCamera
        self.activeCamera = name
        self.cam.on_activate(old)

    # Event handlers

    def on_menu_enter(self,old):
        """
        Fake event handler called by :py:meth:`Layer.on_menu_enter() <peng3d.layer.Layer.on_menu_enter>` when the containing menu is entered.
        """
        self.world.peng.window.push_handlers(self)
    def on_menu_exit(self,new):
        """
        Fake event handler, same as :py:meth:`on_menu_enter()` but for exiting menus instead.
        """
        self.world.peng.window.pop_handlers()

    # Proxy for self.cameras[self.activeCamera]
    @property
    def cam(self):
        """
        Property for getting the currently active camera.

        Always equals ``self.cameras[self.activeCamera]``\ .
        """
        return self.world.cameras[self.activeCamera]

    # Proxy for self.cam.rot
    @property
    def rot(self):
        """
        Property for accessing the current rotation of the active camera.

        This property can also be written to.
        """
        return self.world.cameras[self.activeCamera].rot
    @rot.setter
    def rot(self,value):
        if not (isinstance(value,list) or isinstance(value,tuple)):
            raise TypeError("Invalid type for rot value!")
        self.world.cameras[self.activeCamera].rot = value

    # Proxy for self.cam.pos
    @property
    def pos(self):
        """
        Property for accessing the current position of the active camera.

        The value of this property will always be a list of length 3.

        This property can also be written to.
        """
        return self.world.cameras[self.activeCamera].pos
    @pos.setter
    def pos(self,value):
        if not (isinstance(value,list) or isinstance(value,tuple)):
            raise TypeError("pos must be list or tuple")
        elif len(value)!=3:
            raise ValueError("pos must be of len 3 not len %s"%len(value))
        self.world.cameras[self.activeCamera].pos = list(value)

class WorldViewMouseRotatable(WorldView):
    """
    Subclass of :py:class:`WorldView()` that is rotatable using the user.

    Moving the mouse cursor left or right will rotate the attached camera horizontally and moving the mouse cursor up or down will rotate the camera vertically.

    By default, each pixel traveled changes the angle in degrees by 0.15, though this can be changed via the :confval:`controls.mouse.sensitivity` config value.
    """
    def on_menu_enter(self,old):
        """
        Fake event handler, same as :py:meth:`WorldView.on_menu_enter()` but forces mouse exclusivity.
        """
        super(WorldViewMouseRotatable,self).on_menu_enter(old)
        self.world.peng.window.toggle_exclusivity(True)
    def on_menu_exit(self,new):
        """
        Fake event handler, same as :py:meth:`WorldView.on_menu_exit()` but force-disables mouse exclusivity.
        """
        super(WorldViewMouseRotatable,self).on_menu_exit(new)
        self.world.peng.window.toggle_exclusivity(False)
    def on_key_press(self,symbol,modifiers):
        """
        Keyboard event handler handling only the escape key.

        If an escape key press is detected, mouse exclusivity is toggled via :py:meth:`PengWindow.toggle_exclusivity()`\ .
        """
        if symbol == key.ESCAPE:
            self.world.peng.window.toggle_exclusivity()
            return pyglet.event.EVENT_HANDLED
    def on_mouse_motion(self, x, y, dx, dy):
        """
        Handles mouse motion and rotates the attached camera accordingly.

        For more information about how to customize mouse movement, see the class documentation here :py:class:`WorldViewMouseRotatable()`\ .
        """
        if not self.world.peng.window.exclusive:
            return
        m = self.world.peng.cfg["controls.mouse.sensitivity"]
        x, y = self.rot
        x, y = x + dx * m, y + dy * m
        y = max(-90, min(90, y))
        x %= 360
        newrot = (x,y)
        self.rot= newrot
    def on_mouse_drag(self,x,y,dx,dy,buttons,modifiers):
        """
        Handler used to still enable mouse movement while a button is pressed.
        """
        self.on_mouse_motion(x,y,dx,dy)


import sys
import time

from contextlib import contextmanager

from .main import EventReceiver

class Recorder(object):
    """Use this class to record the result of running python code as a xunit xml

    It allows you to record a series of steps into a single xunit.xml file.

    """

    def __init__(self, xunit_destination, name, package_name=None):
        self.name = name
        self.package_name = package_name
        self.destination = xunit_destination
        self.event_receiver = None

    def __enter__(self):
        self.event_receiver = EventReceiver()
        return self

    def now_seconds(self):
        return time.time()

    def step(self, step_name):
        """Start a new step. returns a context manager which allows you to
        report an error"""

        @contextmanager
        def step_context(step_name):
            if self.event_receiver.current_case is not None:
                raise Exception('cannot open a step within a step')

            self.event_receiver.begin_case(step_name, self.now_seconds(), self.name)
            try:
                yield self.event_receiver
            except:
                etype, evalue, tb = sys.exc_info()
                self.event_receiver.error('%r' % [etype, evalue, tb])
                raise
            finally:
                self.event_receiver.end_case(step_name, self.now_seconds())

        return step_context(step_name)

    def __exit__(self, *exc_info):
        results = self.event_receiver.results()
        if not results:
            already_throwing = exc_info and exc_info[0] is not None
            if not already_throwing:
                raise ValueError('your hook must at least perform one step!')

        self.destination.write_reports(
            self.name, self.name, results, package_name=self.package_name,
        )


#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
#  resource.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__ = ["ResourceManager"]

import os

try:
    import pyglet
    from pyglet.gl import *
except ImportError:
    pass # Probably headless

try:
    import fastjson as json
except ImportError:
    import json
try:
    from .libs import jsoncomment as _jsoncomment
    json = _jsoncomment.JsonComment(json)
except ImportError:
    # try with a locally installed version, may not be fully compatible but worth a try
    try:
        import jsoncomment as _jsoncomment
        json = _jsoncomment.JsonComment(json)
    except ImportError:
        # not found at all, most features will still work, but comments and extraneous commas in JSON files will not
        pass

from . import model

class ResourceManager(object):
    """
    Manager that allows for efficient and simple loading and management of different kinds of resources.

    Currently supports textures and models out of the box, but extension is possible.

    Textures can be queried by any part of the application, they are only loaded on the first request and then cached for every request following it.

    The same caching and lazy-loading principle applies to models loaded via this system.
    """
    missingtexturename = "peng3d:missingtexture"
    def __init__(self,peng,basepath):
        self.basepath = basepath
        self.peng = peng

        maxsize = GLint()
        glGetIntegerv(GL_MAX_TEXTURE_SIZE,maxsize)
        maxsize = min(maxsize.value,self.peng.cfg["rsrc.maxtexsize"]) # This is here to avoid massive memory overhead when only loading a few textures
        self.texsize = maxsize

        self.categories = {}
        self.categoriesTexCache = {}
        self.categoriesTexBin = {}

        self.missingTexture = None

        self.modelcache = {}
        self.modelobjcache = {}

        self.peng.sendEvent("peng3d:rsrc.init",{"peng":self.peng,"rsrcMgr":self})

    def resourceNameToPath(self,name,ext=""):
        """
        Converts the given resource name to a file path.

        A resource path is of the format ``<app>:<cat1>.<cat2>.<name>`` where cat1 and cat2 can be repeated as often as desired.

        ``ext`` is the file extension to use, e.g. ``.png`` or similar.

        As an example, the resource name ``peng3d:some.category.foo`` with the extension ``.png`` results in the path ``<basepath>/assets/peng3d/some/category/foo.png``\ .

        This resource naming scheme is used by most other methods of this class.

        Note that it is currently not possible to define multiple base paths to search through.
        """
        nsplit = name.split(":")[1].split(".")
        return os.path.join(self.basepath,"assets",name.split(":")[0],*nsplit)+ext
    def resourceExists(self,name,ext=""):
        """
        Returns whether or not the resource with the given name and extension exists.

        This must not mean that the resource is meaningful, it simply signals that the file exists.
        """
        return os.path.exists(self.resourceNameToPath(name,ext))
    def addCategory(self,name):
        """
        Adds a new texture category with the given name.

        If the category already exists, it will be overridden.
        """
        self.categories[name]={}
        self.categoriesTexCache[name]={}
        self.categoriesTexBin[name]=pyglet.image.atlas.TextureBin(self.texsize,self.texsize)
        self.peng.sendEvent("peng3d:rsrc.category.add",{"peng":self.peng,"category":name})
    def getTex(self,name,category):
        """
        Gets the texture associated with the given name and category.

        ``category`` must have been created using :py:meth:`addCategory()` before.

        If it was loaded previously, a cached version will be returned.
        If it was not loaded, it will be loaded and inserted into the cache.

        See :py:meth:`loadTex()` for more information.
        """
        if category not in self.categoriesTexCache:
            return self.getMissingTex(category)
        if name not in self.categoriesTexCache[category]:
            self.loadTex(name,category)
        return self.categoriesTexCache[category][name]
    def loadTex(self,name,category):
        """
        Loads the texture of the given name and category.

        All textures currently must be PNG files, although support for more formats may be added soon.

        If the texture cannot be found, a missing texture will instead be returned. See :py:meth:`getMissingTexture()` for more information.

        Currently, all texture mipmaps will be generated and the filters will be set to
        :py:const:`GL_NEAREST` for the magnification filter and :py:const:`GL_NEAREST_MIPMAP_LINEAR` for the minification filter.
        This results in a pixelated texture and not  a blurry one.
        """
        try:
            img = pyglet.image.load(self.resourceNameToPath(name,".png"))
        except FileNotFoundError:
            img = self.getMissingTexture()
        texreg = self.categoriesTexBin[category].add(img)
        #texreg = texreg.get_transform(True,True) # Mirrors the image due to how pyglets coordinate system works
        # Strange behavior, sometimes needed and sometimes not
        self.categories[category][name]=texreg
        target = texreg.target
        texid = texreg.id
        texcoords = texreg.tex_coords
        # Prevents texture bleeding with texture sizes that are powers of 2, else weird lines may appear at certain angles.
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT)
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT)
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST)
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_LINEAR)
        glGenerateMipmap(GL_TEXTURE_2D)

        out = target,texid,texcoords
        self.categoriesTexCache[category][name]=out
        self.peng.sendEvent("peng3d:rsrc.tex.load",{"peng":self.peng,"name":name,"category":category})
        return out
    def getMissingTexture(self):
        """
        Returns a texture to be used as a placeholder for missing textures.

        A default missing texture file is provided in the assets folder of the source distribution.
        It consists of a simple checkerboard pattern of purple and black, this image may be copied to any project using peng3d for similar behavior.

        If this texture cannot be found, a pattern is created in-memory, simply a solid square of purple.

        This texture will also be cached separately from other textures.
        """
        if self.missingTexture is None:
            if self.resourceExists(self.missingtexturename,".png"):
                self.missingTexture = pyglet.image.load(self.resourceNameToPath(self.missingtexturename,".png"))
                return self.missingTexture
            else: # Falls back to create pattern in-memory
                self.missingTexture = pyglet.image.create(1,1,pyglet.image.SolidColorImagePattern([255,0,255,255]))
                return self.missingTexture
        else:
            return self.missingTexture
    def getMissingTex(self,cat):
        if cat not in self.categories:
            self.addCategory(cat)
        return self.loadTex(self.missingtexturename,cat)
    def addFromTex(self,name,img,category):
        """
        Adds a new texture from the given image.

        ``img`` may be any object that supports Pyglet-style copying in form of the ``blit_to_texture()`` method.

        This can be used to add textures that come from non-file sources, e.g. Render-to-texture.
        """
        texreg = self.categoriesTexBin[category].add(img)
        #texreg = texreg.get_transform(True,True) # Mirrors the image due to how pyglets coordinate system works
        # Strange behaviour, sometimes needed and sometimes not

        self.categories[category][name]=texreg
        target = texreg.target
        texid = texreg.id
        texcoords = texreg.tex_coords

        # Prevents texture bleeding with texture sizes that are powers of 2, else weird lines may appear at certain angles.
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT)
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT)
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST)
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_LINEAR)
        glGenerateMipmap(GL_TEXTURE_2D)

        out = target,texid,texcoords
        self.categoriesTexCache[category][name]=out
        return out

    def getModel(self,name):
        """
        Gets the model object by the given name.

        If it was loaded previously, a cached version will be returned.
        If it was not loaded, it will be loaded and inserted into the cache.
        """
        if name in self.modelobjcache:
            return self.modelobjcache[name]
        return self.loadModel(name)
    def loadModel(self,name):
        """
        Loads the model of the given name.

        The model will also be inserted into the cache.
        """
        m = model.Model(self.peng,self,name)
        self.modelobjcache[name]=m
        self.peng.sendEvent("peng3d:rsrc.model.load",{"peng":self.peng,"name":name})
        return m

    def getModelData(self,name):
        """
        Gets the model data associated with the given name.

        If it was loaded, a cached copy will be returned.
        It it was not loaded, it will be loaded and cached.
        """
        if name in self.modelcache:
            return self.modelcache[name]
        return self.loadModelData(name)
    def loadModelData(self,name):
        """
        Loads the model data of the given name.

        The model file must always be a .json file.
        """
        path = self.resourceNameToPath(name,".json")
        try:
            data = json.load(open(path,"r"))
        except Exception:
            # Temporary
            print("Exception during model load: ")
            import traceback;traceback.print_exc()
            return {}# will probably cause other exceptions later on, TODO

        out = {}

        if data.get("version",1)==1:
            # Currently only one version, basic future-proofing
            # This version should get incremented with breaking changes to the structure

            # Materials
            out["materials"]={}
            for name,matdata in data.get("materials",{}).items():
                m = model.Material(self,name,matdata)
                out["materials"][name]=m
            out["default_material"]=out["materials"][data.get("default_material",list(out["materials"].keys())[0])]

            # Bones
            out["bones"]={"__root__":model.RootBone(self,"__root__",{"start_rot":[0,0],"length":0})}
            for name,bonedata in data.get("bones",{}).items():
                b = model.Bone(self,name,bonedata)
                out["bones"][name]=b
            for name,bone in out["bones"].items():
                if name == "__root__":
                    continue
                bone.setParent(out["bones"][bone.bonedata["parent"]])

            # Regions
            out["regions"]={}
            for name,regdata in data.get("regions",{}).items():
                r = model.Region(self,name,regdata)
                r.material = out["materials"][regdata.get("material",out["default_material"])]
                r.bone = out["bones"][regdata.get("bone","__root__")]
                out["bones"][regdata.get("bone","__root__")].addRegion(r)
                out["regions"][name]=r

            # Animations
            out["animations"]={}
            out["animations"]["static"]=model.Animation(self,"static",{"type":"static","bones":{}})
            for name,anidata in data.get("animations",{}).items():
                a = model.Animation(self,name,anidata)
                a.setBones(out["bones"])
                out["animations"][name]=a
            out["default_animation"]=out["animations"][data.get("default_animation",out["animations"]["static"])]
        else:
            raise ValueError("Unknown version %s of model '%s'"%(data.get("version",1),name))

        self.modelcache[name]=out
        return out


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

import pyglet
from pyglet.gl import *

from .widgets import Widget, Background, _WatchingList
from .slider import VerticalSlider
from .button import ButtonBackground
from ..layer import Layer

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

    def getColors(self):
        bg,o,i,s,h = super(ContainerButtonBackground,self).getColors()
        i = bg

        return bg,o,i,s,h
    getColors.__noautodoc__ = getColors

    def bs_oldshadow(self,bg,o,i,s,h):
        if self.change_on_press and self.widget.pressed:
            i = s
            s,h = h,s
        elif self.change_on_press and self.widget.is_hovering:
            i = [min(i[0]+6,255),min(i[1]+6,255),min(i[2]+6,255)]
            s = [min(s[0]+6,255),min(s[1]+6,255),min(s[2]+6,255)]
        cb1 = s+s+s+s
        cb2 = h+h+h+h
        cb3 = h+h+h+h
        cb4 = s+s+s+s
        cc  = i+i+i+i

        return cb1+cb2+cb3+cb4+cc
    bs_oldshadow.__noautodoc__ = getColors

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`\ .
    """
    def __init__(self,name,submenu,window,peng,
                 pos=None,size=None,_skip_draw=False
                ):
        super(Container,self).__init__(name,submenu,window,peng,pos,size)

        self.menu = submenu

        self.widgets = collections.OrderedDict()

        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

    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()

    def addWidget(self,widget):
        """
        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
    def getWidget(self,name):
        """
        Returns the widget with the given name.
        """
        return self.widgets[name]

    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(*self.pos+self.size)

        SubMenu.draw(self)

        if not isinstance(self.submenu,Container):
            glDisable(GL_SCISSOR_TEST)

    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()

    def redraw(self):
        super(Container,self).redraw()

        # Also schedules redraws for sub-widgets
        for widget in self.widgets.values():
            widget.redraw()
    redraw.__noautodoc__ = True

    def on_enter(self,old):
        """
        Dummy method defined for compatibility with :py:class:`peng3d.gui.SubMenu`, simply does nothing.
        """
        pass
    def on_exit(self,new):
        """
        Dummy method defined for compatibility with :py:class:`peng3d.gui.SubMenu`, simply does nothing.
        """
        pass

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,submenu,window,peng,
                 pos=None,size=None,
                 scrollbar_width=12,
                 borderstyle="flat",
                 content_height=100,
                ):
        self.offset_y = 0
        self.content_height = content_height
        super(ScrollableContainer,self).__init__(name,submenu,window,peng,pos,size,_skip_draw=True)
        self._scrollbar = VerticalSlider("__scrollbar_%s"%name,self,self.peng.window,self.peng,
                                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()

    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

# Hack to allow BasicWidget to do isinstance of Container/ScrollableContainer for offset code
from ..gui import widgets
widgets.Container = Container
widgets.ScrollableContainer = ScrollableContainer


#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
#  gui.py
#
#  Copyright 2017 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__ = [
    "mouse_aabb",
    "points2htmlfontsize",
    "ResourceGroup",
    "BlitableTexture",
    ]

import pyglet
from pyglet.gl import *

def mouse_aabb(mpos,size,pos):
    """
    AABB Collision checker that can be used for most axis-aligned collisions.

    Intended for use in widgets to check if the mouse is within the bounds of a particular widget.
    """
    return pos[0]<=mpos[0]<=pos[0]+size[0] and pos[1]<=mpos[1]<=pos[1]+size[1]

def points2htmlfontsize(points):
    """
    Approximate font size converter, converts from Points to HTML ``<font>`` tag font sizes.

    Note that this method is very inaccurate, since there are only seven possible output values that represent at least 25 input values.
    When in doubt, this function always rounds down, e.g. every input value less than eight is converted to HTML size 1.
    """
    # Approximation, always rounds down
    if points<=8:
        return 1
    elif points<=10:
        return 2
    elif points<=12:
        return 3
    elif points<=14:
        return 4
    elif points<=18:
        return 5
    elif points<=24:
        return 6
    else:
        # Inaccurate, represents everything above 24, but should equal 48
        return 7

class ResourceGroup(pyglet.graphics.Group):
    """
    Pyglet Group that represents a Resource as returned by the :py:class:`ResourceManager() <peng3d.resource.ResourceManager>`\ .

    This Group should automatically merge different groups with different resources that are on the same texture atlas.
    """
    def __init__(self,data,parent=None):
        super(ResourceGroup, self).__init__(parent)
        self.data = data

    def set_state(self):
        glEnable(self.data[0])
        glBindTexture(self.data[0], self.data[1])
    set_state.__noautodoc__ = True

    def unset_state(self):
        glDisable(self.data[0])
    unset_state.__noautodoc__ = True

    def __hash__(self):
        return hash((self.data[0], self.data[1], self.parent))

    def __eq__(self, other):
        return (self.__class__ is other.__class__ and
            self.data[0] == other.data[0] and
            self.data[1] == other.data[1] and
            self.parent == other.parent)

    def __repr__(self):
        return '%s(id=%d)' % (self.__class__.__name__, self.data[1])

class BlitableTexture(pyglet.image.Texture):
    """
    Texture subclass allowing :py:class:`pyglet.image.atlas.TextureBin()` to copy a texture to itself.

    This class simply implements the :py:meth:`blit_to_texture()` interface.
    """
    # Basic Texture subclass allowing pyglet.image.atlas.TextureBin to copy a texture to itself
    def blit_to_texture(self,target,level,x,y,z=0):
        glCopyTexSubImage2D(target, level, x,y, 0,0, self.width,self.height)


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

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
    def init_bg(self):
        self.vlist = self.submenu.batch2d.add(24,GL_QUADS,pyglet.graphics.OrderedGroup(1),
            "v2f",
            "c3B",
            )
        self.reg_vlist(self.vlist)
    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

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.
    """
    def __init__(self,name,submenu,window,peng,
                 pos=None,size=None,
                 bg=None,
                 nmin=0,nmax=100,n=0,
                 border=[4,4],
                 borderstyle="flat",
                 colors=[[240,119,70],[240,119,70]],
                 ):
        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

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="flat",
                 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()

    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()
    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")
    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()

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.
    """
    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()
    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

    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

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="flat",
                 nmin=0,nmax=100,n=0,
                 handlesize=[16,24],
                 ):
        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)

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.
    """

    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

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="flat",
                 nmin=0,nmax=100,n=0,
                 handlesize=[24,16],
                 ):
        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