# Sketch - A Python-based interactive drawing program
# Copyright (C) 1997, 1998, 1999, 2000 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


# Classes for handling selections. These include classes that represent
# lists of selected objects and classes that represent the combined
# bounding rectangle of all selected objects. The user can interact in
# the usual fashion with these selection rects to transform (translate,
# rotate, scale, shear) the selected objects.


import operator, math
from types import ListType, InstanceType, TupleType
import time

from Sketch.Lib.util import flatten

from Sketch.UI.skpixmaps import pixmaps
from Sketch.warn import pdebug, warn, INTERNAL
from Sketch import const
from Sketch.const import SelectSet

from Sketch import _, Point, Polar, Rect, UnionRects, RectType, Identity, \
     Trafo, TrafoType, Rotation, CreateListUndo, NullUndo

import handle
from base import SelectAndDrag, Bounded
import selinfo


class SelRectBase(SelectAndDrag, Bounded):

    #
    # Handle/selection numbers:
    #	sx		ex
    #	1	2	3	sy
    #
    #	8		4
    #
    #	7	6	5	ey
    #
    #	-1: whole object

    selTop	= (1, 2, 3)
    selBottom	= (7, 6, 5)
    selLeft	= (1, 8, 7)
    selRight	= (3, 4, 5)
    selAspect	= (1, 3, 5, 7)	# constrain aspect ratio for these selections

    handle_idx_to_sel = (7, 6, 5, 8, 4, 1, 2, 3)

    def __init__(self):
	SelectAndDrag.__init__(self)
	self.outline_object = None

    def update_rects(self):
	self.coord_rect = Rect(self.start, self.end)
	self.bounding_rect = self.coord_rect

    def SetOutlineObject(self, obj):
	# XXX: this is a hack...
	if obj is not None:
	    if obj.is_Compound:
		objects = obj.GetObjects()
		if len(objects) == 1:
		    obj = objects[0]
		else:
		    return
	    if not obj.is_Compound and not obj.is_Text:
		self.outline_object = obj


