# $Id: Scale.py,v 1.40.2.1 2006/08/16 09:30:32 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.

"""Slider widgets for selecting a value from a range."""

from pygame import K_KP_PLUS, K_PLUS, K_RIGHT, K_DOWN, K_KP_MINUS, K_MINUS
from pygame import K_LEFT, K_UP, K_PAGEUP, K_PAGEDOWN, K_HOME, K_END, Rect
from Range import Range
from ocempgui.draw import Draw
from Constants import *
from StyleInformation import StyleInformation
import base

class Scale (Range):
    """Scale (minimum, maximum, step=1.0) -> Scale

    An abstract scaling widget class for numerical value selections.

    The Scale is an abstract widget class, which enhances the Range by
    different events and an activation method. Concrete implementations
    of it are the HScale, a horizontal scale widget, and the VScale,
    vertical scale widget.
    
    Inheriting widgets have to implement the _get_value_from_coords()
    method, which calculates the value of the Scale using a pair of
    absolute screen coordinates relative to a given area. Example
    implementations can be found in the HScale and VScale widget
    classes.
    
    Default action (invoked by activate()):
    Give the Scale the input focus.
    
    Mnemonic action (invoked by activate_mnemonic()):
    None

    Signals:
    SIG_MOUSEDOWN - Invoked, when a mouse button is pressed on the
                    Scale.
    SIG_MOUSEUP   - Invoked, when a mouse buttor is released on the
                    Scale.
    SIG_MOUSEMOVE - Invoked, when the mouse moves over the Scale.
    """
    def __init__ (self, minimum, maximum, step=1.0):
        Range.__init__ (self, minimum, maximum, step)

        # Internal click detection.
        self.__click = False

        self._signals[SIG_MOUSEDOWN] = []
        self._signals[SIG_MOUSEMOVE] = []
        self._signals[SIG_MOUSEUP] = []
        self._signals[SIG_KEYDOWN] = None # Dummy for keyboard activation.

    def activate (self):
        """S.activate () -> None

        Activates the Scale default action.

        Activates the Scale default action. This usually means giving
        the Scale the input focus.
        """
        if base.debug: print "Scale.activate ()"
        if not self.sensitive:
            return
        self.focus = True
    
    def _get_value_from_coords (self, area, coords):
        """S._get_value_from_coords (...) -> float

        Calculates the float value of the Scale.

        Calculates the float value of the Scale from the passed
        absolute coordinates tuple relative to the area.

        This method has to be implemented by inherited widgets.
        """
        raise NotImplementedError

    def notify (self, event):
        """S.notify (...) -> None

        Notifies the Scale about an event.
        """
        if not self.sensitive:
            return

        if event.signal in SIGNALS_MOUSE:
            eventarea = self.rect_to_client ()

            if event.signal == SIG_MOUSEDOWN:
                if eventarea.collidepoint (event.data.pos):
                    if base.debug: print "Scale.MOUSEDOWN"
                    self.focus = True
                    if event.data.button == 1:
                        # Guarantee a correct look, if the signal
                        # handlers run a long time
                        self.state = STATE_ACTIVE
                    self.run_signal_handlers (SIG_MOUSEDOWN, event.data)
                    if event.data.button == 1: # Only react upon left clicks.
                        self.__click = True
                        val = self._get_value_from_coords (eventarea,
                                                           event.data.pos)
                        if val != self.value:
                            self.value = val
                    # Mouse wheel.
                    elif event.data.button == 4:
                        val = self.value - 2 * self.step
                        if val > self.minimum:
                            self.value = val
                        else:
                            self.value = self.minimum
                    elif event.data.button == 5:
                        val = self.value + 2 * self.step
                        if val < self.maximum:
                            self.value = val
                        else:
                            self.value = self.maximum

            elif event.signal == SIG_MOUSEUP:
                if eventarea.collidepoint (event.data.pos):
                    if base.debug: print "Scale.MOUSEUP"
                    if event.data.button == 1:
                        if self.state == STATE_ACTIVE:
                            self.state = STATE_ENTERED
                        else:
                            self.state = STATE_NORMAL
                        self.__click = False
                    self.run_signal_handlers (SIG_MOUSEUP, event.data)
                elif (event.data.button == 1) and self.__click:
                    self.state = STATE_NORMAL
                    self.__click = False
        
            elif event.signal == SIG_MOUSEMOVE:
                if eventarea.collidepoint (event.data.pos):
                    if base.debug: print "Scale.MOUSEMOVE (inner)"
                    self.focus = True
                    if self.state == STATE_NORMAL:
                        self.state = STATE_ENTERED
                    self.run_signal_handlers (SIG_MOUSEMOVE, event.data)
                    if self.__click and self.focus:
                        val = self._get_value_from_coords (eventarea,
                                                           event.data.pos)
                        if val != self.value:
                            self.value = val
                elif self.state == STATE_ENTERED:
                    if base.debug: print "Scale.MOUSEMOVE (outer)"
                    self.state = STATE_NORMAL

        elif (event.signal == SIG_KEYDOWN) and self.focus:
            if event.data.key in (K_KP_PLUS, K_PLUS, K_RIGHT, K_DOWN):
                self.increase ()
            elif event.data.key in (K_KP_MINUS, K_MINUS, K_LEFT, K_UP):
                self.decrease ()
            elif event.data.key == K_PAGEUP:
                val = self.value - 10 * self.step
                if val > self.minimum:
                    self.value = val
                else:
                    self.value = self.minimum
            elif event.data.key == K_PAGEDOWN:
                val = self.value + 10 * self.step
                if val < self.maximum:
                    self.value = val
                else:
                    self.value = self.maximum
            elif event.data.key == K_END:
                self.value = self.maximum
            elif event.data.key == K_HOME:
                self.value = self.minimum

        Range.notify (self, event)

