# Sketch - A Python-based interactive drawing program
# Copyright (C) 1997, 1998, 1999, 2001 by Bernhard Herzog
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Library General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Library General Public License for more details.
#
# You should have received a copy of the GNU Library General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

from Sketch import Trafo, RectType, EmptyRect, InfinityRect
from Sketch.config import preferences
from Sketch.Graphics.papersize import Papersize


class Viewport:

    max_scale = 16
    min_scale = 0.05

    def __init__(self, resolution = None):
	self.hbar = None
	self.vbar = None
	self.hruler = None
	self.vruler = None

	self.base_width = 3000
	self.base_height = 3000
	self.nominal_scale = 1
	self.scale = 1.0
	self.virtual_width = self.base_width
	self.virtual_height =  self.base_height
	self.virtual_x = 0
	self.virtual_y = 0
	self.init_resolution(resolution)
	self.set_page_size(Papersize['A4']) # XXX config
	self.compute_win_to_doc()
	self.clear_rects = []
	self.clear_entire_window = 0

	self.init_viewport_ring()


    def init_resolution(self, resolution):
	if resolution is not None:
	    self.pixel_per_point = resolution
	else:
	    width = self.winfo_screenwidth()
	    width_mm = self.winfo_screenmmwidth()
	    pixel_per_point = (width / float(width_mm)) * 25.4 / 72
	    self.pixel_per_point = float(pixel_per_point)
	self.max_scale = self.__class__.max_scale * self.pixel_per_point

    #
    #	Coordinate conversions
    #

    def set_page_size(self, (width, height)):
	self.page_width = width
	self.page_height = height

    def compute_win_to_doc(self):
	scale = self.scale
	# virtual coords of ll corner of page
	llx = (self.virtual_width - self.page_width * scale) / 2
	lly = (self.virtual_height + self.page_height * scale) / 2
	self.doc_to_win = Trafo(scale, 0.0, 0.0, -scale,
				llx - self.virtual_x, lly - self.virtual_y)
	scale = 1.0 / scale
	self.win_to_doc = Trafo(scale, 0.0, 0.0, -scale,
				(self.virtual_x - llx) * scale,
				(lly - self.virtual_y) * scale)

    #	document to window coordinates

    def DocToWin(self, *args):
	# returns a tuple of ints
	return apply(self.doc_to_win.DocToWin, args)

    #	window to document coordinates

    def WinToDoc(self, *args):
	return apply(self.win_to_doc, args)

    def LengthToDoc(self, len):
	return len / self.scale

    #
    #	Redraw Parts of the window.
    #

    def clear_area_doc(self, rect):
	# Mark the rectangular area of self, given by RECT in document
	# coordinates, as invalid. The rect is put into a list of rects
	# that have to be cleared via do_clear() once RedrawMethod() is
	# invoked.
	if not self.clear_entire_window:
	    if rect is EmptyRect:
		return
	    elif rect is InfinityRect:
		self.clear_window()
	    elif type(rect) == RectType:
		x1, y1 = self.DocToWin(rect.left, rect.top)
		x2, y2 = self.DocToWin(rect.right, rect.bottom)
		if x1 > x2:
		    t = x1; x1 = x2; x2 = t
		if y1 > y2:
		    t = y1; y1 = y2; y2 = t

                # clip the rect to the window
                if x1 < 0:
                    x1 = 0
                elif x1 > self.tkwin.width:
                    return
                if x2 > self.tkwin.width:
                    x2 = self.tkwin.width
                elif x2 < 0:
                    return
                if y1 < 0:
                    y1 = 0
                elif y1 > self.tkwin.height:
                    return
                if y2 > self.tkwin.height:
                    y2 = self.tkwin.height
                elif y2 < 0:
                    return
                
		self.clear_rects.append((x1 - 1, y1 - 1,
					 x2 - x1 + 2, y2 - y1 + 2))
	    else:
		# special case: clear a guide line
		p, horizontal = rect
		x, y = self.DocToWin(p)
		if horizontal:
		    self.clear_rects.append((0, y, self.tkwin.width, 1))
		else:
		    self.clear_rects.append((x, 0, 1, self.tkwin.height))
	self.UpdateWhenIdle()

    def clear_window(self, update = 1):
	# Mark the entire window as invalid.
	self.clear_entire_window = 1
	self.clear_rects = []
	if update:
	    self.UpdateWhenIdle()

    def do_clear(self, region):
	# Clear all areas marked as invalid by clear_area_doc() or
	# clear_window(). These areas are added to REGION via its
	# UnionRectWithRegion method. This function should be called by
	# RedrawMethod() before any drawing is done.
	if region and not self.clear_entire_window:
	    clear_area = self.tkwin.ClearArea
	    union = region.UnionRectWithRegion
	    for rect in self.clear_rects:
		apply(clear_area, rect + (0,))
		apply(union, rect)
	else:
	    self.tkwin.ClearArea(0, 0, 0, 0, 0)
	    region = None
	self.clear_entire_window = 0
	self.clear_rects = []
	return region

    #
    #
    #

    def set_origin(self, xorg, yorg, move_contents = 1):
	old_org_x = xo = self.virtual_x
	old_org_y = yo = self.virtual_y
	if xorg != None:
	    xo = xorg
	if yorg != None:
	    yo = yorg
	offx = round(xo - old_org_x)
	offy = round(yo - old_org_y)
	xo = old_org_x + offx
	yo = old_org_y + offy
	self.virtual_x = xo
	self.virtual_y = yo
	offx = int(offx)
	offy = int(offy)
	if move_contents and (offx or offy):
	    self.move_window_contents(offx, offy)
	self.compute_win_to_doc()
	self.update_scrollbars()
	self.update_rulers()


    def move_window_contents(self, offx, offy):
	# Noop here, because moving the window contents requires a gc.
	# The implementation could be as follows:
	#
	#	w = self.tkwin
	#	width = w.width
	#	height = w.height
	#	w.CopyArea(w, self.gc.gc, offx, offy, width, height, 0, 0)
	pass


    #
    #	Managing the displayed area
    #

    def SetCenter(self, center, move_contents = 1):
	# set origin so that center (in doc-coords) is in the center of the
	# widget
	cx, cy = self.DocToWin(center)
	self.set_origin(self.virtual_x + cx - self.tkwin.width / 2,
			self.virtual_y + cy - self.tkwin.height / 2,
			move_contents = move_contents)

    def SetScale(self, scale, do_center = 1):
	# Set current scale
	scale = scale * self.pixel_per_point
	if scale > self.max_scale:
	    scale = self.max_scale
	elif scale < self.min_scale:
	    scale = self.min_scale
	self.scale = scale
	self.nominal_scale = scale / self.pixel_per_point
	width = int(scale * self.base_width)
	height = int(scale * self.base_height)
	self.virtual_width = width
	self.virtual_height = height
	if do_center:
	    cx = self.tkwin.width / 2
	    cy = self.tkwin.height / 2
	    center = self.WinToDoc(cx, cy)

	self.compute_win_to_doc()

	if do_center:
	    self.SetCenter(center, move_contents = 0)
	else:
	    self.set_origin(0, 0, move_contents = 0)

	self.gc.SetViewportTransform(scale, self.doc_to_win, self.win_to_doc)

	self.clear_window()

    def SetPageSize(self, size):
	self.set_page_size(size)
	self.compute_win_to_doc()

    def Scale(self):
        return self.scale

    #
    #	Some convenient methods
    #

    def zoom_fit_rect(self, rect):
	# set the scale and origin of the canvas so that the document
	# rectangle rect is centered and fills the window (keeping the aspect
	# ratio)
	epsilon = 1e-10

	rw = rect.right - rect.left
	if abs(rw) < epsilon:
	    return
	width = self.tkwin.width
	scalex = float(width) / rw

	rh = rect.top - rect.bottom
	if abs(rh) < epsilon:
	    return
	height = self.tkwin.height
	scaley = float(height) / rh

	scale = min((scalex, scaley, self.max_scale))

	self.SetScale(scale / self.pixel_per_point, do_center = 0)
	self.SetCenter(rect.center(), move_contents = 0)


    def ZoomInfoText(self):
	# Return a string that describes the current zoom factor in percent.
	# Usually this is displayed in a status bar.
        # NLS
	return "%g%%" % round(100 * self.nominal_scale, 1)


    #
    #	Scrollbar handling
    #

    def ResizedMethod(self, width, height):
	self.update_scrollbars()
        self.update_rulers()

    def SetScrollbars(self, hbar, vbar):
	self.hbar = hbar
	self.vbar = vbar

    def SetRulers(self, hruler, vruler):
	self.hruler = hruler
	self.vruler = vruler

    def update_scrollbars(self):
	w = self.tkwin.width
	h = self.tkwin.height
	if self.hbar:
	    vw = float(self.virtual_width)
	    min = self.virtual_x / vw
	    self.hbar.set(min, min + w / vw)
	if self.vbar:
	    vh = float(self.virtual_height)
	    min = self.virtual_y / vh
	    self.vbar.set(min, min + h / vh)

    def update_rulers(self, force = 0):
        start = self.WinToDoc(0, 0)
	if self.hruler:
	    self.hruler.SetRange(start.x, self.scale, force = force)
	if self.vruler:
	    self.vruler.SetRange(start.y, self.scale, force = force)

    def xview(self, *args):
	apply(self.tk.call, (self._w, 'xview') + args)

    def yview(self, *args):
	apply(self.tk.call, (self._w, 'yview') + args)

    def ScrollXMove(self, fraction):
	vw = self.virtual_width
	w = self.tkwin.width
	amount = float(w) / vw
	max = 1.0 - amount
	if fraction > max:
	    fraction = max
	if fraction < 0:
	    fraction = 0
	self.set_origin(fraction * vw, None)

    def ScrollXPages(self, count):
	vw = float(self.virtual_width)
	w = self.tkwin.width
	amount = w / vw
	self.ScrollXMove(self.virtual_x / vw + count * amount)

    def ScrollXUnits(self, count):
	self.set_origin(self.virtual_x + count * 10, None)

    def ScrollYMove(self, fraction):
	vh = self.virtual_height
	h = self.tkwin.height
	amount = float(h) / vh
	max = 1.0 - amount
	if fraction > max:
	    fraction = max
	if fraction < 0:
	    fraction = 0
	self.set_origin(None, fraction * vh)

    def ScrollYPages(self, count):
	vh = float(self.virtual_height)
	h = self.tkwin.height
	amount = h / vh
	self.ScrollYMove(self.virtual_y / vh + count * amount)

    def ScrollYUnits(self, count):
	self.set_origin(None, self.virtual_y + count * 10)


    #
    #	 the viewport ring.
    #

    def init_viewport_ring(self):
	self.viewport_ring = []

    def save_viewport(self):
	self.viewport_ring.insert(0,
				  (self.scale, self.virtual_x, self.virtual_y))
	length = preferences.viewport_ring_length
	self.viewport_ring = self.viewport_ring[:length]

    def restore_viewport(self):
	if self.viewport_ring:
	    scale, vx, vy = self.viewport_ring[0]
	    del self.viewport_ring[0]
	    self.SetScale(scale / self.pixel_per_point, do_center = 0)
	    self.set_origin(vx, vy, move_contents = 0)