class SelectionRectangle(SelRectBase):

    def __init__(self, rect, anchor = None):
	SelRectBase.__init__(self)
	if type(rect) == RectType:
	    self.start = Point(rect.left, rect.bottom)
	    self.end = Point(rect.right, rect.top)
	    self.Normalize()
	    self.anchor = anchor
	else:
	    # assume type Point and interactive creation
	    self.start = rect
	    self.end = rect
	    self.anchor = None
	    self.selection = 5

    def DrawDragged(self, device, partially):
	sel = self.selection

	if sel == -1:
	    sx, sy = self.start + self.off
	    ex, ey = self.end + self.off
	else:
	    if sel in self.selTop:
		sy = self.drag_cur.y
	    else:
		sy = self.start.y

	    if sel in self.selBottom:
		ey = self.drag_cur.y
	    else:
		ey = self.end.y

	    if sel in self.selLeft:
		sx = self.drag_cur.x
	    else:
		sx = self.start.x

	    if sel in self.selRight:
		ex = self.drag_cur.x
	    else:
		ex = self.end.x

	if sx > ex:
	    tmp = sx; sx = ex; ex = tmp
	if sy < ey:
	    tmp = sy; sy = ey; ey = tmp

	device.DrawRubberRect(Point(sx, sy), Point(ex, ey))

    def ButtonDown(self, p, button, state):
	SelectAndDrag.DragStart(self, p)
	sel = self.selection
	if sel == -1:
	    if self.anchor: #XXX shouldn't this be 'if self.anchor is not None'
		start = self.anchor
	    else:
		start = self.start
	    self.drag_start = self.drag_cur = start
	    return (p - start, self.coord_rect.translated(-start))
	ds_x , ds_y = (self.start + self.end) / 2
	if sel in self.selLeft:
	    ds_x = self.start.x
	if sel in self.selTop:
	    ds_y = self.start.y
	if sel in self.selRight:
	    ds_x = self.end.x
	if sel in self.selBottom:
	    ds_y = self.end.y
	self.drag_cur = self.drag_start = ds = Point(ds_x, ds_y)
	self.init_constraint()
	return p - ds

    def init_constraint(self):
	pass

    def apply_constraint(self, p, state):
	return p

    def MouseMove(self, p, state):
	p = self.apply_constraint(p, state)
	SelectAndDrag.MouseMove(self, p, state)

    def compute_endpoints(self):
	cur = self.drag_cur
	start = self.start
	end = self.end
	sel = self.selection
	if sel in self.selTop:
	    start = Point(start.x, cur.y)
	if sel in self.selBottom:
	    end	  = Point(end.x,   cur.y)
	if sel in self.selLeft:
	    start = Point(cur.x, start.y)
	if sel in self.selRight:
	    end = Point(cur.x, end.y)
	if sel == -1:
	    start = start + self.off
	    end = end + self.off
        return start, end

    def ButtonUp(self, p, button, state):
	p = self.apply_constraint(p, state)
	SelectAndDrag.DragStop(self, p)
	cur = self.drag_cur
	oldstart = self.start
	oldend = self.end
	start, end = self.compute_endpoints()
	self.start = start
	self.end = end
	result = self.ComputeTrafo(oldstart, oldend, start, end)
	self.Normalize()
	return result

    def ComputeTrafo(self, oldStart, oldEnd, start, end):
	pass

    def Normalize(self):
	sx, sy = self.start
	ex, ey = self.end
	if sx > ex:
	    sx, ex = ex, sx
	if sy > ey:
	    sy, ey = ey, sy
	self.start = Point(sx, sy)
	self.end = Point(ex, ey)

    def Hit(self, p, rect, device):
	pass

    def Select(self):
	self.selection = -1

    def SelectPoint(self, p, rect, device, mode = SelectSet):
	if p:
	    self.selection = 0
	else:
	    self.selection = -1
	return self.selection

    def SelectHandle(self, handle, mode = SelectSet):
	self.selection = self.handle_idx_to_sel[handle.index]

    def GetHandles(self):
	sx = self.start.x
	sy = self.start.y
	ex = self.end.x
	ey = self.end.y
	x2 = (sx + ex) / 2
	y2 = (sy + ey) / 2
	return map(handle.MakeOffsetHandle,
		   [Point(sx, ey),	Point(x2, ey),	Point(ex, ey),
		    Point(sx, y2),			Point(ex, y2),
		    Point(sx, sy),	Point(x2, sy),	Point(ex, sy)],
		   [(-1,  1),		(0,  1),	( 1,  1),
		    (-1,  0),				( 1,  0),
		    (-1, -1),		(0, -1),	( 1, -1)])



