# $Id: ScrolledWindow.py,v 1.38.2.2 2006/09/14 23:32:01 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.

"""A widget class, which can contain other widgets and uses optional
scrollbars."""

from pygame import K_RIGHT, K_LEFT, K_HOME, K_END, K_DOWN, K_UP, K_PAGEDOWN
from pygame import K_PAGEUP
from ScrollBar import HScrollBar, VScrollBar
from Bin import Bin
from BaseWidget import BaseWidget
from ViewPort import ViewPort
from StyleInformation import StyleInformation
from Constants import *
import base

class ScrolledWindow (Bin):
    """ScrolledWindow (width, height) -> ScrolledWindow

    A widget class, which supports horizontal and vertical scrolling.

    The ScrolledWindow is a viewport, which enables its child to be
    scrolled horizontally and vertically. It offers various scrolling
    types, which customize the bahviour of the supplied scrollbars.

    The scrolling behaviour of the ScrolledWindow can be adjusted
    through the 'scrolling' attribute or set_scrolling() method and can
    be one of the SCROLL_TYPES constants.

    Default action (invoked by activate()):
    Gives the ScrolledWindow the input focus.
    
    Mnemonic action (invoked by activate_mnemonic()):
    None

    Signals:
    SIG_KEYDOWN   - Invoked, when a key is pressed while the ScrolledWindow
                    has the input focus.
    SIG_MOUSEDOWN - Invoked, when a mouse button is pressed over the
                    ScrolledWindow.
    
    Attributes:
    scrolling  - The scrolling behaviour of the ScrolledWindow.
    vscrollbar - The vertical scrollbar of the ScrolledWindow.
    hscrollbar - The horizontal scrollbar of the ScrolledWindow.
    """
    def __init__ (self, width, height):
        Bin.__init__ (self)
        self._scrolling = SCROLL_AUTO

        # Scrollbars.
        self._vscroll = VScrollBar (height, height)
        self._vscroll.connect_signal (SIG_VALCHANGED, self._scroll_child)
        self._vscroll.parent = self
        self._hscroll = HScrollBar (width, width)
        self._hscroll.connect_signal (SIG_VALCHANGED, self._scroll_child)
        self._hscroll.parent = self

        self._vscroll_visible = False
        self._hscroll_visible = False

        # Scrolling will be handled directly by the ScrolledWindow.
        self._hscroll.set_focus = lambda self: False
        self._vscroll.set_focus = lambda self: False

        self.controls.append (self._vscroll)
        self.controls.append (self._hscroll)

        self._signals[SIG_KEYDOWN] = None # Dummy
        self._signals[SIG_MOUSEDOWN] = []

        # Respect the size.
        if width < self._hscroll.minsize[0]:
            width = self._hscroll.minsize[0]
        if height < self._vscroll.minsize[1]:
            height = self._vscroll.minsize[1]
        self.minsize = width, height

    def set_scrolling (self, scrolling):
        """S.set_scrolling (...) -> None

        Sets the scrolling behaviour for the ScrolledWindow.

        The scrolling can be a value of the SCROLL_TYPES list.
        SCROLL_AUTO causes the ScrolledList to display its scrollbars on
        demand only, SCROLL_ALWAYS will show the scrollbars permanently
        and SCROLL_NEVER will disable the scrollbars.

        Raises a ValueError, if the passed argument is not a value of
        the SCROLL_TYPES tuple.
        """
        if scrolling not in SCROLL_TYPES:
            raise ValueError ("scrolling must be a value from SCROLL_TYPES")
        if self._scrolling != scrolling:
            self._scrolling = scrolling
            self.dirty = True

    def set_child (self, child=None):
        """B.set_child (...) -> None

        Sets the child to display in the ScrolledList.

        Creates a parent-child relationship from the ScrolledList to a
        widget. If the widget does not support native scrolling, it will
        be packed into a ViewPort.
        """
        self.lock ()
        if child and not isinstance (child, ViewPort):
            self.vscrollbar.value = 0
            self.hscrollbar.value = 0
            child = ViewPort (child)
            child.minsize = self.minsize
        Bin.set_child (self, child)
        self.unlock ()
        
    def activate (self):
        """S.activate () -> None

        Activates the ScrolledWindow default action.

        Activates the ScrolledWindow default action. This usually means
        giving the ScrolledWindow the input focus.
        """
        if not self.sensitive:
            return
        self.focus = True
    
    def notify (self, event):
        """S.notify (...) -> None

        Notifies the ScrolledWindow 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):
                    self.focus = True
                    self.run_signal_handlers (SIG_MOUSEDOWN, event.data)

                    # Mouse wheel.
                    for c in self.controls:
                        c.notify (event)
                    if not event.handled:
                        if self._vscroll_visible:
                            if event.data.button == 4:
                                self.vscrollbar.decrease ()
                            elif event.data.button == 5:
                                self.vscrollbar.increase ()
                        elif self._hscroll_visible:
                            if event.data.button == 4:
                                self.hscrollbar.decrease ()
                            elif event.data.button == 5:
                                self.hscrollbar.increase ()
        
        elif (event.signal == SIG_KEYDOWN) and self.focus:
            if self._hscroll_visible:
                # Horizontal scrollbar key movement
                if event.data.key == K_RIGHT:
                    self.hscrollbar.increase ()
                elif event.data.key == K_LEFT:
                    self.hscrollbar.decrease ()
                elif event.data.key == K_END:
                    self.hscrollbar.value = self.hscrollbar.maximum
                elif event.data.key == K_HOME:
                    self.hscrollbar.value = self.hscrollbar.minimum
            if self._vscroll_visible:
                # Vertical scrollbar key movement
                if event.data.key == K_DOWN:
                    self.vscrollbar.increase ()
                elif event.data.key == K_UP:
                    self.vscrollbar.decrease ()
                elif event.data.key == K_PAGEUP:
                    val = self.vscrollbar.value - 10 * self.vscrollbar.step
                    if val > self.vscrollbar.minimum:
                        self.vscrollbar.value = val
                    else:
                        self.vscrollbar.value = self.vscrollbar.minimum
                elif event.data.key == K_PAGEDOWN:
                    val = self.vscrollbar.value + 10 * self.vscrollbar.step
                    if val < self.vscrollbar.maximum:
                        self.vscrollbar.value = val
                    else:
                        self.vscrollbar.value = self.vscrollbar.maximum
                elif event.data.key == K_END:
                    self.vscrollbar.value = self.vscrollbar.maximum
                elif event.data.key == K_HOME:
                    self.vscrollbar.value = self.vscrollbar.minimum

        Bin.notify (self, event)

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

        Scrolls the child of the ScrolledWindow.
        """
        if self.child != None:
            self.child.lock ()
            self.child.hadjustment = - int (self.hscrollbar.value)
            self.child.vadjustment = - int (self.vscrollbar.value)
            self.child.unlock ()
    
    def _update_scrollbars (self):
        """S._update_scrollbars (...) -> bool, bool

        Updates the size and maximum values of the attached scrollbars.

        Updates the size and values of the attached scrollbars and
        returns boolean values about their visibility in the order
        hscrollbar, vscrollbar.
        """
        old_vals = self._vscroll_visible, self._hscroll_visible

        if self.scrolling == SCROLL_NEVER:
            if self.child:
                self.child.minsize = self.minsize
            self.hscrollbar.sensitive = False
            self.vscrollbar.sensitive = False
            self._vscroll_visible = False
            self._hscroll_visible = False
            return False, False

        self.hscrollbar.bottom = self.bottom - self.top
        self.vscrollbar.right = self.right - self.left
        
        width = 0
        height = 0

        if self.child:
            width = self.child.real_width + 2 * self.padding
            height = self.child.real_height + 2 * self.padding

        # We are using the draw_border() method for the inner surface,
        # so we have to add the borders to the scrolling maximum.
        if self.scrolling == SCROLL_ALWAYS:
            self.vscrollbar.minsize = (self.vscrollbar.minsize[0],
                                       self.minsize[1] - \
                                       self.hscrollbar.height)
            self.hscrollbar.minsize = (self.minsize[0] - self.vscrollbar.width,
                                       self.hscrollbar.minsize[1])
            if self.child:
                self.child.maxsize = \
                                (self.minsize[0] - self.vscrollbar.size[0],
                                 self.minsize[1] - self.hscrollbar.size[1])
                self.child.minsize = self.child.maxsize
            if width < self.hscrollbar.minsize[0]:
                width = self.hscrollbar.minsize[0]
            self.hscrollbar.maximum = width
            if height < self.vscrollbar.minsize[1]:
                height = self.vscrollbar.minsize[1]
            self.vscrollbar.maximum = height
            self._vscroll_visible = True
            self._hscroll_visible = True
            self.vscrollbar.sensitive = True
            self.hscrollbar.sensitive = True

        elif self.scrolling == SCROLL_AUTO:
            # Check the sizes, so we can determine, how the scrollbars
            # need to be adjusted.
            self._hscroll_visible = self.minsize[0] < width
            self._vscroll_visible = self.minsize[1] < height

            if self._hscroll_visible:
                self._vscroll_visible = (self.minsize[1] -
                                         self.hscrollbar.height) < height
            if self._vscroll_visible:
                self._hscroll_visible = (self.minsize[0] -
                                         self.vscrollbar.width) < width
            
            if self._vscroll_visible and self._hscroll_visible:
                # Both scrollbars need to be shown.
                self.vscrollbar.minsize = (self.vscrollbar.minsize[0],
                                           self.minsize[1] - \
                                           self.hscrollbar.height)
                self.hscrollbar.minsize = (self.minsize[0] - \
                                           self.vscrollbar.width,
                                           self.hscrollbar.minsize[1])
                if self.child:
                    self.child.maxsize = \
                                (self.minsize[0] - self.vscrollbar.size[0],
                                 self.minsize[1] - self.hscrollbar.size[1])
                    self.child.minsize = self.child.maxsize
                self.vscrollbar.maximum = height
                self.hscrollbar.maximum = width
                self.vscrollbar.sensitive = True
                self.hscrollbar.sensitive = True

            elif self._vscroll_visible:
                # Only the vertical.
                self.vscrollbar.minsize = (self.vscrollbar.minsize[0],
                                           self.minsize[1])
                if self.child:
                    self.child.maxsize = \
                                (self.minsize[0] - self.vscrollbar.minsize[0],
                                 self.minsize[1])
                    self.child.minsize = self.child.maxsize
                self.vscrollbar.maximum = height
                self.vscrollbar.sensitive = True
                self.hscrollbar.sensitive = False

            elif self._hscroll_visible:
                # Only the horizontal.
                self.hscrollbar.minsize = (self.minsize[0],
                                           self.hscrollbar.minsize[1])
                if self.child:
                    self.child.maxsize = \
                                (self.minsize[0],
                                 self.minsize[1] - self.hscrollbar.size[1])
                    self.child.minsize = self.child.maxsize
                self.hscrollbar.maximum = width
                self.hscrollbar.sensitive = True
                self.vscrollbar.sensitive = False

            else:
                if self.child:
                    self.child.minsize = self.minsize
                # Neither vertical nor horizontal
                self.hscrollbar.sensitive = False
                self.vscrollbar.sensitive = False

        if not self.sensitive:
            self.hscrollbar.sensitive = False
            self.vscrollbar.sensitive = False

        return self._hscroll_visible, self._vscroll_visible
        
    def draw_bg (self):
        """S.draw_bg () -> Surface

        Draws the ScrolledWindow background surface and returns it.

        Creates the visible background surface of the ScrolledWindow and
        returns it to the caller.
        """
        return base.GlobalStyle.engine.draw_scrolledwindow (self)

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

        Draws the ScrolledWindow surface.

        Creates the visible surface of the ScrolledWindow.
        """
        Bin.draw (self)
        blit = self.image.blit

        # Update the srollbars.
        self.hscrollbar.lock ()
        self.vscrollbar.lock ()
        hscroll, vscroll = self._update_scrollbars ()
        self.hscrollbar.unlock ()
        self.vscrollbar.unlock ()

        if hscroll:
            blit (self.hscrollbar.image, self.hscrollbar.rect)

        if vscroll:
            blit (self.vscrollbar.image, self.vscrollbar.rect)

        if self.child:
            blit (self.child.image, self.child.rect)

    scrolling = property (lambda self: self._scrolling,
                          lambda self, var: self.set_scrolling (var),
                          doc = "The scrolling behaviour for the " \
                          "ScrolledWindow.")
    vscrollbar = property (lambda self: self._vscroll,
                           doc = "The vertical scrollbar.")
    hscrollbar = property (lambda self: self._hscroll,
                           doc = "The herizontal scrollbar.")
