# $Id: Style.py,v 1.66.2.9 2006/12/10 11:04:33 marcusva Exp $
#
# Copyright (c) 2004-2006, Marcus von Appen
# All rights reserved.
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
#  * Redistributions of source code must retain the above copyright notice,
#    this list of conditions and the following disclaimer.
#  * Redistributions in binary form must reproduce the above copyright notice,
#    this list of conditions and the following disclaimer in the documentation
#    and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.

"""Style class for widgets."""

import os.path, copy
from UserDict import IterableUserDict
from Constants import *
from StyleInformation import StyleInformation

# Import the default theme engine path.
import sys
sys.path.append (DEFAULTDATADIR)
from themes import default

class WidgetStyle (IterableUserDict):
    """WidgetStyle (dict=None) -> WidgetStyle

    A style dictionary that tracks changes of its items.

    The WidgetStyle dictionary class allows one to let a bound method be
    executed, whenever a value of the dictionary changed. Additionally,
    the WidgetStyle class supports recursive changes, so that a
    WidgetStyle object within this WidgetStyle can escalate the value
    change and so on.

    To set up the value change handler you can bind it using the
    set_value_changed() method of the WidgetStyle class:

    style.set_value_changed (callback_method)

    This will cause it and all values of it, which are WidgetStyle
    objects to invoke the callback_method on changes.

    To get the actually set value change handler, the
    get_value_changed() method can be used:

    callback = style.get_value_changed ()
    """
    def __init__ (self, dict=None):
        # Notifier slot.
        self._valuechanged = None
        IterableUserDict.__init__ (self, dict)

    def __copy__ (self):
        """W.__copy__ () -> WidgetStyle

        Creates a shallow copy of the WidgetStyle dictionary.
        """
        newdict = WidgetStyle ()
        keys = self.keys ()
        for k in keys:
            newdict[k] = self[k]
            newdict.set_value_changed (self._valuechanged)
        return newdict
        
    def __deepcopy__(self, memo={}):
        """W.__deepcopy__ (...) -> WidgetStyle.

        Creates a deep copy of the WidgetStyle dictionary.
        """
        newdict = WidgetStyle ()
        keys = self.keys ()
        for k in keys:
            newdict[copy.deepcopy (k, memo)] = copy.deepcopy (self[k], memo)
            newdict.set_value_changed (self._valuechanged)
        memo [id (self)] = newdict
        return newdict
    
    def __setitem__ (self, i, y):
        """W.__setitem__ (i, y) <==> w[i] = y
        """
        IterableUserDict.__setitem__ (self, i, y)
        if isinstance (y, WidgetStyle):
            y.set_value_changed (self._valuechanged)
        if self._valuechanged:
            self._valuechanged ()

    def __delitem__ (self, y):
        """W.__delitem__ (i, y) <==> del w[y]
        """
        IterableUserDict.__delitem__ (self, y)
        if self._valuechanged:
            self._valuechanged ()

    def __repr__ (self):
        """W.__repr__ () <==> repr (W)
        """
        return "WidgetStyle %s" % IterableUserDict.__repr__ (self)
    
    def clear (self, y):
        """W.clear () -> None

        Remove all items from the WidgetStyle dictionary.
        """
        changed = self._valuechanged
        self.set_value_changed (None)
        IterableUserDict.clear (self)
        self.set_value_changed (changed)
        if changed:
            changed ()

    def pop (self, k, d=None):
        """W.pop (k, d=None) -> object

        Remove specified key and return the corresponding value.

        If key is not found, d is returned if given, otherwise KeyError
        is raised.
        """
        changed = IterableUserDict.has_key (self, k)
        v = IterableUserDict.pop (self, k, d)
        if changed and self._valuechanged:
            self._valuechanged ()
        return v

    def popitem (self):
        """W.popitem () -> (k, v)

        Remove and return some (key, value) pair as a 2-tuple

        Raises a KeyError if D is empty.
        """
        v = IterableUserDict.popitem (self)
        if self._valuechanged:
            self._valuechanged ()
        return v

    def setdefault (self, k, d=None):
        """W.setdefault (k,d=None) -> W.get (k, d), also set W[k] = d if k not in W
        """
        changed = not IterableUserDict.has_key (self, k)
        v = IterableUserDict.setdefault (self, k, d)
        if changed and self._valuechanged:
            self._valuechanged ()
        return v

    def update (self, E, **F):
        """W.update (E, **F) -> None

        Update W from E and F.

        for k in E: W[k] = E[k] (if E has keys else: for (k, v) in E:
        W[k] = v) then: for k in F: W[k] = F[k]
        """
        amount = len (self)
        IterableUserDict.update (self, E, **F)
        if self._valuechanged and (len (self) != amount):
            self._valuechanged ()

    def get_value_changed (self):
        """W.get_value_changed (...) -> callable

        Gets the set callback method for the dictionary changes.
        """
        return self._valuechanged
    
    def set_value_changed (self, method):
        """W.set_value_changed (...) -> None

        Connects a method to invoke, when an item of the dict changes.

        Raises a TypeError, if the passed argument is not callable.
        """
        if method and not callable (method):
            raise TypeError ("method must be callable")

        values = self.values ()
        for val in values:
            if isinstance (val, WidgetStyle):
                val.set_value_changed (method)
        self._valuechanged = method