class Selection(Bounded):

    is_EditSelection = 0

    _lazy_attrs = Bounded._lazy_attrs.copy()
    _lazy_attrs['rect'] = 'update_rectangle'
    
    def __init__(self, copy_from = None):
	if copy_from is not None:
	    if type(copy_from) == ListType:
		self.objects = copy_from[:]
	    else:
		# assume copy_from is another instance of a selection class
		self.objects = copy_from.objects
		self.coord_rect = copy_from.coord_rect
		self.bounding_rect = copy_from.bounding_rect
		self.anchor = copy_from.anchor
	else:
	    self.objects = []
	    self.anchor = None

    def normalize(self):
	# make sure that self.objects contains no object more than once
	# and that no two objects have a direct or indirect parent/child
	# relationship.
	objs = self.objects
	changed = 0
	if len(objs) > 1:
	    objs.sort()
	    last_info, obj = objs[-1]
	    for idx in range(len(objs) - 2, -1, -1):
		info, obj = objs[idx]
		if info == last_info:
		    del objs[idx]
		    changed = 1
		    continue
		if len(info) < len(last_info):
		    while info == last_info[:len(info)]:
			del objs[idx + 1]
			changed = 1
			if idx + 1 < len(objs):
			    last_info = objs[idx + 1][0]
			else:
			    break
		last_info = info
	return changed

    def update_selinfo(self):
        objs = self.objects
        for i in range(len(objs)):
            objs[i] = objs[i][-1].SelectionInfo()

    def SetSelection(self, info):
	old_objs = self.objects
	if info:
	    if type(info) == ListType:
		self.objects = info
		self.objects.sort()
	    else:
		self.objects = [info]
	else:
	    self.objects = []
	self.del_lazy_attrs()
	return old_objs != self.objects

    def SetSelectionTree(self, info):
	return self.SetSelection(selinfo.tree_to_list(info))

    def Add(self, info):
	if not info:
	    return 0
	old_len = len(self.objects)
	if type(info) == ListType:
	    self.objects = self.objects + info
	elif info:
	    self.objects.append(info)
	changed = self.normalize()
	self.del_lazy_attrs()
	return changed or old_len != len(self.objects)

    def Subtract(self, info):
	if not info:
	    return 0
	old_len = len(self.objects)
	if type(info) != ListType:
	    info = [info]
	objects = self.objects
	for item in info:
	    if item in objects:
		objects.remove(item)
	self.del_lazy_attrs()
	return old_len != len(self.objects)

    def GetObjects(self):
	return map(operator.getitem, self.objects, [-1] * len(self.objects))

    def GetInfo(self):
	return self.objects

    def GetInfoTree(self):
	return selinfo.list_to_tree(self.objects)

    def Depth(self):
	if self.objects:
	    lengths = map(len, map(operator.getitem, self.objects,
				   [0] * len(self.objects)))
	    lmin = min(lengths)
	    lmax = max(lengths)
	    if lmin == lmax:
		return lmin
	    return (lmin, lmax)
	return 0

    def IsSingleDepth(self):
	if self.objects:
	    return type(self.Depth()) != TupleType
	return 1

    def GetPath(self):
	if len(self.objects) == 1:
	    return self.objects[0][0]
	return ()

    def for_all(self, func):
	return map(func, self.GetObjects())

    def ForAllUndo(self, func):
	undoinfo = self.for_all(func)
	self.del_lazy_attrs()
	if len(undoinfo) == 1:
	    undoinfo = undoinfo[0]
	if type(undoinfo) == ListType:
	    return CreateListUndo(undoinfo)
	return undoinfo

    def ForAllUndo2(self, method, *args):
	t = time.clock()
	methods = map(getattr, self.GetObjects(), [method] * len(self.objects))
	#print time.clock() - t,
	#undoinfo = self.for_all(func)
	undoinfo = map(apply, methods, [args] * len(methods))
	#print time.clock() - t
	self.del_lazy_attrs()
	if len(undoinfo) == 1:
	    undoinfo = undoinfo[0]
	if type(undoinfo) == ListType:
	    return CreateListUndo(undoinfo)
	return undoinfo

    def __len__(self):
	return len(self.objects)

    __nonzero__ = __len__

    def update_rects(self):
	objects = self.GetObjects()
	boxes = map(lambda o: o.coord_rect, objects)
	if boxes:
	    self.coord_rect = reduce(UnionRects, boxes)
	else:
	    self.coord_rect = None
	boxes = map(lambda o: o.bounding_rect, objects)
	if boxes:
	    self.bounding_rect = reduce(UnionRects, boxes)
	else:
	    self.bounding_rect = None
	if len(objects) == 1:
	    self.anchor = objects[0].LayoutPoint()
	else:
	    self.anchor = None

    def ChangeRect(self):
	return self.bounding_rect

    def ResetRectangle(self):
	self.del_lazy_attrs()

    def Hit(self, p, rect, device):
	test = rect.overlaps
	for obj in self.GetObjects():
	    if test(obj.bounding_rect):
		if obj.Hit(p, rect, device):
		    return 1
	return 0

    def DragCancel(self):
	self.rect.DragCancel()

    def GetHandles(self):
	rect_handles = self.rect.GetHandles()
	multiple = len(self.objects) > 1
	handles = flatten(self.for_all(lambda o, m = multiple:
				       o.GetObjectHandle(m)))
	rect_handles.append(handle.MakeObjectHandleList(handles))
	return rect_handles

    def CallObjectMethod(self, aclass, methodname, args):
	if len(self.objects) == 1:
	    obj = self.objects[0][-1]
	    if not isinstance(obj, aclass):
		return NullUndo
	    try:
		method = getattr(obj, methodname)
	    except AttributeError:
		return NullUndo

	    undo = apply(method, args)
	    if undo is None:
		undo = NullUndo
	    return undo
	return NullUndo

    def GetObjectMethod(self, aclass, method):
	if len(self.objects) == 1:
	    obj = self.objects[0][-1]
	    if isinstance(obj, aclass):
		try:
		    return getattr(obj, method)
		except AttributeError:
		    pass
	return None

    def InfoText(self):
	# Return a string describing the selected object(s)
        result = _("No Selection")
	if self.objects:
	    sel_info = self.objects
	    document = sel_info[0][1].document
	    if len(sel_info) == 1:
                path, obj = sel_info[0]
                dict = {'layer': document[path[0]].Name()}
		info = obj.Info()
                if type(info) == TupleType:
                    dict.update(info[1])
                    # the %% is correct here. The result has to be a
                    # %-template itself.
                    text = _("%s on `%%(layer)s'") % info[0]
                else:
                    dict['object'] = info
                    text = _("%(object)s on `%(layer)s'")
                result = text, dict
	    else:
		layer = sel_info[0][0][0]
		if layer == sel_info[-1][0][0]:
		    # a single layer
		    layer_name = document.layers[layer].Name()
		    result = _("%(number)d objects on `%(layer)s'") \
                             % {'number':len(sel_info), 'layer':layer_name}
		else:
		    result = _("%d objects on several layers") % len(sel_info)

        return result

    def CurrentInfoText(self):
        return ''

    def _dummy(self, *args):
	pass

    Hide	 = _dummy
    DragStart	 = None
    DragMove	 = None
    DragStop	 = None
    Show	 = _dummy
    Hide	 = _dummy
    SelectPoint	 = _dummy
    SelectHandle = _dummy

    drag_mask = SelectAndDrag.drag_mask