class HScale (Scale):
    """HScale (minimum, maximum, step=1.0) -> HScale

    A horizontal scaling widget for selecting numerical values.

    The HScale widget is a scaling widget with a horizontal orientation
    and allows the user to select and adjust a value from a range moving
    a slider.

    Default action (invoked by activate()):
    See the Scale class.
    
    Mnemonic action (invoked by activate_mnemonic()):
    See the Scale class.
    """
    def __init__ (self, minimum, maximum, step=1.0):
        Scale.__init__ (self, minimum, maximum, step)
        self.minsize = 120, 20 # Default size.

    def _get_value_from_coords (self, area, coords):
        """H._get_value_from_coords (...) -> float

        Calculates the float value of the HScale.

        Calculates the float value of the HScale from the passed
        absolute coordinates tuple relative to the area.
        """
        # We need this for a proper calculation.
        slider = StyleInformation.get ("HSCALE_SLIDER_SIZE")
        
        # The slide range, in which the slider can move.
        slide = self.width - slider[0]

        # Calculate the absolute current position
        n = coords[0] - area.left - slider[0] / 2.0

        # Step range in dependance of the width and value range of the
        # Scale.
        step = (self.maximum - self.minimum) / float (slide)

        # Calculate it.
        val = self.minimum + step * n
        if val > self.maximum:
            val = self.maximum
        elif val < self.minimum:
            val = self.minimum
        return val

    def _get_coords_from_value (self):
        """H._get_coords_from_value () -> float

        Calculates the coordinates from the current value of the HScale.

        Calculates the relative coordinates on the HScale from the
        current value.
        """
        # We need this for a proper calculation.
        slider = StyleInformation.get ("HSCALE_SLIDER_SIZE")

        width = self.width
        
        # The slide range in which the slider can move.
        slide = width - slider[0]

        # Step range in dependance of the width and value range of the
        # Scale.
        step = (self.maximum - self.minimum) / float (slide)

        # Calculate the value
        val  = (self.value - self.minimum) / step + slider[0] / 2.0
        return val

    def draw_bg (self):
        """H.draw_bg () -> Surface

        Draws the background surface of the HScale and returns it.

        Creates the visible surface of the HScale and returns it to the
        caller.
        """
        return base.GlobalStyle.engine.draw_scale (self,
                                                   ORIENTATION_HORIZONTAL)

    def draw (self):
        """H.draw () -> None

        Draws the HScale surface and its slider.
        """
        Scale.draw (self)
        cls = self.__class__
        style = base.GlobalStyle
        active = StyleInformation.get ("ACTIVE_BORDER")
        border = style.get_border_size (cls, self.style,
                                        StyleInformation.get ("SCALE_BORDER"))
        border_active = style.get_border_size (cls, self.style, active)
        slider = StyleInformation.get ("HSCALE_SLIDER_SIZE")

        # Creates the slider surface.
        sf_slider = style.engine.draw_slider (slider[0], slider[1],
                                              self.state, cls, self.style)
        rect_slider = sf_slider.get_rect ()

        # Dashed border.
        if self.focus:
            b = border + border_active
            r = Rect (b, b, rect_slider.width - 2 * b,
                      rect_slider.height - 2 * b)
            style.engine.draw_border \
                            (sf_slider, self.state, cls, self.style, active, r,
                             StyleInformation.get ("ACTIVE_BORDER_SPACE"))

        size = self.width - 2 * border, self.height / 3 - 2 * border

        # Fill the scale line.
        sf_fill = Draw.draw_rect (size[0], size[1],
                                  StyleInformation.get ("SCALE_COLOR"))
        self.image.blit (sf_fill, (border, self.height / 3 + border))

        # Blit slider at the correct position.
        rect_slider.centerx = self._get_coords_from_value ()
        rect_slider.centery = self.image.get_rect ().centery
        self.image.blit (sf_slider, rect_slider)
        
        # Fill until the slider start.
        if rect_slider.x > 0:
            sf_fill = Draw.draw_rect (rect_slider.x - border, size[1],
                                      StyleInformation.get ("PROGRESS_COLOR"))
            self.image.blit (sf_fill, (border, self.height / 3 + border))

