# 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 ""