class SizeRectangle(SelectionRectangle):

    def init_constraint(self):
	sel = self.selection
	if sel == 1:
	    self.reference = tuple(self.end)
	elif sel == 3:
	    self.reference = (self.start.x, self.end.y)
	elif sel == 5:
	    self.reference = tuple(self.start)
	elif sel == 7:
	    self.reference = (self.end.x, self.start.y)
	else:
	    return
	width = abs(self.start.x - self.end.x)
	height = abs(self.start.y - self.end.y)
	if width >= 1e-10:
	    self.aspect = height / width
	else:
	    self.aspect = None

    def apply_constraint(self, p, state):
	if state & const.ConstraintMask:
	    if self.selection in self.selAspect:
		ref_x, ref_y = self.reference
		aspect = self.aspect
		if aspect is None:
		    # width is 0
		    p = Point(self.drag_start.x, p.y)
		else:
		    w = p.x - ref_x
		    h = p.y - ref_y
		    if w == 0:
			w = 0.00001
		    a = h / w
		    if a > 0:
			sign = 1
		    else:
			sign = -1
		    if abs(a) > aspect:
			h = sign * w * aspect
		    else:
			w = sign * h / aspect
		    p = Point(ref_x + w, ref_y + h)
	    elif self.selection == -1:
		pi4 = math.pi / 4
		off = p - self.drag_start
                d = Polar(pi4 * round(math.atan2(off.y, off.x) / pi4))
                p = self.drag_start + (off * d) * d
	return p

    def ButtonDown(self, p, button, state):
        self.trafo = Identity
        return SelectionRectangle.ButtonDown(self, p, button, state)

    def MouseMove(self, p, state):
	p = self.apply_constraint(p, state)
	SelectAndDrag.MouseMove(self, p, state)
        start, end = self.compute_endpoints()
        text, self.trafo = self.ComputeTrafo(self.start, self.end, start, end)

    def ComputeTrafo(self, oldStart, oldEnd, start, end):
	oldDelta = oldEnd - oldStart
	delta	 = end - start
	if self.selection == -1:
	    # a translation.
	    return _("Move Objects"), start - oldStart
	else:
	    try:
		m11 = delta.x / oldDelta.x
	    except ZeroDivisionError:
		m11 = 0
		if __debug__:
		    pdebug(None, 'ComputeTrafo: ZeroDivisionError')
	    try:
		m22 = delta.y / oldDelta.y
	    except ZeroDivisionError:
		m22 = 0
		if __debug__:
		    pdebug(None, 'ComputeTrafo: ZeroDivisionError')
	    offx = start.x - m11 * oldStart.x
	    offy = start.y - m22 * oldStart.y
	    return _("Resize Objects"), Trafo(m11, 0, 0, m22, offx, offy)

    def DrawDragged(self, device, partial):
	SelectionRectangle.DrawDragged(self, device, partial)
	if self.outline_object is not None:
	    trafo = self.trafo
	    device.PushTrafo()
	    if type(trafo) == TrafoType:
		device.Concat(trafo)
	    else:
		device.Translate(trafo.x, trafo.y)
	    self.outline_object.DrawShape(device)
	    device.PopTrafo()

    def CurrentInfoText(self):
        t = self.trafo
        data = {}
        if type(t) == TrafoType:
            x = t.m11
            y = t.m22
            #if round(x, 3) == round(y, 3):
            #    text = _("Uniform Scale %(factor)[factor]")
            #    data['factor'] = x
            #else:
            text = _("Scale %(factorx)[factor], %(factory)[factor]")
            data['factorx'] = x
            data['factory'] = y
        else:
            text = _("Move %(x)[length], %(y)[length]")
            data['x'] = t.x
            data['y'] = t.y
        return text, data



