# Sketch - A Python-based interactive drawing program
# Copyright (C) 1997, 1998, 1999, 2002 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
#
# Class Compound
#
# a baseclass for objects that contain other graphics objects
# (primitives or other compounds)
#
from types import ListType,TupleType,IntType,InstanceType
import operator
from Sketch import _,SketchError,UnionRects,EmptyRect,_sketch
from Sketch import NullUndo,CreateListUndo,Undo
from base import GraphicsObject,Bounded,CHANGED
from blend import Blend,MismatchError
from properties import EmptyProperties
from selinfo import prepend_idx,select_range,build_info,list_to_tree2,\
list_to_tree_sliced
class Compound(GraphicsObject):
has_edit_mode = 0
is_Group = 0
is_Compound = 1
can_be_empty = 0
allow_traversal = 0
def __init__(self, objects = None, duplicate = None):
GraphicsObject.__init__(self, duplicate = duplicate)
if duplicate is not None:
objects = []
for obj in duplicate.objects:
objects.append(obj.Duplicate())
self.objects = objects
elif objects:
self.objects = objects
else:
self.objects = []
self.changing_children = 0
self.set_parent()
def Destroy(self):
self.destroy_objects()
GraphicsObject.Destroy(self)
def destroy_objects(self):
for obj in self.objects:
obj.Destroy()
self.objects = []
def SetParent(self, parent):
if parent is self.parent:
return
GraphicsObject.SetParent(self, parent)
if parent is not None:
self.set_parent()
else:
self.unset_parent()
def set_parent(self):
for child in self.objects:
child.SetParent(self)
def unset_parent(self):
for child in self.objects:
child.SetParent(None)
def SelectionInfo(self, child = None):
info = GraphicsObject.SelectionInfo(self)
if info and child is not None:
path = info[0]
return (path + (_sketch.IdIndex(self.objects, child),),
child)
return info
def ChildChanged(self, child):
self.del_lazy_attrs()
if self.changing_children:
return
self.issue_changed()
def SetDocument(self, doc):
for obj in self.objects:
obj.SetDocument(doc)
GraphicsObject.SetDocument(self, doc)
self.set_parent()
def disconnect_objects(self):
for obj in self.objects:
obj.Disconnect()
def Disconnect(self):
self.disconnect_objects()
self.unset_parent()
def connect_objects(self):
for obj in self.objects:
obj.Connect()
def Connect(self):
self.set_parent()
self.connect_objects()
def set_objects(self, new_objs):
if self.document is not None:
self.document.AddClearRect(self.bounding_rect)
self.disconnect_objects()
self.destroy_objects()
self.objects = new_objs
for obj in self.objects:
if self.document is not None:
obj.SetDocument(self.document)
obj.SetParent(self)
# XXX no connect here ?
self.del_lazy_attrs()
if self.document is not None:
self.document.AddClearRect(self.bounding_rect)
def UntieFromDocument(self):
for obj in self.objects:
obj.UntieFromDocument()
def TieToDocument(self):
for obj in self.objects:
obj.TieToDocument()
def load_AppendObject(self, object):
self.objects.append(object)
def load_Done(self):
pass
def __getitem__(self, idx):
if type(idx) == IntType:
return self.objects[idx]
elif type(idx) == TupleType:
if len(idx) > 1:
return self.objects[idx[0]][idx[1:]]
elif len(idx) == 1:
return self.objects[idx[0]]
raise ValueError, 'invalid index %s' % `idx`
def GetObjects(self):
# XXX should this return a copy of self.objects?
return self.objects
def del_lazy_attrs(self):
Bounded.del_lazy_attrs(self)
return (self.del_lazy_attrs,)
def update_rects(self):
# XXX: should we raise an exception here if self.objects is empty?
boxes = map(lambda o: o.coord_rect, self.objects)
if boxes:
self.coord_rect = reduce(UnionRects, boxes, boxes[0])
else:
self.coord_rect = EmptyRect
boxes = map(lambda o: o.bounding_rect, self.objects)
if boxes:
self.bounding_rect = reduce(UnionRects, boxes, boxes[0])
else:
self.bounding_rect = EmptyRect
def SelectSubobject(self, p, rect, device, path = None, *rest):
return self
def Insert(self, obj, at):
raise SketchError('Cannot insert in compound')
def Remove(self, *args, **kw):
raise SketchError('Cannot remove from compound')
RemoveSlice = Remove
RemoveObjects = Remove
ReplaceChild = Remove
def MoveObjectsToTop(self, infolist):
raise SketchError('Cannot rearrange objects in compound')
MoveObjectsToBottom = MoveObjectsToTop
MoveObjectsDown = MoveObjectsToTop
MoveObjectsUp = MoveObjectsToTop
def move_objects_to_top(self, infolist, to_bottom = 0):
return infolist, NullUndo
def DuplicateObjects(self, infolist, offset):
raise SketchError('Cannot duplicate objects in compound')
def ForAll(self, func):
self.changing_children = 1
try:
return map(func, self.objects)
finally:
self.changing_children = 0
def WalkHierarchy(self, func):
for obj in self.objects:
if obj.is_Compound:
obj.WalkHierarchy(func)
else:
func(obj)
def begin_change_children(self):
self.changing_children = self.changing_children + 1
return (self.end_change_children,)
def end_change_children(self):
self.changing_children = self.changing_children - 1
if not self.changing_children:
self._changed()
return (self.begin_change_children,)
def ForAllUndo(self, func):
if self.objects:
undo = [self.begin_change_children()]
undo = undo + map(func, self.objects)
undo.append(self.end_change_children())
return CreateListUndo(undo)
else:
return NullUndo
def FilterAll(self, func):
return filter(func, self.GetObjects())
def NumObjects(self):
return len(self.objects)
def Info(self):
return _("Compound with %d objects") % len(self.objects)
def AddStyle(self, style):
undo = self.ForAllUndo(lambda o, style = style: o.AddStyle(style))
return undo
def Properties(self):
return EmptyProperties
def SetProperties(self, **kw):
self.del_lazy_attrs()
func = lambda o, kw = kw: apply(o.SetProperties, (), kw)
undo = self.ForAllUndo(func)
return undo
def Hit(self, p, rect, device):
test = rect.overlaps
objects = self.objects
for obj_idx in range(len(objects) - 1, -1, -1):
obj = objects[obj_idx]
if test(obj.bounding_rect):
if obj.Hit(p, rect, device):
return obj_idx + 1
return 0
def Transform(self, trafo):
self.del_lazy_attrs()
undo = self.ForAllUndo(lambda o, t = trafo: o.Transform(t))
return undo
def Translate(self, offset):
undo = self.ForAllUndo(lambda o, p = offset: o.Translate(p))
return undo
def DrawShape(self, device, rect = None):
if rect:
test = rect.overlaps
for o in self.objects:
if test(o.bounding_rect):
o.DrawShape(device, rect)
else:
for obj in self.objects:
obj.DrawShape(device)
def PickObject(self, point, rect, device):
objects = self.objects[:]
objects.reverse()
test = rect.overlaps
for obj in objects:
if test(obj.bounding_rect):
if obj.is_Compound:
result = obj.PickObject(point, rect, device)
if result:
break
elif obj.Hit(point, rect, device):
result = obj
break
else:
result = None
return result
def Blend(self, other, frac1, frac2):
try:
objs = self.objects
oobjs = other.objects
blended = []
for i in range(min(len(objs), len(oobjs))):
blended.append(Blend(objs[i], oobjs[i], frac1, frac2))
return Compound(blended)
except:
raise MismatchError
def GetObjectHandle(self, multiple):
return self.ForAll(lambda o: o.GetObjectHandle(1))
def SelectFirstChild(self):
if self.allow_traversal and self.objects:
return self.objects[0]
def SelectLastChild(self):
if self.allow_traversal and self.objects:
return self.objects[-1]
def SelectNextChild(self, child, idx):
if self.allow_traversal and len(self.objects) > idx + 1:
return self.objects[idx + 1]
def SelectPreviousChild(self, child, idx):
if self.allow_traversal and len(self.objects) > idx - 1 and idx > 0:
return self.objects[idx - 1]
class EditableCompound(Compound):
allow_traversal = 1
def SelectSubobject(self, p, rect, device, path = None, *rest):
test = rect.overlaps
if path is None:
return self
if path:
path_idx = path[0]
path = path[1:]
else:
path_idx = -1
path = None
objects = self.objects
for obj_idx in range(len(objects) - 1, -1, -1):
obj = objects[obj_idx]
if test(obj.bounding_rect) and obj.Hit(p, rect, device):
if obj_idx == path_idx:
result = obj.SelectSubobject(p, rect, device, path)
else:
result = obj.SelectSubobject(p, rect, device)
return prepend_idx(obj_idx, result)
return None
def Insert(self, obj, at):
# Insert OBJ into the object hierarchy at the position described
# by AT. AT should be either an integer or a tuple of integers.
# OBJ can be a graphics object of a list of such objects.
#
# If AT is a tuple of 2 or more ints, self's child at AT[0] has
# to be a compound object and its Insert method is called with
# OBJ and AT[1:] as arguments.
#
# If AT is an int or a singleton of one int, insert OBJ at that
# position in self's children. If OBJ is a graphics object, this
# works just like a list objects insert method (insert(AT,
# OBJ)). If its a list of graphics objects this method
# effectively assigns that list to the slice AT:AT.
#
# As a side effect, this method calls the following methods of
# the inserted objects:
#
# obj.SetDocument(self.document)
# obj.SetParent(self)
# obj.Connect()
#
# Return a tuple (SELINFO, UNDO), where SELINFO is selection
# info for the inserted objects at their new positions, and UNDO
# is the appropriate undo info.
#
# If self is modified directly, issue a CHANGED message.
undo_info = None
try:
if type(at) == TupleType and at:
if len(at) == 1:
at = at[0]
else:
child = at[0]
at = at[1:]
sel_info, undo_info = self.objects[child].Insert(obj, at)
sel_info = prepend_idx(child, sel_info)
return (sel_info, undo_info)
if type(at) != IntType or at > len(self.objects):
at = len(self.objects)
if type(obj) == InstanceType:
self.objects.insert(at, obj)
obj.SetDocument(self.document)
obj.SetParent(self)
obj.Connect()
sel_info = build_info(at, obj)
undo_info = (self.Remove, obj, at)
else:
self.objects[at:at] = obj
for o in obj:
# XXX: should we have undo info for these:
o.SetDocument(self.document)
o.SetParent(self)
o.Connect()
sel_info = select_range(at, obj)
undo_info = (self.RemoveSlice, at, at + len(obj))
self._changed()
return (sel_info, undo_info)
except:
if undo_info is not None:
Undo(undo_info)
raise
def _insert_with_undo(self, obj, at):
# The same as the Insert method but return only the undo info.
return self.Insert(obj, at)[1]
def do_remove_child(self, idx):
obj = self.objects[idx]
del self.objects[idx]
obj.Disconnect()
obj.SetParent(None)
self._changed()
return (self._insert_with_undo, obj, idx)
def Remove(self, obj, idx = None):
if type(idx) == TupleType:
if len(idx) == 1:
idx = idx[0]
else:
return self.objects[idx[0]].Remove(obj, idx[1:])
if idx is None:
idx = self.objects.index(obj)
elif self.objects[idx] is not obj:
raise ValueError, 'Compound.Remove(): invalid index'
return self.do_remove_child(idx)
def RemoveSlice(self, min, max):
objs = self.objects[min:max]
self.objects[min:max] = []
for obj in objs:
obj.Disconnect()
obj.SetParent(None)
self._changed()
return (self._insert_with_undo, objs, min)
def RemoveObjects(self, infolist):
if not infolist:
return NullUndo
sliced = list_to_tree_sliced(infolist)
sliced.reverse() # important!
undo = [self.begin_change_children()]
try:
for start, end in sliced:
if type(end) == IntType:
undo.append(self.RemoveSlice(start, end))
elif type(end) == ListType:
undo.append(self.objects[start].RemoveObjects(end))
else:
undo.append(self.Remove(end, start))
undo.append(self.end_change_children())
return CreateListUndo(undo)
except:
Undo(CreateListUndo(undo))
raise
def ReplaceChild(self, child, object):
# replace self's child child with object. Return undo info
idx = self.objects.index(child)
self.objects[idx] = object
object.SetParent(self)
object.SetDocument(self.document)
child.SetParent(None)
self._changed()
return (self.ReplaceChild, object, child)
def permute_objects(self, permutation):
# permutation must be a list of ints and len(permutation) must be
# equal to len(self.objects). permutation[i] is the index of the
# object that is moved to index i in the result.
objects = self.objects
length = len(objects)
identity = range(length)
if permutation == identity:
return NullUndo
if len(objects) != len(permutation):
raise ValueError, 'len(permutation) != len(self.objects)'
result = map(operator.getitem, [objects] * length, permutation)
inverse = [0] * length
map(operator.setitem, [inverse] * length, permutation, identity)
self.objects = result
self._changed()
return (self.permute_objects, inverse)
def move_objects_to_top(self, infolist, to_bottom = 0):
# Implement the public methods MoveToTop (if to_bottom is false)
# and MoveToBottom (if to_bottom is true).
sliced = list_to_tree_sliced(infolist)
sliced.reverse()
undo = [self.begin_change_children()]
selection = []
idxs = []
permutation = range(len(self.objects))
try:
for start, end in sliced:
if type(end) == IntType:
# a contiguous range of self's children (start:end)
idxs[:0] = permutation[start:end]
del permutation[start:end]
elif type(end) == ListType:
# children of self.objects[start]
child = self.objects[start]
sel, undo_info = child.move_objects_to_top(end, to_bottom)
if undo_info is not NullUndo:
undo.append(undo_info)
selection = selection + prepend_idx(start, sel)
else:
# a single object (self.object[start])
idxs.insert(0, start)
del permutation[start]
if idxs:
# direct children of self are involved: apply the
# permutation
if to_bottom:
permutation = idxs + permutation
else:
permutation = permutation + idxs
undo_info = self.permute_objects(permutation)
if undo_info is not NullUndo:
undo.append(undo_info)
# finished:
undo.append(self.end_change_children())
if len(undo) <= 2:
# We haven't really done anything (undo has length 2),
# so we just pass the selection info back unchanged
selection = infolist
undo = NullUndo
else:
# We have done something, so figure out the new
# selection info
undo = CreateListUndo(undo)
if to_bottom:
selection = selection \
+ select_range(0, self.objects[:len(idxs)])
else:
min = len(self.objects) - len(idxs)
selection = selection \
+ select_range(min, self.objects[min:])
return (selection, undo)
except:
# Ooops, something's gone wrong. Undo everything we've done
# so far... (hmm, this currently fails to undo everything if
# undo.append(undo_info) fails... (the undo_info involved
# would be lost))
Undo(CreateListUndo(undo))
raise
def MoveObjectsToTop(self, infolist):
return self.move_objects_to_top(infolist)
def MoveObjectsToBottom(self, infolist):
return self.move_objects_to_top(infolist, to_bottom = 1)
def MoveObjectsDown(self, infolist):
sliced = list_to_tree_sliced(infolist)
undo = [self.begin_change_children()]
selection = []
permutation = range(len(self.objects))
objects = self.objects
try:
for start, end in sliced:
if type(end) == IntType:
if start > 0:
temp = permutation[start:end]
del permutation[start:end]
permutation[start - 1:start - 1] = temp
selection = selection +select_range(start - 1,
objects[start:end])
else:
selection = selection +select_range(start,
objects[start:end])
elif type(end) == ListType:
sel, undo_info = objects[start].MoveObjectsDown(end)
if undo_info is not NullUndo:
undo.append(undo_info)
selection = selection + prepend_idx(start, sel)
else:
if start > 0:
del permutation[start]
permutation.insert(start - 1, start)
selection.append(build_info(start - 1, objects[start]))
else:
selection.append(build_info(start, objects[start]))
undo_info = self.permute_objects(permutation)
if undo_info is not NullUndo:
undo.append(undo_info)
undo.append(self.end_change_children())
if len(undo) <= 2:
undo = NullUndo
selection = infolist
else:
undo = CreateListUndo(undo)
return (selection, undo)
except:
Undo(CreateListUndo(undo))
raise
def MoveObjectsUp(self, infolist):
sliced = list_to_tree_sliced(infolist)
sliced.reverse()
undo = [self.begin_change_children()]
selection = []
permutation = range(len(self.objects))
objects = self.objects
max = len(objects)
try:
for start, end in sliced:
if type(end) == IntType:
if end < max:
temp = permutation[start:end]
del permutation[start:end]
permutation[start + 1:start + 1] = temp
selection = selection + select_range(start + 1,
objects[start:end])
else:
selection = selection + select_range(start,
objects[start:end])
elif type(end) == ListType:
sel, undo_info = objects[start].MoveObjectsUp(end)
if undo_info is not NullUndo:
undo.append(undo_info)
selection = selection + prepend_idx(start, sel)
else:
if start < max - 1:
del permutation[start]
permutation.insert(start + 1, start)
selection.append(build_info(start + 1, objects[start]))
else:
selection.append(build_info(start, objects[start]))
undo_info = self.permute_objects(permutation)
if undo_info is not NullUndo:
undo.append(undo_info)
undo.append(self.end_change_children())
if len(undo) <= 2:
undo = NullUndo
selection = infolist
else:
undo = CreateListUndo(undo)
return (selection, undo)
except:
Undo(CreateListUndo(undo))
raise
def DuplicateObjects(self, infolist, offset):
infolist = list_to_tree2(infolist)
objects = self.objects
undo = [self.begin_change_children()]
selection = []
added = 0
try:
for idx, obj in infolist:
idx = idx + added
if type(obj) == ListType:
# duplicate in subobj
sel, undoinfo = objects[idx].DuplicateObjects(obj, offset)
undo.append(undoinfo)
selection = selection + prepend_idx(idx, sel)
else:
obj = obj.Duplicate()
obj.Translate(offset)
sel, undoinfo = self.Insert(obj, idx + 1)
undo.append(undoinfo)
selection.append(sel)
added = added + 1
undo.append(self.end_change_children())
return (selection, CreateListUndo(undo))
except:
Undo(CreateListUndo(undo))
raise
|