class Style (object):
    """Style () -> Style

    Style class for drawing objects.

    Style definitions
    -----------------
    Styles are used to change the appearance of the widgets. The drawing
    methods of the Style class will use the specific styles of the
    'styles' attribute to change the fore- and background colors, font
    information and border settings of the specific widgets. The styles
    are set using the lower lettered classname of the widget to draw.
    Additionally the Style class supports cascading styles by falling
    back to the next available class name in the widget its __mro__
    list. If no specific set style could be found for the specific
    widget type, the Style class will use the 'default' style entry of
    its 'styles' dictionary.

    Any object can register its own style definition and request it
    using the correct key. A Button widget for example, could register
    its style definition this way:

    Style.styles['button'] = WidgetStyle ({ .... })

    Any other button widget then will use this style by default, so
    their look is all the same.

    It is also possible to pass an own type dictionary to the drawing
    methods of the attached engine class in order to get a surface,
    which can be shown on the screen then:

    own_style = WidgetStyle ({ ... })
    surface = style.engine.draw_rect (width, height, own_style)

    If the style should be based on an already existing one, the
    copy_style() method can be used to retrieve a copy to work with
    without touching the orignal one:

    own_style = style.copy_style (my_button.__class__)

    The BaseWidget class offers a get_style() method, which will copy
    the style of the widget class to the specific widget instance. Thus
    you can safely modify the instance specific style for the widget
    without touching the style for all widgets of that class.

    The style dictionaries registered within the 'styles' dictionary of
    the Style class need to match some prerequisites to be useful. On
    the one hand they need to incorporate specific key-value pairs,
    which can be evaluated by the various drawing functions, on the
    other they need to have some specific key-value pairs, which are
    evaluated by the Style class.

    The following lists give an overview about the requirements the
    style dictionaries have to match, so that the basic Style class can
    work with them as supposed.

    Style entries
    -------------
    The registered style WidgetStyle dictionaries for the widgets need
    to contain key-value pairs required by the functions of the
    referenced modules. The following key-value pairs are needed to
    create the surfaces:

    bgcolor = WidgetStyle ({ STATE_TYPE : color, ... })

    The background color to use for the widget surface.

    fgcolor = WidgetStyle ({ STATE_TYPE : color, ... })

    The foreground color to use for the widget. This is also the text
    color for widgets, which will display text.

    lightcolor = WidgetStyle ({ STATE_TYPE : color, ... })
    darkcolor = WidgetStyle ({ STATE_TYPE : color, ... })

    Used to create shadow border effects on several widgets. The color
    values usually should be a bit brighter or darker than the bgcolor
    values.

    bordercolor = WidgetStyle ({ STATE_TYPE : color, ... })
    
    Also used to create border effects on several widgets. In contrast
    to the lightcolor and darkcolor entries, this is used, if flat
    borders (BORDER_FLAT) have to drawn.

    shadowcolor = (color1, color2)

    The colors to use for dropshadow effects. The first tuple entry will
    be used as inner shadow (near by the widget), the second as outer
    shadow value.

    image = WidgetStyle ({ STATE_TYPE : string })

    Pixmap files to use instead of the background color. If the file is
    not supplied or cannot be loaded, the respective bgcolor value is
    used.
    
    font = WidgetStyle ({ 'name' : string, 'size' : integer,
                          'alias' : integer, 'style' : integer })

    Widgets, which support the display of text, make use of this
    key-value pair. The 'name' key denotes the font name ('Helvetica')
    or the full path to a font file ('/path/to/Helvetica.ttf').  'size'
    is the font size to use. 'alias' is interpreted as boolean value for
    antialiasing. 'style' is a bit-wise combination of FONT_STYLE_TYPES
    values as defined in ocempgui.draw.Constants and denotes additional
    rendering styles to use.

    shadow = integer

    The size of the 3D border effect for a widget.

    IMPORTANT: It is important to know, that the Style class only
    supports a two-level hierarchy for styles. Especially the
    copy_style() method is not aware of style entries of more than two
    levels. This means, that a dictionary in a style dictionary is
    possible (as done in the 'font' or the various color style entries),
    but more complex style encapsulations are unlikely to work
    correctly.
    Some legal examples for user defined style entries:

    style['ownentry'] = 99                        # One level
    style['ownentry'] = { 'foo' : 1, 'bar' : 2 }  # Two levels: level1[level2]

    This one however is not guaranteed to work correctly and thus should
    be avoided:

    style['ownentry'] = { 'foo' : { 'bar' : 1, 'baz' : 2 }, 'foobar' : { ... }}

    Dicts and WidgetStyle
    ---------------------

    OcempGUI uses a WidgetStyle dictionary class to keep track of
    changes within styles and to update widgets on th fly on changing
    those styles. When you are creating new style files (as explained in
    the next section) you do not need to explicitly use the
    WidgetStyle() dictionary, but can use plain dicts instead. Those
    will be automatically replaced by a WidgetStyle on calling load().

    You should however avoid using plain dicts if you are modifying
    styles of widgets or the Style class at run time and use a
    WidgetStyle instead:

    # generate_new_style() returns a plain dict.
    style = generate_new_style ()
    button.style = WidgetStyle (style)

    Style files
    -----------
    A style file is a key-value pair association of style entries for
    widgets. It can be loaded and used by the Style.load() method to set
    specific themes and styles for the widgets. The style files use the
    python syntax and contain key-value pairs of style information for
    the specific widgets. The general syntax looks like follows:

    widgetclassname = WidgetStyle ({ style_entry : { value } })

    An example style file entry for the Button widget class can look
    like the following:

    button = { 'bgcolor' :{ STATE_NORMAL : (200, 100, 0) },
               'fgcolor' : { STATE_NORMAL : (255, 0, 0) },
               'shadow' : 5 }

    The above example will set the bgcolor[STATE_NORMAL] color style
    entry for the button widget class to (200, 100, 0), the
    fgcolor[STATE_NORMAL] color style entry to (255, 0, 0) and the
    'shadow' value for the border size to 5. Any other value of the
    style will remain untouched.

    Loading a style while running an application does not have any
    effect on widgets

    * with own styles set via the BaseWidget.get_style() method,
    * already drawn widgets using the default style.
    
    The latter ones need to be refreshed via the set_dirty()/update()
    methods explicitly to make use of the new style.
    
    Style files allow user-defined variables, which are prefixed with an
    underscore. A specific color thus can be stored in a variable to allow
    easier access of it.
    
    _red = (255, 0, 0)
    ___myown_font = 'foo/bar/font.ttf'

    Examples
    --------
    The OcempGUI module contains a file named 'default.rc' in the themes
    directory of the installation, which contains several style values
    for the default appearance of the widgets. Additional information
    can be found in the manual of OcempGUI, too.

    Attributes:
    styles - A dictionary with the style definitions of various elements.
    engine - The drawing engine, which takes care of drawing elements.
    """

    __slots__ = ["styles", "_engine"]
    
    def __init__ (self):
        # Initialize the default style.
        self.styles = {
            "default" : WidgetStyle ({
            "bgcolor" : WidgetStyle ({ STATE_NORMAL : (234, 228, 223),
                                       STATE_ENTERED : (239, 236, 231),
                                       STATE_ACTIVE : (205, 200, 194),
                                       STATE_INSENSITIVE : (234, 228, 223) }),
            "fgcolor" : WidgetStyle ({ STATE_NORMAL : (0, 0, 0),
                                       STATE_ENTERED : (0, 0, 0),
                                       STATE_ACTIVE : (0, 0, 0),
                                       STATE_INSENSITIVE : (204, 192, 192) }),
            "lightcolor" : WidgetStyle ({ STATE_NORMAL : (245, 245, 245),
                                          STATE_ENTERED : (245, 245, 245),
                                          STATE_ACTIVE : (30, 30, 30),
                                          STATE_INSENSITIVE : (240, 240, 240)
                                          }),
            "darkcolor" : WidgetStyle ({ STATE_NORMAL : (30, 30, 30),
                                         STATE_ENTERED : (30, 30, 30),
                                         STATE_ACTIVE : (245, 245, 245),
                                         STATE_INSENSITIVE : (204, 192, 192)
                                         }),
            "bordercolor" : WidgetStyle ({ STATE_NORMAL : (0, 0, 0),
                                           STATE_ENTERED : (0, 0, 0),
                                           STATE_ACTIVE : (0, 0, 0),
                                           STATE_INSENSITIVE : (204, 192, 192)
                                           }),
            "shadowcolor" : ((100, 100, 100), (130, 130, 130)),
            "image" : WidgetStyle ({ STATE_NORMAL : None,
                                     STATE_ENTERED : None,
                                     STATE_ACTIVE : None,
                                     STATE_INSENSITIVE : None }),
            "font" : WidgetStyle ({ "name" : None,
                                    "size" : 16,
                                    "alias" : True,
                                    "style" : 0 }),
            "shadow" : 2 })
            }

        # Load the default style and theme engine.
        self.load (os.path.join (DEFAULTDATADIR, "themes", "default",
                                 default.RCFILE))
        self.set_engine (default.DefaultEngine (self))

    def get_style (self, cls):
        """S.get_style (...) -> WidgetStyle
        
        Returns the style for a specific widget class.

        Returns the style for a specific widget class. If no matching
        entry was found, the method searches for the next upper
        entry of the class's __mro__. If it reaches the end of the
        __mro__ list without finding a matching entry, the
        default style will be returned.
        """
        classes = [c.__name__.lower () for c in cls.__mro__]
        for name in classes:
            if name in self.styles:
                return self.styles[name]
        return self.styles.setdefault (cls.__name__.lower (), WidgetStyle ())

    def get_style_entry (self, cls, style, key, subkey=None):
        """S.get_style_entry (...) -> value

        Gets a style entry from the style dictionary.

        Gets a entry from the style dictionary. If the entry could not
        be found, the method searches for the next upper entry of the
        __mro__. If it reaches the end of the __mro__ list without
        finding a matching entry, it will try to return the entry from
        the 'default' style dictionary.
        """
        deeper = subkey != None
        if key in style:
            if deeper:
                if subkey in style[key]:
                    return style[key][subkey]
            else:
                return style[key]

        styles = self.styles
        classes = [c.__name__.lower () for c in cls.__mro__]
        for name in classes:
            if name in styles:
                style = styles[name]
                # Found a higher level class style, check it.
                if key in style:
                    if deeper:
                        if subkey in style[key]:
                            return style[key][subkey]
                    else:
                        return style[key]

        # None found, refer to the default.
        if deeper:
            return styles["default"][key][subkey]
        return styles["default"][key]

    def copy_style (self, cls):
        """S.copy_style (...) -> WidgetStyle

        Creates a plain copy of a specific style.

        Due to the cascading ability of the Style class, an existing
        style will be filled with the entries of the 'default' style
        dictionary which do not exist in it.
        """
        style = copy.deepcopy (self.get_style (cls))
        default = self.styles["default"]
        for key in default:
            if key not in style:
                style[key] = copy.deepcopy (default[key])
            else:
                sub = default[key]
                # No dicts anymore
                for subkey in default[key]:
                    style[key] = copy.deepcopy (sub[subkey])
        return style
    
    def load (self, file):
        """S.load (...) -> None

        Loads style definitions from a file.

        Loads style definitions from a file and adds them to the
        'styles' attribute. Already set values in this dictionary will
        be overwritten.
        """
        glob_dict = {}
        loc_dict = {}
        execfile (file, glob_dict, loc_dict)
        setdefault = self.styles.setdefault
        for key in loc_dict:
            # Skip the Constants import directive and
            # any user-defined variable.
            if key.startswith ("_") or (key == "Constants"): 
                continue

            # Search the style or create a new one from scratch.
            entry = setdefault (key, WidgetStyle ())

            # Look up all entries of our style keys and add them to the
            # style.
            widget = loc_dict[key]
            for key in widget:
                if type (widget[key]) == dict:
                    if key not in entry:
                        entry[key] = WidgetStyle ()
                    for subkey in widget[key]:
                        entry[key][subkey] = widget[key][subkey]
                else:
                    entry[key] = widget[key]

    def create_style_dict (self):
        """Style.create_style_dict () -> dict

	Creates a new style dictionary.

        Creates a new unfilled style dictionary with the most necessary
        entries needed by the Style class specifications.
        """
        style = WidgetStyle ({
            "bgcolor" : WidgetStyle ({ STATE_NORMAL : (0, 0, 0),
                                       STATE_ENTERED : (0, 0, 0),
                                       STATE_ACTIVE : (0, 0, 0),
                                       STATE_INSENSITIVE : (0, 0, 0) }),
            "fgcolor" : WidgetStyle ({ STATE_NORMAL : (0, 0, 0),
                                       STATE_ENTERED : (0, 0, 0),
                                       STATE_ACTIVE : (0, 0, 0),
                                       STATE_INSENSITIVE : (0, 0, 0) }),
            "lightcolor" : WidgetStyle ({ STATE_NORMAL : (0, 0, 0),
                                          STATE_ENTERED : (0, 0, 0),
                                          STATE_ACTIVE : (0, 0, 0),
                                          STATE_INSENSITIVE : (0, 0, 0) }),
            "darkcolor" : WidgetStyle ({ STATE_NORMAL : (0, 0, 0),
                                         STATE_ENTERED : (0, 0, 0),
                                         STATE_ACTIVE : (0, 0, 0),
                                         STATE_INSENSITIVE : (0, 0, 0) }),
            "bordercolor" : WidgetStyle ({ STATE_NORMAL : (0, 0, 0),
                                           STATE_ENTERED : (0, 0, 0),
                                           STATE_ACTIVE : (0, 0, 0),
                                           STATE_INSENSITIVE : (0, 0, 0) }),
            "shadowcolor": ((0, 0, 0), (0, 0, 0)),
            "image" : WidgetStyle ({ STATE_NORMAL : None,
                                     STATE_ENTERED : None,
                                     STATE_ACTIVE : None,
                                     STATE_INSENSITIVE : None }),
            "font" : WidgetStyle ({ "name" : None,
                                    "size" : 0,
                                    "alias" : False,
                                    "style" : 0 }),
            "shadow" : 0
            })
        return style

    def get_border_size (self, cls=None, style=None, bordertype=BORDER_FLAT):
        """S.get_border_size (...) -> int

        Gets the border size for a specific border type and style.

        Gets the size of a border in pixels for the specific border type
        and style. for BORDER_NONE the value will be 0 by
        default. BORDER_FLAT will always return a size of 1.
        The sizes of other border types depend on the passed style.

        If no style is passed, the method will try to retrieve a style
        using the get_style() method.

        Raises a ValueError, if the passed bordertype argument is
        not a value of the BORDER_TYPES tuple.
        """
        if bordertype not in BORDER_TYPES:
            raise ValueError ("bordertype must be a value from BORDER_TYPES")

        if not style:
            style = self.get_style (cls)

        if bordertype == BORDER_FLAT:
            return 1
        elif bordertype in (BORDER_SUNKEN, BORDER_RAISED):
            return self.get_style_entry (cls, style, "shadow")
        elif bordertype in (BORDER_ETCHED_IN, BORDER_ETCHED_OUT):
            return self.get_style_entry (cls, style, "shadow") * 2
        return 0

    def set_engine (self, engine):
        """S.set_engine (...) -> None

        Sets the drawing engine to use for drawing elements.
        """
        if engine == None:
            raise TypeError ("engine must not be None")
        self._engine = engine

    engine = property (lambda self: self._engine,
                       lambda self, var: self.set_engine (var),
                       doc = "The drawing engine, which draws elements.")