class SizeSelection(Selection):

    def __init__(self, arg = None):
	Selection.__init__(self, arg)

    def update_rectangle(self):
	if self:
	    self.rect = SizeRectangle(self.coord_rect, self.anchor)
	else:
	    self.rect = SizeRectangle(Rect(0, 0, 0, 0))

    def ButtonDown(self, p, button, state):
	if len(self.objects) == 1:
	    self.rect.SetOutlineObject(self.objects[0][-1])
	return self.rect.ButtonDown(p, button, state)

    def MouseMove(self, p, state):
	self.rect.MouseMove(p, state)

    def ButtonUp(self, p, button, state, forget_trafo = 0):
	self.rect.SetOutlineObject(None)
	undo_text, trafo = self.rect.ButtonUp(p, button, state)
	if forget_trafo:
	    return None, None
	t = time.clock()
	if type(trafo) == TrafoType:
	    #undo = self.ForAllUndo(lambda o, t = trafo: o.Transform(t))
	    undo = self.ForAllUndo2('Transform', trafo)
	else:
	    # trafo is point representing a translation
	    undo = self.ForAllUndo2('Translate', trafo)
	#print 'transform/translate', time.clock() - t
	self.del_lazy_attrs()
	return undo_text, undo

    def Show(self, device, partially = 0):
	self.rect.Show(device, partially)

    def Hide(self, device, partially = 0):
	self.rect.Hide(device, partially)

    def DrawDragged(self, device, partial):
	self.rect.DrawDragged(device, partial)

    def SelectPoint(self, p, rect, device, mode = SelectSet):
	if not self.rect.SelectPoint(p, rect, device, mode):
	    if self.Hit(p, rect, device):
		self.rect.Select()

    def SelectHandle(self, handle, mode = SelectSet):
	self.rect.SelectHandle(handle, mode)

    def Hit(self, p, rect, device):
	if self.objects:
	    return (rect.overlaps(self.bounding_rect)
		    and Selection.Hit(self, p, rect, device))
	return 0

    def CurrentInfoText(self):
        return self.rect.CurrentInfoText()