class VScale (Scale):
    """VScale (minimum, maximum, step=1.0) -> VScale

    A vertical scaling widget for selecting numerical values.

    The VScale widget is a scaling widget with a vertical orientation
    and allows the user to select and adjust a value from a range moving
    a slider.
    
    Default action (invoked by activate()):
    See the Scale class.
    
    Mnemonic action (invoked by activate_mnemonic()):
    See the Scale class.
    """
    def __init__ (self, minimum, maximum, step=1.0):
        Scale.__init__ (self, minimum, maximum, step)
        self.minsize = 20, 120 # Default size.
    
    def _get_value_from_coords (self, area, coords):
        """V._get_value_from_coords (...) -> float

        Calculates the float value of the VScale.

        Calculates the float value of the VScale from the passed
        absolute coordinates tuple relative to the area.
        """
        # We need this for a proper calculation.
        slider = StyleInformation.get ("VSCALE_SLIDER_SIZE")
        
        # The slide range, in which the slider can move.
        slide = self.height - slider[1]

        # Calculate the absolute current position
        n = coords[1] - area.top - slider[1] / 2.0

        # Step range in dependance of the width and value range of the
        # Scale.
        step = (self.maximum - self.minimum) / float (slide)

        # Calculate it.
        val = self.minimum + step * n
        if val > self.maximum:
            val = self.maximum
        elif val < self.minimum:
            val = self.minimum
        return val

    def _get_coords_from_value (self):
        """V.get_coords_from_value () -> float

        Calculates the coordinates from the current value of the VScale.

        Calculates the relative coordinates on the VScale from the
        current value.
        """
        # We need this for a proper calculation.
        slider = StyleInformation.get ("VSCALE_SLIDER_SIZE")
        height = self.height
        
        # The slide range in which the slider can move.
        slide = height - slider[1]

        # Step range in dependance of the width and value range of the
        # Scale.
        step = (self.maximum - self.minimum) / float (slide)

        # Calculate the value
        val  = (self.value - self.minimum) / step + slider[1] / 2.0
        return val

    def draw_bg (self):
        """V.draw_bg () -> Surface

        Draws the VScale background surface and returns it.

        Creates the visible surface of the VScale and returns it to the
        caller.
        """
        return base.GlobalStyle.engine.draw_scale (self, ORIENTATION_VERTICAL)

    def draw (self):
        """V.draw () -> None

        Draws the VScale surface and its slider.
        """
        Scale.draw (self)
        cls = self.__class__
        style = base.GlobalStyle
        border = style.get_border_size (cls, self.style,
                                        StyleInformation.get ("SCALE_BORDER"))
        active = StyleInformation.get ("ACTIVE_BORDER")
        border_active = style.get_border_size (cls, self.style, active)
        slider = StyleInformation.get ("VSCALE_SLIDER_SIZE")

        # Creates the slider surface.
        sf_slider = style.engine.draw_slider (slider[0], slider[1], self.state,
                                              cls, self.style)
        rect_slider = sf_slider.get_rect ()

        # Dashed border.
        if self.focus:
            b = border + border_active
            r = Rect (b, b, rect_slider.width - 2 * b,
                      rect_slider.height - 2 * b)
            style.engine.draw_border \
                            (sf_slider, self.state, cls, self.style, active, r,
                             StyleInformation.get ("ACTIVE_BORDER_SPACE"))

        size = self.width / 3 - 2 * border, self.height - 2 * border

        # Fill the scale line.
        sf_fill = Draw.draw_rect (size[0], size[1],
                                  StyleInformation.get ("SCALE_COLOR"))
        self.image.blit (sf_fill, (self.width / 3 + border, border))

        # Blit slider at the correct position.
        rect_slider.centerx = self.image.get_rect ().centerx
        rect_slider.centery = self._get_coords_from_value ()
        self.image.blit (sf_slider, rect_slider)

        # Fill until the slider start.
        if rect_slider.y > 0:
            sf_fill = Draw.draw_rect (size[0], rect_slider.y - border,
                                      StyleInformation.get ("PROGRESS_COLOR"))
            self.image.blit (sf_fill, (self.width / 3 + border, border))

