# $Id: Magnifier.py,v 1.1.2.5 2006/08/24 07:48:37 marcusva Exp $
#
# Copyright (c) 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.

"""Magnification tool for pygame."""

import pygame

class Magnifier (object):
    """Magnifier (size=(20, 20), factor=2) -> Magnifier

    A screen magnification class for the pygame screen.

    The Magnifier class allows to zoom a certain portion of the screen,
    determined by the current mouse position by a specific scaling
    factor. It creates a rectangular zoom area that will be blit on the
    screen. The zooming area will be placed around the current mouse
    position:

    ##############################################
    #                                            #  ###
    #               ---------------              #  # # - pygame screen
    #               |\           /|              #  ###
    #               | \         / |              #
    #               |  \ooooooo/  |              #  ooo
    #               |   o     o   |              #  o o - Area to zoom (size)
    #               |   o  X  o   |              #  ooo
    #               |   o     o   |              #
    #               |  /ooooooo\  |              #  ---
    #               | /         \ |              #  | | - Zoomed area.
    #               |/           \|              #  ---
    #               ---------------              #
    #                                            #   X - Mouse cursor position. 
    ##############################################

    The size of the area to zoom can be adjusted using the 'size'
    attribute and set_size() method.

    # Zoom a 40x40 px area around the mouse cursor.
    magnifier.size = 40, 40
    magnifier.set_size (40, 40)

    The factor by which the area around the mouse cursor should be
    magnified can be set using the 'factor' attribute or set_factor() method.

    # Zoom the wanted area by 3 with a result of an area three times as big
    # as the original one.
    magnifier.factor = 3
    magnifier.set_factor (3)

    Attributes:
    factor - The zoom factor to use for magnification.
    size   - The size of the area around the mouse cursor to zoom.
    border - Indicates whether a 1px border around the magnified area should
             be drawn.
    """
    def __init__ (self, size=(20, 20), factor=2):
        self._factor = factor
        self._size = size
        self._oldsurface = None
        self._oldpos = None # The topleft offset of the area.
        self._mousepos = None  # The mouse position.
        self._border = True

    def set_factor (self, factor):
        """M.set_factor (...) -> None

        Sets the zoom factor to use for magnification.

        Raises a TypeError, if the argument is not an integer or float.
        Raises a ValueError, if the argument is smaller than 1.
        """
        if type (factor) not in (int, float):
            raise TypeError ("factor must be an integer or float")
        if factor < 1:
            raise ValueError ("factor must not be smaller than 1")
        self._factor = factor
        if self._mousepos:
            self._update (self._mousepos)
        
    def set_size (self, width, height):
        """M.set_size (...) -> None

        Sets the width and height of the area to zoom.

        Raises a TypeError, if the arguments are not integers.
        Raises a ValueError, if the arguments are smaller than 1.
        """
        if (type (width) != int) or (type (height) != int):
            raise TypeError ("width and height must be integers")
        if (width < 1) or (height < 1):
            raise ValueError ("width and height must not be smaller than 1")
        self._size = width, height
        if self._mousepos:
            self._update (self._mousepos)

    def enable_border (self, border):
        """M.enable_border (...) -> None

        Enables or disables the display of a 1px border around the zoom area.

        The argument will be interpreted as boolean value.
        """
        self._border = border
        if self._mousepos:
            self._update (self._mousepos)

    def restore (self):
        """M.restore () -> None

        Restores the original screen content.
        """
        screen = pygame.display.get_surface ()
        # Restore old surface area.
        if self._oldsurface:
            screen.blit (self._oldsurface, self._oldpos)
            self._oldsurface = None

    def notify (self, *events):
        """M.notify (...) -> bool

        Notifies the Magnifier about a pygame.event.Event.

        The argument must be a pygame.event.Event.
        """
        motion = [e for e in events if e.type == pygame.MOUSEMOTION]
        if motion:
            self._mousepos = motion[-1].pos
            self._update (self._mousepos)
            return True
        # Update the screen for the case it changed.
        self._update (self._mousepos)
        return False

    def _update (self, position):
        """M._update (...) -> None

        Update the magnification area.
        """
        rsize = int (self.size[0] * self.factor), \
                int (self.size[1] * self.factor)
        offset = position[0] - rsize[0] / 2, position[1] - rsize[1] / 2
        zoffset = position[0] - self.size[0] / 2, position[1] - self.size[1] / 2
            
        screen = pygame.display.get_surface ()

        # Restore old surface area.
        if self._oldsurface:
            screen.blit (self._oldsurface, self._oldpos)

        # Preserve surface at the position.
        self._oldsurface = pygame.Surface ((rsize[0], rsize[1]))
        self._oldsurface.blit (screen, (0, 0), (offset[0], offset[1],
                                                rsize[0], rsize[1]))

        # Create zoomable surface.
        surface = pygame.Surface ((self.size[0], self.size[1]))
        surface.blit (screen, (0, 0), (zoffset[0], zoffset[1],
                                       self.size[0], self.size[1]))
            
        # Zoom and blit.
        surface = pygame.transform.scale (surface, (rsize[0], rsize[1]))

        if self.border:
            pygame.draw.line (surface, (0, 0, 0), (0, 0), (0, rsize[1]))
            pygame.draw.line (surface, (0, 0, 0), (0, 0), (rsize[0], 0))
            pygame.draw.line (surface, (0, 0, 0), (rsize[0] - 1, rsize[1] - 1),
                              (0, rsize[1] - 1))
            pygame.draw.line (surface, (0, 0, 0), (rsize[0] - 1, rsize[1] - 1),
                              (rsize[0] - 1, 0))

        screen.blit (surface, offset)
        self._oldpos = offset

    size = property (lambda self: self._size,
                     lambda self, (x, y): self.set_size (x, y),
                     doc = "The size of the magnification area.")
    factor = property (lambda self: self._factor,
                       lambda self, var: self.set_factor (var),
                       doc = "The zoom factor of the magnification area.")
    border = property (lambda self: self._border,
                       lambda self, var: self.enable_border (var),
                       doc = "Indicates whether a border should be shown.")