class TrafoRectangle(SelRectBase):

    selTurn = [1, 3, 5, 7]
    selShear = [2, 4, 6, 8]
    selCenter = 100

    def __init__(self, rect, center = None):
	SelRectBase.__init__(self)
	self.start = Point(rect.left, rect.bottom)
	self.end = Point(rect.right, rect.top)
	if center is None:
	    self.center = rect.center()
	else:
	    self.center = center

    def compute_trafo(self, state = 0):
	sel = self.selection
	if sel in self.selTurn:
	    # rotation
	    vec = self.drag_cur - self.center
	    angle = math.atan2(vec.y, vec.x)
	    angle = angle - self.start_angle + 2 * math.pi
	    if state & const.ConstraintMask:
		pi12 = math.pi / 12
		angle = pi12 * int(angle / pi12 + 0.5)
            self.trafo = Rotation(angle, self.center)
            self.trafo_desc = (1, angle)
	elif sel in self.selShear:
	    if sel in (2,6):
		# horiz. shear
		height = self.drag_start.y - self.reference
		if height:
		    ratio = self.off.x / height
		    self.trafo = Trafo(1, 0, ratio, 1,
                                       - ratio * self.reference, 0)
                    self.trafo_desc = (2, ratio)
	    else:
		# vert. shear
		width = self.drag_start.x - self.reference
		if width:
		    ratio = self.off.y / width
		    self.trafo = Trafo(1, ratio, 0, 1, 0,
                                       - ratio * self.reference)
                    self.trafo_desc = (3, ratio)

    def DrawDragged(self, device, partially):
	sel = self.selection
	if sel == self.selCenter:
	    device.DrawPixmapHandle(self.drag_cur, pixmaps.Center)
	else:
	    trafo = self.trafo
	    if trafo:
		device.PushTrafo()
		device.Concat(trafo)
		device.DrawRubberRect(self.start, self.end)
		if self.outline_object is not None:
		    self.outline_object.DrawShape(device)
		device.PopTrafo()

    def ButtonDown(self, p, button, state):
	self.drag_state = state
        self.trafo = Identity
        self.trafo_desc = (0, 0)
	SelectAndDrag.DragStart(self, p)
	sel = self.selection
	if sel == self.selCenter:
	    self.drag_cur = self.drag_start = self.center
	    return p - self.center
	ds_x = ds_y = 0
	if sel in self.selLeft:
	    ds_x = self.start.x
	if sel in self.selTop:
	    ds_y = self.start.y
	if sel in self.selRight:
	    ds_x = self.end.x
	if sel in self.selBottom:
	    ds_y = self.end.y
	self.drag_cur = self.drag_start = ds = Point(ds_x, ds_y)
	if sel in self.selTurn:
	    vec = ds - self.center
	    self.start_angle = math.atan2(vec.y, vec.x)
	else:
	    if sel == 2:
		self.reference = self.end.y
	    elif sel == 4:
		self.reference = self.start.x
	    elif sel == 6:
		self.reference = self.start.y
	    elif sel == 8:
		self.reference = self.end.x
	return p - ds

    def constrain_center(self, p, state):
	if state & const.ConstraintMask:
	    start = self.start
	    end = self.end
	    if p.x < 0.75 * start.x + 0.25 * end.x:
		x = start.x
	    elif p.x > 0.25 * start.x + 0.75 * end.x:
		x = end.x
	    else:
		x = (start.x + end.x) / 2
	    if p.y < 0.75 * start.y + 0.25 * end.y:
		y = start.y
	    elif p.y > 0.25 * start.y + 0.75 * end.y:
		y = end.y
	    else:
		y = (start.y + end.y) / 2
	    return Point(x, y)
	return p

    def MouseMove(self, p, state):
	self.drag_state = state
	if self.selection == self.selCenter:
	    p = self.constrain_center(p, state)
	SelectAndDrag.MouseMove(self, p, state)
        self.compute_trafo(state)

    def ButtonUp(self, p, button, state):
	if self.selection == self.selCenter:
	    p = self.constrain_center(p, state)
	SelectAndDrag.DragStop(self, p)
	sel = self.selection
	if sel == self.selCenter:
	    self.center = self.drag_cur
	    return '', None
	else:
            self.compute_trafo(state)
	    trafo = self.trafo
	    if self.selection in self.selShear:
		text = _("Shear Objects")
	    else:
		text = _("Rotate Objects")
	    return text, trafo

    def CurrentInfoText(self):
        if self.selection == self.selCenter:
            text = _("Rotation Center at %(position)[position]")
            data = {'position': self.drag_cur}
        else:
            type, value = self.trafo_desc
            if type == 1:
                text = _("Rotate by %(angle)[angle]")
                data = {'angle': value}
            elif type == 2:
                text = _("Horizontal Shear by %(ratio)[factor]")
                data = {'ratio': value}
            elif type == 3:
                text = _("Vertical Shear by %(ratio)[factor]")
                data = {'ratio': value}
            else:
                text = _("Identity Transform")
                data = {}
        return text, data

    def Hit(self, p, rect, device):
	pass

    def Select(self):
	pass

    def SelectPoint(self, p, rect, device, mode = SelectSet):
	self.selection = 0
	return self.selection

    def SelectHandle(self, handle, mode = SelectSet):
	handle = handle.index
	if handle == len(self.handle_idx_to_sel):
	    self.selection = self.selCenter
	else:
	    self.selection = self.handle_idx_to_sel[handle]

    def GetHandles(self):
	sx = self.start.x
	sy = self.start.y
	ex = self.end.x
	ey = self.end.y
	x2 = (sx + ex) / 2
	y2 = (sy + ey) / 2
	return map(handle.MakePixmapHandle,
		   [Point(sx, ey),	Point(x2, ey),	Point(ex, ey),
		    Point(sx, y2),			Point(ex, y2),
		    Point(sx, sy),	Point(x2, sy),	Point(ex, sy)],
		   [(-1,  1),		(0,  1),	( 1,  1),
		    (-1,  0),				( 1,  0),
		    (-1, -1),		(0, -1),	( 1, -1)],
		   [pixmaps.TurnTL, pixmaps.ShearLR,	pixmaps.TurnTR,
		    pixmaps.ShearUD,			pixmaps.ShearUD,
		    pixmaps.TurnBL, pixmaps.ShearLR,	pixmaps.TurnBR],
		   [const.CurTurn] * 8) \
	     + [handle.MakePixmapHandle(self.center, (0, 0), pixmaps.Center)]


