# $Id: Entry.py,v 1.45.2.4 2006/09/16 12:38:51 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, which handles text input."""

from Editable import Editable
from ocempgui.draw import String
from Constants import *
import base

class Entry (Editable):
    """Entry (text="") -> Entry

    A widget class suitable for a single line of text input.

    The Entry widget is a text input box for a single line of text. It
    allows an unlimited amount of text input, but is usually more
    suitable for a small or medium amount, which can be scrolled, if the
    text size exceeds the visible widget size.
    
    The 'padding' attribute and set_padding() method are used to place a
    certain amount of pixels between the text and the outer edges of the
    Entry.

    entry.padding = 10
    entry.set_padding (10)

    The Entry supports different border types by setting its 'border'
    attribute to a valid value of the BORDER_TYPES constants.

    entry.border = BORDER_SUNKEN
    entry.set_border (BORDER_SUNKEN)

    It also features a password mode, which will cause it to display the
    text as asterisks ('*'). It will not encrypt or protect the internal
    text attribute however. The password mode can be set using the
    'password' attribute and set_password() method.

    entry.password = True
    entry.set_password (False)

    The Entry uses a default size for itself by setting the 'size'
    attribute to a width of 94 pixels and a height of 24 pixels.

    Default action (invoked by activate()):
    See the Editable class.
    
    Mnemonic action (invoked by activate_mnemonic()):
    None

    Signals:
    SIG_MOUSEDOWN - Invoked, when a mouse button gets pressed on the
                    Entry.
    SIG_MOUSEMOVE - Invoked, when the mouse moves over the Entry.

    Attributes:
    password - Characters will be drawn as '*' (asterisk).
    padding  - Additional padding between text and borders. Default is 2.
    border   - The border style to set for the Entry.
    """
    def __init__ (self, text=""):
        Editable.__init__ (self)
        self._realtext = None
        self._padding = 2
        self._border = BORDER_SUNKEN
        self._password = False
        
        self._offset = 0 # Front offset in pixels.

        # Pixel sizes of text to the left and right of caret, and char
        # the caret is at.
        self._leftsize = (0, 0)
        self._rightsize = (0, 0)
        self._cursize = (0, 0)
        self._font = None
        
        self._signals[SIG_MOUSEDOWN] = []
        self._signals[SIG_MOUSEMOVE] = []
        
        self.minsize = 94, 24 # Default size to use.
        self.text = text

    def set_padding (self, padding):
        """E.set_padding (...) -> None

        Sets the padding between the edges and text of the Entry.

        The padding value is the amount of pixels to place between the
        edges of the Entry and the displayed text.
        
        Raises a TypeError, if the argument is not a positive integer.
        """
        if (type (padding) != int) or (padding < 0):
            raise TypeError ("Argument must be a positive integer")
        self._padding = padding
        self.dirty = True

    def set_password (self, password):
        """E.set_password (...) -> None

        When this is set this to True, the entry's content will be drawn
        with '*' (asterisk) characters instead of the actual
        characters. This is useful for password dialogs, where it is
        undesirable for the password to be displayed on-screen.
        """
        self._password = password
        self.dirty = True

    def _set_caret_position (self, eventarea, position):
        """W._set_caret_position (...) -> None

        Sets the position of the caret based on the given pixel position.
        """
        # Get the relative mouse point.
        mpos = (position[0] - eventarea.x - self.padding,
                position[1] - eventarea.y - self.padding)
        
        if mpos[0] <= 0:
            # User clicked on the border or into the padding area.
            self.caret = 0
            return
        
        caret = self.caret
        mpoint = self._offset + mpos[0]
        left, right, current = self._get_text_overhang (caret)
        
        if mpoint > (left[0] + right[0]):
            # User clicked past the length of the text.
            self.caret = len (self.text)
            return

        # Find the click inside the text area
        while (mpoint > left[0]) and (caret <= len (self.text)):
            caret += 1
            left, right, current = self._get_text_overhang (caret)
        while (mpoint < left[0]) and (caret > 0):
            caret -= 1
            left, right, current = self._get_text_overhang (caret)

        # Move caret to left or right, based on center of clicked character
        if mpoint > (left[0] + (current[0] / 2) + self.border):
            caret += 1

        self.caret = caret

    def _get_text_overhang (self, pos):
        """E._get_text_overhang (...) -> (int, int), (int, int), (int, int)

        Gets the pixel sizes to the left and right of the caret and
        the character size the caret is at in this order..
        """
        # TODO: the text display should be separated in an own TextView
        # class.
        if self._font == None:
            self._cursize = (0, 0)
            return
        
        text = self.text
        if self.password:
            text = '*' * len (self.text)
        
        self._leftsize = self._font.size (text[:pos])
        self._rightsize = self._font.size (text[pos:])
        try:
            self._cursize = self._font.size (text[pos])
        except IndexError:
            self._cursize = (0, 0)
        return self._leftsize, self._rightsize, self._cursize

    def _calculate_offset (self, textwidth, font):
        """E._calculate_offset (...) -> int

        Calculates the left pixel offset for the Entry text.
        """
        self._font = font
        self._get_text_overhang (self.caret)
        
        bump_size = self.minsize[0] / 4

        while self._leftsize[0] < self._offset:
            new_offset = self._offset - bump_size
            self._offset = max (new_offset, 0)
        while self._leftsize[0] > (self._offset + textwidth):
            self._offset += bump_size
        return min (-self._offset, 0)
    
    def notify (self, event):
        """E.notify (...) -> None

        Notifies the Entry about an event.
        """
        if not self.sensitive:
            return
        
        if event.signal == SIG_MOUSEDOWN:
            eventarea = self.rect_to_client ()
            if eventarea.collidepoint (event.data.pos):
                self.run_signal_handlers (SIG_MOUSEDOWN, event.data)
                if event.data.button == 1:
                    self._caret_visible = True
                    self._set_caret_position (eventarea, event.data.pos)
                    if not self.focus:
                        self.activate ()
                event.handled = True

        elif event.signal == SIG_MOUSEMOVE:
            eventarea = self.rect_to_client ()
            if eventarea.collidepoint (event.data.pos):
                self.run_signal_handlers (SIG_MOUSEMOVE, event.data)
                self.entered = True
                event.handled = True
            else:
                self.entered = False

        Editable.notify (self, event)

    def set_border (self, border):
        """E.set_border (...) -> None

        Sets the border type to be used by the Entry.

        Raises a ValueError, if the passed argument is not a value from
        BORDER_TYPES
        """
        if border not in BORDER_TYPES:
            raise ValueError ("border must be a value from BORDER_TYPES")
        self._border = border
        self.dirty = True

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

        Draws the surface of the Entry and returns it.

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

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

        Draws the Entry surface.
        """
        Editable.draw (self)
        cls = self.__class__
        style = base.GlobalStyle
        engine = style.engine
        st = self.style or style.get_style (cls)
        border = style.get_border_size (cls, st, self.border)
        text = self.text
        rect = self.image.get_rect ()

        if self.password:
            text = '*' * len (self.text)
        rtext = style.engine.draw_string (text, self.state, cls, st)

        # The 'inner' surface, which we will use for blitting the text.
        sf_text = engine.draw_rect (rect.width - 2 * (self.padding + border),
                                    rect.height - 2 * (self.padding + border),
                                    self.state, cls, st)

        # Adjust entry offset based on caret location.
        font = String.create_font \
               (style.get_style_entry (cls, st, "font", "name"),
                style.get_style_entry (cls, st, "font", "size"))
        rect_sftext = sf_text.get_rect ()
        blit_pos = self._calculate_offset (rect_sftext.width, font)
        
        sf_text.blit (rtext, (blit_pos, 0))

        # Draw caret.
        if self.focus and self.caret_visible:
            # The caret position is at the end of the left overhang.
            caret_pos = self._get_text_overhang (self.caret)[0][0]
            engine.draw_caret (sf_text, blit_pos + caret_pos, 1, 2, self.state,
                               cls, st)

        rect_sftext.center = rect.center
        self.image.blit (sf_text, rect_sftext)

    padding = property (lambda self: self._padding,
                        lambda self, var: self.set_padding (var),
                        doc = "The additional padding for the Entry.")
    border = property (lambda self: self._border,
                       lambda self, var: self.set_border (var),
                       doc = "The border style to set for the Entry.")
    password = property (lambda self: self._password,
                         lambda self, var: self.set_password (var),
                         doc = "Indicates the password mode for the Entry.")