class TrafoSelection(Selection):

    def __init__(self, copy_from = None):
	Selection.__init__(self, copy_from)
	self.center = None

    def update_rectangle(self, same_center = 1):
	if self:
	    self.rect = TrafoRectangle(self.coord_rect, self.center)
	else:
	    self.rect = TrafoRectangle(Rect(0, 0, 0, 0))

    def ButtonDown(self, p, button, state):
	if len(self.objects) == 1:
	    self.rect.SetOutlineObject(self.objects[0][-1])
	return self.rect.ButtonDown(p, button, state)

    def MouseMove(self, p, state):
	self.rect.MouseMove(p, state)

    def ButtonUp(self, p, button, state, forget_trafo = 0):
	self.rect.SetOutlineObject(None)
	undo_text, trafo = self.rect.ButtonUp(p, button, state)
	self.center = self.rect.center
	if forget_trafo:
	    return None, None
	if trafo is not None:
	    t = time.clock()
	    #undo = self.ForAllUndo(lambda o, t = trafo: o.Transform(t))
	    undo = self.ForAllUndo2('Transform', trafo)
	    self.del_lazy_attrs()
	    #print 'transform/translate', time.clock() - t
	    return undo_text, undo
	return '', None

    def Show(self, device, partially = 0):
	self.rect.Show(device, partially)

    def Hide(self, device, partially = 0):
	self.rect.Hide(device, partially)

    def DrawDragged(self, device, partial):
	self.rect.DrawDragged(device, partial)

    def SelectPoint(self, p, rect, device, mode = SelectSet):
	if not self.rect.SelectPoint(p, rect, device, mode):
	    if self.Hit(p, rect, device):
		self.rect.Select()

    def SelectHandle(self, handle, mode = SelectSet):
	self.rect.SelectHandle(handle, mode)

    def Hit(self, p, rect, device):
	if self.objects:
	    return (rect.overlaps(self.rect.bounding_rect)
		    and Selection.Hit(self, p, rect, device))
	return 0

    def CurrentInfoText(self):
        return self.rect.CurrentInfoText()

class EditorWrapper:

    def __init__(self, editor):
	self.editor = editor

    def __del__(self):
	self.editor.Destroy()

    def __getattr__(self, attr):
	return getattr(self.editor, attr)

    def compatible(self, aclass):
	obj = self.editor
	return isinstance(obj, aclass) or issubclass(obj.EditedClass, aclass)



class EditSelection(Selection):

    is_EditSelection = 1
    
    drag_this = None
    editor = None

    def __init__(self, copy_from = None):
	Selection.__init__(self, copy_from)
	self.check_edit_mode()
	if type(copy_from) == InstanceType \
	   and copy_from.__class__ == self.__class__:
	    self.editor = copy_from.editor
	else:
	    self.get_editor()

    def check_edit_mode(self):
	# allow only one object for editing at a time
	if self.objects:
	    if len(self.objects) > 1 or not self.objects[0][-1].has_edit_mode:
		self.objects = []

    def get_editor(self):
	if self.objects:
	    self.editor = EditorWrapper(self.objects[0][-1].Editor())
	else:
	    self.editor = None

    def GetHandles(self):
	if self.editor is not None:
	    return self.editor.GetHandles()
	return []

    def ButtonDown(self, p, button, state):
	if self.drag_this is not None:
	    return self.drag_this.ButtonDown(p, button, state)
	else:
	    return None

    def MouseMove(self, p, state):
	if self.drag_this is not None:
	    self.drag_this.MouseMove(p, state)

    def ButtonUp(self, p, button, state, forget_trafo = 0):
	if self.drag_this is not None:
	    self.del_lazy_attrs()
	    return _("Edit Object"), self.drag_this.ButtonUp(p, button, state)
	return ('', None)
	# XXX make the undo text more general by a method of graphics objects

    def Show(self, device, partially = 0):
	if self.editor is not None:
	    self.editor.Show(device, partially)

    def Hide(self, device, partially = 0):
	if self.editor is not None:
	    self.editor.Hide(device, partially)

    def DrawDragged(self, device, partial):
	if self.editor is not None:
            self.editor.DrawDragged(device, partial)

    def SetSelection(self, info):
	old_sel = self.objects
	Selection.SetSelection(self, info)
	self.check_edit_mode()
	if old_sel != self.objects:
	    self.get_editor()
	    return 1
	return 0

    def SelectPoint(self, p, rect, device, mode = const.SelectSet):
	self.drag_this = None
	if self.editor is not None:
	    if self.editor.SelectPoint(p, rect, device, mode):
		self.drag_this = self.editor
	return self.drag_this != None

    def SelectHandle(self, handle, mode = SelectSet):
	if self.editor is not None:
	    self.editor.SelectHandle(handle, mode)
	    self.drag_this = self.editor
	else:
	    self.drag_this = None

    def SelectRect(self, rect, mode = SelectSet):
	self.drag_this = None
	if self.editor is not None:
	    if self.editor.SelectRect(rect, mode):
		self.drag_this = self.editor
	return self.drag_this != None

    def CallObjectMethod(self, aclass, methodname, args):
	if len(self.objects) == 1:
	    if self.editor is not None:
		obj = self.editor
		if not obj.compatible(aclass):
		    warn(INTERNAL, 'EditSelection.GetObjectMethod: '
			 'editor %s is not compatible with class %s',
			 self.editor, aclass)
		    return NullUndo
	    else:
		obj = self.objects[0][-1]
		if not isinstance(obj, aclass):
		    warn(INTERNAL, 'EditSelection.GetObjectMethod: '
			 'object is not instance of %s', aclass)
		    return NullUndo
	    try:
		method = getattr(obj, methodname)
	    except AttributeError:
		warn(INTERNAL, 'EditSelection.GetObjectMethod: '
		     'no method %s for class %s', methodname, aclass)
		return NullUndo

	    undo = apply(method, args)
	    if undo is None:
		undo = NullUndo
	    return undo
	return NullUndo

    def GetObjectMethod(self, aclass, method):
	if len(self.objects) == 1:
	    if self.editor is not None:
		obj = self.editor
		if not obj.compatible(aclass):
		    warn(INTERNAL, 'EditSelection.GetObjectMethod: '
			 'editor is not compatible with class %s', aclass)
		    return None
	    else:
		obj = self.objects[0][-1]
		if not isinstance(obj, aclass):
		    warn(INTERNAL, 'EditSelection.GetObjectMethod: '
			 'object is not instance of %s', aclass)
		    return None
	    try:
		return getattr(obj, method)
	    except AttributeError:
		warn(INTERNAL, 'EditSelection.GetObjectMethod: '
		     'no method %s for class %s', method, aclass)
		pass
	return None


    def ChangeRect(self):
	if self.editor is not None:
	    return self.editor.ChangeRect()
	return self.bounding_rect

    def InfoText(self):
	# Return a string describing the selected object(s)
	# XXX we shouldn't access document.layers directly
	if self.editor is not None:
	    return self.editor.Info()
	else:
	    return _("No Selection")

    def CurrentInfoText(self):
	if self.editor is not None:
	    return self.editor.CurrentInfoText()
	else:
	    return ""
        
