]> git.saurik.com Git - wxWidgets.git/commitdiff
Added a Python port of the OGL library, deprecated the C++ wrapped version.
authorRobin Dunn <robin@alldunn.com>
Wed, 26 May 2004 02:13:04 +0000 (02:13 +0000)
committerRobin Dunn <robin@alldunn.com>
Wed, 26 May 2004 02:13:04 +0000 (02:13 +0000)
git-svn-id: https://svn.wxwidgets.org/svn/wx/wxWidgets/trunk@27447 c3d73ce0-8a6f-49c7-b76d-6d57e0e08775

17 files changed:
wxPython/contrib/ogl/ogl.i
wxPython/demo/Main.py
wxPython/demo/OGL.py
wxPython/distrib/DIRLIST
wxPython/distrib/make_installer.py
wxPython/docs/CHANGES.txt
wxPython/docs/MigrationGuide.txt
wxPython/setup.py
wxPython/wx/lib/ogl/__init__.py [new file with mode: 0644]
wxPython/wx/lib/ogl/basic.py [new file with mode: 0644]
wxPython/wx/lib/ogl/bmpshape.py [new file with mode: 0644]
wxPython/wx/lib/ogl/canvas.py [new file with mode: 0644]
wxPython/wx/lib/ogl/composit.py [new file with mode: 0644]
wxPython/wx/lib/ogl/diagram.py [new file with mode: 0644]
wxPython/wx/lib/ogl/divided.py [new file with mode: 0644]
wxPython/wx/lib/ogl/lines.py [new file with mode: 0644]
wxPython/wx/lib/ogl/oglmisc.py [new file with mode: 0644]

index b0fe5ede3ab247f572a0088d05072e326d910a21..419d5a4361fed4b0aa32051dbbf4c8518496dc45 100644 (file)
@@ -12,7 +12,7 @@
 
 %define DOCSTRING
 "The Object Graphics Library provides for simple drawing and manipulation
-of 2D objects."
+of 2D objects.  (This version is deprecated, please use wx.lib.ogl instead.)"
 %enddef
 %module(docstring=DOCSTRING) ogl
 
@@ -29,6 +29,12 @@ of 2D objects."
 %pythoncode { wx = _core }
 %pythoncode { __docfilter__ = wx.__DocFilter(globals()) }
 
+%pythoncode {
+    import warnings
+    warnings.warn("This module is deprecated.  Please use the wx.lib.ogl pacakge instead.",    
+                  DeprecationWarning, stacklevel=2)
+}
+
 
 MAKE_CONST_WXSTRING_NOSWIG(EmptyString);
 
index 7cd330ffa47696085b8d9b6d0586608b4069b503..899e4aa56313a56bb2e4470a8beb15b52518c99c 100644 (file)
@@ -30,7 +30,7 @@ import images
 
 _treeList = [
     # new stuff
-    ('Recent Additions', [
+    ('Recent Additions and Updates', [
         'VListBox',
         'Listbook',
         'MaskedNumCtrl',
index 7412974ea1b88a0999b254c0ad61bdb350c80ae0..cb9b11e8ffdad960361ebb242fc99e1d7dbb4559 100644 (file)
@@ -1,15 +1,19 @@
+# -*- coding: iso-8859-1 -*-
 # 11/20/2003 - Jeff Grimmett (grimmtooth@softhome.net)
 #
 # o Updated for wx namespace
 # 
+# 20040508 - Pierre Hjälm
+#
+# o Changed to use the python version of OGL
+# o Added TextShape, CompositeShape and CompositeShape with divisions
+#
 
-import  wx
-import  wx.ogl  as  ogl
+import wx
+import wx.lib.ogl as ogl
 
 import  images
 
-##wx.Trap()
-
 #----------------------------------------------------------------------
 
 class DiamondShape(ogl.PolygonShape):
@@ -20,13 +24,6 @@ class DiamondShape(ogl.PolygonShape):
         if h == 0.0:
             h = 60.0
 
-        # Either ogl.RealPoints or 2-tuples of floats  works.
-
-        #points = [ ogl.RealPoint(0.0,    -h/2.0),
-        #          ogl.RealPoint(w/2.0,  0.0),
-        #          ogl.RealPoint(0.0,    h/2.0),
-        #          ogl.RealPoint(-w/2.0, 0.0),
-        #          ]
         points = [ (0.0,    -h/2.0),
                    (w/2.0,  0.0),
                    (0.0,    h/2.0),
@@ -44,6 +41,67 @@ class RoundedRectangleShape(ogl.RectangleShape):
         self.SetCornerRadius(-0.3)
 
 
+#----------------------------------------------------------------------
+
+class CompositeDivisionShape(ogl.CompositeShape):
+    def __init__(self, canvas):
+        ogl.CompositeShape.__init__(self)
+
+        self.SetCanvas(canvas)
+
+        # create a division in the composite
+        self.MakeContainer()
+
+        # add a shape to the original division
+        shape2 = ogl.RectangleShape(40, 60)
+        self.GetDivisions()[0].AddChild(shape2)
+
+        # now divide the division so we get 2
+        self.GetDivisions()[0].Divide(wx.HORIZONTAL)
+
+        # and add a shape to the second division (and move it to the
+        # centre of the division)
+        shape3 = ogl.CircleShape(40)
+        shape3.SetBrush(wx.CYAN_BRUSH)
+        self.GetDivisions()[1].AddChild(shape3)
+        shape3.SetX(self.GetDivisions()[1].GetX())
+
+        for division in self.GetDivisions():
+            division.SetSensitivityFilter(0)
+        
+#----------------------------------------------------------------------
+
+class CompositeShape(ogl.CompositeShape):
+    def __init__(self, canvas):
+        ogl.CompositeShape.__init__(self)
+
+        self.SetCanvas(canvas)
+
+        constraining_shape = ogl.RectangleShape(120, 100)
+        constrained_shape1 = ogl.CircleShape(50)
+        constrained_shape2 = ogl.RectangleShape(80, 20)
+
+        constraining_shape.SetBrush(wx.BLUE_BRUSH)
+        constrained_shape2.SetBrush(wx.RED_BRUSH)
+        
+        self.AddChild(constraining_shape)
+        self.AddChild(constrained_shape1)
+        self.AddChild(constrained_shape2)
+
+        constraint = ogl.Constraint(ogl.CONSTRAINT_MIDALIGNED_BOTTOM, constraining_shape, [constrained_shape1, constrained_shape2])
+        self.AddConstraint(constraint)
+        self.Recompute()
+
+        # If we don't do this, the shapes will be able to move on their
+        # own, instead of moving the composite
+        constraining_shape.SetDraggable(False)
+        constrained_shape1.SetDraggable(False)
+        constrained_shape2.SetDraggable(False)
+
+        # If we don't do this the shape will take all left-clicks for itself
+        constraining_shape.SetSensitivityFilter(0)
+
+        
 #----------------------------------------------------------------------
 
 class DividedShape(ogl.DividedShape):
@@ -88,7 +146,7 @@ class DividedShape(ogl.DividedShape):
 
     def OnSizingEndDragLeft(self, pt, x, y, keys, attch):
         print "***", self
-        self.base_OnSizingEndDragLeft(pt, x, y, keys, attch)
+        ogl.DividedShape.OnSizingEndDragLeft(self, pt, x, y, keys, attch)
         self.SetRegionSizes()
         self.ReformatRegions()
         self.GetCanvas().Refresh()
@@ -103,15 +161,14 @@ class MyEvtHandler(ogl.ShapeEvtHandler):
         self.statbarFrame = frame
 
     def UpdateStatusBar(self, shape):
-        x,y = shape.GetX(), shape.GetY()
+        x, y = shape.GetX(), shape.GetY()
         width, height = shape.GetBoundingBoxMax()
-        self.statbarFrame.SetStatusText("Pos: (%d,%d)  Size: (%d, %d)" %
+        self.statbarFrame.SetStatusText("Pos: (%d, %d)  Size: (%d, %d)" %
                                         (x, y, width, height))
 
 
-    def OnLeftClick(self, x, y, keys = 0, attachment = 0):
+    def OnLeftClick(self, x, y, keys=0, attachment=0):
         shape = self.GetShape()
-        print shape.__class__, shape.GetClassName()
         canvas = shape.GetCanvas()
         dc = wx.ClientDC(canvas)
         canvas.PrepareDC(dc)
@@ -142,9 +199,9 @@ class MyEvtHandler(ogl.ShapeEvtHandler):
         self.UpdateStatusBar(shape)
 
 
-    def OnEndDragLeft(self, x, y, keys = 0, attachment = 0):
+    def OnEndDragLeft(self, x, y, keys=0, attachment=0):
         shape = self.GetShape()
-        self.base_OnEndDragLeft(x, y, keys, attachment)
+        ogl.ShapeEvtHandler.OnEndDragLeft(self, x, y, keys, attachment)
 
         if not shape.Selected():
             self.OnLeftClick(x, y, keys, attachment)
@@ -153,12 +210,12 @@ class MyEvtHandler(ogl.ShapeEvtHandler):
 
 
     def OnSizingEndDragLeft(self, pt, x, y, keys, attch):
-        self.base_OnSizingEndDragLeft(pt, x, y, keys, attch)
+        ogl.ShapeEvtHandler.OnSizingEndDragLeft(self, pt, x, y, keys, attch)
         self.UpdateStatusBar(self.GetShape())
 
 
     def OnMovePost(self, dc, x, y, oldX, oldY, display):
-        self.base_OnMovePost(dc, x, y, oldX, oldY, display)
+        ogl.ShapeEvtHandler.OnMovePost(self, dc, x, y, oldX, oldY, display)
         self.UpdateStatusBar(self.GetShape())
 
 
@@ -188,29 +245,44 @@ class TestWindow(ogl.ShapeCanvas):
         rRectBrush = wx.Brush("MEDIUM TURQUOISE", wx.SOLID)
         dsBrush = wx.Brush("WHEAT", wx.SOLID)
 
+        self.MyAddShape(
+            CompositeDivisionShape(self), 
+            310, 310, wx.BLACK_PEN, wx.BLUE_BRUSH, "Division"
+            )
+        
+        self.MyAddShape(
+            CompositeShape(self), 
+            100, 260, wx.BLACK_PEN, wx.RED_BRUSH, "Composite"
+            )
+        
         self.MyAddShape(
             ogl.CircleShape(80), 
             100, 100, wx.Pen(wx.BLUE, 3), wx.GREEN_BRUSH, "Circle"
             )
             
+        self.MyAddShape(
+            ogl.TextShape(45, 30), 
+            205, 60, wx.GREEN_PEN, wx.LIGHT_GREY_BRUSH, "Text"
+            )
+
         self.MyAddShape(
             ogl.RectangleShape(85, 50), 
             305, 60, wx.BLACK_PEN, wx.LIGHT_GREY_BRUSH, "Rectangle"
             )
 
         ds = self.MyAddShape(
-                    DividedShape(140, 150, self), 
-                    495, 145, wx.BLACK_PEN, dsBrush, ''
-                    )
+            DividedShape(140, 150, self), 
+            515, 145, wx.BLACK_PEN, dsBrush, ''
+            )
 
         self.MyAddShape(
             DiamondShape(90, 90), 
-            345, 235, wx.Pen(wx.BLUE, 3, wx.DOT), wx.RED_BRUSH, "Polygon"
+            445, 305, wx.Pen(wx.BLUE, 3, wx.DOT), wx.RED_BRUSH, "Polygon"
             )
             
         self.MyAddShape(
-            RoundedRectangleShape(95,70), 
-            140, 255, wx.Pen(wx.RED, 2), rRectBrush, "Rounded Rect"
+            RoundedRectangleShape(95, 70), 
+            345, 145, wx.Pen(wx.RED, 2), rRectBrush, "Rounded Rect"
             )
 
         bmp = images.getTest2Bitmap()
@@ -219,7 +291,7 @@ class TestWindow(ogl.ShapeCanvas):
 
         s = ogl.BitmapShape()
         s.SetBitmap(bmp)
-        self.MyAddShape(s, 225, 150, None, None, "Bitmap")
+        self.MyAddShape(s, 225, 130, None, None, "Bitmap")
 
         dc = wx.ClientDC(self)
         self.PrepareDC(dc)
@@ -241,14 +313,15 @@ class TestWindow(ogl.ShapeCanvas):
             self.diagram.AddShape(line)
             line.Show(True)
 
-            # for some reason, the shapes have to be moved for the line to show up...
-            fromShape.Move(dc, fromShape.GetX(), fromShape.GetY())
-
-        self.Bind(wx.EVT_WINDOW_DESTROY, self.OnDestroy)
-
 
     def MyAddShape(self, shape, x, y, pen, brush, text):
-        shape.SetDraggable(True, True)
+        # Composites have to be moved for all children to get in place
+        if isinstance(shape, ogl.CompositeShape):
+            dc = wx.ClientDC(self)
+            self.PrepareDC(dc)
+            shape.Move(dc, x, y)
+        else:
+            shape.SetDraggable(True, True)
         shape.SetCanvas(self)
         shape.SetX(x)
         shape.SetY(y)
@@ -268,16 +341,6 @@ class TestWindow(ogl.ShapeCanvas):
         return shape
 
 
-    def OnDestroy(self, evt):
-        # Do some cleanup
-        for shape in self.diagram.GetShapeList():
-            if shape.GetParent() == None:
-                shape.SetCanvas(None)
-                shape.Destroy()
-
-        self.diagram.Destroy()
-
-
     def OnBeginDragLeft(self, x, y, keys):
         self.log.write("OnBeginDragLeft: %s, %s, %s\n" % (x, y, keys))
 
@@ -298,25 +361,32 @@ def runTest(frame, nb, log):
     
 #----------------------------------------------------------------------
 
-# The OGL library holds some resources that need to be freed before
-# the app shuts down.
-class __Cleanup:
-    def __del__(self, cleanup=ogl.OGLCleanUp):
-        cleanup()
 
-# When this module gets cleaned up by Python then __cu will be cleaned
-# up and it's __dell__ is called, which will then call ogl.OGLCleanUp.
-__cu = __Cleanup()
+overview = """<html><body>
+<h2>Object Graphics Library</h2>
 
-
-overview = """\
 The Object Graphics Library is a library supporting the creation and
 manipulation of simple and complex graphic images on a canvas.
 
+<p>The OGL library was originally written in C++ and provided to
+wxPython via an extension module wrapper as is most of the rest of
+wxPython.  The code has now been ported to Python (with many thanks to
+Pierre Hjälm!) in order to make it be more easily maintainable and
+less likely to get rusty because nobody cares about the C++ lib any
+more.
+
+<p>The Python version should be mostly drop-in compatible with the
+wrapped C++ version, except for the location of the package
+(wx.lib.ogl instead of wx.ogl) and that the base class methods are
+called the normal Python way (superclass.Method(self, ...)) instead of the
+hacky way that had to be done to support overloaded methods with the
+old SWIG (self.base_Method(...))
+
+
 """
 
 if __name__ == '__main__':
-    import sys,os
+    import sys, os
     import run
     run.main(['', os.path.basename(sys.argv[0])] + sys.argv[1:])
 
index 179edf120137bbe360a9cfd197e98e5665bd99d0..2fc48e13ae2a13814c6ee6cc51443694308493b8 100644 (file)
@@ -71,6 +71,7 @@ wxPython/wx/lib/colourchooser
 wxPython/wx/lib/editor
 wxPython/wx/lib/masked
 wxPython/wx/lib/mixins
+wxPython/wx/lib/ogl
 wxPython/wx/py
 wxPython/wx/py/tests
 wxPython/wxPython
index 49ac90cd2e4b354a2c4be847becd48cd478f20f7..f16f158a73b261dbdb8e1b1105a601c3590f5a46 100644 (file)
@@ -106,6 +106,8 @@ Source: "wx\lib\colourchooser\*.py";           DestDir: "{app}\wx\lib\colourchoo
 Source: "wx\lib\editor\*.py";                  DestDir: "{app}\wx\lib\editor"; Components: core
 Source: "wx\lib\editor\*.txt";                 DestDir: "{app}\wx\lib\editor"; Components: core
 Source: "wx\lib\mixins\*.py";                  DestDir: "{app}\wx\lib\mixins"; Components: core
+Source: "wx\lib\masked\*.py";                  DestDir: "{app}\wx\lib\masked"; Components: core
+Source: "wx\lib\ogl\*.py";                     DestDir: "{app}\wx\lib\ogl"; Components: core
 Source: "wx\py\*.py";                          DestDir: "{app}\wx\py"; Components: core
 Source: "wx\py\*.txt";                         DestDir: "{app}\wx\py"; Components: core
 Source: "wx\py\*.ico";                         DestDir: "{app}\wx\py"; Components: core
index 30254e59c48f37cafd76f30c8acadc0d7e1b853a..2873dcb92e73e74689cd85eab54cdc51c6cb4b66 100644 (file)
@@ -80,6 +80,17 @@ remaining compatible with "C".
 Switched gizmos.TreeListCtrl to the newer version of the code from the
 wxCode project.
 
+OGL is dead! LONG LIVE OGL!  (Oops, sorry.  A bit of my dramatic side
+leaked out there...)  The wx.ogl module has been deprecated in favor
+of the new Python port of the OGL library located at wx.lib.ogl
+contributed by Pierre Hjälm.  This will hopefully greatly extend the
+life of OGL within wxPython by making it more easily maintainable and
+less prone to getting rusty as there seems to be less and less
+interest in maintaining the C++ version.  At this point there are just
+a couple minor known compatibility differences, please see the
+MigrationGuide_ file for details.
+
+.. _MigrationGuide: MigrationGuide.html
 
 
 
index d2f34ac2b1db0831445c79af7824ec9d86e8805f..f4f08c27d2d32e4433f22b1de94f73dfe10c3ae5 100644 (file)
@@ -3,10 +3,10 @@ wxPython 2.5 Migration Guide
 ============================
 
 This document will help explain some of the major changes in wxPython
-2.5 and let you know what you need to do to adapt your programs to
-those changes.  Be sure to also check in the CHANGES_ file like
-usual to see info about the not so major changes and other things that
-have been added to wxPython.
+2.5 since the 2.4 series and let you know what you need to do to adapt
+your programs to those changes.  Be sure to also check in the CHANGES_
+file like usual to see info about the not so major changes and other
+things that have been added to wxPython.
 
 .. _CHANGES: CHANGES.html
 
@@ -20,8 +20,8 @@ The **wxWindows** project and library is now known as
 .. _here: http://www.wxwidgets.org/name.htm
 
 This won't really affect wxPython all that much, other than the fact
-that the wxwindows.org domain name will be changing to wxwidgets.org,
-so mail list, CVS, and etc. addresses will be changing.  We're going
+that the wxwindows.org domain name has changed to wxwidgets.org,
+so mail list, CVS, and etc. addresses have also changed.  We're going
 to try and smooth the transition as much as possible, but I wanted you
 all to be aware of this change if you run into any issues.
 
@@ -585,6 +585,34 @@ provided by the makers of the ActiveX control that you are using.
 
 
 
+OGL is dead! LONG LIVE OGL!
+---------------------------
+
+The wx.ogl module has been deprecated in favor of the new Python port
+of the OGL library located at wx.lib.ogl contributed by Pierre Hjälm.
+This will hopefully greatly extend the life of OGL within wxPython by
+making it more easily maintainable and less prone to getting rusty as
+there seems to be less and less interest in maintaining the C++
+version.  
+
+There are only a few known compatibility issues at this time.  First
+is the location of OGL.  The deprecated version is located in the
+wx.ogl module, and the new version is in the wx.lib.ogl package.  So
+this just means that to start using the new version you need to adjust
+your imports.  So if your code currently has something like this::
+
+     import wx
+     import wx.ogl as ogl
+
+Then just change it to this::
+
+     import wx
+     import wx.lib.ogl as ogl
+
+
+
+
+
 Obsolete Modules
 ----------------
 
index 7822c28dcc5d5a8b03f43cfaa3eec8763caa37db..4960145ec4d2a1decc30b5f6f5be622112bfe992 100755 (executable)
@@ -707,6 +707,7 @@ if __name__ == "__main__":
                           'wx.lib.editor',
                           'wx.lib.masked',
                           'wx.lib.mixins',
+                          'wx.lib.ogl',
                           'wx.py',
                           'wx.tools',
                           'wx.tools.XRCed',
diff --git a/wxPython/wx/lib/ogl/__init__.py b/wxPython/wx/lib/ogl/__init__.py
new file mode 100644 (file)
index 0000000..667ed0a
--- /dev/null
@@ -0,0 +1,14 @@
+"""
+The Object Graphics Library provides for simple drawing and manipulation
+of 2D objects.
+"""
+
+__all__ = ["basic", "diagram", "canvas", "lines", "bmpshape", "divided", "composit"]
+
+from basic import *
+from diagram import *
+from canvas import *
+from lines import *
+from bmpshape import *
+from divided import *
+from composit import *
diff --git a/wxPython/wx/lib/ogl/basic.py b/wxPython/wx/lib/ogl/basic.py
new file mode 100644 (file)
index 0000000..3aacb59
--- /dev/null
@@ -0,0 +1,3173 @@
+# -*- coding: iso-8859-1 -*-
+#----------------------------------------------------------------------------
+# Name:         basic.py
+# Purpose:      The basic OGL shapes
+#
+# Author:       Pierre Hjälm (from C++ original by Julian Smart)
+#
+# Created:      20040508
+# RCS-ID:       
+# Copyright:    (c) 2004 Pierre Hjälm - 1998 Julian Smart
+# Licence:      wxWindows license
+#----------------------------------------------------------------------------
+
+from __future__ import division
+
+import wx
+from math import pi, sqrt, atan, sin, cos
+
+from oglmisc import *
+
+DragOffsetX = 0.0
+DragOffsetY = 0.0
+
+
+def OGLInitialize():
+    global WhiteBackgroundPen, WhiteBackgroundBrush, TransparentPen
+    global BlackForegroundPen, NormalFont
+    
+    WhiteBackgroundPen = wx.Pen(wx.WHITE, 1, wx.SOLID)
+    WhiteBackgroundBrush = wx.Brush(wx.WHITE, wx.SOLID)
+
+    TransparentPen = wx.Pen(wx.WHITE, 1, wx.TRANSPARENT)
+    BlackForegroundPen = wx.Pen(wx.BLACK, 1, wx.SOLID)
+
+    NormalFont = wx.Font(10, wx.SWISS, wx.NORMAL, wx.NORMAL)
+
+
+def OGLCleanUp():
+    pass
+
+
+class ShapeTextLine(object):
+    def __init__(self, the_x, the_y, the_line):
+        self._x = the_x
+        self._y = the_y
+        self._line = the_line
+
+    def GetX(self):
+        return self._x
+
+    def GetY(self):
+        return self._y
+
+    def SetX(self, x):
+        self._x = x
+
+    def SetY(self, y):
+        self._y = y
+
+    def SetText(self, text):
+        self._line = text
+
+    def GetText(self):
+        return self._line
+
+
+    
+class ShapeEvtHandler(object):
+    def __init__(self, prev = None, shape = None):
+        self._previousHandler = prev
+        self._handlerShape = shape
+
+    def __del__(self):
+        pass
+
+    def SetShape(self, sh):
+        self._handlerShape = sh
+
+    def GetShape(self):
+        return self._handlerShape
+
+    def SetPreviousHandler(self, handler):
+        self._previousHandler = handler
+
+    def GetPreviousHandler(self):
+        return self._previousHandler
+    
+    def OnDraw(self, dc):
+        if self._previousHandler:
+            self._previousHandler.OnDraw(dc)
+
+    def OnMoveLinks(self, dc):
+        if self._previousHandler:
+            self._previousHandler.OnMoveLinks(dc)
+            
+    def OnMoveLink(self, dc, moveControlPoints = True):
+        if self._previousHandler:
+            self._previousHandler.OnMoveLink(dc, moveControlPoints)
+            
+    def OnDrawContents(self, dc):
+        if self._previousHandler:
+            self._previousHandler.OnDrawContents(dc)
+            
+    def OnDrawBranches(self, dc, erase = False):
+        if self._previousHandler:
+            self._previousHandler.OnDrawBranches(dc, erase = erase)
+
+    def OnSize(self, x, y):
+        if self._previousHandler:
+            self._previousHandler.OnSize(x, y)
+            
+    def OnMovePre(self, dc, x, y, old_x, old_y, display = True):
+        if self._previousHandler:
+            return self._previousHandler.OnMovePre(dc, x, y, old_x, old_y, display)
+        else:
+            return True
+
+    def OnMovePost(self, dc, x, y, old_x, old_y, display = True):
+        if self._previousHandler:
+            return self._previousHandler.OnMovePost(dc, x, y, old_x, old_y, display)
+        else:
+            return True
+
+    def OnErase(self, dc):
+        if self._previousHandler:
+            self._previousHandler.OnErase(dc)
+
+    def OnEraseContents(self, dc):
+        if self._previousHandler:
+            self._previousHandler.OnEraseContents(dc)
+
+    def OnHighlight(self, dc):
+        if self._previousHandler:
+            self._previousHandler.OnHighlight(dc)
+
+    def OnLeftClick(self, x, y, keys, attachment):
+        if self._previousHandler:
+            self._previousHandler.OnLeftClick(x, y, keys, attachment)
+            
+    def OnLeftDoubleClick(self, x, y, keys = 0, attachment = 0):
+        if self._previousHandler:
+            self._previousHandler.OnLeftDoubleClick(x, y, keys, attachment)
+
+    def OnRightClick(self, x, y, keys = 0, attachment = 0):
+        if self._previousHandler:
+            self._previousHandler.OnRightClick(x, y, keys, attachment)
+
+    def OnDragLeft(self, draw, x, y, keys = 0, attachment = 0):
+        if self._previousHandler:
+            self._previousHandler.OnDragLeft(draw, x, y, keys, attachment)
+
+    def OnBeginDragLeft(self, x, y, keys = 0, attachment = 0):
+        if self._previousHandler:
+            self._previousHandler.OnBeginDragLeft(x, y, keys, attachment)
+
+    def OnEndDragLeft(self, x, y, keys = 0, attachment = 0):
+        if self._previousHandler:
+            self._previousHandler.OnEndDragLeft(x, y, keys, attachment)
+        
+    def OnDragRight(self, draw, x, y, keys = 0, attachment = 0):
+        if self._previousHandler:
+            self._previousHandler.OnDragRight(draw, x, y, keys, attachment)
+
+    def OnBeginDragRight(self, x, y, keys = 0, attachment = 0):
+        if self._previousHandler:
+            self._previousHandler.OnBeginDragRight(x, y, keys, attachment)
+
+    def OnEndDragRight(self, x, y, keys = 0, attachment = 0):
+        if self._previousHandler:
+            self._previousHandler.OnEndDragRight(x, y, keys, attachment)
+
+    # Control points ('handles') redirect control to the actual shape,
+    # to make it easier to override sizing behaviour.
+    def OnSizingDragLeft(self, pt, draw, x, y, keys = 0, attachment = 0):
+        if self._previousHandler:
+            self._previousHandler.OnSizingDragLeft(pt, draw, x, y, keys, attachment)
+
+    def OnSizingBeginDragLeft(self, pt, x, y, keys = 0, attachment = 0):
+        if self._previousHandler:
+            self._previousHandler.OnSizingBeginDragLeft(pt, x, y, keys, attachment)
+            
+    def OnSizingEndDragLeft(self, pt, x, y, keys = 0, attachment = 0):
+        if self._previousHandler:
+            self._previousHandler.OnSizingEndDragLeft(pt, x, y, keys, attachment)
+
+    def OnBeginSize(self, w, h):
+        pass
+    
+    def OnEndSize(self, w, h):
+        pass
+    
+    def OnDrawOutline(self, dc, x, y, w, h):
+        if self._previousHandler:
+            self._previousHandler.OnDrawOutline(dc, x, y, w, h)
+
+    def OnDrawControlPoints(self, dc):
+        if self._previousHandler:
+            self._previousHandler.OnDrawControlPoints(dc)
+
+    def OnEraseControlPoints(self, dc):
+        if self._previousHandler:
+            self._previousHandler.OnEraseControlPoints(dc)
+
+    # Can override this to prevent or intercept line reordering.
+    def OnChangeAttachment(self, attachment, line, ordering):
+        if self._previousHandler:
+            self._previousHandler.OnChangeAttachment(attachment, line, ordering)
+
+
+            
+class Shape(ShapeEvtHandler):
+    """OGL base class
+
+    Shape(canvas = None)
+    
+    The wxShape is the top-level, abstract object that all other objects
+    are derived from. All common functionality is represented by wxShape's
+    members, and overriden members that appear in derived classes and have
+    behaviour as documented for wxShape, are not documented separately.
+    """
+    
+    GraphicsInSizeToContents = False
+    
+    def __init__(self, canvas = None):
+        ShapeEvtHandler.__init__(self)
+        
+        self._eventHandler = self
+        self.SetShape(self)
+        self._id = 0
+        self._formatted = False
+        self._canvas = canvas
+        self._xpos = 0.0
+        self._ypos = 0.0
+        self._pen = wx.Pen(wx.BLACK, 1, wx.SOLID)
+        self._brush = wx.WHITE_BRUSH
+        self._font = wx.Font(10, wx.SWISS, wx.NORMAL, wx.NORMAL)
+        self._textColour = wx.BLACK
+        self._textColourName = wx.BLACK
+        self._visible = False
+        self._selected = False
+        self._attachmentMode = ATTACHMENT_MODE_NONE
+        self._spaceAttachments = True
+        self._disableLabel = False
+        self._fixedWidth = False
+        self._fixedHeight = False
+        self._drawHandles = True
+        self._sensitivity = OP_ALL
+        self._draggable = True
+        self._parent = None
+        self._formatMode = FORMAT_CENTRE_HORIZ | FORMAT_CENTRE_VERT
+        self._shadowMode = SHADOW_NONE
+        self._shadowOffsetX = 6
+        self._shadowOffsetY = 6
+        self._shadowBrush = wx.BLACK_BRUSH
+        self._textMarginX = 5
+        self._textMarginY = 5
+        self._regionName="0"
+        self._centreResize = True
+        self._maintainAspectRatio = False
+        self._highlighted = False
+        self._rotation = 0.0
+        self._branchNeckLength = 10
+        self._branchStemLength = 10
+        self._branchSpacing = 10
+        self._branchStyle = BRANCHING_ATTACHMENT_NORMAL
+        
+        self._regions = []
+        self._lines = []
+        self._controlPoints = []
+        self._attachmentPoints = []
+        self._text = []
+        self._children = []
+        
+        # Set up a default region. Much of the above will be put into
+        # the region eventually (the duplication is for compatibility)
+        region = ShapeRegion()
+        region.SetName("0")
+        region.SetFont(wx.Font(10, wx.SWISS, wx.NORMAL, wx.NORMAL))
+        region.SetFormatMode(FORMAT_CENTRE_HORIZ | FORMAT_CENTRE_VERT)
+        region.SetColour("BLACK")
+        self._regions.append(region)
+
+    def __str__(self):
+        return "<%s.%s>" % (self.__class__.__module__, self.__class__.__name__)
+
+    def GetClassName(self):
+        return str(self.__class__).split(".")[-1][:-2]
+    
+    def __del__(self):
+        if self._parent:
+            i = self._parent.GetChildren().index(self)
+            self._parent.GetChildren(i).remove(self)
+
+        self.ClearText()
+        self.ClearRegions()
+        self.ClearAttachments()
+
+        if self._canvas:
+            self._canvas.RemoveShape(self)
+
+        self.GetEventHandler().OnDelete()
+
+    def Draggable(self):
+        """TRUE if the shape may be dragged by the user."""
+        return True
+    
+    def SetShape(self, sh):
+        self._handlerShape = sh
+
+    def GetCanvas(self):
+        """Get the internal canvas."""
+        return self._canvas
+
+    def GetBranchStyle(self):
+        return self._branchStyle
+
+    def GetRotation(self):
+        """Return the angle of rotation in radians."""
+        return self._rotation
+
+    def SetRotation(self, rotation):
+        self._rotation = rotation
+        
+    def SetHighlight(self, hi, recurse = False):
+        """Set the highlight for a shape. Shape highlighting is unimplemented."""
+        self._highlighted = hi
+        if recurse:
+            for shape in self._children:
+                shape.SetHighlight(hi, recurse)
+
+    def SetSensitivityFilter(self, sens = OP_ALL, recursive = False):
+        """Set the shape to be sensitive or insensitive to specific mouse
+        operations.
+
+        sens is a bitlist of the following:
+
+        * OP_CLICK_LEFT
+        * OP_CLICK_RIGHT
+        * OP_DRAG_LEFT
+        * OP_DRAG_RIGHT
+        * OP_ALL (equivalent to a combination of all the above).
+        """
+        self._draggable = sens & OP_DRAG_LEFT
+
+        self._sensitivity = sens
+        if recursive:
+            for shape in self._children:
+                shape.SetSensitivityFilter(sens, True)
+
+    def SetDraggable(self, drag, recursive = False):
+        """Set the shape to be draggable or not draggable."""
+        self._draggable = drag
+        if drag:
+            self._sensitivity |= OP_DRAG_LEFT
+        elif self._sensitivity & OP_DRAG_LEFT:
+            self._sensitivity -= OP_DRAG_LEFT
+
+        if recursive:
+            for shape in self._children:
+                shape.SetDraggable(drag, True)
+
+    def SetDrawHandles(self, drawH):
+        """Set the drawHandles flag for this shape and all descendants.
+        If drawH is TRUE (the default), any handles (control points) will
+        be drawn. Otherwise, the handles will not be drawn.
+        """
+        self._drawHandles = drawH
+        for shape in self._children:
+            shape.SetDrawHandles(drawH)
+
+    def SetShadowMode(self, mode, redraw = False):
+        """Set the shadow mode (whether a shadow is drawn or not).
+        mode can be one of the following:
+
+        SHADOW_NONE
+          No shadow (the default). 
+        SHADOW_LEFT
+          Shadow on the left side. 
+        SHADOW_RIGHT
+          Shadow on the right side.
+        """
+        if redraw and self.GetCanvas():
+            dc = wx.ClientDC(self.GetCanvas())
+            self.GetCanvas().PrepareDC(dc)
+            self.Erase(dc)
+            self._shadowMode = mode
+            self.Draw(dc)
+        else:
+            self._shadowMode = mode
+
+    def SetCanvas(self, theCanvas):
+        """Identical to Shape.Attach."""
+        self._canvas = theCanvas
+        for shape in self._children:
+            shape.SetCanvas(theCanvas)
+
+    def AddToCanvas(self, theCanvas, addAfter = None):
+        """Add the shape to the canvas's shape list.
+        If addAfter is non-NULL, will add the shape after this one.
+        """
+        theCanvas.AddShape(self, addAfter)
+
+        lastImage = self
+        for object in self._children:
+            object.AddToCanvas(theCanvas, lastImage)
+            lastImage = object
+
+    def InsertInCanvas(self, theCanvas):
+        """Insert the shape at the front of the shape list of canvas."""
+        theCanvas.InsertShape(self)
+
+        lastImage = self
+        for object in self._children:
+            object.AddToCanvas(theCanvas, lastImage)
+            lastImage = object
+
+    def RemoveFromCanvas(self, theCanvas):
+        """Remove the shape from the canvas."""
+        if self.Selected():
+            self.Select(False)
+        theCanvas.RemoveShape(self)
+        for object in self._children:
+            object.RemoveFromCanvas(theCanvas)
+
+    def ClearAttachments(self):
+        """Clear internal custom attachment point shapes (of class
+        wxAttachmentPoint).
+        """
+        self._attachmentPoints = []
+
+    def ClearText(self, regionId = 0):
+        """Clear the text from the specified text region."""
+        if regionId == 0:
+            self._text=""
+        if regionId<len(self._regions):
+            self._regions[regionId].ClearText()
+            
+    def ClearRegions(self):
+        """Clear the ShapeRegions from the shape."""
+        self._regions = []
+            
+    def AddRegion(self, region):
+        """Add a region to the shape."""
+        self._regions.append(region)
+
+    def SetDefaultRegionSize(self):
+        """Set the default region to be consistent with the shape size."""
+        if not self._regions:
+            return
+        w, h = self.GetBoundingBoxMax()
+        self._regions[0].SetSize(w, h)
+
+    def HitTest(self, x, y):
+        """Given a point on a canvas, returns TRUE if the point was on the
+        shape, and returns the nearest attachment point and distance from
+        the given point and target.
+        """
+        width, height = self.GetBoundingBoxMax()
+        if abs(width)<4:
+            width = 4.0
+        if abs(height)<4:
+            height = 4.0
+
+        width += 4 # Allowance for inaccurate mousing
+        height += 4
+        
+        left = self._xpos - (width / 2)
+        top = self._ypos - (height / 2)
+        right = self._xpos + (width / 2)
+        bottom = self._ypos + (height / 2)
+
+        nearest_attachment = 0
+
+        # If within the bounding box, check the attachment points
+        # within the object.
+        if x >= left and x <= right and y >= top and y <= bottom:
+            n = self.GetNumberOfAttachments()
+            nearest = 999999
+
+            # GetAttachmentPosition[Edge] takes a logical attachment position,
+            # i.e. if it's rotated through 90%, position 0 is East-facing.
+
+            for i in range(n):
+                e = self.GetAttachmentPositionEdge(i)
+                if e:
+                    xp, yp = e
+                    l = sqrt(((xp - x) * (xp - x)) + (yp - y) * (yp - y))
+                    if l<nearest:
+                        nearest = l
+                        nearest_attachment = i
+
+            return nearest_attachment, nearest
+        return False
+    
+    # Format a text string according to the region size, adding
+    # strings with positions to region text list
+    
+    def FormatText(self, dc, s, i = 0):
+        """Reformat the given text region; defaults to formatting the
+        default region.
+        """
+        self.ClearText(i)
+
+        if not self._regions:
+            return
+
+        if i>len(self._regions):
+            return
+
+        region = self._regions[i]
+        region._regionText = s
+        dc.SetFont(region.GetFont())
+
+        w, h = region.GetSize()
+
+        stringList = FormatText(dc, s, (w - 2 * self._textMarginX), (h - 2 * self._textMarginY), region.GetFormatMode())
+        for s in stringList:
+            line = ShapeTextLine(0.0, 0.0, s)
+            region.GetFormattedText().append(line)
+
+        actualW = w
+        actualH = h
+        # Don't try to resize an object with more than one image (this
+        # case should be dealt with by overriden handlers)
+        if (region.GetFormatMode() & FORMAT_SIZE_TO_CONTENTS) and \
+           region.GetFormattedText().GetCount() and \
+           len(self._regions) == 1 and \
+           not Shape.GraphicsInSizeToContents:
+
+            actualW, actualH = GetCentredTextExtent(dc, region.GetFormattedText())
+            if actualW + 2 * self._textMarginX != w or actualH + 2 * self._textMarginY != h:
+                # If we are a descendant of a composite, must make sure
+                # the composite gets resized properly
+
+                topAncestor = self.GetTopAncestor()
+                if topAncestor != self:
+                    Shape.GraphicsInSizeToContents = True
+
+                    composite = topAncestor
+                    composite.Erase(dc)
+                    self.SetSize(actualW + 2 * self._textMarginX, actualH + 2 * self._textMarginY)
+                    self.Move(dc, self._xpos, self._ypos)
+                    composite.CalculateSize()
+                    if composite.Selected():
+                        composite.DeleteControlPoints(dc)
+                        composite.MakeControlPoints()
+                        composite.MakeMandatoryControlPoints()
+                    # Where infinite recursion might happen if we didn't stop it
+                    composite.Draw(dc)
+                    Shape.GraphicsInSizeToContents = False
+                else:
+                    self.Erase(dc)
+                    
+                self.SetSize(actualW + 2 * self._textMarginX, actualH + 2 * self._textMarginY)
+                self.Move(dc, self._xpos, self._ypos)
+                self.EraseContents(dc)
+        CentreText(dc, region.GetFormattedText(), self._xpos, self._ypos, actualW - 2 * self._textMarginX, actualH - 2 * self._textMarginY, region.GetFormatMode())
+        self._formatted = True
+
+    def Recentre(self, dc):
+        """Do recentring (or other formatting) for all the text regions
+        for this shape.
+        """
+        w, h = self.GetBoundingBoxMin()
+        for region in self._regions:
+            CentreText(dc, region.GetFormattedText(), self._xpos, self._ypos, w - 2 * self._textMarginX, h - 2 * self._textMarginY, region.GetFormatMode())
+
+    def GetPerimeterPoint(self, x1, y1, x2, y2):
+        """Get the point at which the line from (x1, y1) to (x2, y2) hits
+        the shape. Returns False if the line doesn't hit the perimeter.
+        """
+        return False
+
+    def SetPen(self, the_pen):
+        """Set the pen for drawing the shape's outline."""
+        self._pen = the_pen
+
+    def SetBrush(self, the_brush):
+        """Set the brush for filling the shape's shape."""
+        self._brush = the_brush
+
+    # Get the top - most (non-division) ancestor, or self
+    def GetTopAncestor(self):
+        """Return the top-most ancestor of this shape (the root of
+        the composite).
+        """
+        if not self.GetParent():
+            return self
+
+        if isinstance(self.GetParent(), DivisionShape):
+            return self
+        return self.GetParent().GetTopAncestor()
+
+    # Region functions
+    def SetFont(self, the_font, regionId = 0):
+        """Set the font for the specified text region."""
+        self._font = the_font
+        if regionId<len(self._regions):
+            self._regions[regionId].SetFont(the_font)
+
+    def GetFont(self, regionId = 0):
+        """Get the font for the specified text region."""
+        if regionId >= len(self._regions):
+            return None
+        return self._regions[regionId].GetFont()
+
+    def SetFormatMode(self, mode, regionId = 0):
+        """Set the format mode of the default text region. The argument
+        can be a bit list of the following:
+
+        FORMAT_NONE
+          No formatting. 
+        FORMAT_CENTRE_HORIZ
+          Horizontal centring. 
+        FORMAT_CENTRE_VERT
+          Vertical centring.
+        """
+        if regionId<len(self._regions):
+            self._regions[regionId].SetFormatMode(mode)
+
+    def GetFormatMode(self, regionId = 0):
+        if regionId >= len(self._regions):
+            return 0
+        return self._regions[regionId].GetFormatMode()
+
+    def SetTextColour(self, the_colour, regionId = 0):
+        """Set the colour for the specified text region."""
+        self._textColour = wx.TheColourDatabase.Find(the_colour)
+        self._textColourName = the_colour
+
+        if regionId<len(self._regions):
+            self._regions[regionId].SetColour(the_colour)
+            
+    def GetTextColour(self, regionId = 0):
+        """Get the colour for the specified text region."""
+        if regionId >= len(self._regions):
+            return ""
+        return self._regions[regionId].GetTextColour()
+
+    def SetRegionName(self, name, regionId = 0):
+        """Set the name for this region.
+        The name for a region is unique within the scope of the whole
+        composite, whereas a region id is unique only for a single image.
+        """
+        if regionId<len(self._regions):
+            self._regions[regionId].SetName(name)
+
+    def GetRegionName(self, regionId = 0):
+        """Get the region's name.
+        A region's name can be used to uniquely determine a region within
+        an entire composite image hierarchy. See also Shape.SetRegionName.
+        """
+        if regionId >= len(self._regions):
+            return ""
+        return self._regions[regionId].GetName()
+
+    def GetRegionId(self, name):
+        """Get the region's identifier by name.
+        This is not unique for within an entire composite, but is unique
+        for the image.
+        """
+        for i, r in enumerate(self._regions):
+            if r.GetName() == name:
+                return i
+        return -1
+
+    # Name all _regions in all subimages recursively
+    def NameRegions(self, parentName=""):
+        """Make unique names for all the regions in a shape or composite shape."""
+        n = self.GetNumberOfTextRegions()
+        for i in range(n):
+            if parentName:
+                buff = parentName+"."+str(i)
+            else:
+                buff = str(i)
+            self.SetRegionName(buff, i)
+
+        for j, child in enumerate(self._children):
+            if parentName:
+                buff = parentName+"."+str(j)
+            else:
+                buff = str(j)
+            child.NameRegions(buff)
+
+    # Get a region by name, possibly looking recursively into composites
+    def FindRegion(self, name):
+        """Find the actual image ('this' if non-composite) and region id
+        for the given region name.
+        """
+        id = self.GetRegionId(name)
+        if id>-1:
+            return self, id
+
+        for child in self._children:
+            actualImage, regionId = child.FindRegion(name)
+            if actualImage:
+                return actualImage, regionId
+
+        return None,-1
+
+    # Finds all region names for this image (composite or simple).
+    def FindRegionNames(self):
+        """Get a list of all region names for this image (composite or simple)."""
+        list = []
+        n = self.GetNumberOfTextRegions()
+        for i in range(n):
+            list.append(self.GetRegionName(i))
+
+        for child in self._children:
+            list += child.FindRegionNames()
+
+        return list
+
+    def AssignNewIds(self):
+        """Assign new ids to this image and its children."""
+        self._id = wx.NewId()
+        for child in self._children:
+            child.AssignNewIds()
+
+    def OnDraw(self, dc):
+        pass
+
+    def OnMoveLinks(self, dc):
+        # Want to set the ends of all attached links
+        # to point to / from this object
+
+        for line in self._lines:
+            line.GetEventHandler().OnMoveLink(dc)
+
+    def OnDrawContents(self, dc):
+        if not self._regions:
+            return
+        
+        bound_x, bound_y = self.GetBoundingBoxMin()
+
+        if self._pen:
+            dc.SetPen(self._pen)
+
+        region = self._regions[0]
+        if region.GetFont():
+            dc.SetFont(region.GetFont())
+
+        dc.SetTextForeground(region.GetActualColourObject())
+        dc.SetBackgroundMode(wx.TRANSPARENT)
+        if not self._formatted:
+            CentreText(dc, region.GetFormattedText(), self._xpos, self._ypos, bound_x - 2 * self._textMarginX, bound_y - 2 * self._textMarginY, region.GetFormatMode())
+            self._formatted = True
+
+        if not self.GetDisableLabel():
+            DrawFormattedText(dc, region.GetFormattedText(), self._xpos, self._ypos, bound_x - 2 * self._textMarginX, bound_y - 2 * self._textMarginY, region.GetFormatMode())
+            
+
+    def DrawContents(self, dc):
+        """Draw the internal graphic of the shape (such as text).
+
+        Do not override this function: override OnDrawContents, which
+        is called by this function.
+        """
+        self.GetEventHandler().OnDrawContents(dc)
+
+    def OnSize(self, x, y):
+        pass
+
+    def OnMovePre(self, dc, x, y, old_x, old_y, display = True):
+        return True
+
+    def OnErase(self, dc):
+        if not self._visible:
+            return
+
+        # Erase links
+        for line in self._lines:
+            line.GetEventHandler().OnErase(dc)
+
+        self.GetEventHandler().OnEraseContents(dc)
+
+    def OnEraseContents(self, dc):
+        if not self._visible:
+            return
+
+        xp, yp = self.GetX(), self.GetY()
+        minX, minY = self.GetBoundingBoxMin()
+        maxX, maxY = self.GetBoundingBoxMax()
+
+        topLeftX = xp - maxX / 2 - 2
+        topLeftY = yp - maxY / 2 - 2
+
+        penWidth = 0
+        if self._pen:
+            penWidth = self._pen.GetWidth()
+
+        dc.SetPen(self.GetBackgroundPen())
+        dc.SetBrush(self.GetBackgroundBrush())
+
+        dc.DrawRectangle(topLeftX - penWidth, topLeftY - penWidth, maxX + penWidth * 2 + 4, maxY + penWidth * 2 + 4)
+
+    def EraseLinks(self, dc, attachment=-1, recurse = False):
+        """Erase links attached to this shape, but do not repair damage
+        caused to other shapes.
+        """
+        if not self._visible:
+            return
+
+        for line in self._lines:
+            if attachment==-1 or (line.GetTo() == self and line.GetAttachmentTo() == attachment or line.GetFrom() == self and line.GetAttachmentFrom() == attachment):
+                line.GetEventHandler().OnErase(dc)
+
+        if recurse:
+            for child in self._children:
+                child.EraseLinks(dc, attachment, recurse)
+
+    def DrawLinks(self, dc, attachment=-1, recurse = False):
+        """Draws any lines linked to this shape."""
+        if not self._visible:
+            return
+
+        for line in self._lines:
+            if attachment==-1 or (line.GetTo() == self and line.GetAttachmentTo() == attachment or line.GetFrom() == self and line.GetAttachmentFrom() == attachment):
+                line.GetEventHandler().Draw(dc)
+                
+        if recurse:
+            for child in self._children:
+                child.DrawLinks(dc, attachment, recurse)
+
+    #  Returns TRUE if pt1 <= pt2 in the sense that one point comes before
+    #  another on an edge of the shape.
+    # attachmentPoint is the attachment point (= side) in question.
+
+    # This is the default, rectangular implementation.
+    def AttachmentSortTest(self, attachmentPoint, pt1, pt2):
+        """Return TRUE if pt1 is less than or equal to pt2, in the sense
+        that one point comes before another on an edge of the shape.
+
+        attachment is the attachment point (side) in question.
+
+        This function is used in Shape.MoveLineToNewAttachment to determine
+        the new line ordering.
+        """
+        physicalAttachment = self.LogicalToPhysicalAttachment(attachmentPoint)
+        if physicalAttachment in [0, 2]:
+            return pt1.x <= pt2.x
+        elif physicalAttachment in [1, 3]:
+            return pt1.y <= pt2.y
+
+        return False
+
+    def MoveLineToNewAttachment(self, dc, to_move, x, y):
+        """Move the given line (which must already be attached to the shape)
+        to a different attachment point on the shape, or a different order
+        on the same attachment.
+
+        Calls Shape.AttachmentSortTest and then
+        ShapeEvtHandler.OnChangeAttachment.
+        """
+        if self.GetAttachmentMode() == ATTACHMENT_MODE_NONE:
+            return False
+
+        # Is (x, y) on this object? If so, find the new attachment point
+        # the user has moved the point to
+        hit = self.HitTest(x, y)
+        if not hit:
+            return False
+
+        newAttachment, distance = hit
+        
+        self.EraseLinks(dc)
+
+        if to_move.GetTo() == self:
+            oldAttachment = to_move.GetAttachmentTo()
+        else:
+            oldAttachment = to_move.GetAttachmentFrom()
+
+        # The links in a new ordering
+        # First, add all links to the new list
+        newOrdering = self._lines[:]
+        
+        # Delete the line object from the list of links; we're going to move
+        # it to another position in the list
+        del newOrdering[newOrdering.index(to_move)]
+
+        old_x=-99999.9
+        old_y=-99999.9
+
+        found = False
+
+        for line in newOrdering:
+            if line.GetTo() == self and oldAttachment == line.GetAttachmentTo() or \
+               line.GetFrom() == self and oldAttachment == line.GetAttachmentFrom():
+                startX, startY, endX, endY = line.GetEnds()
+                if line.GetTo() == self:
+                    xp = endX
+                    yp = endY
+                else:
+                    xp = startX
+                    yp = startY
+
+                thisPoint = wx.RealPoint(xp, yp)
+                lastPoint = wx.RealPoint(old_x, old_y)
+                newPoint = wx.RealPoint(x, y)
+
+                if self.AttachmentSortTest(newAttachment, newPoint, thisPoint) and self.AttachmentSortTest(newAttachment, lastPoint, newPoint):
+                    found = True
+                    newOrdering.insert(newOrdering.index(line), to_move)
+
+                old_x = xp
+                old_y = yp
+            if found:
+                break
+
+        if not found:
+            newOrdering.append(to_move)
+
+        self.GetEventHandler().OnChangeAttachment(newAttachment, to_move, newOrdering)
+        return True
+
+    def OnChangeAttachment(self, attachment, line, ordering):
+        if line.GetTo() == self:
+            line.SetAttachmentTo(attachment)
+        else:
+            line.SetAttachmentFrom(attachment)
+
+        self.ApplyAttachmentOrdering(ordering)
+
+        dc = wx.ClientDC(self.GetCanvas())
+        self.GetCanvas().PrepareDC(dc)
+        self.MoveLinks(dc)
+
+        if not self.GetCanvas().GetQuickEditMode():
+            self.GetCanvas().Redraw(dc)
+
+    # Reorders the lines according to the given list
+    def ApplyAttachmentOrdering(self, linesToSort):
+        """Apply the line ordering in linesToSort to the shape, to reorder
+        the way lines are attached.
+        """
+        linesStore = self._lines[:]
+
+        self._lines = []
+
+        for line in linesToSort:
+            if line in linesStore:
+                del linesStore[linesStore.index(line)]
+                self._lines.append(line)
+
+        # Now add any lines that haven't been listed in linesToSort
+        self._lines += linesStore
+
+    def SortLines(self, attachment, linesToSort):
+        """ Reorder the lines coming into the node image at this attachment
+        position, in the order in which they appear in linesToSort.
+
+        Any remaining lines not in the list will be added to the end.
+        """
+        # This is a temporary store of all the lines at this attachment
+        # point. We'll tick them off as we've processed them.
+        linesAtThisAttachment = []
+
+        for line in self._lines[:]:
+            if line.GetTo() == self and line.GetAttachmentTo() == attachment or \
+               line.GetFrom() == self and line.GetAttachmentFrom() == attachment:
+                linesAtThisAttachment.append(line)
+                del self._lines[self._lines.index(line)]
+
+        for line in linesToSort:
+            if line in linesAtThisAttachment:
+                # Done this one
+                del linesAtThisAttachment[linesAtThisAttachment.index(line)]
+                self._lines.Append(line)
+
+        # Now add any lines that haven't been listed in linesToSort
+        self._lines += linesAtThisAttachment
+
+    def OnHighlight(self, dc):
+        pass
+
+    def OnLeftClick(self, x, y, keys = 0, attachment = 0):
+        if self._sensitivity & OP_CLICK_LEFT != OP_CLICK_LEFT:
+            if self._parent:
+                attachment, dist = self._parent.HitTest(x, y)
+                self._parent.GetEventHandler().OnLeftClick(x, y, keys, attachment)
+
+    def OnRightClick(self, x, y, keys = 0, attachment = 0):
+        if self._sensitivity & OP_CLICK_RIGHT != OP_CLICK_RIGHT:
+            attachment, dist = self._parent.HitTest(x, y)
+            self._parent.GetEventHandler().OnRightClick(x, y, keys, attachment)
+            
+    def OnDragLeft(self, draw, x, y, keys = 0, attachment = 0):
+        if self._sensitivity & OP_DRAG_LEFT != OP_DRAG_LEFT:
+            if self._parent:
+                hit = self._parent.HitTest(x, y)
+                if hit:
+                    attachment, dist = hit
+                self._parent.GetEventHandler().OnDragLeft(draw, x, y, keys, attachment)
+            return
+
+        dc = wx.ClientDC(self.GetCanvas())
+        self.GetCanvas().PrepareDC(dc)
+        dc.SetLogicalFunction(OGLRBLF)
+
+        dottedPen = wx.Pen(wx.Colour(0, 0, 0), 1, wx.DOT)
+        dc.SetPen(dottedPen)
+        dc.SetBrush(wx.TRANSPARENT_BRUSH)
+
+        xx = x + DragOffsetX
+        yy = y + DragOffsetY
+
+        xx, yy = self._canvas.Snap(xx, yy)
+        w, h = self.GetBoundingBoxMax()
+        self.GetEventHandler().OnDrawOutline(dc, xx, yy, w, h)
+
+    def OnBeginDragLeft(self, x, y, keys = 0, attachment = 0):
+        global DragOffsetX, DragOffsetY
+        
+        if self._sensitivity & OP_DRAG_LEFT != OP_DRAG_LEFT:
+            if self._parent:
+                hit = self._parent.HitTest(x, y)
+                if hit:
+                    attachment, dist = hit
+                self._parent.GetEventHandler().OnBeginDragLeft(x, y, keys, attachment)
+            return
+
+        DragOffsetX = self._xpos - x
+        DragOffsetY = self._ypos - y
+
+        dc = wx.ClientDC(self.GetCanvas())
+        self.GetCanvas().PrepareDC(dc)
+        
+        # New policy: don't erase shape until end of drag.
+        # self.Erase(dc)
+        xx = x + DragOffsetX
+        yy = y + DragOffsetY
+        xx, yy = self._canvas.Snap(xx, yy)
+        dc.SetLogicalFunction(OGLRBLF)
+
+        dottedPen = wx.Pen(wx.Colour(0, 0, 0), 1, wx.DOT)
+        dc.SetPen(dottedPen)
+        dc.SetBrush(wx.TRANSPARENT_BRUSH)
+
+        w, h = self.GetBoundingBoxMax()
+        self.GetEventHandler().OnDrawOutline(dc, xx, yy, w, h)
+        self._canvas.CaptureMouse()
+
+    def OnEndDragLeft(self, x, y, keys = 0, attachment = 0):
+        if self._canvas.HasCapture():
+            self._canvas.ReleaseMouse()
+        if self._sensitivity & OP_DRAG_LEFT != OP_DRAG_LEFT:
+            if self._parent:
+                hit = self._parent.HitTest(x, y)
+                if hit:
+                    attachment, dist = hit
+            self._parent.GetEventHandler().OnEndDragLeft(x, y, keys, attachment)
+            return
+
+        dc = wx.ClientDC(self.GetCanvas())
+        self.GetCanvas().PrepareDC(dc)
+
+        dc.SetLogicalFunction(wx.COPY)
+        xx = x + DragOffsetX
+        yy = y + DragOffsetY
+        xx, yy = self._canvas.Snap(xx, yy)
+
+        # New policy: erase shape at end of drag.
+        self.Erase(dc)
+
+        self.Move(dc, xx, yy)
+        if self._canvas and not self._canvas.GetQuickEditMode():
+            self._canvas.Redraw(dc)
+
+    def OnDragRight(self, draw, x, y, keys = 0, attachment = 0):
+        if self._sensitivity & OP_DRAG_RIGHT != OP_DRAG_RIGHT:
+            if self._parent:
+                attachment, dist = self._parent.HitTest(x, y)
+                self._parent.GetEventHandler().OnDragRight(draw, x, y, keys, attachment)
+            return
+
+    def OnBeginDragRight(self, x, y, keys = 0, attachment = 0):
+        if self._sensitivity & OP_DRAG_RIGHT != OP_DRAG_RIGHT:
+            if self._parent:
+                attachment, dist = self._parent.HitTest(x, y)
+                self._parent.GetEventHandler().OnBeginDragRight(x, y, keys, attachment)
+            return
+
+    def OnEndDragRight(self, x, y, keys = 0, attachment = 0):
+        if self._sensitivity & OP_DRAG_RIGHT != OP_DRAG_RIGHT:
+            if self._parent:
+                attachment, dist = self._parent.HitTest(x, y)
+                self._parent.GetEventHandler().OnEndDragRight(x, y, keys, attachment)
+            return
+
+    def OnDrawOutline(self, dc, x, y, w, h):
+        points = [[x - w / 2, y - h / 2],
+                [x + w / 2, y - h / 2],
+                [x + w / 2, y + h / 2],
+                [x - w / 2, y + h / 2],
+                [x - w / 2, y - h / 2],
+                ]
+
+        #dc.DrawLines([[round(x), round(y)] for x, y in points])
+        dc.DrawLines(points)
+        
+    def Attach(self, can):
+        """Set the shape's internal canvas pointer to point to the given canvas."""
+        self._canvas = can
+
+    def Detach(self):
+        """Disassociates the shape from its canvas."""
+        self._canvas = None
+
+    def Move(self, dc, x, y, display = True):
+        """Move the shape to the given position.
+        Redraw if display is TRUE.
+        """
+        old_x = self._xpos
+        old_y = self._ypos
+
+        if not self.GetEventHandler().OnMovePre(dc, x, y, old_x, old_y, display):
+            return
+
+        self._xpos, self._ypos = x, y
+
+        self.ResetControlPoints()
+
+        if display:
+            self.Draw(dc)
+
+        self.MoveLinks(dc)
+
+        self.GetEventHandler().OnMovePost(dc, x, y, old_x, old_y, display)
+
+    def MoveLinks(self, dc):
+        """Redraw all the lines attached to the shape."""
+        self.GetEventHandler().OnMoveLinks(dc)
+
+    def Draw(self, dc):
+        """Draw the whole shape and any lines attached to it.
+
+        Do not override this function: override OnDraw, which is called
+        by this function.
+        """
+        if self._visible:
+            self.GetEventHandler().OnDraw(dc)
+            self.GetEventHandler().OnDrawContents(dc)
+            self.GetEventHandler().OnDrawControlPoints(dc)
+            self.GetEventHandler().OnDrawBranches(dc)
+
+    def Flash(self):
+        """Flash the shape."""
+        if self.GetCanvas():
+            dc = wx.ClientDC(self.GetCanvas())
+            self.GetCanvas.PrepareDC(dc)
+
+            dc.SetLogicalFunction(OGLRBLF)
+            self.Draw(dc)
+            dc.SetLogicalFunction(wx.COPY)
+            self.Draw(dc)
+
+    def Show(self, show):
+        """Set a flag indicating whether the shape should be drawn."""
+        self._visible = show
+        for child in self._children:
+            child.Show(show)
+
+    def Erase(self, dc):
+        """Erase the shape.
+        Does not repair damage caused to other shapes.
+        """
+        self.GetEventHandler().OnErase(dc)
+        self.GetEventHandler().OnEraseControlPoints(dc)
+        self.GetEventHandler().OnDrawBranches(dc, erase = True)
+
+    def EraseContents(self, dc):
+        """Erase the shape contents, that is, the area within the shape's
+        minimum bounding box.
+        """
+        self.GetEventHandler().OnEraseContents(dc)
+
+    def AddText(self, string):
+        """Add a line of text to the shape's default text region."""
+        if not self._regions:
+            return
+
+        region = self._regions[0]
+        region.ClearText()
+        new_line = ShapeTextLine(0, 0, string)
+        text = region.GetFormattedText()
+        text.append(new_line)
+
+        self._formatted = False
+
+    def SetSize(self, x, y, recursive = True):
+        """Set the shape's size."""
+        self.SetAttachmentSize(x, y)
+        self.SetDefaultRegionSize()
+
+    def SetAttachmentSize(self, w, h):
+        width, height = self.GetBoundingBoxMin()
+        if width == 0:
+            scaleX = 1.0
+        else:
+            scaleX = long(w) / width
+        if height == 0:
+            scaleY = 1.0
+        else:
+            scaleY = long(h) / height
+
+        for point in self._attachmentPoints:
+            point._x = point._x * scaleX
+            point._y = point._y * scaleY
+
+    # Add line FROM this object
+    def AddLine(self, line, other, attachFrom = 0, attachTo = 0, positionFrom=-1, positionTo=-1):
+        """Add a line between this shape and the given other shape, at the
+        specified attachment points.
+
+        The position in the list of lines at each end can also be specified,
+        so that the line will be drawn at a particular point on its attachment
+        point.
+        """
+        if positionFrom==-1:
+            if not line in self._lines:
+                self._lines.append(line)
+        else:
+            # Don't preserve old ordering if we have new ordering instructions
+            try:
+                self._lines.remove(line)
+            except ValueError:
+                pass
+            if positionFrom<len(self._lines):
+                self._lines.insert(positionFrom, line)
+            else:
+                self._lines.append(line)
+
+        if positionTo==-1:
+            if not other in other._lines:
+                other._lines.append(line)
+        else:
+            # Don't preserve old ordering if we have new ordering instructions
+            try:
+                other._lines.remove(line)
+            except ValueError:
+                pass
+            if positionTo<len(other._lines):
+                other._lines.insert(positionTo, line)
+            else:
+                other._lines.append(line)
+            
+        line.SetFrom(self)
+        line.SetTo(other)
+        line.SetAttachments(attachFrom, attachTo)
+
+        dc = wx.ClientDC(self._canvas)
+        self._canvas.PrepareDC(dc)
+        self.MoveLinks(dc)
+        
+    def RemoveLine(self, line):
+        """Remove the given line from the shape's list of attached lines."""
+        if line.GetFrom() == self:
+            line.GetTo()._lines.remove(line)
+        else:
+            line.GetFrom()._lines.remove(line)
+
+        self._lines.remove(line)
+
+    # Default - make 6 control points
+    def MakeControlPoints(self):
+        """Make a list of control points (draggable handles) appropriate to
+        the shape.
+        """
+        maxX, maxY = self.GetBoundingBoxMax()
+        minX, minY = self.GetBoundingBoxMin()
+
+        widthMin = minX + CONTROL_POINT_SIZE + 2
+        heightMin = minY + CONTROL_POINT_SIZE + 2
+
+        # Offsets from main object
+        top=-heightMin / 2
+        bottom = heightMin / 2 + (maxY - minY)
+        left=-widthMin / 2
+        right = widthMin / 2 + (maxX - minX)
+
+        control = ControlPoint(self._canvas, self, CONTROL_POINT_SIZE, left, top, CONTROL_POINT_DIAGONAL)
+        self._canvas.AddShape(control)
+        self._controlPoints.append(control)
+
+        control = ControlPoint(self._canvas, self, CONTROL_POINT_SIZE, 0, top, CONTROL_POINT_VERTICAL)
+        self._canvas.AddShape(control)
+        self._controlPoints.append(control)
+
+        control = ControlPoint(self._canvas, self, CONTROL_POINT_SIZE, right, top, CONTROL_POINT_DIAGONAL)
+        self._canvas.AddShape(control)
+        self._controlPoints.append(control)
+
+        control = ControlPoint(self._canvas, self, CONTROL_POINT_SIZE, right, 0, CONTROL_POINT_HORIZONTAL)
+        self._canvas.AddShape(control)
+        self._controlPoints.append(control)
+
+        control = ControlPoint(self._canvas, self, CONTROL_POINT_SIZE, right, bottom, CONTROL_POINT_DIAGONAL)
+        self._canvas.AddShape(control)
+        self._controlPoints.append(control)
+
+        control = ControlPoint(self._canvas, self, CONTROL_POINT_SIZE, 0, bottom, CONTROL_POINT_VERTICAL)
+        self._canvas.AddShape(control)
+        self._controlPoints.append(control)
+
+        control = ControlPoint(self._canvas, self, CONTROL_POINT_SIZE, left, bottom, CONTROL_POINT_DIAGONAL)
+        self._canvas.AddShape(control)
+        self._controlPoints.append(control)
+
+        control = ControlPoint(self._canvas, self, CONTROL_POINT_SIZE, left, 0, CONTROL_POINT_HORIZONTAL)
+        self._canvas.AddShape(control)
+        self._controlPoints.append(control)
+
+    def MakeMandatoryControlPoints(self):
+        """Make the mandatory control points.
+
+        For example, the control point on a dividing line should appear even
+        if the divided rectangle shape's handles should not appear (because
+        it is the child of a composite, and children are not resizable).
+        """
+        for child in self._children:
+            child.MakeMandatoryControlPoints()
+
+    def ResetMandatoryControlPoints(self):
+        """Reset the mandatory control points."""
+        for child in self._children:
+            child.ResetMandatoryControlPoints()
+
+    def ResetControlPoints(self):
+        """Reset the positions of the control points (for instance when the
+        shape's shape has changed).
+        """
+        self.ResetMandatoryControlPoints()
+
+        if len(self._controlPoints) == 0:
+            return
+
+        maxX, maxY = self.GetBoundingBoxMax()
+        minX, minY = self.GetBoundingBoxMin()
+
+        widthMin = minX + CONTROL_POINT_SIZE + 2
+        heightMin = minY + CONTROL_POINT_SIZE + 2
+        
+        # Offsets from main object
+        top=-heightMin / 2
+        bottom = heightMin / 2 + (maxY - minY)
+        left=-widthMin / 2
+        right = widthMin / 2 + (maxX - minX)
+
+        self._controlPoints[0]._xoffset = left
+        self._controlPoints[0]._yoffset = top
+
+        self._controlPoints[1]._xoffset = 0
+        self._controlPoints[1]._yoffset = top
+
+        self._controlPoints[2]._xoffset = right
+        self._controlPoints[2]._yoffset = top
+
+        self._controlPoints[3]._xoffset = right
+        self._controlPoints[3]._yoffset = 0
+
+        self._controlPoints[4]._xoffset = right
+        self._controlPoints[4]._yoffset = bottom
+
+        self._controlPoints[5]._xoffset = 0
+        self._controlPoints[5]._yoffset = bottom
+
+        self._controlPoints[6]._xoffset = left
+        self._controlPoints[6]._yoffset = bottom
+
+        self._controlPoints[7]._xoffset = left
+        self._controlPoints[7]._yoffset = 0
+
+    def DeleteControlPoints(self, dc = None):
+        """Delete the control points (or handles) for the shape.
+
+        Does not redraw the shape.
+        """
+        for control in self._controlPoints:
+            if dc:
+                control.GetEventHandler().OnErase(dc)
+            self._canvas.RemoveShape(control)
+            del control
+        self._controlPoints = []
+
+        # Children of divisions are contained objects,
+        # so stop here
+        if not isinstance(self, DivisionShape):
+            for child in self._children:
+                child.DeleteControlPoints(dc)
+
+    def OnDrawControlPoints(self, dc):
+        if not self._drawHandles:
+            return
+
+        dc.SetBrush(wx.BLACK_BRUSH)
+        dc.SetPen(wx.BLACK_PEN)
+
+        for control in self._controlPoints:
+            control.Draw(dc)
+
+        # Children of divisions are contained objects,
+        # so stop here.
+        # This test bypasses the type facility for speed
+        # (critical when drawing)
+
+        if not isinstance(self, DivisionShape):
+            for child in self._children:
+                child.GetEventHandler().OnDrawControlPoints(dc)
+
+    def OnEraseControlPoints(self, dc):
+        for control in self._controlPoints:
+            control.Erase(dc)
+
+        if not isinstance(self, DivisionShape):
+            for child in self._children:
+                child.GetEventHandler().OnEraseControlPoints(dc)
+
+    def Select(self, select, dc = None):
+        """Select or deselect the given shape, drawing or erasing control points
+        (handles) as necessary.
+        """
+        self._selected = select
+        if select:
+            self.MakeControlPoints()
+            # Children of divisions are contained objects,
+            # so stop here
+            if not isinstance(self, DivisionShape):
+                for child in self._children:
+                    child.MakeMandatoryControlPoints()
+            if dc:
+                self.GetEventHandler().OnDrawControlPoints(dc)
+        else:
+            self.DeleteControlPoints(dc)
+            if not isinstance(self, DivisionShape):
+                for child in self._children:
+                    child.DeleteControlPoints(dc)
+
+    def Selected(self):
+        """TRUE if the shape is currently selected."""
+        return self._selected
+
+    def AncestorSelected(self):
+        """TRUE if the shape's ancestor is currently selected."""
+        if self._selected:
+            return True
+        if not self.GetParent():
+            return False
+        return self.GetParent().AncestorSelected()
+
+    def GetNumberOfAttachments(self):
+        """Get the number of attachment points for this shape."""
+        # Should return the MAXIMUM attachment point id here,
+        # so higher-level functions can iterate through all attachments,
+        # even if they're not contiguous.
+
+        if len(self._attachmentPoints) == 0:
+            return 4
+        else:
+            maxN = 3
+            for point in self._attachmentPoints:
+                if point._id>maxN:
+                    maxN = point._id
+            return maxN + 1
+
+    def AttachmentIsValid(self, attachment):
+        """TRUE if attachment is a valid attachment point."""
+        if len(self._attachmentPoints) == 0:
+            return attachment in range(4)
+
+        for point in self._attachmentPoints:
+            if point._id == attachment:
+                return True
+        return False
+
+    def GetAttachmentPosition(self, attachment, nth = 0, no_arcs = 1, line = None):
+        """Get the position at which the given attachment point should be drawn.
+
+        If attachment isn't found among the attachment points of the shape,
+        returns None.
+        """
+        if self._attachmentMode == ATTACHMENT_MODE_NONE:
+            return self._xpos, self._ypos
+        elif self._attachmentMode == ATTACHMENT_MODE_BRANCHING:
+            pt, stemPt = self.GetBranchingAttachmentPoint(attachment, nth)
+            return pt.x, pt.y
+        elif self._attachmentMode == ATTACHMENT_MODE_EDGE:
+            if len(self._attachmentPoints):
+                for point in self._attachmentPoints:
+                    if point._id == attachment:
+                        return self._xpos + point._x, self._ypos + point._y
+                return None
+            else:
+                # Assume is rectangular
+                w, h = self.GetBoundingBoxMax()
+                top = self._ypos + h / 2
+                bottom = self._ypos - h / 2
+                left = self._xpos - w / 2
+                right = self._xpos + w / 2
+
+                # wtf?
+                line and line.IsEnd(self)
+
+                physicalAttachment = self.LogicalToPhysicalAttachment(attachment)
+
+                # Simplified code
+                if physicalAttachment == 0:
+                    pt = self.CalcSimpleAttachment((left, bottom), (right, bottom), nth, no_arcs, line)
+                elif physicalAttachment == 1:
+                    pt = self.CalcSimpleAttachment((right, bottom), (right, top), nth, no_arcs, line)
+                elif physicalAttachment == 2:
+                    pt = self.CalcSimpleAttachment((left, top), (right, top), nth, no_arcs, line)
+                elif physicalAttachment == 3:
+                    pt = self.CalcSimpleAttachment((left, bottom), (left, top), nth, no_arcs, line)
+                else:
+                    return None
+                return pt[0], pt[1]
+        return None
+
+    def GetBoundingBoxMax(self):
+        """Get the maximum bounding box for the shape, taking into account
+        external features such as shadows.
+        """
+        ww, hh = self.GetBoundingBoxMin()
+        if self._shadowMode != SHADOW_NONE:
+            ww += self._shadowOffsetX
+            hh += self._shadowOffsetY
+        return ww, hh
+
+    def GetBoundingBoxMin(self):
+        """Get the minimum bounding box for the shape, that defines the area
+        available for drawing the contents (such as text).
+
+        Must be overridden.
+        """
+        return 0, 0
+    
+    def HasDescendant(self, image):
+        """TRUE if image is a descendant of this composite."""
+        if image == self:
+            return True
+        for child in self._children:
+            if child.HasDescendant(image):
+                return True
+        return False
+
+    # Clears points from a list of wxRealPoints, and clears list
+    # Useless in python? /pi
+    def ClearPointList(self, list):
+        list = []
+
+    # Assuming the attachment lies along a vertical or horizontal line,
+    # calculate the position on that point.
+    def CalcSimpleAttachment(self, pt1, pt2, nth, noArcs, line):
+        """Assuming the attachment lies along a vertical or horizontal line,
+        calculate the position on that point.
+
+        Parameters:
+
+        pt1
+            The first point of the line repesenting the edge of the shape.
+
+        pt2
+            The second point of the line representing the edge of the shape.
+
+        nth
+            The position on the edge (for example there may be 6 lines at
+            this attachment point, and this may be the 2nd line.
+
+        noArcs
+            The number of lines at this edge.
+
+        line
+            The line shape.
+
+        Remarks
+
+        This function expects the line to be either vertical or horizontal,
+        and determines which.
+        """
+        isEnd = line and line.IsEnd(self)
+
+        # Are we horizontal or vertical?
+        isHorizontal = RoughlyEqual(pt1[1], pt2[1])
+
+        if isHorizontal:
+            if pt1[0]>pt2[0]:
+                firstPoint = pt2
+                secondPoint = pt1
+            else:
+                firstPoint = pt1
+                secondPoint = pt2
+
+            if self._spaceAttachments:
+                if line and line.GetAlignmentType(isEnd) == LINE_ALIGNMENT_TO_NEXT_HANDLE:
+                    # Align line according to the next handle along
+                    point = line.GetNextControlPoint(self)
+                    if point[0]<firstPoint[0]:
+                        x = firstPoint[0]
+                    elif point[0]>secondPoint[0]:
+                        x = secondPoint[0]
+                    else:
+                        x = point[0]
+                else:
+                    x = firstPoint[0] + (nth + 1) * (secondPoint[0] - firstPoint[0]) / (noArcs + 1)
+            else:
+                x = (secondPoint[0] - firstPoint[0]) / 2 # Midpoint
+            y = pt1[1]
+        else:
+            assert RoughlyEqual(pt1[0], pt2[0])
+
+            if pt1[1]>pt2[1]:
+                firstPoint = pt2
+                secondPoint = pt1
+            else:
+                firstPoint = pt1
+                secondPoint = pt2
+
+            if self._spaceAttachments:
+                if line and line.GetAlignmentType(isEnd) == LINE_ALIGNMENT_TO_NEXT_HANDLE:
+                    # Align line according to the next handle along
+                    point = line.GetNextControlPoint(self)
+                    if point[1]<firstPoint[1]:
+                        y = firstPoint[1]
+                    elif point[1]>secondPoint[1]:
+                        y = secondPoint[1]
+                    else:
+                        y = point[1]
+                else:
+                    y = firstPoint[1] + (nth + 1) * (secondPoint[1] - firstPoint[1]) / (noArcs + 1)
+            else:
+                y = (secondPoint[1] - firstPoint[1]) / 2 # Midpoint
+            x = pt1[0]
+
+        return x, y
+
+    # Return the zero-based position in m_lines of line
+    def GetLinePosition(self, line):
+        """Get the zero-based position of line in the list of lines
+        for this shape.
+        """
+        try:
+            return self._lines.index(line)
+        except:
+            return 0
+
+
+    #            |________|
+    #                 | <- root
+    #                 | <- neck
+    # shoulder1 ->---------<- shoulder2
+    #             | | | | |
+    #                      <- branching attachment point N-1
+
+    def GetBranchingAttachmentInfo(self, attachment):
+        """Get information about where branching connections go.
+        
+        Returns FALSE if there are no lines at this attachment.
+        """
+        physicalAttachment = self.LogicalToPhysicalAttachment(attachment)
+
+        # Number of lines at this attachment
+        lineCount = self.GetAttachmentLineCount(attachment)
+
+        if not lineCount:
+            return False
+
+        totalBranchLength = self._branchSpacing * (lineCount - 1)
+        root = self.GetBranchingAttachmentRoot(attachment)
+
+        neck = wx.RealPoint()
+        shoulder1 = wx.RealPoint()
+        shoulder2 = wx.RealPoint()
+        
+        # Assume that we have attachment points 0 to 3: top, right, bottom, left
+        if physicalAttachment == 0:
+            neck.x = self.GetX()
+            neck.y = root.y - self._branchNeckLength
+
+            shoulder1.x = root.x - totalBranchLength / 2
+            shoulder2.x = root.x + totalBranchLength / 2
+
+            shoulder1.y = neck.y
+            shoulder2.y = neck.y
+        elif physicalAttachment == 1:
+            neck.x = root.x + self._branchNeckLength
+            neck.y = root.y
+            
+            shoulder1.x = neck.x
+            shoulder2.x = neck.x
+
+            shoulder1.y = neck.y - totalBranchLength / 2
+            shoulder1.y = neck.y + totalBranchLength / 2
+        elif physicalAttachment == 2:
+            neck.x = self.GetX()
+            neck.y = root.y + self._branchNeckLength
+
+            shoulder1.x = root.x - totalBranchLength / 2
+            shoulder2.x = root.x + totalBranchLength / 2
+
+            shoulder1.y = neck.y
+            shoulder2.y = neck.y
+        elif physicalAttachment == 3:
+            neck.x = root.x - self._branchNeckLength
+            neck.y = root.y
+
+            shoulder1.x = neck.x
+            shoulder2.x = neck.x
+
+            shoulder1.y = neck.y - totalBranchLength / 2
+            shoulder2.y = neck.y + totalBranchLength / 2
+        else:
+            raise "Unrecognised attachment point in GetBranchingAttachmentInfo"
+        return root, neck, shoulder1, shoulder2
+
+    def GetBranchingAttachmentPoint(self, attachment, n):
+        physicalAttachment = self.LogicalToPhysicalAttachment(attachment)
+
+        root, neck, shoulder1, shoulder2 = self.GetBranchingAttachmentInfo(attachment)
+        pt = wx.RealPoint()
+        stemPt = wx.RealPoint()
+
+        if physicalAttachment == 0:
+            pt.y = neck.y - self._branchStemLength
+            pt.x = shoulder1.x + n * self._branchSpacing
+
+            stemPt.x = pt.x
+            stemPt.y = neck.y
+        elif physicalAttachment == 2:
+            pt.y = neck.y + self._branchStemLength
+            pt.x = shoulder1.x + n * self._branchStemLength
+            
+            stemPt.x = pt.x
+            stemPt.y = neck.y
+        elif physicalAttachment == 1:
+            pt.x = neck.x + self._branchStemLength
+            pt.y = shoulder1.y + n * self._branchSpacing
+
+            stemPt.x = neck.x
+            stemPt.y = pt.y
+        elif physicalAttachment == 3:
+            pt.x = neck.x - self._branchStemLength
+            pt.y = shoulder1.y + n * self._branchSpacing
+
+            stemPt.x = neck.x
+            stemPt.y = pt.y
+        else:
+            raise "Unrecognised attachment point in GetBranchingAttachmentPoint"
+
+        return pt, stemPt
+
+    def GetAttachmentLineCount(self, attachment):
+        """Get the number of lines at this attachment position."""
+        count = 0
+        for lineShape in self._lines:
+            if lineShape.GetFrom() == self and lineShape.GetAttachmentFrom() == attachment:
+                count += 1
+            elif lineShape.GetTo() == self and lineShape.GetAttachmentTo() == attachment:
+                count += 1
+        return count
+    
+    def GetBranchingAttachmentRoot(self, attachment):
+        """Get the root point at the given attachment."""
+        physicalAttachment = self.LogicalToPhysicalAttachment(attachment)
+
+        root = wx.RealPoint()
+
+        width, height = self.GetBoundingBoxMax()
+
+        # Assume that we have attachment points 0 to 3: top, right, bottom, left
+        if physicalAttachment == 0:
+            root.x = self.GetX()
+            root.y = self.GetY() - height / 2
+        elif physicalAttachment == 1:
+            root.x = self.GetX() + width / 2
+            root.y = self.GetY()
+        elif physicalAttachment == 2:
+            root.x = self.GetX()
+            root.y = self.GetY() + height / 2
+        elif physicalAttachment == 3:
+            root.x = self.GetX() - width / 2
+            root.y = self.GetY()
+        else:
+            raise "Unrecognised attachment point in GetBranchingAttachmentRoot"
+
+        return root
+
+    # Draw or erase the branches (not the actual arcs though)
+    def OnDrawBranchesAttachment(self, dc, attachment, erase = False):
+        count = self.GetAttachmentLineCount(attachment)
+        if count == 0:
+            return
+
+        root, neck, shoulder1, shoulder2 = self.GetBranchingAttachmentInfo(attachment)
+
+        if erase:
+            dc.SetPen(wx.WHITE_PEN)
+            dc.SetBrush(wx.WHITE_BRUSH)
+        else:
+            dc.SetPen(wx.BLACK_PEN)
+            dc.SetBrush(wx.BLACK_BRUSH)
+
+        # Draw neck
+        dc.DrawLine(root, neck)
+
+        if count>1:
+            # Draw shoulder-to-shoulder line
+            dc.DrawLine(shoulder1, shoulder2)
+        # Draw all the little branches
+        for i in range(count):
+            pt, stemPt = self.GetBranchingAttachmentPoint(attachment, i)
+            dc.DrawLine(stemPt, pt)
+
+            if self.GetBranchStyle() & BRANCHING_ATTACHMENT_BLOB and count>1:
+                blobSize = 6
+                dc.DrawEllipse(stemPt.x - blobSize / 2, stemPt.y - blobSize / 2, blobSize, blobSize)
+
+    def OnDrawBranches(self, dc, erase = False):
+        if self._attachmentMode != ATTACHMENT_MODE_BRANCHING:
+            return
+        for i in range(self.GetNumberOfAttachments()):
+            self.OnDrawBranchesAttachment(dc, i, erase)
+
+    def GetAttachmentPositionEdge(self, attachment, nth = 0, no_arcs = 1, line = None):
+        """ Only get the attachment position at the _edge_ of the shape,
+        ignoring branching mode. This is used e.g. to indicate the edge of
+        interest, not the point on the attachment branch.
+        """
+        oldMode = self._attachmentMode
+
+        # Calculate as if to edge, not branch
+        if self._attachmentMode == ATTACHMENT_MODE_BRANCHING:
+            self._attachmentMode = ATTACHMENT_MODE_EDGE
+        res = self.GetAttachmentPosition(attachment, nth, no_arcs, line)
+        self._attachmentMode = oldMode
+
+        return res
+
+    def PhysicalToLogicalAttachment(self, physicalAttachment):
+        """ Rotate the standard attachment point from physical
+        (0 is always North) to logical (0 -> 1 if rotated by 90 degrees)
+        """
+        if RoughlyEqual(self.GetRotation(), 0):
+            i = physicalAttachment
+        elif RoughlyEqual(self.GetRotation(), pi / 2):
+            i = physicalAttachment - 1
+        elif RoughlyEqual(self.GetRotation(), pi):
+            i = physicalAttachment - 2
+        elif RoughlyEqual(self.GetRotation(), 3 * pi / 2):
+            i = physicalAttachment - 3
+        else:
+            # Can't handle -- assume the same
+            return physicalAttachment
+
+        if i<0:
+            i += 4
+
+        return i
+
+    def LogicalToPhysicalAttachment(self, logicalAttachment):
+        """Rotate the standard attachment point from logical
+        to physical (0 is always North).
+        """
+        if RoughlyEqual(self.GetRotation(), 0):
+            i = logicalAttachment
+        elif RoughlyEqual(self.GetRotation(), pi / 2):
+            i = logicalAttachment + 1
+        elif RoughlyEqual(self.GetRotation(), pi):
+            i = logicalAttachment + 2
+        elif RoughlyEqual(self.GetRotation(), 3 * pi / 2):
+            i = logicalAttachment + 3
+        else:
+            return logicalAttachment
+
+        if i>3:
+            i -= 4
+
+        return i
+
+    def Rotate(self, x, y, theta):
+        """Rotate about the given axis by the given amount in radians."""
+        self._rotation = theta
+        if self._rotation<0:
+            self._rotation += 2 * pi
+        elif self._rotation>2 * pi:
+            self._rotation -= 2 * pi
+
+    def GetBackgroundPen(self):
+        """Return pen of the right colour for the background."""
+        if self.GetCanvas():
+            return wx.Pen(self.GetCanvas().GetBackgroundColour(), 1, wx.SOLID)
+        return WhiteBackgroundPen
+
+    def GetBackgroundBrush(self):
+        """Return brush of the right colour for the background."""
+        if self.GetCanvas():
+            return wx.Brush(self.GetCanvas().GetBackgroundColour(), wx.SOLID)
+        return WhiteBackgroundBrush
+
+    def GetX(self):
+        """Get the x position of the centre of the shape."""
+        return self._xpos
+
+    def GetY(self):
+        """Get the y position of the centre of the shape."""
+        return self._ypos
+
+    def SetX(self, x):
+        """Set the x position of the shape."""
+        self._xpos = x
+
+    def SetY(self, y):
+        """Set the y position of the shape."""
+        self._ypos = y
+
+    def GetParent(self):
+        """Return the parent of this shape, if it is part of a composite."""
+        return self._parent
+
+    def SetParent(self, p):
+        self._parent = p
+
+    def GetChildren(self):
+        """Return the list of children for this shape."""
+        return self._children
+
+    def GetDrawHandles(self):
+        """Return the list of drawhandles."""
+        return self._drawHandles
+
+    def GetEventHandler(self):
+        """Return the event handler for this shape."""
+        return self._eventHandler
+
+    def SetEventHandler(self, handler):
+        """Set the event handler for this shape."""
+        self._eventHandler = handler
+
+    def Recompute(self):
+        """Recomputes any constraints associated with the shape.
+
+        Normally applicable to CompositeShapes only, but harmless for
+        other classes of Shape.
+        """
+        return True
+
+    def IsHighlighted(self):
+        """TRUE if the shape is highlighted. Shape highlighting is unimplemented."""
+        return self._highlighted
+
+    def GetSensitivityFilter(self):
+        """Return the sensitivity filter, a bitlist of values.
+
+        See Shape.SetSensitivityFilter.
+        """
+        return self._sensitivity
+
+    def SetFixedSize(self, x, y):
+        """Set the shape to be fixed size."""
+        self._fixedWidth = x
+        self._fixedHeight = y
+
+    def GetFixedSize(self):
+        """Return flags indicating whether the shape is of fixed size in
+        either direction.
+        """
+        return self._fixedWidth, self._fixedHeight
+
+    def GetFixedWidth(self):
+        """TRUE if the shape cannot be resized in the horizontal plane."""
+        return self._fixedWidth
+
+    def GetFixedHeight(self):
+        """TRUE if the shape cannot be resized in the vertical plane."""
+        return self._fixedHeight
+    
+    def SetSpaceAttachments(self, sp):
+        """Indicate whether lines should be spaced out evenly at the point
+        they touch the node (sp = True), or whether they should join at a single
+        point (sp = False).
+        """
+        self._spaceAttachments = sp
+
+    def GetSpaceAttachments(self):
+        """Return whether lines should be spaced out evenly at the point they
+        touch the node (True), or whether they should join at a single point
+        (False).
+        """
+        return self._spaceAttachments
+
+    def SetCentreResize(self, cr):
+        """Specify whether the shape is to be resized from the centre (the
+        centre stands still) or from the corner or side being dragged (the
+        other corner or side stands still).
+        """
+        self._centreResize = cr
+
+    def GetCentreResize(self):
+        """TRUE if the shape is to be resized from the centre (the centre stands
+        still), or FALSE if from the corner or side being dragged (the other
+        corner or side stands still)
+        """
+        return self._centreResize
+
+    def SetMaintainAspectRatio(self, ar):
+        """Set whether a shape that resizes should not change the aspect ratio
+        (width and height should be in the original proportion).
+        """
+        self._maintainAspectRatio = ar
+
+    def GetMaintainAspectRatio(self):
+        """TRUE if shape keeps aspect ratio during resize."""
+        return self._maintainAspectRatio
+
+    def GetLines(self):
+        """Return the list of lines connected to this shape."""
+        return self._lines
+
+    def SetDisableLabel(self, flag):
+        """Set flag to TRUE to stop the default region being shown."""
+        self._disableLabel = flag
+
+    def GetDisableLabel(self):
+        """TRUE if the default region will not be shown, FALSE otherwise."""
+        return self._disableLabel
+
+    def SetAttachmentMode(self, mode):
+        """Set the attachment mode.
+
+        If TRUE, attachment points will be significant when drawing lines to
+        and from this shape.
+        If FALSE, lines will be drawn as if to the centre of the shape.
+        """
+        self._attachmentMode = mode
+
+    def GetAttachmentMode(self):
+        """Return the attachment mode.
+
+        See Shape.SetAttachmentMode.
+        """
+        return self._attachmentMode
+
+    def SetId(self, i):
+        """Set the integer identifier for this shape."""
+        self._id = i
+
+    def GetId(self):
+        """Return the integer identifier for this shape."""
+        return self._id
+
+    def IsShown(self):
+        """TRUE if the shape is in a visible state, FALSE otherwise.
+
+        Note that this has nothing to do with whether the window is hidden
+        or the shape has scrolled off the canvas; it refers to the internal
+        visibility flag.
+        """
+        return self._visible
+
+    def GetPen(self):
+        """Return the pen used for drawing the shape's outline."""
+        return self._pen
+
+    def GetBrush(self):
+        """Return the brush used for filling the shape."""
+        return self._brush
+
+    def GetNumberOfTextRegions(self):
+        """Return the number of text regions for this shape."""
+        return len(self._regions)
+
+    def GetRegions(self):
+        """Return the list of ShapeRegions."""
+        return self._regions
+
+    # Control points ('handles') redirect control to the actual shape, to
+    # make it easier to override sizing behaviour.
+    def OnSizingDragLeft(self, pt, draw, x, y, keys = 0, attachment = 0):
+        bound_x, bound_y = self.GetBoundingBoxMin()
+
+        dc = wx.ClientDC(self.GetCanvas())
+        self.GetCanvas().PrepareDC(dc)
+
+        dc.SetLogicalFunction(OGLRBLF)
+
+        dottedPen = wx.Pen(wx.Colour(0, 0, 0), 1, wx.DOT)
+        dc.SetPen(dottedPen)
+        dc.SetBrush(wx.TRANSPARENT_BRUSH)
+
+        if self.GetCentreResize():
+            # Maintain the same centre point
+            new_width = 2.0 * abs(x - self.GetX())
+            new_height = 2.0 * abs(y - self.GetY())
+
+            # Constrain sizing according to what control point you're dragging
+            if pt._type == CONTROL_POINT_HORIZONTAL:
+                if self.GetMaintainAspectRatio():
+                    new_height = bound_y * (new_width / bound_x)
+                else:
+                    new_height = bound_y
+            elif pt._type == CONTROL_POINT_VERTICAL:
+                if self.GetMaintainAspectRatio():
+                    new_width = bound_x * (new_height / bound_y)
+                else:
+                    new_width = bound_x
+            elif pt._type == CONTROL_POINT_DIAGONAL and (keys & KEY_SHIFT):
+                new_height = bound_y * (new_width / bound_x)
+
+            if self.GetFixedWidth():
+                new_width = bound_x
+
+            if self.GetFixedHeight():
+                new_height = bound_y
+
+            pt._controlPointDragEndWidth = new_width
+            pt._controlPointDragEndHeight = new_height
+
+            self.GetEventHandler().OnDrawOutline(dc, self.GetX(), self.GetY(), new_width, new_height)
+        else:
+            # Don't maintain the same centre point
+            newX1 = min(pt._controlPointDragStartX, x)
+            newY1 = min(pt._controlPointDragStartY, y)
+            newX2 = max(pt._controlPointDragStartX, x)
+            newY2 = max(pt._controlPointDragStartY, y)
+            if pt._type == CONTROL_POINT_HORIZONTAL:
+                newY1 = pt._controlPointDragStartY
+                newY2 = newY1 + pt._controlPointDragStartHeight
+            elif pt._type == CONTROL_POINT_VERTICAL:
+                newX1 = pt._controlPointDragStartX
+                newX2 = newX1 + pt._controlPointDragStartWidth
+            elif pt._type == CONTROL_POINT_DIAGONAL and (keys & KEY_SHIFT or self.GetMaintainAspectRatio()):
+                newH = (newX2 - newX1) * (pt._controlPointDragStartHeight / pt._controlPointDragStartWidth)
+                if self.GetY()>pt._controlPointDragStartY:
+                    newY2 = newY1 + newH
+                else:
+                    newY1 = newY2 - newH
+
+            newWidth = newX2 - newX1
+            newHeight = newY2 - newY1
+
+            if pt._type == CONTROL_POINT_VERTICAL and self.GetMaintainAspectRatio():
+                newWidth = bound_x * (newHeight / bound_y)
+
+            if pt._type == CONTROL_POINT_HORIZONTAL and self.GetMaintainAspectRatio():
+                newHeight = bound_y * (newWidth / bound_x)
+
+            pt._controlPointDragPosX = newX1 + newWidth / 2
+            pt._controlPointDragPosY = newY1 + newHeight / 2
+            if self.GetFixedWidth():
+                newWidth = bound_x
+
+            if self.GetFixedHeight():
+                newHeight = bound_y
+
+            pt._controlPointDragEndWidth = newWidth
+            pt._controlPointDragEndHeight = newHeight
+            self.GetEventHandler().OnDrawOutline(dc, pt._controlPointDragPosX, pt._controlPointDragPosY, newWidth, newHeight)
+
+    def OnSizingBeginDragLeft(self, pt, x, y, keys = 0, attachment = 0):
+        self._canvas.CaptureMouse()
+
+        dc = wx.ClientDC(self.GetCanvas())
+        self.GetCanvas().PrepareDC(dc)
+
+        dc.SetLogicalFunction(OGLRBLF)
+
+        bound_x, bound_y = self.GetBoundingBoxMin()
+        self.GetEventHandler().OnEndSize(bound_x, bound_y)
+
+        # Choose the 'opposite corner' of the object as the stationary
+        # point in case this is non-centring resizing.
+        if pt.GetX()<self.GetX():
+            pt._controlPointDragStartX = self.GetX() + bound_x / 2
+        else:
+            pt._controlPointDragStartX = self.GetX() - bound_x / 2
+
+        if pt.GetY()<self.GetY():
+            pt._controlPointDragStartY = self.GetY() + bound_y / 2
+        else:
+            pt._controlPointDragStartY = self.GetY() - bound_y / 2
+
+        if pt._type == CONTROL_POINT_HORIZONTAL:
+            pt._controlPointDragStartY = self.GetY() - bound_y / 2
+        elif pt._type == CONTROL_POINT_VERTICAL:
+            pt._controlPointDragStartX = self.GetX() - bound_x / 2
+
+        # We may require the old width and height
+        pt._controlPointDragStartWidth = bound_x
+        pt._controlPointDragStartHeight = bound_y
+
+        dottedPen = wx.Pen(wx.Colour(0, 0, 0), 1, wx.DOT)
+        dc.SetPen(dottedPen)
+        dc.SetBrush(wx.TRANSPARENT_BRUSH)
+
+        if self.GetCentreResize():
+            new_width = 2.0 * abs(x - self.GetX())
+            new_height = 2.0 * abs(y - self.GetY())
+
+            # Constrain sizing according to what control point you're dragging
+            if pt._type == CONTROL_POINT_HORIZONTAL:
+                if self.GetMaintainAspectRatio():
+                    new_height = bound_y * (new_width / bound_x)
+                else:
+                    new_height = bound_y
+            elif pt._type == CONTROL_POINT_VERTICAL:
+                if self.GetMaintainAspectRatio():
+                    new_width = bound_x * (new_height / bound_y)
+                else:
+                    new_width = bound_x
+            elif pt._type == CONTROL_POINT_DIAGONAL and (keys & KEY_SHIFT):
+                new_height = bound_y * (new_width / bound_x)
+
+            if self.GetFixedWidth():
+                new_width = bound_x
+
+            if self.GetFixedHeight():
+                new_height = bound_y
+
+            pt._controlPointDragEndWidth = new_width
+            pt._controlPointDragEndHeight = new_height
+            self.GetEventHandler().OnDrawOutline(dc, self.GetX(), self.GetY(), new_width, new_height)
+        else:
+            # Don't maintain the same centre point
+            newX1 = min(pt._controlPointDragStartX, x)
+            newY1 = min(pt._controlPointDragStartY, y)
+            newX2 = max(pt._controlPointDragStartX, x)
+            newY2 = max(pt._controlPointDragStartY, y)
+            if pt._type == CONTROL_POINT_HORIZONTAL:
+                newY1 = pt._controlPointDragStartY
+                newY2 = newY1 + pt._controlPointDragStartHeight
+            elif pt._type == CONTROL_POINT_VERTICAL:
+                newX1 = pt._controlPointDragStartX
+                newX2 = newX1 + pt._controlPointDragStartWidth
+            elif pt._type == CONTROL_POINT_DIAGONAL and (keys & KEYS or self.GetMaintainAspectRatio()):
+                newH = (newX2 - newX1) * (pt._controlPointDragStartHeight / pt._controlPointDragStartWidth)
+                if pt.GetY()>pt._controlPointDragStartY:
+                    newY2 = newY1 + newH
+                else:
+                    newY1 = newY2 - newH
+
+            newWidth = newX2 - newX1
+            newHeight = newY2 - newY1
+
+            if pt._type == CONTROL_POINT_VERTICAL and self.GetMaintainAspectRatio():
+                newWidth = bound_x * (newHeight / bound_y)
+
+            if pt._type == CONTROL_POINT_HORIZONTAL and self.GetMaintainAspectRatio():
+                newHeight = bound_y * (newWidth / bound_x)
+
+            pt._controlPointDragPosX = newX1 + newWidth / 2
+            pt._controlPointDragPosY = newY1 + newHeight / 2
+            if self.GetFixedWidth():
+                newWidth = bound_x
+
+            if self.GetFixedHeight():
+                newHeight = bound_y
+
+            pt._controlPointDragEndWidth = newWidth
+            pt._controlPointDragEndHeight = newHeight
+            self.GetEventHandler().OnDrawOutline(dc, pt._controlPointDragPosX, pt._controlPointDragPosY, newWidth, newHeight)
+            
+    def OnSizingEndDragLeft(self, pt, x, y, keys = 0, attachment = 0):
+        dc = wx.ClientDC(self.GetCanvas())
+        self.GetCanvas().PrepareDC(dc)
+
+        if self._canvas.HasCapture():
+            self._canvas.ReleaseMouse()
+        dc.SetLogicalFunction(wx.COPY)
+        self.Recompute()
+        self.ResetControlPoints()
+
+        self.Erase(dc)
+
+        self.SetSize(pt._controlPointDragEndWidth, pt._controlPointDragEndHeight)
+
+        # The next operation could destroy this control point (it does for
+        # label objects, via formatting the text), so save all values we're
+        # going to use, or we'll be accessing garbage.
+
+        #return
+
+        if self.GetCentreResize():
+            self.Move(dc, self.GetX(), self.GetY())
+        else:
+            self.Move(dc, pt._controlPointDragPosX, pt._controlPointDragPosY)
+
+        # Recursively redraw links if we have a composite
+        if len(self.GetChildren()):
+            self.DrawLinks(dc,-1, True)
+
+        width, height = self.GetBoundingBoxMax()
+        self.GetEventHandler().OnEndSize(width, height)
+
+        if not self._canvas.GetQuickEditMode() and pt._eraseObject:
+            self._canvas.Redraw(dc)
+
+
+            
+class RectangleShape(Shape):
+    """
+    The wxRectangleShape has rounded or square corners.
+
+    Derived from:
+      Shape
+    """
+    def __init__(self, w = 0.0, h = 0.0):
+        Shape.__init__(self)
+        self._width = w
+        self._height = h
+        self._cornerRadius = 0.0
+        self.SetDefaultRegionSize()
+
+    def OnDraw(self, dc):
+        x1 = self._xpos - self._width / 2
+        y1 = self._ypos - self._height / 2
+
+        if self._shadowMode != SHADOW_NONE:
+            if self._shadowBrush:
+                dc.SetBrush(self._shadowBrush)
+            dc.SetPen(TransparentPen)
+
+            if self._cornerRadius:
+                dc.DrawRoundedRectangle(x1 + self._shadowOffsetX, y1 + self._shadowOffsetY, self._width, self._height, self._cornerRadius)
+            else:
+                dc.DrawRectangle(x1 + self._shadowOffsetX, y1 + self._shadowOffsetY, self._width, self._height)
+
+        if self._pen:
+            if self._pen.GetWidth() == 0:
+                dc.SetPen(TransparentPen)
+            else:
+                dc.SetPen(self._pen)
+        if self._brush:
+            dc.SetBrush(self._brush)
+
+        if self._cornerRadius:
+            dc.DrawRoundedRectangle(x1, y1, self._width, self._height, self._cornerRadius)
+        else:
+            dc.DrawRectangle(x1, y1, self._width, self._height)
+
+    def GetBoundingBoxMin(self):
+        return self._width, self._height
+
+    def SetSize(self, x, y, recursive = False):
+        self.SetAttachmentSize(x, y)
+        self._width = max(x, 1)
+        self._height = max(y, 1)
+        self.SetDefaultRegionSize()
+
+    def GetCornerRadius(self):
+        """Get the radius of the rectangle's rounded corners."""
+        return self._cornerRadius
+    
+    def SetCornerRadius(self, rad):
+        """Set the radius of the rectangle's rounded corners.
+
+        If the radius is zero, a non-rounded rectangle will be drawn.
+        If the radius is negative, the value is the proportion of the smaller
+        dimension of the rectangle.
+        """
+        self._cornerRadius = rad
+
+    # Assume (x1, y1) is centre of box (most generally, line end at box)
+    def GetPerimeterPoint(self, x1, y1, x2, y2):
+        bound_x, bound_y = self.GetBoundingBoxMax()
+        return FindEndForBox(bound_x, bound_y, self._xpos, self._ypos, x2, y2)
+
+    def GetWidth(self):
+        return self._width
+
+    def GetHeight(self):
+        return self._height
+
+    def SetWidth(self, w):
+        self._width = w
+
+    def SetHeight(self, h):
+        self._height = h
+
+
+        
+class PolygonShape(Shape):
+    """A PolygonShape's shape is defined by a number of points passed to
+    the object's constructor. It can be used to create new shapes such as
+    diamonds and triangles.
+    """
+    def __init__(self):
+        Shape.__init__(self)
+        
+        self._points = None
+        self._originalPoints = None
+
+    def Create(self, the_points = None):
+        """Takes a list of wx.RealPoints or tuples; each point is an offset
+        from the centre.
+        """
+        self.ClearPoints()
+
+        if not the_points:
+            self._originalPoints = []
+            self._points = []
+        else:
+            self._originalPoints = the_points
+
+            # Duplicate the list of points
+            self._points = []
+            for point in the_points:
+                new_point = wx.Point(point[0], point[1])
+                self._points.append(new_point)
+            self.CalculateBoundingBox()
+            self._originalWidth = self._boundWidth
+            self._originalHeight = self._boundHeight
+            self.SetDefaultRegionSize()
+
+    def ClearPoints(self):
+        self._points = []
+        self._originalPoints = []
+
+    # Width and height. Centre of object is centre of box
+    def GetBoundingBoxMin(self):
+        return self._boundWidth, self._boundHeight
+
+    def GetPoints(self):
+        """Return the internal list of polygon vertices."""
+        return self._points
+
+    def GetOriginalPoints(self):
+        return self._originalPoints
+
+    def GetOriginalWidth(self):
+        return self._originalWidth
+
+    def GetOriginalHeight(self):
+        return self._originalHeight
+
+    def SetOriginalWidth(self, w):
+        self._originalWidth = w
+
+    def SetOriginalHeight(self, h):
+        self._originalHeight = h
+        
+    def CalculateBoundingBox(self):
+        # Calculate bounding box at construction (and presumably resize) time
+        left = 10000
+        right=-10000
+        top = 10000
+        bottom=-10000
+
+        for point in self._points:
+            if point.x<left:
+                left = point.x
+            if point.x>right:
+                right = point.x
+
+            if point.y<top:
+                top = point.y
+            if point.y>bottom:
+                bottom = point.y
+
+        self._boundWidth = right - left
+        self._boundHeight = bottom - top
+
+    def CalculatePolygonCentre(self):
+        """Recalculates the centre of the polygon, and
+        readjusts the point offsets accordingly.
+        Necessary since the centre of the polygon
+        is expected to be the real centre of the bounding
+        box.
+        """
+        left = 10000
+        right=-10000
+        top = 10000
+        bottom=-10000
+
+        for point in self._points:
+            if point.x<left:
+                left = point.x
+            if point.x>right:
+                right = point.x
+
+            if point.y<top:
+                top = point.y
+            if point.y>bottom:
+                bottom = point.y
+
+        bwidth = right - left
+        bheight = bottom - top
+
+        newCentreX = left + bwidth / 2
+        newCentreY = top + bheight / 2
+
+        for point in self._points:
+            point.x -= newCentreX
+            point.y -= newCentreY
+        self._xpos += newCentreX
+        self._ypos += newCentreY
+
+    def HitTest(self, x, y):
+        # Imagine four lines radiating from this point. If all of these lines
+        # hit the polygon, we're inside it, otherwise we're not. Obviously
+        # we'd need more radiating lines to be sure of correct results for
+        # very strange (concave) shapes.
+        endPointsX = [x, x + 1000, x, x - 1000]
+        endPointsY = [y - 1000, y, y + 1000, y]
+
+        xpoints = []
+        ypoints = []
+
+        for point in self._points:
+            xpoints.append(point.x + self._xpos)
+            ypoints.append(point.y + self._ypos)
+
+        # We assume it's inside the polygon UNLESS one or more
+        # lines don't hit the outline.
+        isContained = True
+
+        for i in range(4):
+            if not PolylineHitTest(xpoints, ypoints, x, y, endPointsX[i], endPointsY[i]):
+                isContained = False
+
+        if not isContained:
+            return False
+
+        nearest_attachment = 0
+
+        # If a hit, check the attachment points within the object
+        nearest = 999999
+
+        for i in range(self.GetNumberOfAttachments()):
+            e = self.GetAttachmentPositionEdge(i)
+            if e:
+                xp, yp = e
+                l = sqrt((xp - x) * (xp - x) + (yp - y) * (yp - y))
+                if l<nearest:
+                    nearest = l
+                    nearest_attachment = i
+
+        return nearest_attachment, nearest
+
+    # Really need to be able to reset the shape! Otherwise, if the
+    # points ever go to zero, we've lost it, and can't resize.
+    def SetSize(self, new_width, new_height, recursive = True):
+        self.SetAttachmentSize(new_width, new_height)
+
+        # Multiply all points by proportion of new size to old size
+        x_proportion = abs(new_width / self._originalWidth)
+        y_proportion = abs(new_height / self._originalHeight)
+
+        for i in range(max(len(self._points), len(self._originalPoints))):
+            self._points[i].x = self._originalPoints[i][0] * x_proportion
+            self._points[i].y = self._originalPoints[i][1] * y_proportion
+
+        self._boundWidth = abs(new_width)
+        self._boundHeight = abs(new_height)
+        self.SetDefaultRegionSize()
+
+    # Make the original points the same as the working points
+    def UpdateOriginalPoints(self):
+        """If we've changed the shape, must make the original points match the
+        working points with this function.
+        """
+        self._originalPoints = []
+
+        for point in self._points:
+            original_point = wx.RealPoint(point.x, point.y)
+            self._originalPoints.append(original_point)
+
+        self.CalculateBoundingBox()
+        self._originalWidth = self._boundWidth
+        self._originalHeight = self._boundHeight
+
+    def AddPolygonPoint(self, pos):
+        """Add a control point after the given point."""
+        try:
+            firstPoint = self._points[pos]
+        except ValueError:
+            firstPoint = self._points[0]
+
+        try:
+            secondPoint = self._points[pos + 1]
+        except ValueError:
+            secondPoint = self._points[0]
+
+        x = (secondPoint.x - firstPoint.x) / 2 + firstPoint.x
+        y = (secondPoint.y - firstPoint.y) / 2 + firstPoint.y
+        point = wx.RealPoint(x, y)
+
+        if pos >= len(self._points) - 1:
+            self._points.append(point)
+        else:
+            self._points.insert(pos + 1, point)
+
+        self.UpdateOriginalPoints()
+
+        if self._selected:
+            self.DeleteControlPoints()
+            self.MakeControlPoints()
+
+    def DeletePolygonPoint(self, pos):
+        """Delete the given control point."""
+        if pos<len(self._points):
+            del self._points[pos]
+            self.UpdateOriginalPoints()
+            if self._selected:
+                self.DeleteControlPoints()
+                self.MakeControlPoints()
+
+    # Assume (x1, y1) is centre of box (most generally, line end at box)
+    def GetPerimeterPoint(self, x1, y1, x2, y2):
+        # First check for situation where the line is vertical,
+        # and we would want to connect to a point on that vertical --
+        # oglFindEndForPolyline can't cope with this (the arrow
+        # gets drawn to the wrong place).
+        if self._attachmentMode == ATTACHMENT_MODE_NONE and x1 == x2:
+            # Look for the point we'd be connecting to. This is
+            # a heuristic...
+            for point in self._points:
+                if point.x == 0:
+                    if y2>y1 and point.y>0:
+                        return point.x + self._xpos, point.y + self._ypos
+                    elif y2<y1 and point.y<0:
+                        return point.x + self._xpos, point.y + self._ypos
+
+        xpoints = []
+        ypoints = []
+        for point in self._points:
+            xpoints.append(point.x + self._xpos)
+            ypoints.append(point.y + self._ypos)
+
+        return FindEndForPolyline(xpoints, ypoints, x1, y1, x2, y2)
+
+    def OnDraw(self, dc):
+        if self._shadowMode != SHADOW_NONE:
+            if self._shadowBrush:
+                dc.SetBrush(self._shadowBrush)
+            dc.SetPen(TransparentPen)
+
+            dc.DrawPolygon(self._points, self._xpos + self._shadowOffsetX, self._ypos, self._shadowOffsetY)
+
+        if self._pen:
+            if self._pen.GetWidth() == 0:
+                dc.SetPen(TransparentPen)
+            else:
+                dc.SetPen(self._pen)
+        if self._brush:
+            dc.SetBrush(self._brush)
+        dc.DrawPolygon(self._points, self._xpos, self._ypos)
+
+    def OnDrawOutline(self, dc, x, y, w, h):
+        dc.SetBrush(wx.TRANSPARENT_BRUSH)
+        # Multiply all points by proportion of new size to old size
+        x_proportion = abs(w / self._originalWidth)
+        y_proportion = abs(h / self._originalHeight)
+
+        intPoints = []
+        for point in self._originalPoints:
+            intPoints.append(wx.Point(x_proportion * point[0], y_proportion * point[1]))
+        dc.DrawPolygon(intPoints, x, y)
+
+    # Make as many control points as there are vertices
+    def MakeControlPoints(self):
+        for point in self._points:
+            control = PolygonControlPoint(self._canvas, self, CONTROL_POINT_SIZE, point, point.x, point.y)
+            self._canvas.AddShape(control)
+            self._controlPoints.append(control)
+
+    def ResetControlPoints(self):
+        for i in range(min(len(self._points), len(self._controlPoints))):
+            point = self._points[i]
+            self._controlPoints[i]._xoffset = point.x
+            self._controlPoints[i]._yoffset = point.y
+            self._controlPoints[i].polygonVertex = point
+
+    def GetNumberOfAttachments(self):
+        maxN = max(len(self._points) - 1, 0)
+        for point in self._attachmentPoints:
+            if point._id>maxN:
+                maxN = point._id
+        return maxN + 1
+
+    def GetAttachmentPosition(self, attachment, nth = 0, no_arcs = 1, line = None):
+        if self._attachmentMode == ATTACHMENT_MODE_EDGE and self._points and attachment<len(self._points):
+            point = self._points[0]
+            return point.x + self._xpos, point.y + self._ypos
+        return Shape.GetAttachmentPosition(self, attachment, nth, no_arcs, line)
+
+    def AttachmentIsValid(self, attachment):
+        if not self._points:
+            return False
+
+        if attachment >= 0 and attachment<len(self._points):
+            return True
+
+        for point in self._attachmentPoints:
+            if point._id == attachment:
+                return True
+
+        return False
+
+    # Rotate about the given axis by the given amount in radians
+    def Rotate(self, x, y, theta):
+        actualTheta = theta - self._rotation
+
+        # Rotate attachment points
+        sinTheta = sin(actualTheta)
+        cosTheta = cos(actualTheta)
+
+        for point in self._attachmentPoints:
+            x1 = point._x
+            y1 = point._y
+
+            point._x = x1 * cosTheta - y1 * sinTheta + x * (1 - cosTheta) + y * sinTheta
+            point._y = x1 * sinTheta + y1 * cosTheta + y * (1 - cosTheta) + x * sinTheta
+
+        for point in self._points:
+            x1 = point.x
+            y1 = point.y
+
+            point.x = x1 * cosTheta - y1 * sinTheta + x * (1 - cosTheta) + y * sinTheta
+            point.y = x1 * sinTheta + y1 * cosTheta + y * (1 - cosTheta) + x * sinTheta
+
+        for point in self._originalPoints:
+            x1 = point.x
+            y1 = point.y
+
+            point.x = x1 * cosTheta - y1 * sinTheta + x * (1 - cosTheta) + y * sinTheta
+            point.y = x1 * sinTheta + y1 * cosTheta + y * (1 - cosTheta) + x * sinTheta
+
+        self._rotation = theta
+
+        self.CalculatePolygonCentre()
+        self.CalculateBoundingBox()
+        self.ResetControlPoints()
+
+    # Control points ('handles') redirect control to the actual shape, to
+    # make it easier to override sizing behaviour.
+    def OnSizingDragLeft(self, pt, draw, x, y, keys = 0, attachment = 0):
+        dc = wx.ClientDC(self.GetCanvas())
+        self.GetCanvas().PrepareDC(dc)
+
+        dc.SetLogicalFunction(OGLRBLF)
+
+        dottedPen = wx.Pen(wx.Colour(0, 0, 0), 1, wx.DOT)
+        dc.SetPen(dottedPen)
+        dc.SetBrush(wx.TRANSPARENT_BRUSH)
+
+        # Code for CTRL-drag in C++ version commented out
+        
+        pt.CalculateNewSize(x, y)
+
+        self.GetEventHandler().OnDrawOutline(dc, self.GetX(), self.GetY(), pt.GetNewSize().x, pt.GetNewSize().y)
+
+    def OnSizingBeginDragLeft(self, pt, x, y, keys = 0, attachment = 0):
+        dc = wx.ClientDC(self.GetCanvas())
+        self.GetCanvas().PrepareDC(dc)
+
+        self.Erase(dc)
+        
+        dc.SetLogicalFunction(OGLRBLF)
+
+        bound_x, bound_y = self.GetBoundingBoxMin()
+
+        dist = sqrt((x - self.GetX()) * (x - self.GetX()) + (y - self.GetY()) * (y - self.GetY()))
+
+        pt._originalDistance = dist
+        pt._originalSize.x = bound_x
+        pt._originalSize.y = bound_y
+
+        if pt._originalDistance == 0:
+            pt._originalDistance = 0.0001
+
+        dottedPen = wx.Pen(wx.Colour(0, 0, 0), 1, wx.DOT)
+        dc.SetPen(dottedPen)
+        dc.SetBrush(wx.TRANSPARENT_BRUSH)
+
+        # Code for CTRL-drag in C++ version commented out
+
+        pt.CalculateNewSize(x, y)
+
+        self.GetEventHandler().OnDrawOutline(dc, self.GetX(), self.GetY(), pt.GetNewSize().x, pt.GetNewSize().y)
+
+        self._canvas.CaptureMouse()
+
+    def OnSizingEndDragLeft(self, pt, x, y, keys = 0, attachment = 0):
+        dc = wx.ClientDC(self.GetCanvas())
+        self.GetCanvas().PrepareDC(dc)
+
+        if self._canvas.HasCapture():
+            self._canvas.ReleaseMouse()
+        dc.SetLogicalFunction(wx.COPY)
+
+        # If we're changing shape, must reset the original points
+        if keys & KEY_CTRL:
+            self.CalculateBoundingBox()
+            self.CalculatePolygonCentre()
+        else:
+            self.SetSize(pt.GetNewSize().x, pt.GetNewSize().y)
+
+        self.Recompute()
+        self.ResetControlPoints()
+        self.Move(dc, self.GetX(), self.GetY())
+        if not self._canvas.GetQuickEditMode():
+            self._canvas.Redraw(dc)
+
+
+
+class EllipseShape(Shape):
+    """The EllipseShape behaves similarly to the RectangleShape but is
+    elliptical.
+
+    Derived from:
+      wxShape
+    """
+    def __init__(self, w, h):
+        Shape.__init__(self)
+        self._width = w
+        self._height = h
+        self.SetDefaultRegionSize()
+
+    def GetBoundingBoxMin(self):
+        return self._width, self._height
+
+    def GetPerimeterPoint(self, x1, y1, x2, y2):
+        bound_x, bound_y = self.GetBoundingBoxMax()
+
+        return DrawArcToEllipse(self._xpos, self._ypos, bound_x, bound_y, x2, y2, x1, y1)
+
+    def GetWidth(self):
+        return self._width
+
+    def GetHeight(self):
+        return self._height
+
+    def SetWidth(self, w):
+        self._width = w
+
+    def SetHeight(self, h):
+        self._height = h
+        
+    def OnDraw(self, dc):
+        if self._shadowMode != SHADOW_NONE:
+            if self._shadowBrush:
+                dc.SetBrush(self._shadowBrush)
+            dc.SetPen(TransparentPen)
+            dc.DrawEllipse(self._xpos - self.GetWidth() / 2 + self._shadowOffsetX,
+                           self._ypos - self.GetHeight() / 2 + self._shadowOffsetY,
+                           self.GetWidth(), self.GetHeight())
+
+        if self._pen:
+            if self._pen.GetWidth() == 0:
+                dc.SetPen(TransparentPen)
+            else:
+                dc.SetPen(self._pen)
+        if self._brush:
+            dc.SetBrush(self._brush)
+        dc.DrawEllipse(self._xpos - self.GetWidth() / 2, self._ypos - self.GetHeight() / 2, self.GetWidth(), self.GetHeight())
+
+    def SetSize(self, x, y, recursive = True):
+        self.SetAttachmentSize(x, y)
+        self._width = x
+        self._height = y
+        self.SetDefaultRegionSize()
+
+    def GetNumberOfAttachments(self):
+        return Shape.GetNumberOfAttachments(self)
+
+    # There are 4 attachment points on an ellipse - 0 = top, 1 = right,
+    # 2 = bottom, 3 = left.
+    def GetAttachmentPosition(self, attachment, nth = 0, no_arcs = 1, line = None):
+        if self._attachmentMode == ATTACHMENT_MODE_BRANCHING:
+            return Shape.GetAttachmentPosition(self, attachment, nth, no_arcs, line)
+
+        if self._attachmentMode != ATTACHMENT_MODE_NONE:
+            top = self._ypos + self._height / 2
+            bottom = self._ypos - self._height / 2
+            left = self._xpos - self._width / 2
+            right = self._xpos + self._width / 2
+
+            physicalAttachment = self.LogicalToPhysicalAttachment(attachment)
+
+            if physicalAttachment == 0:
+                if self._spaceAttachments:
+                    x = left + (nth + 1) * self._width / (no_arcs + 1)
+                else:
+                    x = self._xpos
+                y = top
+                # We now have the point on the bounding box: but get the point
+                # on the ellipse by imagining a vertical line from
+                # (x, self._ypos - self_height - 500) to (x, self._ypos) intersecting
+                # the ellipse.
+
+                return DrawArcToEllipse(self._xpos, self._ypos, self._width, self._height, x, self._ypos - self._height - 500, x, self._ypos)
+            elif physicalAttachment == 1:
+                x = right
+                if self._spaceAttachments:
+                    y = bottom + (nth + 1) * self._height / (no_arcs + 1)
+                else:
+                    y = self._ypos
+                return DrawArcToEllipse(self._xpos, self._ypos, self._width, self._height, self._xpos + self._width + 500, y, self._xpos, y)
+            elif physicalAttachment == 2:
+                if self._spaceAttachments:
+                    x = left + (nth + 1) * self._width / (no_arcs + 1)
+                else:
+                    x = self._xpos
+                y = bottom
+                return DrawArcToEllipse(self._xpos, self._ypos, self._width, self._height, x, self._ypos + self._height + 500, x, self._ypos)
+            elif physicalAttachment == 3:
+                x = left
+                if self._spaceAttachments:
+                    y = bottom + (nth + 1) * self._height / (no_arcs + 1)
+                else:
+                    y = self._ypos
+                return DrawArcToEllipse(self._xpos, self._ypos, self._width, self._height, self._xpos - self._width - 500, y, self._xpos, y)
+            else:
+                return Shape.GetAttachmentPosition(self, attachment, x, y, nth, no_arcs, line)
+        else:
+            return self._xpos, self._ypos
+
+
+
+class CircleShape(EllipseShape):
+    """An EllipseShape whose width and height are the same."""
+    def __init__(self, diameter):
+        EllipseShape.__init__(self, diameter, diameter)
+        self.SetMaintainAspectRatio(True)
+
+    def GetPerimeterPoint(self, x1, y1, x2, y2):
+        return FindEndForCircle(self._width / 2, self._xpos, self._ypos, x2, y2)
+
+
+
+class TextShape(RectangleShape):
+    """As wxRectangleShape, but only the text is displayed."""
+    def __init__(self, width, height):
+        RectangleShape.__init__(self, width, height)
+
+    def OnDraw(self, dc):
+        pass
+
+
+
+class ShapeRegion(object):
+    """Object region."""
+    def __init__(self, region = None):
+        if region:
+            self._regionText = region._regionText
+            self._regionName = region._regionName
+            self._textColour = region._textColour
+
+            self._font = region._font
+            self._minHeight = region._minHeight
+            self._minWidth = region._minWidth
+            self._width = region._width
+            self._height = region._height
+            self._x = region._x
+            self._y = region._y
+
+            self._regionProportionX = region._regionProportionX
+            self._regionProportionY = region._regionProportionY
+            self._formatMode = region._formatMode
+            self._actualColourObject = region._actualColourObject
+            self._actualPenObject = None
+            self._penStyle = region._penStyle
+            self._penColour = region._penColour
+
+            self.ClearText()
+            for line in region._formattedText:
+                new_line = ShapeTextLine(line.GetX(), line.GetY(), line.GetText())
+                self._formattedText.append(new_line)
+        else:
+            self._regionText=""
+            self._font = NormalFont
+            self._minHeight = 5.0
+            self._minWidth = 5.0
+            self._width = 0.0
+            self._height = 0.0
+            self._x = 0.0
+            self._y = 0.0
+
+            self._regionProportionX=-1.0
+            self._regionProportionY=-1.0
+            self._formatMode = FORMAT_CENTRE_HORIZ | FORMAT_CENTRE_VERT
+            self._regionName=""
+            self._textColour="BLACK"
+            self._penColour="BLACK"
+            self._penStyle = wx.SOLID
+            self._actualColourObject = wx.TheColourDatabase.Find("BLACK")
+            self._actualPenObject = None
+
+        self._formattedText = []
+
+    def ClearText(self):
+        self._formattedText = []
+
+    def SetFont(self, f):
+        self._font = f
+
+    def SetMinSize(self, w, h):
+        self._minWidth = w
+        self._minHeight = h
+
+    def SetSize(self, w, h):
+        self._width = w
+        self._height = h
+
+    def SetPosition(self, xp, yp):
+        self._x = xp
+        self._y = yp
+
+    def SetProportions(self, xp, yp):
+        self._regionProportionX = xp
+        self._regionProportionY = yp
+
+    def SetFormatMode(self, mode):
+        self._formatMode = mode
+
+    def SetColour(self, col):
+        self._textColour = col
+        self._actualColourObject = col
+
+    def GetActualColourObject(self):
+        self._actualColourObject = wx.TheColourDatabase.Find(self.GetColour())
+        return self._actualColourObject
+
+    def SetPenColour(self, col):
+        self._penColour = col
+        self._actualPenObject = None
+
+    # Returns NULL if the pen is invisible
+    # (different to pen being transparent; indicates that
+    # region boundary should not be drawn.)
+    def GetActualPen(self):
+        if self._actualPenObject:
+            return self._actualPenObject
+
+        if not self._penColour:
+            return None
+        if self._penColour=="Invisible":
+            return None
+        self._actualPenObject = wx.ThePenList.FindOrCreatePen(self._penColour, 1, self._penStyle)
+        return self._actualPenObject
+
+    def SetText(self, s):
+        self._regionText = s
+
+    def SetName(self, s):
+        self._regionName = s
+
+    def GetText(self):
+        return self._regionText
+
+    def GetFont(self):
+        return self._font
+
+    def GetMinSize(self):
+        return self._minWidth, self._minHeight
+
+    def GetProportion(self):
+        return self._regionProportionX, self._regionProportionY
+
+    def GetSize(self):
+        return self._width, self._height
+
+    def GetPosition(self):
+        return self._x, self._y
+
+    def GetFormatMode(self):
+        return self._formatMode
+
+    def GetName(self):
+        return self._regionName
+
+    def GetColour(self):
+        return self._textColour
+
+    def GetFormattedText(self):
+        return self._formattedText
+
+    def GetPenColour(self):
+        return self._penColour
+
+    def GetPenStyle(self):
+        return self._penStyle
+
+    def SetPenStyle(self, style):
+        self._penStyle = style
+        self._actualPenObject = None
+
+    def GetWidth(self):
+        return self._width
+
+    def GetHeight(self):
+        return self._height
+
+
+
+class ControlPoint(RectangleShape):
+    def __init__(self, theCanvas, object, size, the_xoffset, the_yoffset, the_type):
+        RectangleShape.__init__(self, size, size)
+
+        self._canvas = theCanvas
+        self._shape = object
+        self._xoffset = the_xoffset
+        self._yoffset = the_yoffset
+        self._type = the_type
+        self.SetPen(BlackForegroundPen)
+        self.SetBrush(wx.BLACK_BRUSH)
+        self._oldCursor = None
+        self._visible = True
+        self._eraseObject = True
+        
+    # Don't even attempt to draw any text - waste of time
+    def OnDrawContents(self, dc):
+        pass
+
+    def OnDraw(self, dc):
+        self._xpos = self._shape.GetX() + self._xoffset
+        self._ypos = self._shape.GetY() + self._yoffset
+        RectangleShape.OnDraw(self, dc)
+
+    def OnErase(self, dc):
+        RectangleShape.OnErase(self, dc)
+
+    # Implement resizing of canvas object
+    def OnDragLeft(self, draw, x, y, keys = 0, attachment = 0):
+        self._shape.GetEventHandler().OnSizingDragLeft(self, draw, x, y, keys, attachment)
+
+    def OnBeginDragLeft(self, x, y, keys = 0, attachment = 0):
+        self._shape.GetEventHandler().OnSizingBeginDragLeft(self, x, y, keys, attachment)
+
+    def OnEndDragLeft(self, x, y, keys = 0, attachment = 0):
+        self._shape.GetEventHandler().OnSizingEndDragLeft(self, x, y, keys, attachment)
+
+    def GetNumberOfAttachments(self):
+        return 1
+
+    def GetAttachmentPosition(self, attachment, nth = 0, no_arcs = 1, line = None):
+        return self._xpos, self._ypos
+
+    def SetEraseObject(self, er):
+        self._eraseObject = er
+
+        
+class PolygonControlPoint(ControlPoint):
+    def __init__(self, theCanvas, object, size, vertex, the_xoffset, the_yoffset):
+        ControlPoint.__init__(self, theCanvas, object, size, the_xoffset, the_yoffset, 0)
+        self._polygonVertex = vertex
+        self._originalDistance = 0.0
+        self._newSize = wx.RealPoint()
+        self._originalSize = wx.RealPoint()
+
+    def GetNewSize(self):
+        return self._newSize
+    
+    # Calculate what new size would be, at end of resize
+    def CalculateNewSize(self, x, y):
+        bound_x, bound_y = self.GetShape().GetBoundingBoxMax()
+        dist = sqrt((x - self._shape.GetX()) * (x - self._shape.GetX()) + (y - self._shape.GetY()) * (y - self._shape.GetY()))
+
+        self._newSize.x = dist / self._originalDistance * self._originalSize.x
+        self._newSize.y = dist / self._originalDistance * self._originalSize.y
+
+    # Implement resizing polygon or moving the vertex
+    def OnDragLeft(self, draw, x, y, keys = 0, attachment = 0):
+        self._shape.GetEventHandler().OnSizingDragLeft(self, draw, x, y, keys, attachment)
+
+    def OnBeginDragLeft(self, x, y, keys = 0, attachment = 0):
+        self._shape.GetEventHandler().OnSizingBeginDragLeft(self, x, y, keys, attachment)
+
+    def OnEndDragLeft(self, x, y, keys = 0, attachment = 0):
+        self._shape.GetEventHandler().OnSizingEndDragLeft(self, x, y, keys, attachment)
+
+from canvas import *
+from lines import *
+from composit import *
diff --git a/wxPython/wx/lib/ogl/bmpshape.py b/wxPython/wx/lib/ogl/bmpshape.py
new file mode 100644 (file)
index 0000000..76e06de
--- /dev/null
@@ -0,0 +1,66 @@
+# -*- coding: iso-8859-1 -*-
+#----------------------------------------------------------------------------
+# Name:         bmpshape.py
+# Purpose:      Bitmap shape
+#
+# Author:       Pierre Hjälm (from C++ original by Julian Smart)
+#
+# Created:      20040508
+# RCS-ID:       
+# Copyright:    (c) 2004 Pierre Hjälm - 1998 Julian Smart
+# Licence:      wxWindows license
+#----------------------------------------------------------------------------
+
+from __future__ import division
+
+from basic import RectangleShape
+
+
+class BitmapShape(RectangleShape):
+    """Draws a bitmap (non-resizable)."""
+    def __init__(self):
+        RectangleShape.__init__(self, 100, 50)
+        self._filename=""
+
+    def OnDraw(self, dc):
+        if not self._bitmap.Ok():
+            return
+
+        x = self._xpos-self._bitmap.GetWidth() / 2
+        y = self._ypos-self._bitmap.GetHeight() / 2
+        dc.DrawBitmap(self._bitmap, x, y, True)
+
+    def SetSize(self, w, h, recursive = True):
+        if self._bitmap.Ok():
+            w = self._bitmap.GetWidth()
+            h = self._bitmap.GetHeight()
+
+        self.SetAttachmentSize(w, h)
+
+        self._width = w
+        self._height = h
+
+        self.SetDefaultRegionSize()
+
+    def GetBitmap(self):
+        """Return a the bitmap associated with this shape."""
+        return self._bitmap
+    
+    def SetBitmap(self, bitmap):
+        """Set the bitmap associated with this shape.
+
+        You can delete the bitmap from the calling application, since
+        reference counting will take care of holding on to the internal bitmap
+        data.
+        """
+        self._bitmap = bitmap
+        if self._bitmap.Ok():
+            self.SetSize(self._bitmap.GetWidth(), self._bitmap.GetHeight())
+            
+    def SetFilename(self, f):
+        """Set the bitmap filename."""
+        self._filename = f
+
+    def GetFilename(self):
+        """Return the bitmap filename."""
+        return self._filename
diff --git a/wxPython/wx/lib/ogl/canvas.py b/wxPython/wx/lib/ogl/canvas.py
new file mode 100644 (file)
index 0000000..aaafd4d
--- /dev/null
@@ -0,0 +1,360 @@
+# -*- coding: iso-8859-1 -*-
+#----------------------------------------------------------------------------
+# Name:         canvas.py
+# Purpose:      The canvas class
+#
+# Author:       Pierre Hjälm (from C++ original by Julian Smart)
+#
+# Created:      20040508
+# RCS-ID:       
+# Copyright:    (c) 2004 Pierre Hjälm - 1998 Julian Smart
+# Licence:      wxWindows license
+#----------------------------------------------------------------------------
+
+from __future__ import division
+
+import wx
+from lines import LineShape
+from composit import *
+
+NoDragging, StartDraggingLeft, ContinueDraggingLeft, StartDraggingRight, ContinueDraggingRight = 0, 1, 2, 3, 4
+
+KEY_SHIFT, KEY_CTRL = 1, 2
+
+
+
+# Helper function: True if 'contains' wholly contains 'contained'.
+def WhollyContains(contains, contained):
+    xp1, yp1 = contains.GetX(), contains.GetY()
+    xp2, yp2 = contained.GetX(), contained.GetY()
+    
+    w1, h1 = contains.GetBoundingBoxMax()
+    w2, h2 = contained.GetBoundingBoxMax()
+    
+    left1 = xp1-w1 / 2.0
+    top1 = yp1-h1 / 2.0
+    right1 = xp1 + w1 / 2.0
+    bottom1 = yp1 + h1 / 2.0
+    
+    left2 = xp2-w2 / 2.0
+    top2 = yp2-h2 / 2.0
+    right2 = xp2 + w2 / 2.0
+    bottom2 = yp2 + h2 / 2.0
+    
+    return ((left1 <= left2) and (top1 <= top2) and (right1 >= right2) and (bottom1 >= bottom2))
+    
+
+
+class ShapeCanvas(wx.ScrolledWindow):
+    def __init__(self, parent = None, id=-1, pos = wx.DefaultPosition, size = wx.DefaultSize, style = wx.BORDER, name="ShapeCanvas"):
+        wx.ScrolledWindow.__init__(self, parent, id, pos, size, style, name)
+
+        self._shapeDiagram = None
+        self._dragState = NoDragging
+        self._draggedShape = None
+        self._oldDragX = 0
+        self._oldDragY = 0
+        self._firstDragX = 0
+        self._firstDragY = 0
+        self._checkTolerance = True
+
+        wx.EVT_PAINT(self, self.OnPaint)
+        wx.EVT_MOUSE_EVENTS(self, self.OnMouseEvent)
+
+    def SetDiagram(self, diag):
+        self._shapeDiagram = diag
+
+    def GetDiagram(self):
+        return self._shapeDiagram
+    
+    def OnPaint(self, evt):
+        dc = wx.PaintDC(self)
+        self.PrepareDC(dc)
+        
+        dc.SetBackground(wx.Brush(self.GetBackgroundColour(), wx.SOLID))
+        dc.Clear()
+
+        if self.GetDiagram():
+            self.GetDiagram().Redraw(dc)
+
+    def OnMouseEvent(self, evt):
+        dc = wx.ClientDC(self)
+        self.PrepareDC(dc)
+        
+        x, y = evt.GetLogicalPosition(dc)
+
+        keys = 0
+        if evt.ShiftDown():
+            keys |= KEY_SHIFT
+        if evt.ControlDown():
+            keys |= KEY_CTRL
+
+        dragging = evt.Dragging()
+
+        # Check if we're within the tolerance for mouse movements.
+        # If we're very close to the position we started dragging
+        # from, this may not be an intentional drag at all.
+        if dragging:
+            dx = abs(dc.LogicalToDeviceX(x-self._firstDragX))
+            dy = abs(dc.LogicalToDeviceY(y-self._firstDragY))
+            if self._checkTolerance and (dx <= self.GetDiagram().GetMouseTolerance()) and (dy <= self.GetDiagram().GetMouseTolerance()):
+                return
+            # If we've ignored the tolerance once, then ALWAYS ignore
+            # tolerance in this drag, even if we come back within
+            # the tolerance range.
+            self._checkTolerance = False
+
+        # Dragging - note that the effect of dragging is left entirely up
+        # to the object, so no movement is done unless explicitly done by
+        # object.
+        if dragging and self._draggedShape and self._dragState == StartDraggingLeft:
+            self._dragState = ContinueDraggingLeft
+
+            # If the object isn't m_draggable, transfer message to canvas
+            if self._draggedShape.Draggable():
+                self._draggedShape.GetEventHandler().OnBeginDragLeft(x, y, keys, self._draggedAttachment)
+            else:
+                self._draggedShape = None
+                self.OnBeginDragLeft(x, y, keys)
+
+            self._oldDragX, self._oldDragY = x, y
+
+        elif dragging and self._draggedShape and self._dragState == ContinueDraggingLeft:
+            # Continue dragging
+            self._draggedShape.GetEventHandler().OnDragLeft(False, self._oldDragX, self._oldDragY, keys, self._draggedAttachment)
+            self._draggedShape.GetEventHandler().OnDragLeft(True, x, y, keys, self._draggedAttachment)
+            self._oldDragX, self._oldDragY = x, y
+
+        elif evt.LeftUp and self._draggedShape and self._dragState == ContinueDraggingLeft:
+            self._dragState = NoDragging
+            self._checkTolerance = True
+
+            self._draggedShape.GetEventHandler().OnDragLeft(False, self._oldDragX, self._oldDragY, keys, self._draggedAttachment)
+            self._draggedShape.GetEventHandler().OnEndDragLeft(x, y, keys, self._draggedAttachment)
+            self._draggedShape = None
+
+        elif dragging and self._draggedShape and self._dragState == StartDraggingRight:
+            self._dragState = ContinueDraggingRight
+            if self._draggedShape.Draggable:
+                self._draggedShape.GetEventHandler().OnBeginDragRight(x, y, keys, self._draggedAttachment)
+            else:
+                self._draggedShape = None
+                self.OnBeginDragRight(x, y, keys)
+            self._oldDragX, self._oldDragY = x, y
+
+        elif dragging and self._draggedShape and self._dragState == ContinueDraggingRight:
+            # Continue dragging
+            self._draggedShape.GetEventHandler().OnDragRight(False, self._oldDragX, self._oldDragY, keys, self._draggedAttachment)
+            self._draggedShape.GetEventHandler().OnDragRight(True, x, y, keys, self._draggedAttachment)
+            self._oldDragX, self._oldDragY = x, y
+
+        elif evt.RightUp() and self._draggedShape and self._dragState == ContinueDraggingRight:
+            self._dragState = NoDragging
+            self._checkTolerance = True
+
+            self._draggedShape.GetEventHandler().OnDragRight(False, self._oldDragX, self._oldDragY, keys, self._draggedAttachment)
+            self._draggedShape.GetEventHandler().OnEndDragRight(x, y, keys, self._draggedAttachment)
+            self._draggedShape = None
+
+        # All following events sent to canvas, not object
+        elif dragging and not self._draggedShape and self._dragState == StartDraggingLeft:
+            self._dragState = ContinueDraggingLeft
+            self.OnBeginDragLeft(x, y, keys)
+            self._oldDragX, self._oldDragY = x, y
+
+        elif dragging and not self._draggedShape and self._dragState == ContinueDraggingLeft:
+            # Continue dragging
+            self.OnDragLeft(False, self._oldDragX, self._oldDragY, keys)
+            self.OnDragLeft(True, x, y, keys)
+            self._oldDragX, self._oldDragY = x, y                
+
+        elif evt.LeftUp() and not self._draggedShape and self._dragState == ContinueDraggingLeft:
+            self._dragState = NoDragging
+            self._checkTolerance = True
+
+            self.OnDragLeft(False, self._oldDragX, self._oldDragY, keys)
+            self.OnEndDragLeft(x, y, keys)
+            self._draggedShape = None
+
+        elif dragging and not self._draggedShape and self._dragState == StartDraggingRight:
+            self._dragState = ContinueDraggingRight
+            self.OnBeginDragRight(x, y, keys)
+            self._oldDragX, self._oldDragY = x, y
+
+        elif dragging and not self._draggedShape and self._dragState == ContinueDraggingRight:
+            # Continue dragging
+            self.OnDragRight(False, self._oldDragX, self._oldDragY, keys)
+            self.OnDragRight(True, x, y, keys)
+            self._oldDragX, self._oldDragY = x, y
+
+        elif evt.RightUp() and not self._draggedShape and self._dragState == ContinueDraggingRight:
+            self._dragState = NoDragging
+            self._checkTolerance = True
+
+            self.OnDragRight(False, self._oldDragX, self._oldDragY, keys)
+            self.OnEndDragRight(x, y, keys)
+            self._draggedShape = None
+
+        # Non-dragging events
+        elif evt.IsButton():
+            self._checkTolerance = True
+
+            # Find the nearest object
+            attachment = 0
+
+            nearest_object, attachment = self.FindShape(x, y)
+            if nearest_object: # Object event
+                if evt.LeftDown():
+                    self._draggedShape = nearest_object
+                    self._draggedAttachment = attachment
+                    self._dragState = StartDraggingLeft
+                    self._firstDragX = x
+                    self._firstDragY = y
+
+                elif evt.LeftUp():
+                    # N.B. Only register a click if the same object was
+                    # identified for down *and* up.
+                    if nearest_object == self._draggedShape:
+                        nearest_object.GetEventHandler().OnLeftClick(x, y, keys, attachment)
+                    self._draggedShape = None
+                    self._dragState = NoDragging
+
+                elif evt.LeftDClick():
+                    nearest_object.GetEventHandler().OnLeftDoubleClick(x, y, keys, attachment)
+                    self._draggedShape = None
+                    self._dragState = NoDragging
+
+                elif evt.RightDown():
+                    self._draggedShape = nearest_object
+                    self._draggedAttachment = attachment
+                    self._dragState = StartDraggingRight
+                    self._firstDragX = x
+                    self._firstDragY = y
+
+                elif evt.RightUp():
+                    if nearest_object == self._draggedShape:
+                        nearest_object.GetEventHandler().OnRightClick(x, y, keys, attachment)
+                    self._draggedShape = None
+                    self._dragState = NoDragging
+
+            else: # Canvas event
+                if evt.LeftDown():
+                    self._draggedShape = None
+                    self._dragState = StartDraggingLeft
+                    self._firstDragX = x
+                    self._firstDragY = y
+
+                elif evt.LeftUp():
+                    self.OnLeftClick(x, y, keys)
+                    self._draggedShape = None
+                    self._dragState = NoDragging
+
+                elif evt.RightDown():
+                    self._draggedShape = None
+                    self._dragState = StartDraggingRight
+                    self._firstDragX = x
+                    self._firstDragY = y
+
+                elif evt.RightUp():
+                    self.OnRightClick(x, y, keys)
+                    self._draggedShape = None
+                    self._dragState = NoDragging
+
+    def FindShape(self, x, y, info = None, notObject = None):
+        nearest = 100000.0
+        nearest_attachment = 0
+        nearest_object = None
+
+        # Go backward through the object list, since we want:
+        # (a) to have the control points drawn LAST to overlay
+        #     the other objects
+        # (b) to find the control points FIRST if they exist
+
+        for object in self.GetDiagram().GetShapeList()[::-1]:
+            # First pass for lines, which might be inside a container, so we
+            # want lines to take priority over containers. This first loop
+            # could fail if we clickout side a line, so then we'll
+            # try other shapes.
+            if object.IsShown() and \
+               isinstance(object, LineShape) and \
+               object.HitTest(x, y) and \
+               ((info == None) or isinstance(object, info)) and \
+               (not notObject or not notObject.HasDescendant(object)):
+                temp_attachment, dist = object.HitTest(x, y)
+                # A line is trickier to spot than a normal object.
+                # For a line, since it's the diagonal of the box
+                # we use for the hit test, we may have several
+                # lines in the box and therefore we need to be able
+                # to specify the nearest point to the centre of the line
+                # as our hit criterion, to give the user some room for
+                # manouevre.
+                if dist<nearest:
+                    nearest = dist
+                    nearest_object = object
+                    nearest_attachment = temp_attachment
+
+        for object in self.GetDiagram().GetShapeList()[::-1]:
+            # On second pass, only ever consider non-composites or
+            # divisions. If children want to pass up control to
+            # the composite, that's up to them.
+            if (object.IsShown() and 
+                   (isinstance(object, DivisionShape) or 
+                    not isinstance(object, CompositeShape)) and 
+                    object.HitTest(x, y) and 
+                    (info == None or isinstance(object, info)) and 
+                    (not notObject or not notObject.HasDescendant(object))):
+                temp_attachment, dist = object.HitTest(x, y)
+                if not isinstance(object, LineShape):
+                    # If we've hit a container, and we have already
+                    # found a line in the first pass, then ignore
+                    # the container in case the line is in the container.
+                    # Check for division in case line straddles divisions
+                    # (i.e. is not wholly contained).
+                    if not nearest_object or not (isinstance(object, DivisionShape) or WhollyContains(object, nearest_object)):
+                        nearest_object = object
+                        nearest_attachment = temp_attachment
+                        break
+
+        return nearest_object, nearest_attachment
+
+    def AddShape(self, object, addAfter = None):
+        self.GetDiagram().AddShape(object, addAfter)
+
+    def InsertShape(self, object):
+        self.GetDiagram().InsertShape(object)
+
+    def RemoveShape(self, object):
+        self.GetDiagram().RemoveShape(object)
+
+    def GetQuickEditMode(self):
+        return self.GetDiagram().GetQuickEditMode()
+    
+    def Redraw(self, dc):
+        self.GetDiagram().Redraw(dc)
+
+    def Snap(self, x, y):
+        return self.GetDiagram().Snap(x, y)
+
+    def OnLeftClick(self, x, y, keys = 0):
+        pass
+
+    def OnRightClick(self, x, y, keys = 0):
+        pass
+
+    def OnDragLeft(self, draw, x, y, keys = 0):
+        pass
+
+    def OnBeginDragLeft(self, x, y, keys = 0):
+        pass
+
+    def OnEndDragLeft(self, x, y, keys = 0):
+        pass
+
+    def OnDragRight(self, draw, x, y, keys = 0):
+        pass
+
+    def OnBeginDragRight(self, x, y, keys = 0):
+        pass
+
+    def OnEndDragRight(self, x, y, keys = 0):
+        pass
diff --git a/wxPython/wx/lib/ogl/composit.py b/wxPython/wx/lib/ogl/composit.py
new file mode 100644 (file)
index 0000000..8d53b9d
--- /dev/null
@@ -0,0 +1,1410 @@
+# -*- coding: iso-8859-1 -*-
+#----------------------------------------------------------------------------
+# Name:         composit.py
+# Purpose:      Composite class
+#
+# Author:       Pierre Hjälm (from C++ original by Julian Smart)
+#
+# Created:      20040508
+# RCS-ID:       
+# Copyright:    (c) 2004 Pierre Hjälm - 1998 Julian Smart
+# Licence:      wxWindows license
+#----------------------------------------------------------------------------
+
+from __future__ import division
+
+import sys
+import wx
+
+from basic import RectangleShape, Shape, ControlPoint
+from oglmisc import *
+
+KEY_SHIFT, KEY_CTRL = 1, 2
+
+objectStartX = 0.0
+objectStartY = 0.0
+
+CONSTRAINT_CENTRED_VERTICALLY   = 1
+CONSTRAINT_CENTRED_HORIZONTALLY = 2
+CONSTRAINT_CENTRED_BOTH         = 3
+CONSTRAINT_LEFT_OF              = 4
+CONSTRAINT_RIGHT_OF             = 5
+CONSTRAINT_ABOVE                = 6
+CONSTRAINT_BELOW                = 7
+CONSTRAINT_ALIGNED_TOP          = 8
+CONSTRAINT_ALIGNED_BOTTOM       = 9
+CONSTRAINT_ALIGNED_LEFT         = 10
+CONSTRAINT_ALIGNED_RIGHT        = 11
+
+# Like aligned, but with the objects centred on the respective edge
+# of the reference object.
+CONSTRAINT_MIDALIGNED_TOP       = 12
+CONSTRAINT_MIDALIGNED_BOTTOM    = 13
+CONSTRAINT_MIDALIGNED_LEFT      = 14
+CONSTRAINT_MIDALIGNED_RIGHT     = 15
+
+
+
+class ConstraintType(object):
+    def __init__(self, theType, theName, thePhrase):
+        self._type = theType
+        self._name = theName
+        self._phrase = thePhrase
+
+
+
+ConstraintTypes = [
+    [CONSTRAINT_CENTRED_VERTICALLY,
+        ConstraintType(CONSTRAINT_CENTRED_VERTICALLY, "Centre vertically", "centred vertically w.r.t.")],
+
+    [CONSTRAINT_CENTRED_HORIZONTALLY,
+        ConstraintType(CONSTRAINT_CENTRED_HORIZONTALLY, "Centre horizontally", "centred horizontally w.r.t.")],
+
+    [CONSTRAINT_CENTRED_BOTH,
+        ConstraintType(CONSTRAINT_CENTRED_BOTH, "Centre", "centred w.r.t.")],
+
+    [CONSTRAINT_LEFT_OF,
+        ConstraintType(CONSTRAINT_LEFT_OF, "Left of", "left of")],
+
+    [CONSTRAINT_RIGHT_OF,
+        ConstraintType(CONSTRAINT_RIGHT_OF, "Right of", "right of")],
+
+    [CONSTRAINT_ABOVE,
+        ConstraintType(CONSTRAINT_ABOVE, "Above", "above")],
+
+    [CONSTRAINT_BELOW,
+        ConstraintType(CONSTRAINT_BELOW, "Below", "below")],
+
+    # Alignment
+    [CONSTRAINT_ALIGNED_TOP,
+        ConstraintType(CONSTRAINT_ALIGNED_TOP, "Top-aligned", "aligned to the top of")],
+
+    [CONSTRAINT_ALIGNED_BOTTOM,
+        ConstraintType(CONSTRAINT_ALIGNED_BOTTOM, "Bottom-aligned", "aligned to the bottom of")],
+
+    [CONSTRAINT_ALIGNED_LEFT,
+        ConstraintType(CONSTRAINT_ALIGNED_LEFT, "Left-aligned", "aligned to the left of")],
+
+    [CONSTRAINT_ALIGNED_RIGHT,
+        ConstraintType(CONSTRAINT_ALIGNED_RIGHT, "Right-aligned", "aligned to the right of")],
+
+    # Mid-alignment
+    [CONSTRAINT_MIDALIGNED_TOP,
+        ConstraintType(CONSTRAINT_MIDALIGNED_TOP, "Top-midaligned", "centred on the top of")],
+
+    [CONSTRAINT_MIDALIGNED_BOTTOM,
+        ConstraintType(CONSTRAINT_MIDALIGNED_BOTTOM, "Bottom-midaligned", "centred on the bottom of")],
+
+    [CONSTRAINT_MIDALIGNED_LEFT,
+        ConstraintType(CONSTRAINT_MIDALIGNED_LEFT, "Left-midaligned", "centred on the left of")],
+
+    [CONSTRAINT_MIDALIGNED_RIGHT,
+        ConstraintType(CONSTRAINT_MIDALIGNED_RIGHT, "Right-midaligned", "centred on the right of")]
+    ]
+
+
+
+
+class Constraint(object):
+    """A Constraint object helps specify how child shapes are laid out with
+    respect to siblings and parents.
+
+    Derived from:
+      wxObject
+    """
+    def __init__(self, type, constraining, constrained):
+        self._xSpacing = 0.0
+        self._ySpacing = 0.0
+
+        self._constraintType = type
+        self._constraintingObject = constraining
+
+        self._constraintId = 0
+        self._constraintName="noname"
+
+        self._constrainedObjects = constrained[:]
+
+    def __repr__(self):
+        return "<%s.%s>" % (self.__class__.__module__, self.__class__.__name__)
+
+    def SetSpacing(self, x, y):
+        """Sets the horizontal and vertical spacing for the constraint."""
+        self._xSpacing = x
+        self._ySpacing = y
+        
+    def Equals(self, a, b):
+        """Return TRUE if x and y are approximately equal (for the purposes
+        of evaluating the constraint).
+        """
+        marg = 0.5
+
+        return b <= a + marg and b >= a-marg
+
+    def Evaluate(self):
+        """Evaluate this constraint and return TRUE if anything changed."""
+        maxWidth, maxHeight = self._constraintingObject.GetBoundingBoxMax()
+        minWidth, minHeight = self._constraintingObject.GetBoundingBoxMin()
+        x = self._constraintingObject.GetX()
+        y = self._constraintingObject.GetY()
+
+        dc = wx.ClientDC(self._constraintingObject.GetCanvas())
+        self._constraintingObject.GetCanvas().PrepareDC(dc)
+
+        if self._constraintType == CONSTRAINT_CENTRED_VERTICALLY:
+            n = len(self._constrainedObjects)
+            totalObjectHeight = 0.0
+            for constrainedObject in self._constrainedObjects:
+                width2, height2 = constrainedObject.GetBoundingBoxMax()
+                totalObjectHeight += height2
+
+            # Check if within the constraining object...
+            if totalObjectHeight + (n + 1) * self._ySpacing <= minHeight:
+                spacingY = (minHeight-totalObjectHeight) / (n + 1)
+                startY = y-minHeight / 2
+            else: # Otherwise, use default spacing
+                spacingY = self._ySpacing
+                startY = y-(totalObjectHeight + (n + 1) * spacingY) / 2
+
+            # Now position the objects
+            changed = False
+            for constrainedObject in self._constrainedObjects:
+                width2, height2 = constrainedObject.GetBoundingBoxMax()
+                startY += spacingY + height2 / 2
+                if not self.Equals(startY, constrainedObject.GetY()):
+                    constrainedObject.Move(dc, constrainedObject.GetX(), startY, False)
+                    changed = True
+                startY += height2 / 2
+            return changed
+        elif self._constraintType == CONSTRAINT_CENTRED_HORIZONTALLY:
+            n = len(self._constrainedObjects)
+            totalObjectWidth = 0.0
+            for constrainedObject in self._constrainedObjects:
+                width2, height2 = constrainedObject.GetBoundingBoxMax()
+                totalObjectWidth += width2
+
+            # Check if within the constraining object...
+            if totalObjectWidth + (n + 1) * self._xSpacing<minWidth:
+                spacingX = (minWidth-totalObjectWidth) / (n + 1)
+                startX = x-minWidth / 2
+            else: # Otherwise, use default spacing
+                spacingX = self._xSpacing
+                startX = x-(totalObjectWidth + (n + 1) * spacingX) / 2
+
+            # Now position the objects
+            changed = False
+            for constrainedObject in self._constrainedObjects:
+                width2, height2 = constrainedObject.GetBoundingBoxMax()
+                startX += spacingX + width2 / 2
+                if not self.Equals(startX, constrainedObject.GetX()):
+                    constrainedObject.Move(dc, startX, constrainedObject.GetY(), False)
+                    changed = True
+                startX += width2 / 2
+            return changed
+        elif self._constraintType == CONSTRAINT_CENTRED_BOTH:
+            n = len(self._constrainedObjects)
+            totalObjectWidth = 0.0
+            totalObjectHeight = 0.0
+
+            for constrainedObject in self._constrainedObjects:
+                width2, height2 = constrainedObject.GetBoundingBoxMax()
+                totalObjectWidth += width2
+                totalObjectHeight += height2
+
+            # Check if within the constraining object...
+            if totalObjectHeight + (n + 1) * self._xSpacing <= minWidth:
+                spacingX = (minWidth-totalObjectWidth) / (n + 1)
+                startX = x-minWidth / 2
+            else: # Otherwise, use default spacing
+                spacingX = self._xSpacing
+                startX = x-(totalObjectWidth + (n + 1) * spacingX) / 2
+
+            # Check if within the constraining object...
+            if totalObjectHeight + (n + 1) * self._ySpacing <= minHeight:
+                spacingY = (minHeight-totalObjectHeight) / (n + 1)
+                startY = y-minHeight / 2
+            else: # Otherwise, use default spacing
+                spacingY = self._ySpacing
+                startY = y-(totalObjectHeight + (n + 1) * spacingY) / 2
+
+            # Now position the objects
+            changed = False
+            for constrainedObject in self._constrainedObjects:
+                width2, height2 = constrainedObject.GetBoundingBoxMax()
+                startX += spacingX + width2 / 2
+                startY += spacingY + height2 / 2
+
+                if not self.Equals(startX, constrainedObject.GetX()) or not self.Equals(startY, constrainedObject.GetY()):
+                    constrainedObject.Move(dc, startX, startY, False)
+                    changed = True
+
+                startX += width2 / 2
+                startY += height2 / 2
+            return changed
+        elif self._constraintType == CONSTRAINT_LEFT_OF:
+            changed = False
+            for constrainedObject in self._constrainedObjects:
+                width2, height2 = constrainedObject.GetBoundingBoxMax()
+
+                x3 = x-minWidth / 2-width2 / 2-self._xSpacing
+                if not self.Equals(x3, constrainedObject.GetX()):
+                    changed = True
+                    constrainedObject.Move(dc, x3, constrainedObject.GetY(), False)
+            return changed
+        elif self._constraintType == CONSTRAINT_RIGHT_OF:
+            changed = False
+
+            for constrainedObject in self._constrainedObjects:
+                width2, height2 = constrainedObject.GetBoundingBoxMax()
+                x3 = x + minWidth / 2 + width2 / 2 + self._xSpacing
+                if not self.Equals(x3, constrainedObject.GetX()):
+                    constrainedObject.Move(dc, x3, constrainedObject.GetY(), False)
+                    changed = True
+            return changed
+        elif self._constraintType == CONSTRAINT_ABOVE:
+            changed = False
+
+            for constrainedObject in self._constrainedObjects:
+                width2, height2 = constrainedObject.GetBoundingBoxMax()
+
+                y3 = y-minHeight / 2-height2 / 2-self._ySpacing
+                if not self.Equals(y3, constrainedObject.GetY()):
+                    changed = True
+                    constrainedObject.Move(dc, constrainedObject.GetX(), y3, False)
+            return changed
+        elif self._constraintType == CONSTRAINT_BELOW:
+            changed = False
+
+            for constrainedObject in self._constrainedObjects:
+                width2, height2 = constrainedObject.GetBoundingBoxMax()
+
+                y3 = y + minHeight / 2 + height2 / 2 + self._ySpacing
+                if not self.Equals(y3, constrainedObject.GetY()):
+                    changed = True
+                    constrainedObject.Move(dc, constrainedObject.GetX(), y3, False)
+            return changed
+        elif self._constraintType == CONSTRAINT_ALIGNED_LEFT:
+            changed = False
+            for constrainedObject in self._constrainedObjects:
+                width2, height2 = constrainedObject.GetBoundingBoxMax()
+                x3 = x-minWidth / 2 + width2 / 2 + self._xSpacing
+                if not self.Equals(x3, constrainedObject.GetX()):
+                    changed = True
+                    constrainedObject.Move(dc, x3, constrainedObject.GetY(), False)
+            return changed
+        elif self._constraintType == CONSTRAINT_ALIGNED_RIGHT:
+            changed = False
+            for constrainedObject in self._constrainedObjects:
+                width2, height2 = constrainedObject.GetBoundingBoxMax()
+                x3 = x + minWidth / 2-width2 / 2-self._xSpacing
+                if not self.Equals(x3, constrainedObject.GetX()):
+                    changed = True
+                    constrainedObject.Move(dc, x3, constrainedObject.GetY(), False)
+            return changed
+        elif self._constraintType == CONSTRAINT_ALIGNED_TOP:
+            changed = False
+            for constrainedObject in self._constrainedObjects:
+                width2, height2 = constrainedObject.GetBoundingBoxMax()
+                y3 = y-minHeight / 2 + height2 / 2 + self._ySpacing
+                if not self.Equals(y3, constrainedObject.GetY()):
+                    changed = True
+                    constrainedObject.Move(dc, constrainedObject.GetX(), y3, False)
+            return changed
+        elif self._constraintType == CONSTRAINT_ALIGNED_BOTTOM:
+            changed = False
+            for constrainedObject in self._constrainedObjects:
+                width2, height2 = constrainedObject.GetBoundingBoxMax()
+                y3 = y + minHeight / 2-height2 / 2-self._ySpacing
+                if not self.Equals(y3, constrainedObject.GetY()):
+                    changed = True
+                    constrainedObject.Move(dc, constrainedObject.GetX(), y3, False)
+            return changed
+        elif self._constraintType == CONSTRAINT_MIDALIGNED_LEFT:
+            changed = False
+            for constrainedObject in self._constrainedObjects:
+                x3 = x-minWidth / 2
+                if not self.Equals(x3, constrainedObject.GetX()):
+                    changed = True
+                    constrainedObject.Move(dc, x3, constrainedObject.GetY(), False)
+            return changed
+        elif self._constraintType == CONSTRAINT_MIDALIGNED_RIGHT:
+            changed = False
+            for constrainedObject in self._constrainedObjects:
+                x3 = x + minWidth / 2
+                if not self.Equals(x3, constrainedObject.GetX()):
+                    changed = True
+                    constrainedObject.Move(dc, x3, constrainedObject.GetY(), False)
+            return changed
+        elif self._constraintType == CONSTRAINT_MIDALIGNED_TOP:
+            changed = False
+            for constrainedObject in self._constrainedObjects:
+                y3 = y-minHeight / 2
+                if not self.Equals(y3, constrainedObject.GetY()):
+                    changed = True
+                    constrainedObject.Move(dc, constrainedObject.GetX(), y3, False)
+            return changed
+        elif self._constraintType == CONSTRAINT_MIDALIGNED_BOTTOM:
+            changed = False
+            for constrainedObject in self._constrainedObjects:
+                y3 = y + minHeight / 2
+                if not self.Equals(y3, constrainedObject.GetY()):
+                    changed = True
+                    constrainedObject.Move(dc, constrainedObject.GetX(), y3, False)
+            return changed
+        
+        return False
+    
+
+
+class CompositeShape(RectangleShape):
+    """This is an object with a list of child objects, and a list of size
+    and positioning constraints between the children.
+
+    Derived from:
+      wxRectangleShape
+    """
+    def __init__(self):
+        RectangleShape.__init__(self, 100.0, 100.0)
+
+        self._oldX = self._xpos
+        self._oldY = self._ypos
+
+        self._constraints = [] 
+        self._divisions = [] # In case it's a container
+        
+    def OnDraw(self, dc):
+        x1 = self._xpos-self._width / 2
+        y1 = self._ypos-self._height / 2
+
+        if self._shadowMode != SHADOW_NONE:
+            if self._shadowBrush:
+                dc.SetBrush(self._shadowBrush)
+            dc.SetPen(wx.Pen(wx.WHITE, 1, wx.TRANSPARENT))
+
+            if self._cornerRadius:
+                dc.DrawRoundedRectangle(x1 + self._shadowOffsetX, y1 + self._shadowOffsetY, self._width, self._height, self._cornerRadius)
+            else:
+                dc.DrawRectangle(x1 + self._shadowOffsetX, y1 + self._shadowOffsetY, self._width, self._height)
+
+        # For debug purposes /pi
+        #dc.DrawRectangle(x1, y1, self._width, self._height)
+        
+    def OnDrawContents(self, dc):
+        for object in self._children:
+            object.Draw(dc)
+            object.DrawLinks(dc)
+
+        Shape.OnDrawContents(self, dc)
+
+    def OnMovePre(self, dc, x, y, old_x, old_y, display = True):
+        diffX = x-old_x
+        diffY = y-old_y
+
+        for object in self._children:
+            object.Erase(dc)
+            object.Move(dc, object.GetX() + diffX, object.GetY() + diffY, display)
+
+        return True
+
+    def OnErase(self, dc):
+        RectangleShape.OnErase(self, dc)
+        for object in self._children:
+            object.Erase(dc)
+
+    def OnDragLeft(self, draw, x, y, keys = 0, attachment = 0):
+        xx, yy = self._canvas.Snap(x, y)
+        offsetX = xx-objectStartX
+        offsetY = yy-objectStartY
+
+        dc = wx.ClientDC(self.GetCanvas())
+        self.GetCanvas().PrepareDC(dc)
+
+        dc.SetLogicalFunction(OGLRBLF)
+        dottedPen = wx.Pen(wx.Colour(0, 0, 0), 1, wx.DOT)
+        dc.SetPen(dottedPen)
+        dc.SetBrush(wx.TRANSPARENT_BRUSH)
+
+        self.GetEventHandler().OnDrawOutline(dc, self.GetX() + offsetX, self.GetY() + offsetY, self.GetWidth(), self.GetHeight())
+
+    def OnBeginDragLeft(self, x, y, keys = 0, attachment = 0):
+        global objectStartX, objectStartY
+
+        objectStartX = x
+        objectStartY = y
+
+        dc = wx.ClientDC(self.GetCanvas())
+        self.GetCanvas().PrepareDC(dc)
+
+        #self.Erase(dc)
+        
+        dc.SetLogicalFunction(OGLRBLF)
+        dottedPen = wx.Pen(wx.Colour(0, 0, 0), 1, wx.DOT)
+        dc.SetPen(dottedPen)
+        dc.SetBrush(wx.TRANSPARENT_BRUSH)
+        self._canvas.CaptureMouse()
+
+        xx, yy = self._canvas.Snap(x, y)
+        offsetX = xx-objectStartX
+        offsetY = yy-objectStartY
+
+        self.GetEventHandler().OnDrawOutline(dc, self.GetX() + offsetX, self.GetY() + offsetY, self.GetWidth(), self.GetHeight())
+
+    def OnEndDragLeft(self, x, y, keys = 0, attachment = 0):
+        dc = wx.ClientDC(self.GetCanvas())
+        self.GetCanvas().PrepareDC(dc)
+
+        if self._canvas.HasCapture():
+            self._canvas.ReleaseMouse()
+
+        if not self._draggable:
+            if self._parent:
+                self._parent.GetEventHandler().OnEndDragLeft(x, y, keys, 0)
+            return
+            
+        self.Erase(dc)
+        
+        dc.SetLogicalFunction(wx.COPY)
+        
+        xx, yy = self._canvas.Snap(x, y)
+        offsetX = xx-objectStartX
+        offsetY = yy-objectStartY
+
+        self.Move(dc, self.GetX() + offsetX, self.GetY() + offsetY)
+
+        if self._canvas and not self._canvas.GetQuickEditMode():
+            self._canvas.Redraw(dc)
+
+    def OnRightClick(self, x, y, keys = 0, attachment = 0):
+        # If we get a ctrl-right click, this means send the message to
+        # the division, so we can invoke a user interface for dealing
+        # with regions.
+        if keys & KEY_CTRL:
+            for division in self._divisions:
+                hit = division.HitTest(x, y)
+                if hit:
+                    division.GetEventHandler().OnRightClick(x, y, keys, hit[0])
+                    break
+
+    def SetSize(self, w, h, recursive = True):
+        self.SetAttachmentSize(w, h)
+
+        xScale = w / max(1, self.GetWidth())
+        yScale = h / max(1, self.GetHeight())
+
+        self._width = w
+        self._height = h
+
+        if not recursive:
+            return
+
+        dc = wx.ClientDC(self.GetCanvas())
+        self.GetCanvas().PrepareDC(dc)
+
+        for object in self._children:
+            # Scale the position first
+            newX = (object.GetX()-self.GetX()) * xScale + self.GetX()
+            newY = (object.GetY()-self.GetY()) * yScale + self.GetY()
+            object.Show(False)
+            object.Move(dc, newX, newY)
+            object.Show(True)
+
+            # Now set the scaled size
+            xbound, ybound = object.GetBoundingBoxMax()
+            if not object.GetFixedWidth():
+                xbound *= xScale
+            if not object.GetFixedHeight():
+                ybound *= yScale
+            object.SetSize(xbound, ybound)
+
+        self.SetDefaultRegionSize()
+
+    def AddChild(self, child, addAfter = None):
+        """Adds a child shape to the composite.
+
+        If addAfter is not None, the shape will be added after this shape.
+        """
+        self._children.append(child)
+        child.SetParent(self)
+        if self._canvas:
+            # Ensure we add at the right position
+            if addAfter:
+                child.RemoveFromCanvas(self._canvas)
+            child.AddToCanvas(self._canvas, addAfter)
+
+    def RemoveChild(self, child):
+        """Removes the child from the composite and any constraint
+        relationships, but does not delete the child.
+        """
+        self._children.remove(child)
+        self._divisions.remove(child)
+        self.RemoveChildFromConstraints(child)
+        child.SetParent(None)
+
+    def DeleteConstraintsInvolvingChild(self, child):
+        """This function deletes constraints which mention the given child.
+
+        Used when deleting a child from the composite.
+        """
+        for constraint in self._constraints:
+            if constraint._constrainingObject == child or child in constraint._constrainedObjects:
+                self._constraints.remove(constraint)
+
+    def RemoveChildFromConstraints(self, child):
+        for constraint in self._constraints:
+            if child in constraint._constrainedObjects:
+                constraint._constrainedObjects.remove(child)
+            if constraint._constrainingObject == child:
+                constraint._constrainingObject = None
+
+            # Delete the constraint if no participants left
+            if not constraint._constrainingObject:
+                self._constraints.remove(constraint)
+
+    def AddConstraint(self, constraint):
+        """Adds a constraint to the composite."""
+        self._constraints.append(constraint)
+        if constraint._constraintId == 0:
+            constraint._constraintId = wx.NewId()
+        return constraint
+
+    def AddSimpleConstraint(self, type, constraining, constrained):
+        """Add a constraint of the given type to the composite.
+
+        constraining is the shape doing the constraining
+        constrained is a list of shapes being constrained
+        """
+        constraint = Constraint(type, constraining, constrained)
+        if constraint._constraintId == 0:
+            constraint._constraintId = wx.NewId()
+        self._constraints.append(constraint)
+        return constraint
+
+    def FindConstraint(self, cId):
+        """Finds the constraint with the given id.
+
+        Returns a tuple of the constraint and the actual composite the
+        constraint was in, in case that composite was a descendant of
+        this composit.
+
+        Returns None if not found.
+        """
+        for constraint in self._constraints:
+            if constraint._constraintId == cId:
+                return constraint, self
+
+        # If not found, try children
+        for child in self._children:
+            if isinstance(child, CompositeShape):
+                constraint = child.FindConstraint(cId)
+                if constraint:
+                    return constraint[0], child
+
+        return None
+
+    def DeleteConstraint(self, constraint):
+        """Deletes constraint from composite."""
+        self._constraints.remove(constraint)
+
+    def CalculateSize(self):
+        """Calculates the size and position of the composite based on
+        child sizes and positions.
+        """
+        maxX=-999999.9
+        maxY=-999999.9
+        minX = 999999.9
+        minY = 999999.9
+
+        for child in self._children:
+            # Recalculate size of composite objects because may not conform
+            # to size it was set to - depends on the children.
+            if isinstance(child, CompositeShape):
+                child.CalculateSize()
+
+            w, h = child.GetBoundingBoxMax()
+            if child.GetX() + w / 2>maxX:
+                maxX = child.GetX() + w / 2
+            if child.GetX()-w / 2<minX:
+                minX = child.GetX()-w / 2
+            if child.GetY() + h / 2>maxY:
+                maxY = child.GetY() + h / 2
+            if child.GetY()-h / 2<minY:
+                minY = child.GetY()-h / 2
+
+        self._width = maxX-minX
+        self._height = maxY-minY
+        self._xpos = self._width / 2 + minX
+        self._ypos = self._height / 2 + minY
+
+    def Recompute(self):
+        """Recomputes any constraints associated with the object. If FALSE is
+        returned, the constraints could not be satisfied (there was an
+        inconsistency).
+        """
+        noIterations = 0
+        changed = True
+        while changed and noIterations<500:
+            changed = self.Constrain()
+            noIterations += 1
+
+        return not changed
+
+    def Constrain(self):
+        self.CalculateSize()
+
+        changed = False
+        for child in self._children:
+            if isinstance(child, CompositeShape) and child.Constrain():
+                changed = True
+
+        for constraint in self._constraints:
+            if constraint.Evaluate():
+                changed = True
+
+        return changed
+
+    def MakeContainer(self):
+        """Makes this composite into a container by creating one child
+        DivisionShape.
+        """
+        division = self.OnCreateDivision()
+        self._divisions.append(division)
+        self.AddChild(division)
+
+        division.SetSize(self._width, self._height)
+
+        dc = wx.ClientDC(self.GetCanvas())
+        self.GetCanvas().PrepareDC(dc)
+        
+        division.Move(dc, self.GetX(), self.GetY())
+        self.Recompute()
+        division.Show(True)
+
+    def OnCreateDivision(self):
+        return DivisionShape()
+
+    def FindContainerImage(self):
+        """Finds the image used to visualize a container. This is any child of
+        the composite that is not in the divisions list.
+        """
+        for child in self._children:
+            if child in self._divisions:
+                return child
+
+        return None
+
+    def ContainsDivision(self, division):
+        """Returns TRUE if division is a descendant of this container."""
+        if division in self._divisions:
+            return True
+
+        for child in self._children:
+            if isinstance(child, CompositeShape):
+                return child.ContainsDivision(division)
+
+        return False
+
+    def GetDivisions(self):
+        """Return the list of divisions."""
+        return self._divisions
+
+    def GetConstraints(self):
+        """Return the list of constraints."""
+        return self._constraints
+
+
+#  A division object is a composite with special properties,
+#  to be used for containment. It's a subdivision of a container.
+#  A containing node image consists of a composite with a main child shape
+#  such as rounded rectangle, plus a list of division objects.
+#  It needs to be a composite because a division contains pieces
+#  of diagram.
+#  NOTE a container has at least one wxDivisionShape for consistency.
+#  This can be subdivided, so it turns into two objects, then each of
+#  these can be subdivided, etc.
+
+DIVISION_SIDE_NONE      =0
+DIVISION_SIDE_LEFT      =1
+DIVISION_SIDE_TOP       =2
+DIVISION_SIDE_RIGHT     =3
+DIVISION_SIDE_BOTTOM    =4
+
+originalX = 0.0
+originalY = 0.0
+originalW = 0.0
+originalH = 0.0
+
+
+
+class DivisionControlPoint(ControlPoint):
+    def __init__(self, the_canvas, object, size, the_xoffset, the_yoffset, the_type):
+        ControlPoint.__init__(self, the_canvas, object, size, the_xoffset, the_yoffset, the_type)
+        self.SetEraseObject(False)
+
+    # Implement resizing of canvas object
+    def OnDragLeft(self, draw, x, y, keys = 0, attachment = 0):
+        ControlPoint.OnDragLeft(self, draw, x, y, keys, attachment)
+
+    def OnBeginDragLeft(self, x, y, keys = 0, attachment = 0):
+        global originalX, originalY, originalW, originalH
+
+        originalX = self._shape.GetX()
+        originalY = self._shape.GetY()
+        originalW = self._shape.GetWidth()
+        originalH = self._shape.GetHeight()
+
+        ControlPoint.OnBeginDragLeft(self, x, y, keys, attachment)
+
+    def OnEndDragLeft(self, x, y, keys = 0, attachment = 0):
+        ControlPoint.OnEndDragLeft(self, x, y, keys, attachment)
+
+        dc = wx.ClientDC(self.GetCanvas())
+        self.GetCanvas().PrepareDC(dc)
+
+        division = self._shape
+        divisionParent = division.GetParent()
+
+        # Need to check it's within the bounds of the parent composite
+        x1 = divisionParent.GetX()-divisionParent.GetWidth() / 2
+        y1 = divisionParent.GetY()-divisionParent.GetHeight() / 2
+        x2 = divisionParent.GetX() + divisionParent.GetWidth() / 2
+        y2 = divisionParent.GetY() + divisionParent.GetHeight() / 2
+
+        # Need to check it has not made the division zero or negative
+        # width / height
+        dx1 = division.GetX()-division.GetWidth() / 2
+        dy1 = division.GetY()-division.GetHeight() / 2
+        dx2 = division.GetX() + division.GetWidth() / 2
+        dy2 = division.GetY() + division.GetHeight() / 2
+
+        success = True
+        if division.GetHandleSide() == DIVISION_SIDE_LEFT:
+            if x <= x1 or x >= x2 or x >= dx2:
+                success = False
+            # Try it out first...
+            elif not division.ResizeAdjoining(DIVISION_SIDE_LEFT, x, True):
+                success = False
+            else:
+                division.ResizeAdjoining(DIVISION_SIDE_LEFT, x, False)
+        elif division.GetHandleSide() == DIVISION_SIDE_TOP:
+            if y <= y1 or y >= y2 or y >= dy2:
+                success = False
+            elif not division.ResizeAdjoining(DIVISION_SIDE_TOP, y, True):
+                success = False
+            else:
+                division.ResizingAdjoining(DIVISION_SIDE_TOP, y, False)
+        elif division.GetHandleSide() == DIVISION_SIDE_RIGHT:
+            if x <= x1 or x >= x2 or x <= dx1:
+                success = False
+            elif not division.ResizeAdjoining(DIVISION_SIDE_RIGHT, x, True):
+                success = False
+            else:
+                division.ResizeAdjoining(DIVISION_SIDE_RIGHT, x, False)
+        elif division.GetHandleSide() == DIVISION_SIDE_BOTTOM:
+            if y <= y1 or y >= y2 or y <= dy1:
+                success = False
+            elif not division.ResizeAdjoining(DIVISION_SIDE_BOTTOM, y, True):
+                success = False
+            else:
+                division.ResizeAdjoining(DIVISION_SIDE_BOTTOM, y, False)
+
+        if not success:
+            division.SetSize(originalW, originalH)
+            division.Move(dc, originalX, originalY)
+
+        divisionParent.Draw(dc)
+        division.GetEventHandler().OnDrawControlPoints(dc)
+
+
+
+DIVISION_MENU_SPLIT_HORIZONTALLY    =1
+DIVISION_MENU_SPLIT_VERTICALLY      =2
+DIVISION_MENU_EDIT_LEFT_EDGE        =3
+DIVISION_MENU_EDIT_TOP_EDGE         =4
+DIVISION_MENU_EDIT_RIGHT_EDGE       =5
+DIVISION_MENU_EDIT_BOTTOM_EDGE      =6
+DIVISION_MENU_DELETE_ALL            =7
+    
+
+
+class PopupDivisionMenu(wx.Menu):
+    def __init__(self):
+        wx.Menu.__init__(self)
+        self.Append(DIVISION_MENU_SPLIT_HORIZONTALLY,"Split horizontally")
+        self.Append(DIVISION_MENU_SPLIT_VERTICALLY,"Split vertically")
+        self.AppendSeparator()
+        self.Append(DIVISION_MENU_EDIT_LEFT_EDGE,"Edit left edge")
+        self.Append(DIVISION_MENU_EDIT_TOP_EDGE,"Edit top edge")
+
+        wx.EVT_MENU_RANGE(self, DIVISION_MENU_SPLIT_HORIZONTALLY, DIVISION_MENU_EDIT_BOTTOM_EDGE, self.OnMenu)
+
+    def SetClientData(self, data):
+        self._clientData = data
+
+    def GetClientData(self):
+        return self._clientData
+    
+    def OnMenu(self, event):
+        division = self.GetClientData()
+        if event.GetId() == DIVISION_MENU_SPLIT_HORIZONTALLY:
+            division.Divide(wx.HORIZONTAL)
+        elif event.GetId() == DIVISION_MENU_SPLIT_VERTICALLY:
+            division.Divide(wx.VERTICAL)
+        elif event.GetId() == DIVISION_MENU_EDIT_LEFT_EDGE:
+            division.EditEdge(DIVISION_SIDE_LEFT)
+        elif event.GetId() == DIVISION_MENU_EDIT_TOP_EDGE:
+            division.EditEdge(DIVISION_SIDE_TOP)
+            
+
+
+class DivisionShape(CompositeShape):
+    """A division shape is like a composite in that it can contain further
+    objects, but is used exclusively to divide another shape into regions,
+    or divisions. A wxDivisionShape is never free-standing.
+
+    Derived from:
+      wxCompositeShape
+    """
+    def __init__(self):
+        CompositeShape.__init__(self)
+        self.SetSensitivityFilter(OP_CLICK_LEFT | OP_CLICK_RIGHT | OP_DRAG_RIGHT)
+        self.SetCentreResize(False)
+        self.SetAttachmentMode(True)
+        self._leftSide = None
+        self._rightSide = None
+        self._topSide = None
+        self._bottomSide = None
+        self._handleSide = DIVISION_SIDE_NONE
+        self._leftSidePen = wx.BLACK_PEN
+        self._topSidePen = wx.BLACK_PEN
+        self._leftSideColour="BLACK"
+        self._topSideColour="BLACK"
+        self._leftSideStyle="Solid"
+        self._topSideStyle="Solid"
+        self.ClearRegions()
+
+    def SetLeftSide(self, shape):
+        """Set the the division on the left side of this division."""
+        self._leftSide = shape
+
+    def SetTopSide(self, shape):
+        """Set the the division on the top side of this division."""
+        self._topSide = shape
+        
+    def SetRightSide(self, shape):
+        """Set the the division on the right side of this division."""
+        self._rightSide = shape
+        
+    def SetBottomSide(self, shape):
+        """Set the the division on the bottom side of this division."""
+        self._bottomSide = shape
+
+    def GetLeftSide(self):
+        """Return the division on the left side of this division."""
+        return self._leftSide
+    
+    def GetTopSide(self):
+        """Return the division on the top side of this division."""
+        return self._topSide
+    
+    def GetRightSide(self):
+        """Return the division on the right side of this division."""
+        return self._rightSide
+    
+    def GetBottomSide(self):
+        """Return the division on the bottom side of this division."""
+        return self._bottomSide
+
+    def SetHandleSide(self, side):
+        """Sets the side which the handle appears on.
+
+        Either DIVISION_SIDE_LEFT or DIVISION_SIDE_TOP.
+        """
+        self._handleSide = side
+
+    def GetHandleSide(self):
+        """Return the side which the handle appears on."""
+        return self._handleSide
+
+    def SetLeftSidePen(self, pen):
+        """Set the colour for drawing the left side of the division."""
+        self._leftSidePen = pen
+        
+    def SetTopSidePen(self, pen):
+        """Set the colour for drawing the top side of the division."""
+        self._topSidePen = pen
+        
+    def GetLeftSidePen(self):
+        """Return the pen used for drawing the left side of the division."""
+        return self._leftSidePen
+
+    def GetTopSidePen(self):
+        """Return the pen used for drawing the top side of the division."""
+        return self._topSidePen
+
+    def GetLeftSideColour(self):
+        """Return the colour used for drawing the left side of the division."""
+        return self._leftSideColour
+    
+    def GetTopSideColour(self):
+        """Return the colour used for drawing the top side of the division."""
+        return self._topSideColour
+
+    def SetLeftSideColour(self, colour):
+        """Set the colour for drawing the left side of the division."""
+        self._leftSideColour = colour
+        
+    def SetTopSideColour(self, colour):
+        """Set the colour for drawing the top side of the division."""
+        self._topSideColour = colour
+        
+    def GetLeftSideStyle(self):
+        """Return the style used for the left side of the division."""
+        return self._leftSideStyle
+    
+    def GetTopSideStyle(self):
+        """Return the style used for the top side of the division."""
+        return self._topSideStyle
+
+    def SetLeftSideStyle(self, style):
+        self._leftSideStyle = style
+        
+    def SetTopSideStyle(self, style):
+        self._lefttopStyle = style
+        
+    def OnDraw(self, dc):
+        dc.SetBrush(wx.TRANSPARENT_BRUSH)
+        dc.SetBackgroundMode(wx.TRANSPARENT)
+
+        x1 = self.GetX()-self.GetWidth() / 2
+        y1 = self.GetY()-self.GetHeight() / 2
+        x2 = self.GetX() + self.GetWidth() / 2
+        y2 = self.GetY() + self.GetHeight() / 2
+
+        # Should subtract 1 pixel if drawing under Windows
+        if sys.platform[:3]=="win":
+            y2 -= 1
+
+        if self._leftSide:
+            dc.SetPen(self._leftSidePen)
+            dc.DrawLine(x1, y2, x1, y1)
+
+        if self._topSide:
+            dc.SetPen(self._topSidePen)
+            dc.DrawLine(x1, y1, x2, y1)
+
+        # For testing purposes, draw a rectangle so we know
+        # how big the division is.
+        #dc.SetBrush(wx.RED_BRUSH)
+        #dc.DrawRectangle(x1, y1, self.GetWidth(), self.GetHeight())
+        
+    def OnDrawContents(self, dc):
+        CompositeShape.OnDrawContents(self, dc)
+
+    def OnMovePre(self, dc, x, y, oldx, oldy, display = True):
+        diffX = x-oldx
+        diffY = y-oldy
+        for object in self._children:
+            object.Erase(dc)
+            object.Move(dc, object.GetX() + diffX, object.GetY() + diffY, display)
+        return True
+
+    def OnDragLeft(self, draw, x, y, keys = 0, attachment = 0):
+        if self._sensitivity & OP_DRAG_LEFT != OP_DRAG_LEFT:
+            if self._parent:
+                hit = self._parent.HitTest(x, y)
+                if hit:
+                    attachment, dist = hit
+                self._parent.GetEventHandler().OnDragLeft(draw, x, y, keys, attachment)
+            return
+        Shape.OnDragLeft(self, draw, x, y, keys, attachment)
+
+    def OnBeginDragLeft(self, x, y, keys = 0, attachment = 0):
+        if self._sensitivity & OP_DRAG_LEFT != OP_DRAG_LEFT:
+            if self._parent:
+                hit = self._parent.HitTest(x, y)
+                if hit:
+                    attachment, dist = hit
+                self._parent.GetEventHandler().OnBeginDragLeft(x, y, keys, attachment)
+            return
+        Shape.OnBeginDragLeft(x, y, keys, attachment)
+            
+    def OnEndDragLeft(self, x, y, keys = 0, attachment = 0):
+        if self._canvas.HasCapture():
+            self._canvas.ReleaseMouse()
+        if self._sensitivity & OP_DRAG_LEFT != OP_DRAG_LEFT:
+            if self._parent:
+                hit = self._parent.HitTest(x, y)
+                if hit:
+                    attachment, dist = hit
+                self._parent.GetEventHandler().OnEndDragLeft(x, y, keys, attachment)
+            return
+
+        dc = wx.ClientDC(self.GetCanvas())
+        self.GetCanvas().PrepareDC(dc)
+
+        dc.SetLogicalFunction(wx.COPY)
+
+        self._xpos, self._ypos = self._canvas.Snap(self._xpos, self._ypos)
+        self.GetEventHandler().OnMovePre(dc, x, y, self._oldX, self._oldY)
+
+        self.ResetControlPoints()
+        self.Draw(dc)
+        self.MoveLinks(dc)
+        self.GetEventHandler().OnDrawControlPoints(dc)
+
+        if self._canvas and not self._canvas.GetQuickEditMode():
+            self._canvas.Redraw(dc)
+
+    def SetSize(self, w, h, recursive = True):
+        self._width = w
+        self._height = h
+        RectangleShape.SetSize(self, w, h, recursive)
+
+    def CalculateSize(self):
+        pass
+
+    # Experimental
+    def OnRightClick(self, x, y, keys = 0, attachment = 0):
+        if keys & KEY_CTRL:
+            self.PopupMenu(x, y)
+        else:
+            if self._parent:
+                hit = self._parent.HitTest(x, y)
+                if hit:
+                    attachment, dist = hit
+                self._parent.GetEventHandler().OnRightClick(x, y, keys, attachment)
+
+    # Divide wx.HORIZONTALly or wx.VERTICALly
+    def Divide(self, direction):
+        """Divide this division into two further divisions,
+        horizontally (direction is wxHORIZONTAL) or
+        vertically (direction is wxVERTICAL).
+        """
+        # Calculate existing top-left, bottom-right
+        x1 = self.GetX()-self.GetWidth() / 2
+        y1 = self.GetY()-self.GetHeight() / 2
+
+        compositeParent = self.GetParent()
+        oldWidth = self.GetWidth()
+        oldHeight = self.GetHeight()
+        if self.Selected():
+            self.Select(False)
+
+        dc = wx.ClientDC(self.GetCanvas())
+        self.GetCanvas().PrepareDC(dc)
+
+        if direction == wx.VERTICAL:
+            # Dividing vertically means notionally putting a horizontal
+            # line through it.
+            # Break existing piece into two.
+            newXPos1 = self.GetX()
+            newYPos1 = y1 + self.GetHeight() / 4
+            newXPos2 = self.GetX()
+            newYPos2 = y1 + 3 * self.GetHeight() / 4
+            newDivision = compositeParent.OnCreateDivision()
+            newDivision.Show(True)
+
+            self.Erase(dc)
+
+            # Anything adjoining the bottom of this division now adjoins the
+            # bottom of the new division.
+            for obj in compositeParent.GetDivisions():
+                if obj.GetTopSide() == self:
+                    obj.SetTopSide(newDivision)
+
+            newDivision.SetTopSide(self)
+            newDivision.SetBottomSide(self._bottomSide)
+            newDivision.SetLeftSide(self._leftSide)
+            newDivision.SetRightSide(self._rightSide)
+            self._bottomSide = newDivision
+
+            compositeParent.GetDivisions().append(newDivision)
+
+            # CHANGE: Need to insert this division at start of divisions in the
+            # object list, because e.g.:
+            # 1) Add division
+            # 2) Add contained object
+            # 3) Add division
+            # Division is now receiving mouse events _before_ the contained
+            # object, because it was added last (on top of all others)
+
+            # Add after the image that visualizes the container
+            compositeParent.AddChild(newDivision, compositeParent.FindContainerImage())
+
+            self._handleSide = DIVISION_SIDE_BOTTOM
+            newDivision.SetHandleSide(DIVISION_SIDE_TOP)
+
+            self.SetSize(oldWidth, oldHeight / 2)
+            self.Move(dc, newXPos1, newYPos1)
+
+            newDivision.SetSize(oldWidth, oldHeight / 2)
+            newDivision.Move(dc, newXPos2, newYPos2)
+        else:
+            # Dividing horizontally means notionally putting a vertical line
+            # through it.
+            # Break existing piece into two.
+            newXPos1 = x1 + self.GetWidth() / 4
+            newYPos1 = self.GetY()
+            newXPos2 = x1 + 3 * self.GetWidth() / 4
+            newYPos2 = self.GetY()
+            newDivision = compositeParent.OnCreateDivision()
+            newDivision.Show(True)
+
+            self.Erase(dc)
+
+            # Anything adjoining the left of this division now adjoins the
+            # left of the new division.
+            for obj in compositeParent.GetDivisions():
+                if obj.GetLeftSide() == self:
+                    obj.SetLeftSide(newDivision)
+
+            newDivision.SetTopSide(self._topSide)
+            newDivision.SetBottomSide(self._bottomSide)
+            newDivision.SetLeftSide(self)
+            newDivision.SetRightSide(self._rightSide)
+            self._rightSide = newDivision
+
+            compositeParent.GetDivisions().append(newDivision)
+            compositeParent.AddChild(newDivision, compositeParent.FindContainerImage())
+
+            self._handleSide = DIVISION_SIDE_RIGHT
+            newDivision.SetHandleSide(DIVISION_SIDE_LEFT)
+
+            self.SetSize(oldWidth / 2, oldHeight)
+            self.Move(dc, newXPos1, newYPos1)
+
+            newDivision.SetSize(oldWidth / 2, oldHeight)
+            newDivision.Move(dc, newXPos2, newYPos2)
+
+        if compositeParent.Selected():
+            compositeParent.DeleteControlPoints(dc)
+            compositeParent.MakeControlPoints()
+            compositeParent.MakeMandatoryControlPoints()
+
+        compositeParent.Draw(dc)
+        return True
+
+    def MakeControlPoints(self):
+        self.MakeMandatoryControlPoints()
+
+    def MakeMandatoryControlPoints(self):
+        maxX, maxY = self.GetBoundingBoxMax()
+        x = y = 0.0
+        direction = 0
+
+        if self._handleSide == DIVISION_SIDE_LEFT:
+            x=-maxX / 2
+            direction = CONTROL_POINT_HORIZONTAL
+        elif self._handleSide == DIVISION_SIDE_TOP:
+            y=-maxY / 2
+            direction = CONTROL_POINT_VERTICAL
+        elif self._handleSide == DIVISION_SIDE_RIGHT:
+            x = maxX / 2
+            direction = CONTROL_POINT_HORIZONTAL
+        elif self._handleSide == DIVISION_SIDE_BOTTOM:
+            y = maxY / 2
+            direction = CONTROL_POINT_VERTICAL
+
+        if self._handleSide != DIVISION_SIDE_NONE:
+            control = DivisionControlPoint(self._canvas, self, CONTROL_POINT_SIZE, x, y, direction)
+            self._canvas.AddShape(control)
+            self._controlPoints.append(control)
+
+    def ResetControlPoints(self):
+        self.ResetMandatoryControlPoints()
+
+    def ResetMandatoryControlPoints(self):
+        if not self._controlPoints:
+            return
+
+        maxX, maxY = self.GetBoundingBoxMax()
+
+        node = self._controlPoints[0]
+
+        if self._handleSide == DIVISION_SIDE_LEFT and node:
+            node._xoffset=-maxX / 2
+            node._yoffset = 0.0
+
+        if self._handleSide == DIVISION_SIDE_TOP and node:
+            node._xoffset = 0.0
+            node._yoffset=-maxY / 2
+
+        if self._handleSide == DIVISION_SIDE_RIGHT and node:
+            node._xoffset = maxX / 2
+            node._yoffset = 0.0
+
+        if self._handleSide == DIVISION_SIDE_BOTTOM and node:
+            node._xoffset = 0.0
+            node._yoffset = maxY / 2
+
+    def AdjustLeft(self, left, test):
+        """Adjust a side.
+
+        Returns FALSE if it's not physically possible to adjust it to
+        this point.
+        """
+        x2 = self.GetX() + self.GetWidth() / 2
+
+        if left >= x2:
+            return False
+
+        if test:
+            return True
+
+        newW = x2-left
+        newX = left + newW / 2
+        self.SetSize(newW, self.GetHeight())
+
+        dc = wx.ClientDC(self.GetCanvas())
+        self.GetCanvas().PrepareDC(dc)
+
+        self.Move(dc, newX, self.GetY())
+        return True
+
+    def AdjustTop(self, top, test):
+        """Adjust a side.
+
+        Returns FALSE if it's not physically possible to adjust it to
+        this point.
+        """
+        y2 = self.GetY() + self.GetHeight() / 2
+
+        if top >= y2:
+            return False
+
+        if test:
+            return True
+
+        newH = y2-top
+        newY = top + newH / 2
+        self.SetSize(self.GetWidth(), newH)
+
+        dc = wx.ClientDC(self.GetCanvas())
+        self.GetCanvas().PrepareDC(dc)
+
+        self.Move(dc, self.GetX(), newY)
+        return True
+
+    def AdjustRight(self, right, test):
+        """Adjust a side.
+
+        Returns FALSE if it's not physically possible to adjust it to
+        this point.
+        """
+        x1 = self.GetX()-self.GetWidth() / 2
+
+        if right <= x1:
+            return False
+
+        if test:
+            return True
+
+        newW = right-x1
+        newX = x1 + newW / 2
+        self.SetSize(newW, self.GetHeight())
+
+        dc = wx.ClientDC(self.GetCanvas())
+        self.GetCanvas().PrepareDC(dc)
+
+        self.Move(dc, newX, self.GetY())
+        return True
+    
+    def AdjustTop(self, top, test):
+        """Adjust a side.
+
+        Returns FALSE if it's not physically possible to adjust it to
+        this point.
+        """
+        y1 = self.GetY()-self.GetHeight() / 2
+
+        if bottom <= y1:
+            return False
+
+        if test:
+            return True
+
+        newH = bottom-y1
+        newY = y1 + newH / 2
+        self.SetSize(self.GetWidth(), newH)
+
+        dc = wx.ClientDC(self.GetCanvas())
+        self.GetCanvas().PrepareDC(dc)
+
+        self.Move(dc, self.GetX(), newY)
+        return True
+
+    # Resize adjoining divisions.
+    
+    # Behaviour should be as follows:
+    # If right edge moves, find all objects whose left edge
+    # adjoins this object, and move left edge accordingly.
+    # If left..., move ... right.
+    # If top..., move ... bottom.
+    # If bottom..., move top.
+    # If size goes to zero or end position is other side of start position,
+    # resize to original size and return.
+    #
+    def ResizeAdjoining(self, side, newPos, test):
+        """Resize adjoining divisions at the given side.
+
+        If test is TRUE, just see whether it's possible for each adjoining
+        region, returning FALSE if it's not.
+
+        side can be one of:
+
+        * DIVISION_SIDE_NONE
+        * DIVISION_SIDE_LEFT
+        * DIVISION_SIDE_TOP
+        * DIVISION_SIDE_RIGHT
+        * DIVISION_SIDE_BOTTOM
+        """
+        divisionParent = self.GetParent()
+        for division in divisionParent.GetDivisions():
+            if side == DIVISION_SIDE_LEFT:
+                if division._rightSide == self:
+                    success = division.AdjustRight(newPos, test)
+                    if not success and test:
+                        return false
+            elif side == DIVISION_SIDE_TOP:
+                if division._bottomSide == self:
+                    success = division.AdjustBottom(newPos, test)
+                    if not success and test:
+                        return False
+            elif side == DIVISION_SIDE_RIGHT:
+                if division._leftSide == self:
+                    success = division.AdjustLeft(newPos, test)
+                    if not success and test:
+                        return False
+            elif side == DIVISION_SIDE_BOTTOM:
+                if division._topSide == self:
+                    success = division.AdjustTop(newPos, test)
+                    if not success and test:
+                        return False
+        return True
+    
+    def EditEdge(self, side):
+        print "EditEdge() not implemented."
+
+    def PopupMenu(self, x, y):
+        menu = PopupDivisionMenu()
+        menu.SetClientData(self)
+        if self._leftSide:
+            menu.Enable(DIVISION_MENU_EDIT_LEFT_EDGE, True)
+        else:
+            menu.Enable(DIVISION_MENU_EDIT_LEFT_EDGE, False)
+        if self._topSide:
+            menu.Enable(DIVISION_MENU_EDIT_TOP_EDGE, True)
+        else:
+            menu.Enable(DIVISION_MENU_EDIT_TOP_EDGE, False)
+
+        x1, y1 = self._canvas.GetViewStart()
+        unit_x, unit_y = self._canvas.GetScrollPixelsPerUnit()
+
+        dc = wx.ClientDC(self.GetCanvas())
+        self.GetCanvas().PrepareDC(dc)
+
+        mouse_x = dc.LogicalToDeviceX(x-x1 * unit_x)
+        mouse_y = dc.LogicalToDeviceY(y-y1 * unit_y)
+
+        self._canvas.PopupMenu(menu, (mouse_x, mouse_y))
+
+        
diff --git a/wxPython/wx/lib/ogl/diagram.py b/wxPython/wx/lib/ogl/diagram.py
new file mode 100644 (file)
index 0000000..86c0dd9
--- /dev/null
@@ -0,0 +1,160 @@
+# -*- coding: iso-8859-1 -*-
+#----------------------------------------------------------------------------
+# Name:         diagram.py
+# Purpose:      Diagram class
+#
+# Author:       Pierre Hjälm (from C++ original by Julian Smart)
+#
+# Created:      20040508
+# RCS-ID:       
+# Copyright:    (c) 2004 Pierre Hjälm - 1998 Julian Smart
+# Licence:      wxWindows license
+#----------------------------------------------------------------------------
+
+from __future__ import division
+
+import wx
+
+DEFAULT_MOUSE_TOLERANCE = 3
+
+
+
+class Diagram(object):
+    """Encapsulates an entire diagram, with methods for drawing. A diagram has
+    an associated ShapeCanvas.
+
+    Derived from:
+      Object
+    """
+    def __init__(self):
+        self._diagramCanvas = None
+        self._quickEditMode = False
+        self._snapToGrid = True
+        self._gridSpacing = 5.0
+        self._shapeList = []
+        self._mouseTolerance = DEFAULT_MOUSE_TOLERANCE
+
+    def Redraw(self, dc):
+        """Draw the shapes in the diagram on the specified device context."""
+        if self._shapeList:
+            if self.GetCanvas():
+                self.GetCanvas().SetCursor(wx.HOURGLASS_CURSOR)
+            for object in self._shapeList:
+                object.Draw(dc)
+            if self.GetCanvas():
+                self.GetCanvas().SetCursor(wx.STANDARD_CURSOR)
+
+    def Clear(self, dc):
+        """Clear the specified device context."""
+        dc.Clear()
+
+    def AddShape(self, object, addAfter = None):
+        """Adds a shape to the diagram. If addAfter is not None, the shape
+        will be added after addAfter.
+        """
+        if not object in self._shapeList:
+            if addAfter:
+                self._shapeList.insert(self._shapeList.index(addAfter) + 1, object)
+            else:
+                self._shapeList.append(object)
+
+            object.SetCanvas(self.GetCanvas())
+
+    def InsertShape(self, object):
+        """Insert a shape at the front of the shape list."""
+        self._shapeList.insert(0, object)
+
+    def RemoveShape(self, object):
+        """Remove the shape from the diagram (non-recursively) but do not
+        delete it.
+        """
+        if object in self._shapeList:
+            self._shapeList.remove(object)
+            
+    def RemoveAllShapes(self):
+        """Remove all shapes from the diagram but do not delete the shapes."""
+        self._shapeList = []
+
+    def DeleteAllShapes(self):
+        """Remove and delete all shapes in the diagram."""
+        for shape in self._shapeList[:]:
+            if not shape.GetParent():
+                self.RemoveShape(shape)
+                
+    def ShowAll(self, show):
+        """Call Show for each shape in the diagram."""
+        for shape in self._shapeList:
+            shape.Show()
+
+    def DrawOutLine(self, dc, x1, y1, x2, y2):
+        """Draw an outline rectangle on the current device context."""
+        dc.SetPen(wx.Pen(wx.Color(0, 0, 0), 1, wx.DOT))
+        dc.SetBrush(wx.TRANSPARENT_BRUSH)
+
+        dc.DrawLines([[x1, y1], [x2, y1], [x2, y2], [x1, y2], [x1, y1]])
+
+    def RecentreAll(self, dc):
+        """Make sure all text that should be centred, is centred."""
+        for shape in self._shapeList:
+            shape.Recentre(dc)
+
+    def SetCanvas(self, canvas):
+        """Set the canvas associated with this diagram."""
+        self._diagramCanvas = canvas
+
+    def GetCanvas(self):
+        """Return the shape canvas associated with this diagram."""
+        return self._diagramCanvas
+        
+    def FindShape(self, id):
+        """Return the shape for the given identifier."""
+        for shape in self._shapeList:
+            if shape.GetId() == id:
+                return shape
+        return None
+
+    def Snap(self, x, y):
+        """'Snaps' the coordinate to the nearest grid position, if
+        snap-to-grid is on."""
+        if self._snapToGrid:
+            return self._gridSpacing * int(x / self._gridSpacing + 0.5), self._gridSpacing * int(y / self._gridSpacing + 0.5)
+        return x, y
+
+    def GetGridSpacing(self):
+        """Return the grid spacing."""
+        return self._gridSpacing
+
+    def GetSnapToGrid(self):
+        """Return snap-to-grid mode."""
+        return self._snapToGrid
+
+    def SetQuickEditMode(self, mode):
+        """Set quick-edit-mode on of off.
+
+        In this mode, refreshes are minimized, but the diagram may need
+        manual refreshing occasionally.
+        """
+        self._quickEditMode = mode
+
+    def GetQuickEditMode(self):
+        """Return quick edit mode."""
+        return self._quickEditMode
+
+    def SetMouseTolerance(self, tolerance):
+        """Set the tolerance within which a mouse move is ignored.
+
+        The default is 3 pixels.
+        """
+        self._mouseTolerance = tolerance
+
+    def GetMouseTolerance(self):
+        """Return the tolerance within which a mouse move is ignored."""
+        return self._mouseTolerance
+
+    def GetShapeList(self):
+        """Return the internal shape list."""
+        return self._shapeList
+
+    def GetCount(self):
+        """Return the number of shapes in the diagram."""
+        return len(self._shapeList)
diff --git a/wxPython/wx/lib/ogl/divided.py b/wxPython/wx/lib/ogl/divided.py
new file mode 100644 (file)
index 0000000..73ea006
--- /dev/null
@@ -0,0 +1,404 @@
+# -*- coding: iso-8859-1 -*-
+#----------------------------------------------------------------------------
+# Name:         divided.py
+# Purpose:      DividedShape class
+#
+# Author:       Pierre Hjälm (from C++ original by Julian Smart)
+#
+# Created:      20040508
+# RCS-ID:       
+# Copyright:    (c) 2004 Pierre Hjälm - 1998 Julian Smart
+# Licence:      wxWindows license
+#----------------------------------------------------------------------------
+
+from __future__ import division
+
+import sys
+import wx
+
+from basic import ControlPoint, RectangleShape, Shape
+from oglmisc import *
+
+
+
+class DividedShapeControlPoint(ControlPoint):
+    def __init__(self, the_canvas, object, region, size, the_m_xoffset, the_m_yoffset, the_type):
+        ControlPoint.__init__(self, the_canvas, object, size, the_m_xoffset, the_m_yoffset, the_type)
+        self.regionId = region
+
+    # Implement resizing of divided object division
+    def OnDragLeft(self, draw, x, y, keys = 0, attachment = 0):
+        dc = wx.ClientDC(self.GetCanvas())
+        self.GetCanvas().PrepareDC(dc)
+
+        dc.SetLogicalFunction(OGLRBLF)
+        dottedPen = wx.Pen(wx.Colour(0, 0, 0), 1, wx.DOT)
+        dc.SetPen(dottedPen)
+        dc.SetBrush(wx.TRANSPARENT_BRUSH)
+
+        dividedObject = self._shape
+        x1 = dividedObject.GetX()-dividedObject.GetWidth() / 2
+        y1 = y
+        x2 = dividedObject.GetX() + dividedObject.GetWidth() / 2
+        y2 = y
+
+        dc.DrawLine(x1, y1, x2, y2)
+
+    def OnBeginDragLeft(self, x, y, keys = 0, attachment = 0):
+        dc = wx.ClientDC(self.GetCanvas())
+        self.GetCanvas().PrepareDC(dc)
+
+        dc.SetLogicalFunction(OGLRBLF)
+        dottedPen = wx.Pen(wx.Colour(0, 0, 0), 1, wx.DOT)
+        dc.SetPen(dottedPen)
+        dc.SetBrush(wx.TRANSPARENT_BRUSH)
+
+        dividedObject = self._shape
+        
+        x1 = dividedObject.GetX()-dividedObject.GetWidth() / 2
+        y1 = y
+        x2 = dividedObject.GetX() + dividedObject.GetWidth() / 2
+        y2 = y
+
+        dc.DrawLine(x1, y1, x2, y2)
+        self._canvas.CaptureMouse()
+
+    def OnEndDragLeft(self, x, y, keys = 0, attachment = 0):
+        dc = wx.ClientDC(self.GetCanvas())
+        self.GetCanvas().PrepareDC(dc)
+
+        dividedObject = self._shape
+        if not dividedObject.GetRegions()[self.regionId]:
+            return
+        
+        thisRegion = dividedObject.GetRegions()[self.regionId]
+        nextRegion = None
+
+        dc.SetLogicalFunction(wx.COPY)
+
+        if self._canvas.HasCapture():
+            self._canvas.ReleaseMouse()
+
+        # Find the old top and bottom of this region,
+        # and calculate the new proportion for this region
+        # if legal.
+        currentY = dividedObject.GetY()-dividedObject.GetHeight() / 2
+        maxY = dividedObject.GetY() + dividedObject.GetHeight() / 2
+
+        # Save values
+        theRegionTop = 0
+        nextRegionBottom = 0
+        
+        for i in range(len(dividedObject.GetRegions())):
+            region = dividedObject.GetRegions()[i]
+            proportion = region._regionProportionY
+            yy = currentY + dividedObject.GetHeight() * proportion
+            actualY = min(maxY, yy)
+
+            if region == thisRegion:
+                thisRegionTop = currentY
+                
+                if i + 1<len(dividedObject.GetRegions()):
+                    nextRegion = dividedObject.GetRegions()[i + 1]
+            if region == nextRegion:
+                nextRegionBottom = actualY
+
+            currentY = actualY
+
+        if not nextRegion:
+            return
+
+        # Check that we haven't gone above this region or below
+        # next region.
+        if y <= thisRegionTop or y >= nextRegionBottom:
+            return
+
+        dividedObject.EraseLinks(dc)
+
+        # Now calculate the new proportions of this region and the next region
+        thisProportion = (y-thisRegionTop) / dividedObject.GetHeight()
+        nextProportion = (nextRegionBottom-y) / dividedObject.GetHeight()
+
+        thisRegion.SetProportions(0, thisProportion)
+        nextRegion.SetProportions(0, nextProportion)
+        self._yoffset = y-dividedObject.GetY()
+
+        # Now reformat text
+        for i, region in enumerate(dividedObject.GetRegions()):
+            if region.GetText():
+                s = region.GetText()
+                dividedObject.FormatText(dc, s, i)
+
+        dividedObject.SetRegionSizes()
+        dividedObject.Draw(dc)
+        dividedObject.GetEventHandler().OnMoveLinks(dc)
+        
+
+
+class DividedShape(RectangleShape):
+    """A DividedShape is a rectangle with a number of vertical divisions.
+    Each division may have its text formatted with independent characteristics,
+    and the size of each division relative to the whole image may be specified.
+
+    Derived from:
+      RectangleShape
+    """
+    def __init__(self, w, h):
+        RectangleShape.__init__(self, w, h)
+        self.ClearRegions()
+
+    def OnDraw(self, dc):
+        RectangleShape.OnDraw(self, dc)
+
+    def OnDrawContents(self, dc):
+        if self.GetRegions():
+            defaultProportion = 1 / len(self.GetRegions())
+        else:
+            defaultProportion = 0
+        currentY = self._ypos-self._height / 2
+        maxY = self._ypos + self._height / 2
+
+        leftX = self._xpos-self._width / 2
+        rightX = self._xpos + self._width / 2
+
+        if self._pen:
+            dc.SetPen(self._pen)
+
+        dc.SetTextForeground(self._textColour)
+
+        # For efficiency, don't do this under X - doesn't make
+        # any visible difference for our purposes.
+        if sys.platform[:3]=="win":
+            dc.SetTextBackground(self._brush.GetColour())
+
+        if self.GetDisableLabel():
+            return
+
+        xMargin = 2
+        yMargin = 2
+
+        dc.SetBackgroundMode(wx.TRANSPARENT)
+
+        for region in self.GetRegions():
+            dc.SetFont(region.GetFont())
+            dc.SetTextForeground(region.GetActualColourObject())
+
+            if region._regionProportionY<0:
+                proportion = defaultProportion
+            else:
+                proportion = region._regionProportionY
+
+            y = currentY + self._height * proportion
+            actualY = min(maxY, y)
+
+            centreX = self._xpos
+            centreY = currentY + (actualY-currentY) / 2
+
+            DrawFormattedText(dc, region._formattedText, centreX, centreY, self._width-2 * xMargin, actualY-currentY-2 * yMargin, region._formatMode)
+
+            if y <= maxY and region != self.GetRegions()[-1]:
+                regionPen = region.GetActualPen()
+                if regionPen:
+                    dc.SetPen(regionPen)
+                    dc.DrawLine(leftX, y, rightX, y)
+
+            currentY = actualY
+
+    def SetSize(self, w, h, recursive = True):
+        self.SetAttachmentSize(w, h)
+        self._width = w
+        self._height = h
+        self.SetRegionSizes()
+
+    def SetRegionSizes(self):
+        """Set all region sizes according to proportions and this object
+        total size.
+        """
+        if not self.GetRegions():
+            return
+
+        if self.GetRegions():
+            defaultProportion = 1 / len(self.GetRegions())
+        else:
+            defaultProportion = 0
+        currentY = self._ypos-self._height / 2
+        maxY = self._ypos + self._height / 2
+        
+        for region in self.GetRegions():
+            if region._regionProportionY <= 0:
+                proportion = defaultProportion
+            else:
+                proportion = region._regionProportionY
+
+            sizeY = proportion * self._height
+            y = currentY + sizeY
+            actualY = min(maxY, y)
+
+            centreY = currentY + (actualY-currentY) / 2
+
+            region.SetSize(self._width, sizeY)
+            region.SetPosition(0, centreY-self._ypos)
+
+            currentY = actualY
+
+    # Attachment points correspond to regions in the divided box
+    def GetAttachmentPosition(self, attachment, nth = 0, no_arcs = 1, line = None):
+        totalNumberAttachments = len(self.GetRegions()) * 2 + 2
+        if self.GetAttachmentMode() == ATTACHMENT_MODE_NONE or attachment >= totalNumberAttachments:
+            return Shape.GetAttachmentPosition(self, attachment, nth, no_arcs)
+
+        n = len(self.GetRegions())
+        isEnd = line and line.IsEnd(self)
+
+        left = self._xpos-self._width / 2
+        right = self._xpos + self._width / 2
+        top = self._ypos-self._height / 2
+        bottom = self._ypos + self._height / 2
+
+        # Zero is top, n + 1 is bottom
+        if attachment == 0:
+            y = top
+            if self._spaceAttachments:
+                if line and line.GetAlignmentType(isEnd) == LINE_ALIGNMENT_TO_NEXT_HANDLE:
+                    # Align line according to the next handle along
+                    point = line.GetNextControlPoint(self)
+                    if point.x<left:
+                        x = left
+                    elif point.x>right:
+                        x = right
+                    else:
+                        x = point.x
+                else:
+                    x = left + (nth + 1) * self._width / (no_arcs + 1)
+            else:
+                x = self._xpos
+        elif attachment == n + 1:
+            y = bottom
+            if self._spaceAttachments:
+                if line and line.GetAlignmentType(isEnd) == LINE_ALIGNMENT_TO_NEXT_HANDLE:
+                    # Align line according to the next handle along
+                    point = line.GetNextControlPoint(self)
+                    if point.x<left:
+                        x = left
+                    elif point.x>right:
+                        x = right
+                    else:
+                        x = point.x
+                else:
+                    x = left + (nth + 1) * self._width / (no_arcs + 1)
+            else:
+                x = self._xpos
+        else: # Left or right
+            isLeft = not attachment<(n + 1)
+            if isLeft:
+                i = totalNumberAttachments-attachment-1
+            else:
+                i = attachment-1
+
+            region = self.GetRegions()[i]
+            if region:
+                if isLeft:
+                    x = left
+                else:
+                    x = right
+
+                # Calculate top and bottom of region
+                top = self._ypos + region._y-region._height / 2
+                bottom = self._ypos + region._y + region._height / 2
+
+                # Assuming we can trust the absolute size and
+                # position of these regions
+                if self._spaceAttachments:
+                    if line and line.GetAlignmentType(isEnd) == LINE_ALIGNMENT_TO_NEXT_HANDLE:
+                        # Align line according to the next handle along
+                        point = line.GetNextControlPoint(self)
+                        if point.y<bottom:
+                            y = bottom
+                        elif point.y>top:
+                            y = top
+                        else:
+                            y = point.y
+                    else:
+                        y = top + (nth + 1) * region._height / (no_arcs + 1)
+                else:
+                    y = self._ypos + region._y
+            else:
+                return False
+        return x, y
+
+    def GetNumberOfAttachments(self):
+        # There are two attachments for each region (left and right),
+        # plus one on the top and one on the bottom.
+        n = len(self.GetRegions()) * 2 + 2
+
+        maxN = n-1
+        for point in self._attachmentPoints:
+            if point._id>maxN:
+                maxN = point._id
+
+        return maxN + 1
+
+    def AttachmentIsValid(self, attachment):
+        totalNumberAttachments = len(self.GetRegions()) * 2 + 2
+        if attachment >= totalNumberAttachments:
+            return Shape.AttachmentIsValid(self, attachment)
+        else:
+            return attachment >= 0
+
+    def MakeControlPoints(self):
+        RectangleShape.MakeControlPoints(self)
+        self.MakeMandatoryControlPoints()
+
+    def MakeMandatoryControlPoints(self):
+        currentY = self.GetY()-self._height / 2
+        maxY = self.GetY() + self._height / 2
+
+        for i, region in enumerate(self.GetRegions()):
+            proportion = region._regionProportionY
+
+            y = currentY + self._height * proportion
+            actualY = min(maxY, y)
+
+            if region != self.GetRegions()[-1]:
+                controlPoint = DividedShapeControlPoint(self._canvas, self, i, CONTROL_POINT_SIZE, 0, actualY-self.GetY(), 0)
+                self._canvas.AddShape(controlPoint)
+                self._controlPoints.append(controlPoint)
+
+            currentY = actualY
+
+    def ResetControlPoints(self):
+        # May only have the region handles, (n - 1) of them
+        if len(self._controlPoints)>len(self.GetRegions())-1:
+            RectangleShape.ResetControlPoints(self)
+
+        self.ResetMandatoryControlPoints()
+
+    def ResetMandatoryControlPoints(self):
+        currentY = self.GetY()-self._height / 2
+        maxY = self.GetY() + self._height / 2
+
+        i = 0
+        for controlPoint in self._controlPoints:
+            if isinstance(controlPoint, DividedShapeControlPoint):
+                region = self.GetRegions()[i]
+                proportion = region._regionProportionY
+
+                y = currentY + self._height * proportion
+                actualY = min(maxY, y)
+
+                controlPoint._xoffset = 0
+                controlPoint._yoffset = actualY-self.GetY()
+
+                currentY = actualY
+
+                i += 1
+                
+    def EditRegions(self):
+        """Edit the region colours and styles. Not implemented."""
+        print "EditRegions() is unimplemented"
+        
+    def OnRightClick(self, x, y, keys = 0, attachment = 0):
+        if keys & KEY_CTRL:
+            self.EditRegions()
+        else:
+            RectangleShape.OnRightClick(self, x, y, keys, attachment)
diff --git a/wxPython/wx/lib/ogl/lines.py b/wxPython/wx/lib/ogl/lines.py
new file mode 100644 (file)
index 0000000..cb02964
--- /dev/null
@@ -0,0 +1,1533 @@
+# -*- coding: iso-8859-1 -*-
+#----------------------------------------------------------------------------
+# Name:         lines.py
+# Purpose:      LineShape class
+#
+# Author:       Pierre Hjälm (from C++ original by Julian Smart)
+#
+# Created:      20040508
+# RCS-ID:       
+# Copyright:    (c) 2004 Pierre Hjälm - 1998 Julian Smart
+# Licence:      wxWindows license
+#----------------------------------------------------------------------------
+
+from __future__ import division
+
+import sys
+
+from math import sqrt
+
+from basic import Shape, ShapeRegion, ControlPoint, RectangleShape
+from oglmisc import *
+
+# Line alignment flags
+# Vertical by default
+LINE_ALIGNMENT_HORIZ=              1
+LINE_ALIGNMENT_VERT=               0
+LINE_ALIGNMENT_TO_NEXT_HANDLE=     2
+LINE_ALIGNMENT_NONE=               0
+
+
+
+class LineControlPoint(ControlPoint):
+    def __init__(self, theCanvas = None, object = None, size = 0.0, x = 0.0, y = 0.0, the_type = 0):
+        ControlPoint.__init__(self, theCanvas, object, size, x, y, the_type)
+        self._xpos = x
+        self._ypos = y
+        self._type = the_type
+        self._point = None
+        self._originalPos = None
+
+    def OnDraw(self, dc):
+        RectangleShape.OnDraw(self, dc)
+
+    # Implement movement of Line point
+    def OnDragLeft(self, draw, x, y, keys = 0, attachment = 0):
+        self._shape.GetEventHandler().OnSizingDragLeft(self, draw, x, y, keys, attachment)
+
+    def OnBeginDragLeft(self, x, y, keys = 0, attachment = 0):
+        self._shape.GetEventHandler().OnSizingBeginDragLeft(self, x, y, keys, attachment)
+
+    def OnEndDragLeft(self, x, y, keys = 0, attachment = 0):
+        self._shape.GetEventHandler().OnSizingEndDragLeft(self, x, y, keys, attachment)
+
+
+
+class ArrowHead(object):
+    def __init__(self, type = 0, end = 0, size = 0.0, dist = 0.0, name="",mf = None, arrowId=-1):
+        if isinstance(type, ArrowHead):
+            pass
+        else:
+            self._arrowType = type
+            self._arrowEnd = end
+            self._arrowSize = size
+            self._xOffset = dist
+            self._yOffset = 0.0
+            self._spacing = 5.0
+
+            self._arrowName = name
+            self._metaFile = mf
+            self._id = arrowId
+            if self._id==-1:
+                self._id = wx.NewId()
+            
+    def _GetType(self):
+        return self._arrowType
+
+    def GetPosition(self):
+        return self._arrowEnd
+
+    def SetPosition(self, pos):
+        self._arrowEnd = pos
+
+    def GetXOffset(self):
+        return self._xOffset
+
+    def GetYOffset(self):
+        return self._yOffset
+    
+    def GetSpacing(self):
+        return self._spacing
+
+    def GetSize(self):
+        return self._arrowSize
+
+    def SetSize(self, size):
+        self._arrowSize = size
+        if self._arrowType == ARROW_METAFILE and self._metaFile:
+            oldWidth = self._metaFile._width
+            if oldWidth == 0:
+                return
+
+            scale = size / oldWidth
+            if scale != 1:
+                self._metaFile.Scale(scale, scale)
+                
+    def GetName(self):
+        return self._arrowName
+
+    def SetXOffset(self, x):
+        self._xOffset = x
+
+    def SetYOffset(self, y):
+        self._yOffset = y
+
+    def GetMetaFile(self):
+        return self._metaFile
+
+    def GetId(self):
+        return self._id
+
+    def GetArrowEnd(self):
+        return self._arrowEnd
+
+    def GetArrowSize(self):
+        return self._arrowSize
+
+    def SetSpacing(self, sp):
+        self._spacing = sp
+
+
+
+class LabelShape(RectangleShape):
+    def __init__(self, parent, region, w, h):
+        RectangleShape.__init__(self, w, h)
+        self._lineShape = parent
+        self._shapeRegion = region
+        self.SetPen(wx.ThePenList.FindOrCreatePen(wx.Colour(0, 0, 0), 1, wx.DOT))
+
+    def OnDraw(self, dc):
+        if self._lineShape and not self._lineShape.GetDrawHandles():
+            return
+
+        x1 = self._xpos-self._width / 2
+        y1 = self._ypos-self._height / 2
+
+        if self._pen:
+            if self._pen.GetWidth() == 0:
+                dc.SetPen(wx.Pen(wx.WHITE, 1, wx.TRANSPARENT))
+            else:
+                dc.SetPen(self._pen)
+        dc.SetBrush(wx.TRANSPARENT_BRUSH)
+
+        if self._cornerRadius>0:
+            dc.DrawRoundedRectangle(x1, y1, self._width, self._height, self._cornerRadius)
+        else:
+            dc.DrawRectangle(x1, y1, self._width, self._height)
+
+    def OnDrawContents(self, dc):
+        pass
+
+    def OnDragLeft(self, draw, x, y, keys = 0, attachment = 0):
+        RectangleShape.OnDragLeft(self, draw, x, y, keys, attachment)
+
+    def OnBeginDragLeft(self, x, y, keys = 0, attachment = 0):
+        RectangleShape.OnBeginDragLeft(self, x, y, keys, attachment)
+
+    def OnEndDragLeft(self, x, y, keys = 0, attachment = 0):
+        RectangleShape.OnEndDragLeft(self, x, y, keys, attachment)
+
+    def OnMovePre(self, dc, x, y, old_x, old_y, display):
+        return self._lineShape.OnLabelMovePre(dc, self, x, y, old_x, old_y, display)
+
+    # Divert left and right clicks to line object
+    def OnLeftClick(self, x, y, keys = 0, attachment = 0):
+        self._lineShape.GetEventHandler().OnLeftClick(x, y, keys, attachment)
+
+    def OnRightClick(self, x, y, keys = 0, attachment = 0):
+        self._lineShape.GetEventHandler().OnRightClick(x, y, keys, attachment)
+        
+
+
+class LineShape(Shape):
+    """LineShape may be attached to two nodes;
+    it may be segmented, in which case a control point is drawn for each joint.
+
+    A wxLineShape may have arrows at the beginning, end and centre.
+
+    Derived from:
+      Shape
+    """
+    def __init__(self):
+        Shape.__init__(self)
+
+        self._sensitivity = OP_CLICK_LEFT | OP_CLICK_RIGHT
+        self._draggable = False
+        self._attachmentTo = 0
+        self._attachmentFrom = 0
+        self._from = None
+        self._to = None
+        self._erasing = False
+        self._arrowSpacing = 5.0
+        self._ignoreArrowOffsets = False
+        self._isSpline = False
+        self._maintainStraightLines = False
+        self._alignmentStart = 0
+        self._alignmentEnd = 0
+
+        self._lineControlPoints = None
+
+        # Clear any existing regions (created in an earlier constructor)
+        # and make the three line regions.
+        self.ClearRegions()
+        for name in ["Middle","Start","End"]:
+            newRegion = ShapeRegion()
+            newRegion.SetName(name)
+            newRegion.SetSize(150, 50)
+            self._regions.append(newRegion)
+
+        self._labelObjects = [None, None, None]
+        self._lineOrientations = []
+        self._lineControlPoints = []
+        self._arcArrows = []
+
+    def __del__(self):
+        if self._lineControlPoints:
+            self.ClearPointList(self._lineControlPoints)
+            self._lineControlPoints = []
+        for i in range(3):
+            if self._labelObjects[i]:
+                self._labelObjects[i].Select(False)
+                self._labelObjects[i].RemoveFromCanvas(self._canvas)
+        self._labelObjects = []
+        self.ClearArrowsAtPosition(-1)
+
+    def GetFrom(self):
+        """Return the 'from' object."""
+        return self._from
+    
+    def GetTo(self):
+        """Return the 'to' object."""
+        return self._to
+
+    def GetAttachmentFrom(self):
+        """Return the attachment point on the 'from' node."""
+        return self._attachmentFrom
+
+    def GetAttachmentTo(self):
+        """Return the attachment point on the 'to' node."""
+        return self._attachmentTo
+
+    def GetLineControlPoints(self):
+        return self._lineControlPoints
+
+    def SetSpline(self, spline):
+        """Specifies whether a spline is to be drawn through the control points."""
+        self._isSpline = spline
+
+    def IsSpline(self):
+        """TRUE if a spline is drawn through the control points."""
+        return self._isSpline
+
+    def SetAttachmentFrom(self, attach):
+        """Set the 'from' shape attachment."""
+        self._attachmentFrom = attach
+
+    def SetAttachmentTo(self, attach):
+        """Set the 'to' shape attachment."""
+        self._attachmentTo = attach
+
+    # This is really to distinguish between lines and other images.
+    # For lines, want to pass drag to canvas, since lines tend to prevent
+    # dragging on a canvas (they get in the way.)
+    def Draggable(self):
+        return False
+
+    def SetIgnoreOffsets(self, ignore):
+        """Set whether to ignore offsets from the end of the line when drawing."""
+        self._ignoreArrowOffsets = ignore
+
+    def GetArrows(self):
+        return self._arcArrows
+
+    def GetAlignmentStart(self):
+        return self._alignmentStart
+
+    def GetAlignmentEnd(self):
+        return self._alignmentEnd
+
+    def IsEnd(self, nodeObject):
+        """TRUE if shape is at the end of the line."""
+        return self._to == nodeObject
+
+    def MakeLineControlPoints(self, n):
+        """Make a given number of control points (minimum of two)."""
+        if self._lineControlPoints:
+            self.ClearPointList(self._lineControlPoints)
+        self._lineControlPoints = []
+        
+        for _ in range(n):
+            point = wx.RealPoint(-999,-999)
+            self._lineControlPoints.append(point)
+
+    def InsertLineControlPoint(self, dc = None):
+        """Insert a control point at an arbitrary position."""
+        if dc:
+            self.Erase(dc)
+
+        last_point = self._lineControlPoints[-1]
+        second_last_point = self._lineControlPoints[-2]
+
+        line_x = (last_point[0] + second_last_point[0]) / 2
+        line_y = (last_point[1] + second_last_point[1]) / 2
+
+        point = wx.RealPoint(line_x, line_y)
+        self._lineControlPoints.insert(len(self._lineControlPoints), point)
+
+    def DeleteLineControlPoint(self):
+        """Delete an arbitary point on the line."""
+        if len(self._lineControlPoints)<3:
+            return False
+
+        del self._lineControlPoints[-2]
+        return True
+
+    def Initialise(self):
+        """Initialise the line object."""
+        if self._lineControlPoints:
+            # Just move the first and last control points
+            first_point = self._lineControlPoints[0]
+            last_point = self._lineControlPoints[-1]
+
+            # If any of the line points are at -999, we must
+            # initialize them by placing them half way between the first
+            # and the last.
+
+            for point in self._lineControlPoints[1:]:
+                if point[0]==-999:
+                    if first_point[0]<last_point[0]:
+                        x1 = first_point[0]
+                        x2 = last_point[0]
+                    else:
+                        x2 = first_point[0]
+                        x1 = last_point[0]
+                    if first_point[1]<last_point[1]:
+                        y1 = first_point[1]
+                        y2 = last_point[1]
+                    else:
+                        y2 = first_point[1]
+                        y1 = last_point[1]
+                    point[0] = (x2-x1) / 2 + x1
+                    point[1] = (y2-y1) / 2 + y1
+                    
+    def FormatText(self, dc, s, i):
+        """Format a text string according to the region size, adding
+        strings with positions to region text list.
+        """
+        self.ClearText(i)
+
+        if len(self._regions) == 0 or i >= len(self._regions):
+            return
+
+        region = self._regions[i]
+        region.SetText(s)
+        dc.SetFont(region.GetFont())
+
+        w, h = region.GetSize()
+        # Initialize the size if zero
+        if (w == 0 or h == 0) and s:
+            w, h = 100, 50
+            region.SetSize(w, h)
+
+        string_list = FormatText(dc, s, w-5, h-5, region.GetFormatMode())
+        for s in string_list:
+            line = ShapeTextLine(0.0, 0.0, s)
+            region.GetFormattedText().append(line)
+
+        actualW = w
+        actualH = h
+        if region.GetFormatMode() & FORMAT_SIZE_TO_CONTENTS:
+            actualW, actualH = GetCentredTextExtent(dc, region.GetFormattedText(), self._xpos, self._ypos, w, h)
+            if actualW != w or actualH != h:
+                xx, yy = self.GetLabelPosition(i)
+                self.EraseRegion(dc, region, xx, yy)
+                if len(self._labelObjects)<i:
+                    self._labelObjects[i].Select(False, dc)
+                    self._labelObjects[i].Erase(dc)
+                    self._labelObjects[i].SetSize(actualW, actualH)
+
+                region.SetSize(actualW, actualH)
+
+                if len(self._labelObjects)<i:
+                    self._labelObjects[i].Select(True, dc)
+                    self._labelObjects[i].Draw(dc)
+
+        CentreText(dc, region.GetFormattedText(), self._xpos, self._ypos, actualW, actualH, region.GetFormatMode())
+        self._formatted = True
+
+    def DrawRegion(self, dc, region, x, y):
+        """Format one region at this position."""
+        if self.GetDisableLabel():
+            return
+
+        w, h = region.GetSize()
+
+        # Get offset from x, y
+        xx, yy = region.GetPosition()
+
+        xp = xx + x
+        yp = yy + y
+
+        # First, clear a rectangle for the text IF there is any
+        if len(region.GetFormattedText()):
+            dc.SetPen(self.GetBackgroundPen())
+            dc.SetBrush(self.GetBackgroundBrush())
+
+            # Now draw the text
+            if region.GetFont():
+                dc.SetFont(region.GetFont())
+                dc.DrawRectangle(xp-w / 2, yp-h / 2, w, h)
+
+                if self._pen:
+                    dc.SetPen(self._pen)
+                dc.SetTextForeground(region.GetActualColourObject())
+
+                DrawFormattedText(dc, region.GetFormattedText(), xp, yp, w, h, region.GetFormatMode())
+
+    def EraseRegion(self, dc, region, x, y):
+        """Erase one region at this position."""
+        if self.GetDisableLabel():
+            return
+
+        w, h = region.GetSize()
+
+        # Get offset from x, y
+        xx, yy = region.GetPosition()
+
+        xp = xx + x
+        yp = yy + y
+
+        if region.GetFormattedText():
+            dc.SetPen(self.GetBackgroundPen())
+            dc.SetBrush(self.GetBackgroundBrush())
+
+            dc.DrawRectangle(xp-w / 2, yp-h / 2, w, h)
+
+    def GetLabelPosition(self, position):
+        """Get the reference point for a label.
+
+        Region x and y are offsets from this.
+        position is 0 (middle), 1 (start), 2 (end).
+        """
+        if position == 0:
+            # Want to take the middle section for the label
+            half_way = int(len(self._lineControlPoints) / 2)
+
+            # Find middle of this line
+            point = self._lineControlPoints[half_way-1]
+            next_point = self._lineControlPoints[half_way]
+
+            dx = next_point[0]-point[0]
+            dy = next_point[1]-point[1]
+
+            return point[0] + dx / 2, point[1] + dy / 2
+        elif position == 1:
+            return self._lineControlPoints[0][0], self._lineControlPoints[0][1]
+        elif position == 2:
+            return self._lineControlPoints[-1][0], self._lineControlPoints[-1][1]
+
+    def Straighten(self, dc = None):
+        """Straighten verticals and horizontals."""
+        if len(self._lineControlPoints)<3:
+            return
+
+        if dc:
+            self.Erase(dc)
+
+        GraphicsStraightenLine(self._lineControlPoints[-1], self._lineControlPoints[-2])
+
+        for i in range(len(self._lineControlPoints)-2):
+            GraphicsStraightenLine(self._lineControlPoints[i], self._lineControlPoints[i + 1])
+                
+        if dc:
+            self.Draw(dc)
+
+    def Unlink(self):
+        """Unlink the line from the nodes at either end."""
+        if self._to:
+            self._to.GetLines().remove(self)
+        if self._from:
+            self._from.GetLines().remove(self)
+        self._to = None
+        self._from = None
+
+    def SetEnds(self, x1, y1, x2, y2):
+        """Set the end positions of the line."""
+        # Find centre point
+        first_point = self._lineControlPoints[0]
+        last_point = self._lineControlPoints[-1]
+
+        first_point[0] = x1
+        first_point[1] = y1
+        last_point[0] = x2
+        last_point[1] = y2
+
+        self._xpos = (x1 + x2) / 2
+        self._ypos = (y1 + y2) / 2
+
+    # Get absolute positions of ends
+    def GetEnds(self):
+        """Get the visible endpoints of the lines for drawing between two objects."""
+        first_point = self._lineControlPoints[0]
+        last_point = self._lineControlPoints[-1]
+
+        return (first_point[0], first_point[1]), (last_point[0], last_point[1])
+
+    def SetAttachments(self, from_attach, to_attach):
+        """Specify which object attachment points should be used at each end
+        of the line.
+        """
+        self._attachmentFrom = from_attach
+        self._attachmentTo = to_attach
+
+    def HitTest(self, x, y):
+        if not self._lineControlPoints:
+            return False
+
+        # Look at label regions in case mouse is over a label
+        inLabelRegion = False
+        for i in range(3):
+            if self._regions[i]:
+                region = self._regions[i]
+                if len(region._formattedText):
+                    xp, yp = self.GetLabelPosition(i)
+                    # Offset region from default label position
+                    cx, cy = region.GetPosition()
+                    cw, ch = region.GetSize()
+                    cx += xp
+                    cy += yp
+                    
+                    rLeft = cx-cw / 2
+                    rTop = cy-ch / 2
+                    rRight = cx + cw / 2
+                    rBottom = cy + ch / 2
+                    if x>rLeft and x<rRight and y>rTop and y<rBottom:
+                        inLabelRegion = True
+                        break
+
+        for i in range(len(self._lineControlPoints)-1):
+            point1 = self._lineControlPoints[i]
+            point2 = self._lineControlPoints[i + 1]
+
+            # For inaccurate mousing allow 8 pixel corridor
+            extra = 4
+
+            dx = point2[0]-point1[0]
+            dy = point2[1]-point1[1]
+
+            seg_len = sqrt(dx * dx + dy * dy)
+            if dy == 0 or dx == 0:
+                return False
+            distance_from_seg = seg_len * ((x-point1[0]) * dy-(y-point1[1]) * dx) / (dy * dy + dx * dx)
+            distance_from_prev = seg_len * ((y-point1[1]) * dy + (x-point1[0]) * dx) / (dy * dy + dx * dx)
+
+            if abs(distance_from_seg)<extra and distance_from_prev >= 0 and distance_from_prev <= seg_len or inLabelRegion:
+                return 0, distance_from_seg
+
+        return False
+
+    def DrawArrows(self, dc):
+        """Draw all arrows."""
+        # Distance along line of each arrow: space them out evenly
+        startArrowPos = 0.0
+        endArrowPos = 0.0
+        middleArrowPos = 0.0
+
+        for arrow in self._arcArrows:
+            ah = arrow.GetArrowEnd()
+            if ah == ARROW_POSITION_START:
+                if arrow.GetXOffset() and not self._ignoreArrowOffsets:
+                    # If specified, x offset is proportional to line length
+                    self.DrawArrow(dc, arrow, arrow.GetXOffset(), True)
+                else:
+                    self.DrawArrow(dc, arrow, startArrowPos, False)
+                    startArrowPos += arrow.GetSize() + arrow.GetSpacing()
+            elif ah == ARROW_POSITION_END:
+                if arrow.GetXOffset() and not self._ignoreArrowOffsets:
+                    self.DrawArrow(dc, arrow, arrow.GetXOffset(), True)
+                else:
+                    self.DrawArrow(dc, arrow, endArrowPos, False)
+                    endArrowPos += arrow.GetSize() + arrow.GetSpacing()
+            elif ah == ARROW_POSITION_MIDDLE:
+                arrow.SetXOffset(middleArrowPos)
+                if arrow.GetXOffset() and not self._ignoreArrowOffsets:
+                    self.DrawArrow(dc, arrow, arrow.GetXOffset(), True)
+                else:
+                    self.DrawArrow(dc, arrow, middleArrowPos, False)
+                    middleArrowPos += arrow.GetSize() + arrow.GetSpacing()
+
+    def DrawArrow(self, dc, arrow, XOffset, proportionalOffset):
+        """Draw the given arrowhead (or annotation)."""
+        first_line_point = self._lineControlPoints[0]
+        second_line_point = self._lineControlPoints[1]
+
+        last_line_point = self._lineControlPoints[-1]
+        second_last_line_point = self._lineControlPoints[-2]
+
+        # Position of start point of line, at the end of which we draw the arrow
+        startPositionX, startPositionY = 0.0, 0.0
+
+        ap = arrow.GetPosition()
+        if ap == ARROW_POSITION_START:
+            # If we're using a proportional offset, calculate just where this
+            # will be on the line.
+            realOffset = XOffset
+            if proportionalOffset:
+                totalLength = sqrt((second_line_point[0]-first_line_point[0]) * (second_line_point[0]-first_line_point[0]) + (second_line_point[1]-first_line_point[1]) * (second_line_point[1]-first_line_point[1]))
+                realOffset = XOffset * totalLength
+
+            positionOnLineX, positionOnLineY = GetPointOnLine(second_line_point[0], second_line_point[1], first_line_point[0], first_line_point[1], realOffset)
+            
+            startPositionX = second_line_point[0]
+            startPositionY = second_line_point[1]
+        elif ap == ARROW_POSITION_END:
+            # If we're using a proportional offset, calculate just where this
+            # will be on the line.
+            realOffset = XOffset
+            if proportionalOffset:
+                totalLength = sqrt((second_last_line_point[0]-last_line_point[0]) * (second_last_line_point[0]-last_line_point[0]) + (second_last_line_point[1]-last_line_point[1]) * (second_last_line_point[1]-last_line_point[1]));
+                realOffset = XOffset * totalLength
+            
+            positionOnLineX, positionOnLineY = GetPointOnLine(second_last_line_point[0], second_last_line_point[1], last_line_point[0], last_line_point[1], realOffset)
+            
+            startPositionX = second_last_line_point[0]
+            startPositionY = second_last_line_point[1]
+        elif ap == ARROW_POSITION_MIDDLE:
+            # Choose a point half way between the last and penultimate points
+            x = (last_line_point[0] + second_last_line_point[0]) / 2
+            y = (last_line_point[1] + second_last_line_point[1]) / 2
+
+            # If we're using a proportional offset, calculate just where this
+            # will be on the line.
+            realOffset = XOffset
+            if proportionalOffset:
+                totalLength = sqrt((second_last_line_point[0]-x) * (second_last_line_point[0]-x) + (second_last_line_point[1]-y) * (second_last_line_point[1]-y));
+                realOffset = XOffset * totalLength
+
+            positionOnLineX, positionOnLineY = GetPointOnLine(second_last_line_point[0], second_last_line_point[1], x, y, realOffset)
+            startPositionX = second_last_line_point[0]
+            startPositionY = second_last_line_point[1]
+
+        # Add yOffset to arrow, if any
+
+        # The translation that the y offset may give
+        deltaX = 0.0
+        deltaY = 0.0
+        if arrow.GetYOffset and not self._ignoreArrowOffsets:
+            #                             |(x4, y4)
+            #                             |d
+            #                             |
+            #   (x1, y1)--------------(x3, y3)------------------(x2, y2)
+            #   x4 = x3 - d * sin(theta)
+            #   y4 = y3 + d * cos(theta)
+            #
+            #   Where theta = tan(-1) of (y3-y1) / (x3-x1)
+            x1 = startPositionX
+            y1 = startPositionY
+            x3 = positionOnLineX
+            y3 = positionOnLineY
+            d=-arrow.GetYOffset() # Negate so +offset is above line
+
+            if x3 == x1:
+                theta = pi / 2
+            else:
+                theta = atan((y3-y1) / (x3-x1))
+
+            x4 = x3-d * sin(theta)
+            y4 = y3 + d * cos(theta)
+            
+            deltaX = x4-positionOnLineX
+            deltaY = y4-positionOnLineY
+
+        at = arrow._GetType()
+        if at == ARROW_ARROW:
+            arrowLength = arrow.GetSize()
+            arrowWidth = arrowLength / 3
+
+            tip_x, tip_y, side1_x, side1_y, side2_x, side2_y = GetArrowPoints(startPositionX + deltaX, startPositionY + deltaY, positionOnLineX + deltaX, positionOnLineY + deltaY, arrowLength, arrowWidth)
+
+            points = [[tip_x, tip_y],
+                    [side1_x, side1_y],
+                    [side2_x, side2_y],
+                    [tip_x, tip_y]]
+
+            dc.SetPen(self._pen)
+            dc.SetBrush(self._brush)
+            dc.DrawPolygon(points)
+        elif at in [ARROW_HOLLOW_CIRCLE, ARROW_FILLED_CIRCLE]:
+            # Find point on line of centre of circle, which is a radius away
+            # from the end position
+            diameter = arrow.GetSize()
+            x, y = GetPointOnLine(startPositionX + deltaX, startPositionY + deltaY,
+                               positionOnLineX + deltaX, positionOnLineY + deltaY,
+                               diameter / 2)
+            x1 = x-diameter / 2
+            y1 = y-diameter / 2
+            dc.SetPen(self._pen)
+            if arrow._GetType() == ARROW_HOLLOW_CIRCLE:
+                dc.SetBrush(self.GetBackgroundBrush())
+            else:
+                dc.SetBrush(self._brush)
+
+            dc.DrawEllipse(x1, y1, diameter, diameter)
+        elif at == ARROW_SINGLE_OBLIQUE:
+            pass
+        elif at == ARROW_METAFILE:
+            if arrow.GetMetaFile():
+                # Find point on line of centre of object, which is a half-width away
+                # from the end position
+                #
+                #                 width
+                #  <-- start pos  <-----><-- positionOnLineX
+                #                 _____
+                #  --------------|  x  | <-- e.g. rectangular arrowhead
+                #                 -----
+                #
+                x, y = GetPointOnLine(startPositionX, startPositionY,
+                                   positionOnLineX, positionOnLineY,
+                                   arrow.GetMetaFile()._width / 2)
+                # Calculate theta for rotating the metafile.
+                #
+                # |
+                # |     o(x2, y2)   'o' represents the arrowhead.
+                # |    /
+                # |   /
+                # |  /theta
+                # | /(x1, y1)
+                # |______________________
+                #
+                theta = 0.0
+                x1 = startPositionX
+                y1 = startPositionY
+                x2 = positionOnLineX
+                y2 = positionOnLineY
+
+                if x1 == x2 and y1 == y2:
+                    theta = 0.0
+                elif x1 == x2 and y1>y2:
+                    theta = 3.0 * pi / 2
+                elif x1 == x2 and y2>y1:
+                    theta = pi / 2
+                elif x2>x1 and y2 >= y1:
+                    theta = atan((y2-y1) / (x2-x1))
+                elif x2<x1:
+                    theta = pi + atan((y2-y1) / (x2-x1))
+                elif x2>x1 and y2<y1:
+                    theta = 2 * pi + atan((y2-y1) / (x2-x1))
+                else:
+                    raise "Unknown arrowhead rotation case"
+
+                # Rotate about the centre of the object, then place
+                # the object on the line.
+                if arrow.GetMetaFile().GetRotateable():
+                    arrow.GetMetaFile().Rotate(0.0, 0.0, theta)
+
+                if self._erasing:
+                    # If erasing, just draw a rectangle
+                    minX, minY, maxX, maxY = arrow.GetMetaFile().GetBounds()
+                    # Make erasing rectangle slightly bigger or you get droppings
+                    extraPixels = 4
+                    dc.DrawRectangle(deltaX + x + minX-extraPixels / 2, deltaY + y + minY-extraPixels / 2, maxX-minX + extraPixels, maxY-minY + extraPixels)
+                else:
+                    arrow.GetMetaFile().Draw(dc, x + deltaX, y + deltaY)
+
+    def OnErase(self, dc):
+        old_pen = self._pen
+        old_brush = self._brush
+
+        bg_pen = self.GetBackgroundPen()
+        bg_brush = self.GetBackgroundBrush()
+        self.SetPen(bg_pen)
+        self.SetBrush(bg_brush)
+
+        bound_x, bound_y = self.GetBoundingBoxMax()
+        if self._font:
+            dc.SetFont(self._font)
+
+        # Undraw text regions
+        for i in range(3):
+            if self._regions[i]:
+                x, y = self.GetLabelPosition(i)
+                self.EraseRegion(dc, self._regions[i], x, y)
+
+        # Undraw line
+        dc.SetPen(self.GetBackgroundPen())
+        dc.SetBrush(self.GetBackgroundBrush())
+
+        # Drawing over the line only seems to work if the line has a thickness
+        # of 1.
+        if old_pen and old_pen.GetWidth()>1:
+            dc.DrawRectangle(self._xpos-bound_x / 2-2, self._ypos-bound_y / 2-2,
+                             bound_x + 4, bound_y + 4)
+        else:
+            self._erasing = True
+            self.GetEventHandler().OnDraw(dc)
+            self.GetEventHandler().OnEraseControlPoints(dc)
+            self._erasing = False
+
+        if old_pen:
+            self.SetPen(old_pen)
+        if old_brush:
+            self.SetBrush(old_brush)
+
+    def GetBoundingBoxMin(self):
+        x1, y1 = 10000, 10000
+        x2, y2=-10000,-10000
+
+        for point in self._lineControlPoints:
+            if point[0]<x1:
+                x1 = point[0]
+            if point[1]<y1:
+                y1 = point[1]
+            if point[0]>x2:
+                x2 = point[0]
+            if point[1]>y2:
+                y2 = point[1]
+
+        return x2-x1, y2-y1
+        
+    # For a node image of interest, finds the position of this arc
+    # amongst all the arcs which are attached to THIS SIDE of the node image,
+    # and the number of same.
+    def FindNth(self, image, incoming):
+        """Find the position of the line on the given object.
+
+        Specify whether incoming or outgoing lines are being considered
+        with incoming.
+        """
+        n=-1
+        num = 0
+        
+        if image == self._to:
+            this_attachment = self._attachmentTo
+        else:
+            this_attachment = self._attachmentFrom
+
+        # Find number of lines going into / out of this particular attachment point
+        for line in image.GetLines():
+            if line._from == image:
+                # This is the nth line attached to 'image'
+                if line == self and not incoming:
+                    n = num
+
+                # Increment num count if this is the same side (attachment number)
+                if line._attachmentFrom == this_attachment:
+                    num += 1
+
+            if line._to == image:
+                # This is the nth line attached to 'image'
+                if line == self and incoming:
+                    n = num
+
+                # Increment num count if this is the same side (attachment number)
+                if line._attachmentTo == this_attachment:
+                    num += 1
+
+        return n, num
+
+    def OnDrawOutline(self, dc, x, y, w, h):
+        old_pen = self._pen
+        old_brush = self._brush
+
+        dottedPen = wx.Pen(wx.Colour(0, 0, 0), 1, wx.DOT)
+        self.SetPen(dottedPen)
+        self.SetBrush(wx.TRANSPARENT_BRUSH)
+
+        self.GetEventHandler().OnDraw(dc)
+
+        if old_pen:
+            self.SetPen(old_pen)
+        else:
+            self.SetPen(None)
+        if old_brush:
+            self.SetBrush(old_brush)
+        else:
+            self.SetBrush(None)
+
+    def OnMovePre(self, dc, x, y, old_x, old_y, display = True):
+        x_offset = x-old_x
+        y_offset = y-old_y
+
+        if self._lineControlPoints and not (x_offset == 0 and y_offset == 0):
+            for point in self._lineControlPoints:
+                point[0] += x_offset
+                point[1] += y_offset
+
+        # Move temporary label rectangles if necessary
+        for i in range(3):
+            if self._labelObjects[i]:
+                self._labelObjects[i].Erase(dc)
+                xp, yp = self.GetLabelPosition(i)
+                if i<len(self._regions):
+                    xr, yr = self._regions[i].GetPosition()
+                else:
+                    xr, yr = 0, 0
+                self._labelObjects[i].Move(dc, xp + xr, yp + yr)
+        return True
+
+    def OnMoveLink(self, dc, moveControlPoints = True):
+        """Called when a connected object has moved, to move the link to
+        correct position
+        """
+        if not self._from or not self._to:
+            return
+
+        if len(self._lineControlPoints)>2:
+            self.Initialise()
+
+        # Do each end - nothing in the middle. User has to move other points
+        # manually if necessary
+        end_x, end_y, other_end_x, other_end_y = self.FindLineEndPoints()
+
+        first = self._lineControlPoints[0]
+        last = self._lineControlPoints[-1]
+
+        oldX, oldY = self._xpos, self._ypos
+
+        self.SetEnds(end_x, end_y, other_end_x, other_end_y)
+
+        # Do a second time, because one may depend on the other
+        end_x, end_y, other_end_x, other_end_y = self.FindLineEndPoints()
+        self.SetEnds(end_x, end_y, other_end_x, other_end_y)
+
+        # Try to move control points with the arc
+        x_offset = self._xpos-oldX
+        y_offset = self._ypos-oldY
+
+        # Only move control points if it's a self link. And only works
+        # if attachment mode is ON
+        if self._from == self._to and self._from.GetAttachmentMode() != ATTACHMENT_MODE_NONE and moveControlPoints and self._lineControlPoints and not (x_offset == 0 and y_offset == 0):
+            for point in self._lineControlPoints[1:-1]:
+                point.x += x_offset
+                point.y += y_offset
+
+        self.Move(dc, self._xpos, self._ypos)
+
+    def FindLineEndPoints(self):
+        """Finds the x, y points at the two ends of the line.
+
+        This function can be used by e.g. line-routing routines to
+        get the actual points on the two node images where the lines will be
+        drawn to / from.
+        """
+        if not self._from or not self._to:
+            return
+
+        # Do each end - nothing in the middle. User has to move other points
+        # manually if necessary.
+        second_point = self._lineControlPoints[1]
+        second_last_point = self._lineControlPoints[-2]
+        
+        if len(self._lineControlPoints)>2:
+            if self._from.GetAttachmentMode() != ATTACHMENT_MODE_NONE:
+                nth, no_arcs = self.FindNth(self._from, False) # Not incoming
+                end_x, end_y = self._from.GetAttachmentPosition(self._attachmentFrom, nth, no_arcs, self)
+            else:
+                end_x, end_y = self._from.GetPerimeterPoint(self._from.GetX(), self._from.GetY(), second_point[0], second_point[1])
+
+            if self._to.GetAttachmentMode() != ATTACHMENT_MODE_NONE:
+                nth, no_arch = self.FindNth(self._to, True) # Incoming
+                other_end_x, other_end_y = self._to.GetAttachmentPosition(self._attachmentTo, nth, no_arch, self)
+            else:
+                other_end_x, other_end_y = self._to.GetPerimeterPoint(self._to.GetX(), self._to.GetY(), second_last_point[0], second_last_point[1])
+        else:
+            fromX = self._from.GetX()
+            fromY = self._from.GetY()
+            toX = self._to.GetX()
+            toY = self._to.GetY()
+
+            if self._from.GetAttachmentMode() != ATTACHMENT_MODE_NONE:
+                nth, no_arcs = self.FindNth(self._from, False)
+                end_x, end_y = self._from.GetAttachmentPosition(self._attachmentFrom, nth, no_arcs, self)
+                fromX = end_x
+                fromY = end_y
+
+            if self._to.GetAttachmentMode() != ATTACHMENT_MODE_NONE:
+                nth, no_arcs = self.FindNth(self._to, True)
+                other_end_x, other_end_y = self._to.GetAttachmentPosition(self._attachmentTo, nth, no_arcs, self)
+                toX = other_end_x
+                toY = other_end_y
+
+            if self._from.GetAttachmentMode() == ATTACHMENT_MODE_NONE:
+                end_x, end_y = self._from.GetPerimeterPoint(self._from.GetX(), self._from.GetY(), toX, toY)
+
+            if self._to.GetAttachmentMode() == ATTACHMENT_MODE_NONE:
+                other_end_x, other_end_y = self._to.GetPerimeterPoint(self._to.GetX(), self._to.GetY(), fromX, fromY)
+
+            #print type(self._from), type(self._to), end_x, end_y, other_end_x, other_end_y
+            return end_x, end_y, other_end_x, other_end_y
+
+    def OnDraw(self, dc):
+        if not self._lineControlPoints:
+            return
+
+        if self._pen:
+            dc.SetPen(self._pen)
+        if self._brush:
+            dc.SetBrush(self._brush)
+
+        points = []
+        for point in self._lineControlPoints:
+            points.append(wx.Point(point.x, point.y))
+
+        #print points
+        if self._isSpline:
+            dc.DrawSpline(points)
+        else:
+            dc.DrawLines(points)
+
+        if sys.platform[:3]=="win":
+            # For some reason, last point isn't drawn under Windows
+            dc.DrawPoint(points[-1])
+
+        # Problem with pen - if not a solid pen, does strange things
+        # to the arrowhead. So make (get) a new pen that's solid.
+        if self._pen and self._pen.GetStyle() != wx.SOLID:
+            solid_pen = wx.ThePenList().FindOrCreatePen(self._pen.GetColour(), 1, wx.SOLID)
+            if solid_pen:
+                dc.SetPen(solid_pen)
+
+        self.DrawArrows(dc)
+
+    def OnDrawControlPoints(self, dc):
+        if not self._drawHandles:
+            return
+
+        # Draw temporary label rectangles if necessary
+        for i in range(3):
+            if self._labelObjects[i]:
+                self._labelObjects[i].Draw(dc)
+
+        Shape.OnDrawControlPoints(self, dc)
+
+    def OnEraseControlPoints(self, dc):
+        # Erase temporary label rectangles if necessary
+        
+        for i in range(3):
+            if self._labelObjects[i]:
+                self._labelObjects[i].Erase(dc)
+
+        Shape.OnEraseControlPoints(self, dc)
+
+    def OnDragLeft(self, draw, x, y, keys = 0, attachment = 0):
+        pass
+
+    def OnBeginDragLeft(self, x, y, keys = 0, attachment = 0):
+        pass
+
+    def OnEndDragLeft(self, x, y, keys = 0, attachment = 0):
+        pass
+
+    def OnDrawContents(self, dc):
+        if self.GetDisableLabel():
+            return
+
+        for i in range(3):
+            if self._regions[i]:
+                x, y = self.GetLabelPosition(i)
+                self.DrawRegion(dc, self._regions[i], x, y)
+
+    def SetTo(self, object):
+        """Set the 'to' object for the line."""
+        self._to = object
+
+    def SetFrom(self, object):
+        """Set the 'from' object for the line."""
+        self._from = object
+
+    def MakeControlPoints(self):
+        """Make handle control points."""
+        if self._canvas and self._lineControlPoints:
+            first = self._lineControlPoints[0]
+            last = self._lineControlPoints[-1]
+
+            control = LineControlPoint(self._canvas, self, CONTROL_POINT_SIZE, first[0], first[1], CONTROL_POINT_ENDPOINT_FROM)
+            control._point = first
+            self._canvas.AddShape(control)
+            self._controlPoints.Append(control)
+
+            for point in self._lineControlPoints[1:-1]:
+                control = LineControlPoint(self._canvas, self, CONTROL_POINT_SIZE, point[0], point[1], CONTROL_POINT_LINE)
+                control._point = point
+                self._canvas.AddShape(control)
+                self._controlPoints.Append(control)
+
+            control = LineControlPoint(self._canvas, self, CONTROL_POINT_SIZE, last[0], last[1], CONTROL_POINT_ENDPOINT_TO)
+            control._point = last
+            self._canvas.AddShape(control)
+            self._controlPoints.Append(control)
+
+    def ResetControlPoints(self):
+        if self._canvas and self._lineControlPoints:
+            for i in range(min(len(self._controlPoints), len(self._lineControlPoints))):
+                point = self._lineControlPoints[i]
+                control = self._controlPoints[i]
+                control.SetX(point[0])
+                control.SetY(point[1])
+
+    # Override select, to create / delete temporary label-moving objects
+    def Select(self, select, dc = None):
+        Shape.Select(self, select, dc)
+        if select:
+            for i in range(3):
+                if self._regions[i]:
+                    region = self._regions[i]
+                    if region._formattedText:
+                        w, h = region.GetSize()
+                        x, y = region.GetPosition()
+                        xx, yy = self.GetLabelPosition(i)
+
+                        if self._labelObjects[i]:
+                            self._labelObjects[i].Select(False)
+                            self._labelObjects[i].RemoveFromCanvas(self._canvas)
+
+                        self._labelObjects[i] = self.OnCreateLabelShape(self, region, w, h)
+                        self._labelObjects[i].AddToCanvas(self._canvas)
+                        self._labelObjects[i].Show(True)
+                        if dc:
+                            self._labelObjects[i].Move(dc, x + xx, y + yy)
+                        self._labelObjects[i].Select(True, dc)
+        else:
+            for i in range(3):
+                if self._labelObjects[i]:
+                    self._labelObjects[i].Select(False, dc)
+                    self._labelObjects[i].Erase(dc)
+                    self._labelObjects[i].RemoveFromCanvas(self._canvas)
+                    self._labelObjects[i] = None
+
+    # Control points ('handles') redirect control to the actual shape, to
+    # make it easier to override sizing behaviour.
+    def OnSizingDragLeft(self, pt, draw, x, y, keys = 0, attachment = 0):
+        dc = wx.ClientDC(self.GetCanvas())
+        self.GetCanvas().PrepareDC(dc)
+
+        dc.SetLogicalFunction(OGLRBLF)
+
+        dottedPen = wx.Pen(wx.Colour(0, 0, 0), 1, wx.DOT)
+        dc.SetPen(dottedPen)
+        dc.SetBrush(wx.TRANSPARENT_BRUSH)
+
+        if pt._type == CONTROL_POINT_LINE:
+            x, y = self._canvas.Snap()
+
+            pt.SetX(x)
+            pt.SetY(y)
+            pt._point[0] = x
+            pt._point[1] = y
+
+            old_pen = self.GetPen()
+            old_brush = self.GetBrush()
+
+            self.SetPen(dottedPen)
+            self.SetBrush(wx.TRANSPARENT_BRUSH)
+
+            self.GetEventHandler().OnMoveLink(dc, False)
+            
+            self.SetPen(old_pen)
+            self.SetBrush(old_brush)
+
+    def OnSizingBeginDragLeft(self, pt, x, y, keys = 0, attachment = 0):
+        dc = wx.ClientDC(self.GetCanvas())
+        self.GetCanvas().PrepareDC(dc)
+
+        if pt._type == CONTROL_POINT_LINE:
+            pt._originalPos = pt._point
+            x, y = self._canvas.Snap()
+
+            self.Erase(dc)
+
+            # Redraw start and end objects because we've left holes
+            # when erasing the line
+            self.GetFrom().OnDraw(dc)
+            self.GetFrom().OnDrawContents(dc)
+            self.GetTo().OnDraw(dc)
+            self.GetTo().OnDrawContents(dc)
+
+            self.SetDisableLabel(True)
+            dc.SetLogicalFunction(OGLRBLF)
+
+            pt._xpos = x
+            pt._ypos = y
+            pt._point[0] = x
+            pt._point[1] = y
+
+            old_pen = self.GetPen()
+            old_brush = self.GetBrush()
+
+            dottedPen = wx.Pen(wx.Colour(0, 0, 0), 1, wx.DOT)
+            self.SetPen(dottedPen)
+            self.SetBrush(wx.TRANSPARENT_BRUSH)
+
+            self.GetEventHandler().OnMoveLink(dc, False)
+
+            self.SetPen(old_pen)
+            self.SetBrush(old_brush)
+
+        if pt._type == CONTROL_POINT_ENDPOINT_FROM or pt._type == CONTROL_POINT_ENDPOINT_TO:
+            self._canvas.SetCursor(wx.Cursor(wx.CURSOR_BULLSEYE))
+            pt._oldCursor = wx.STANDARD_CURSOR
+
+    def OnSizingEndDragLeft(self, pt, x, y, keys = 0, attachment = 0):
+        dc = wx.ClientDC(self.GetCanvas())
+        self.GetCanvas().PrepareDC(dc)
+
+        self.SetDisableLabel(False)
+
+        if pt._type == CONTROL_POINT_LINE:
+            x, y = self._canvas.Snap()
+
+            rpt = wx.RealPoint(x, y)
+
+            # Move the control point back to where it was;
+            # MoveControlPoint will move it to the new position
+            # if it decides it wants. We only moved the position
+            # during user feedback so we could redraw the line
+            # as it changed shape.
+            pt._xpos = pt._originalPos[0]
+            pt._ypos = pt._originalPos[1]
+            pt._point[0] = pt._originalPos[0]
+            pt._point[1] = pt._originalPos[1]
+
+            self.OnMoveMiddleControlPoint(dc, pt, rpt)
+
+        if pt._type == CONTROL_POINT_ENDPOINT_FROM:
+            if pt._oldCursor:
+                self._canvas.SetCursor(pt._oldCursor)
+
+                if self.GetFrom():
+                    self.GetFrom().MoveLineToNewAttachment(dc, self, x, y)
+
+        if pt._type == CONTROL_POINT_ENDPOINT_TO:
+            if pt._oldCursor:
+                self._canvas.SetCursor(pt._oldCursor)
+
+                if self.GetTo():
+                    self.GetTo().MoveLineToNewAttachment(dc, self, x, y)
+
+    # This is called only when a non-end control point is moved
+    def OnMoveMiddleControlPoint(self, dc, lpt, pt):
+        lpt._xpos = pt[0]
+        lpt._ypos = pt[1]
+
+        lpt._point[0] = pt[0]
+        lpt._point[1] = pt[1]
+
+        self.GetEventHandler().OnMoveLink(dc)
+
+        return True
+
+    def AddArrow(self, type, end = ARROW_POSITION_END, size = 10.0, xOffset = 0.0, name="",mf = None, arrowId=-1):
+        """Add an arrow (or annotation) to the line.
+
+        type may currently be one of:
+
+        ARROW_HOLLOW_CIRCLE
+          Hollow circle. 
+        ARROW_FILLED_CIRCLE
+          Filled circle. 
+        ARROW_ARROW
+          Conventional arrowhead. 
+        ARROW_SINGLE_OBLIQUE
+          Single oblique stroke. 
+        ARROW_DOUBLE_OBLIQUE
+          Double oblique stroke. 
+        ARROW_DOUBLE_METAFILE
+          Custom arrowhead. 
+
+        end may currently be one of:
+
+        ARROW_POSITION_END
+          Arrow appears at the end. 
+        ARROW_POSITION_START
+          Arrow appears at the start. 
+
+        arrowSize specifies the length of the arrow.
+
+        xOffset specifies the offset from the end of the line.
+
+        name specifies a name for the arrow.
+
+        mf can be a wxPseduoMetaFile, perhaps loaded from a simple Windows
+        metafile.
+
+        arrowId is the id for the arrow.
+        """
+        arrow = ArrowHead(type, end, size, xOffset, name, mf, arrowId)
+        self._arcArrows.append(arrow)
+        return arrow
+
+    # Add arrowhead at a particular position in the arrowhead list
+    def AddArrowOrdered(self, arrow, referenceList, end):
+        """Add an arrowhead in the position indicated by the reference list
+        of arrowheads, which contains all legal arrowheads for this line, in
+        the correct order. E.g.
+
+        Reference list:      a b c d e
+        Current line list:   a d
+
+        Add c, then line list is: a c d.
+
+        If no legal arrowhead position, return FALSE. Assume reference list
+        is for one end only, since it potentially defines the ordering for
+        any one of the 3 positions. So we don't check the reference list for
+        arrowhead position.
+        """
+        if not referenceList:
+            return False
+
+        targetName = arrow.GetName()
+
+        # First check whether we need to insert in front of list,
+        # because this arrowhead is the first in the reference
+        # list and should therefore be first in the current list.
+        refArrow = referenceList[0]
+        if refArrow.GetName() == targetName:
+            self._arcArrows.insert(0, arrow)
+            return True
+
+        i1 = i2 = 0
+        while i1<len(referenceList) and i2<len(self._arcArrows):
+            refArrow = referenceList[i1]
+            currArrow = self._arcArrows[i2]
+
+            # Matching: advance current arrow pointer
+            if currArrow.GetArrowEnd() == end and currArrow.GetName() == refArrow.GetName():
+                i2 += 1
+
+            # Check if we're at the correct position in the
+            # reference list
+            if targetName == refArrow.GetName():
+                if i2<len(self._arcArrows):
+                    self._arcArrows.insert(i2, arrow)
+                else:
+                    self._arcArrows.append(arrow)
+                return True
+            i1 += 1
+
+        self._arcArrows.append(arrow)
+        return True
+
+    def ClearArrowsAtPosition(self, end):
+        """Delete the arrows at the specified position, or at any position
+        if position is -1.
+        """
+        if end==-1:
+            self._arcArrows = []
+            return
+
+        for arrow in self._arcArrows:
+            if arrow.GetArrowEnd() == end:
+                self._arcArrows.remove(arrow)
+
+    def ClearArrow(self, name):
+        """Delete the arrow with the given name."""
+        for arrow in self._arcArrows:
+            if arrow.GetName() == name:
+                self._arcArrows.remove(arrow)
+                return True
+        return False
+
+    def FindArrowHead(self, position, name):
+        """Find arrowhead by position and name.
+
+        if position is -1, matches any position.
+        """
+        for arrow in self._arcArrows:
+            if (position==-1 or position == arrow.GetArrowEnd()) and arrow.GetName() == name:
+                return arow
+
+        return None
+
+    def FindArrowHeadId(self, arrowId):
+        """Find arrowhead by id."""
+        for arrow in self._arcArrows:
+            if arrowId == arrow.GetId():
+                return arrow
+
+        return None
+
+    def DeleteArrowHead(self, position, name):
+        """Delete arrowhead by position and name.
+
+        if position is -1, matches any position.
+        """
+        for arrow in self._arcArrows:
+            if (position==-1 or position == arrow.GetArrowEnd()) and arrow.GetName() == name:
+                self._arcArrows.remove(arrow)
+                return True
+        return False
+    
+    def DeleteArrowHeadId(self, id):
+        """Delete arrowhead by id."""
+        for arrow in self._arcArrows:
+            if arrowId == arrow.GetId():
+                self._arcArrows.remove(arrow)
+                return True
+        return False
+
+    # Calculate the minimum width a line
+    # occupies, for the purposes of drawing lines in tools.
+    def FindMinimumWidth(self):
+        """Find the horizontal width for drawing a line with arrows in
+        minimum space. Assume arrows at end only.
+        """
+        minWidth = 0.0
+        for arrowHead in self._arcArrows:
+            minWidth += arrowHead.GetSize()
+            if arrowHead != self._arcArrows[-1]:
+                minWidth += arrowHead + GetSpacing
+
+        # We have ABSOLUTE minimum now. So
+        # scale it to give it reasonable aesthetics
+        # when drawing with line.
+        if minWidth>0:
+            minWidth = minWidth * 1.4
+        else:
+            minWidth = 20.0
+
+        self.SetEnds(0.0, 0.0, minWidth, 0.0)
+        self.Initialise()
+
+        return minWidth
+
+    def FindLinePosition(self, x, y):
+        """Find which position we're talking about at this x, y.
+
+        Returns ARROW_POSITION_START, ARROW_POSITION_MIDDLE, ARROW_POSITION_END.
+        """
+        startX, startY, endX, endY = self.GetEnds()
+
+        # Find distances from centre, start and end. The smallest wins
+        centreDistance = sqrt((x-self._xpos) * (x-self._xpos) + (y-self._ypos) * (y-self._ypos))
+        startDistance = sqrt((x-startX) * (x-startX) + (y-startY) * (y-startY))
+        endDistance = sqrt((x-endX) * (x-endX) + (y-endY) * (y-endY))
+
+        if centreDistance<startDistance and centreDistance<endDistance:
+            return ARROW_POSITION_MIDDLE
+        elif startDistance<endDistance:
+            return ARROW_POSITION_START
+        else:
+            return ARROW_POSITION_END
+
+    def SetAlignmentOrientation(self, isEnd, isHoriz):
+        if isEnd:
+            if isHoriz and self._alignmentEnd & LINE_ALIGNMENT_HORIZ != LINE_ALIGNMENT_HORIZ:
+                self._alignmentEnd != LINE_ALIGNMENT_HORIZ
+            elif not isHoriz and self._alignmentEnd & LINE_ALIGNMENT_HORIZ == LINE_ALIGNMENT_HORIZ:
+                self._alignmentEnd -= LINE_ALIGNMENT_HORIZ
+        else:
+            if isHoriz and self._alignmentStart & LINE_ALIGNMENT_HORIZ != LINE_ALIGNMENT_HORIZ:
+                self._alignmentStart != LINE_ALIGNMENT_HORIZ
+            elif not isHoriz and self._alignmentStart & LINE_ALIGNMENT_HORIZ == LINE_ALIGNMENT_HORIZ:
+                self._alignmentStart -= LINE_ALIGNMENT_HORIZ
+            
+    def SetAlignmentType(self, isEnd, alignType):
+        if isEnd:
+            if alignType == LINE_ALIGNMENT_TO_NEXT_HANDLE:
+                if self._alignmentEnd & LINE_ALIGNMENT_TO_NEXT_HANDLE != LINE_ALIGNMENT_TO_NEXT_HANDLE:
+                    self._alignmentEnd |= LINE_ALIGNMENT_TO_NEXT_HANDLE
+            elif self._alignmentEnd & LINE_ALIGNMENT_TO_NEXT_HANDLE == LINE_ALIGNMENT_TO_NEXT_HANDLE:
+                self._alignmentEnd -= LINE_ALIGNMENT_TO_NEXT_HANDLE
+        else:
+            if alignType == LINE_ALIGNMENT_TO_NEXT_HANDLE:
+                if self._alignmentStart & LINE_ALIGNMENT_TO_NEXT_HANDLE != LINE_ALIGNMENT_TO_NEXT_HANDLE:
+                    self._alignmentStart |= LINE_ALIGNMENT_TO_NEXT_HANDLE
+            elif self._alignmentStart & LINE_ALIGNMENT_TO_NEXT_HANDLE == LINE_ALIGNMENT_TO_NEXT_HANDLE:
+                self._alignmentStart -= LINE_ALIGNMENT_TO_NEXT_HANDLE
+            
+    def GetAlignmentOrientation(self, isEnd):
+        if isEnd:
+            return self._alignmentEnd & LINE_ALIGNMENT_HORIZ == LINE_ALIGNMENT_HORIZ
+        else:
+            return self._alignmentStart & LINE_ALIGNMENT_HORIZ == LINE_ALIGNMENT_HORIZ
+
+    def GetAlignmentType(self, isEnd):
+        if isEnd:
+            return self._alignmentEnd & LINE_ALIGNMENT_TO_NEXT_HANDLE
+        else:
+            return self._alignmentStart & LINE_ALIGNMENT_TO_NEXT_HANDLE
+
+    def GetNextControlPoint(self, shape):
+        """Find the next control point in the line after the start / end point,
+        depending on whether the shape is at the start or end.
+        """
+        n = len(self._lineControlPoints)
+        if self._to == shape:
+            # Must be END of line, so we want (n - 1)th control point.
+            # But indexing ends at n-1, so subtract 2.
+            nn = n-2
+        else:
+            nn = 1
+        if nn<len(self._lineControlPoints):
+            return self._lineControlPoints[nn]
+        return None
+
+    def OnCreateLabelShape(self, parent, region, w, h):
+        return LabelShape(parent, region, w, h)
+
+    
+    def OnLabelMovePre(self, dc, labelShape, x, y, old_x, old_y, display):
+        labelShape._shapeRegion.SetSize(labelShape.GetWidth(), labelShape.GetHeight())
+
+        # Find position in line's region list
+        i = 0
+        for region in self.GetRegions():
+            if labelShape._shapeRegion == region:
+                self.GetRegions().remove(region)
+            else:
+                i += 1
+                
+        xx, yy = self.GetLabelPosition(i)
+        # Set the region's offset, relative to the default position for
+        # each region.
+        labelShape._shapeRegion.SetPosition(x-xx, y-yy)
+        labelShape.SetX(x)
+        labelShape.SetY(y)
+
+        # Need to reformat to fit region
+        if labelShape._shapeRegion.GetText():
+            s = labelShape._shapeRegion.GetText()
+            labelShape.FormatText(dc, s, i)
+            self.DrawRegion(dc, labelShape._shapeRegion, xx, yy)
+        return True
+    
diff --git a/wxPython/wx/lib/ogl/oglmisc.py b/wxPython/wx/lib/ogl/oglmisc.py
new file mode 100644 (file)
index 0000000..d7cfbc8
--- /dev/null
@@ -0,0 +1,416 @@
+# -*- coding: iso-8859-1 -*-
+#----------------------------------------------------------------------------
+# Name:         oglmisc.py
+# Purpose:      Miscellaneous OGL support functions
+#
+# Author:       Pierre Hjälm (from C++ original by Julian Smart)
+#
+# Created:      20040508
+# RCS-ID:       
+# Copyright:    (c) 2004 Pierre Hjälm - 1998 Julian Smart
+# Licence:      wxWindows license
+#----------------------------------------------------------------------------
+
+from __future__ import division
+from math import *
+
+import wx
+
+# Control point types
+# Rectangle and most other shapes
+CONTROL_POINT_VERTICAL = 1
+CONTROL_POINT_HORIZONTAL = 2
+CONTROL_POINT_DIAGONAL = 3
+
+# Line
+CONTROL_POINT_ENDPOINT_TO = 4
+CONTROL_POINT_ENDPOINT_FROM = 5
+CONTROL_POINT_LINE = 6
+
+# Types of formatting: can be combined in a bit list
+FORMAT_NONE = 0             # Left justification
+FORMAT_CENTRE_HORIZ = 1     # Centre horizontally
+FORMAT_CENTRE_VERT = 2      # Centre vertically
+FORMAT_SIZE_TO_CONTENTS = 4 # Resize shape to contents
+
+# Attachment modes
+ATTACHMENT_MODE_NONE, ATTACHMENT_MODE_EDGE, ATTACHMENT_MODE_BRANCHING = 0, 1, 2
+
+# Shadow mode
+SHADOW_NONE, SHADOW_LEFT, SHADOW_RIGHT = 0, 1, 2
+
+OP_CLICK_LEFT, OP_CLICK_RIGHT, OP_DRAG_LEFT, OP_DRAG_RIGHT = 1, 2, 4, 8
+OP_ALL = OP_CLICK_LEFT | OP_CLICK_RIGHT | OP_DRAG_LEFT | OP_DRAG_RIGHT
+
+# Sub-modes for branching attachment mode
+BRANCHING_ATTACHMENT_NORMAL = 1
+BRANCHING_ATTACHMENT_BLOB = 2
+
+# logical function to use when drawing rubberband boxes, etc.
+OGLRBLF = wx.INVERT
+
+CONTROL_POINT_SIZE = 6
+
+# Types of arrowhead
+# (i) Built-in
+ARROW_HOLLOW_CIRCLE   = 1
+ARROW_FILLED_CIRCLE   = 2
+ARROW_ARROW           = 3
+ARROW_SINGLE_OBLIQUE  = 4
+ARROW_DOUBLE_OBLIQUE  = 5
+# (ii) Custom
+ARROW_METAFILE        = 20
+
+# Position of arrow on line
+ARROW_POSITION_START  = 0
+ARROW_POSITION_END    = 1
+ARROW_POSITION_MIDDLE = 2
+
+# Line alignment flags
+# Vertical by default
+LINE_ALIGNMENT_HORIZ              = 1
+LINE_ALIGNMENT_VERT               = 0
+LINE_ALIGNMENT_TO_NEXT_HANDLE     = 2
+LINE_ALIGNMENT_NONE               = 0
+
+
+
+# Format a string to a list of strings that fit in the given box.
+# Interpret %n and 10 or 13 as a new line.
+def FormatText(dc, text, width, height, formatMode):
+    i = 0
+    word=""
+    word_list = []
+    end_word = False
+    new_line = False
+    while i<len(text):
+        if text[i]=="%":
+            i += 1
+            if i == len(text):
+                word+="%"
+            else:
+                if text[i]=="n":
+                    new_line = True
+                    end_word = True
+                    i += 1
+                else:
+                    word+="%"+text[i]
+                    i += 1
+        elif text[i] in ["\012","\015"]:
+            new_line = True
+            end_word = True
+            i += 1
+        elif text[i]==" ":
+            end_word = True
+            i += 1
+        else:
+            word += text[i]
+            i += 1
+
+        if i == len(text):
+            end_word = True
+
+        if end_word:
+            word_list.append(word)
+            word=""
+            end_word = False
+        if new_line:
+            word_list.append(None)
+            new_line = False
+
+    # Now, make a list of strings which can fit in the box
+    string_list = []
+    buffer=""
+    for s in word_list:
+        oldBuffer = buffer
+        if s is None:
+            # FORCE NEW LINE
+            if len(buffer)>0:
+                string_list.append(buffer)
+            buffer=""
+        else:
+            if len(buffer):
+                buffer+=" "
+            buffer += s
+            x, y = dc.GetTextExtent(buffer)
+
+            # Don't fit within the bounding box if we're fitting
+            # shape to contents
+            if (x>width) and not (formatMode & FORMAT_SIZE_TO_CONTENTS):
+                # Deal with first word being wider than box
+                if len(oldBuffer):
+                    string_list.append(oldBuffer)
+                buffer = s
+    if len(buffer):
+        string_list.append(buffer)
+
+    return string_list
+
+
+
+def GetCentredTextExtent(dc, text_list, xpos = 0, ypos = 0, width = 0, height = 0):
+    if not text_list:
+        return 0, 0
+
+    max_width = 0
+    for line in text_list:
+        current_width, char_height = dc.GetTextExtent(line)
+        if current_width>max_width:
+            max_width = current_width
+
+    return max_width, len(text_list) * char_height
+
+
+
+def CentreText(dc, text_list, xpos, ypos, width, height, formatMode):
+    if not text_list:
+        return
+
+    # First, get maximum dimensions of box enclosing text
+    char_height = 0
+    max_width = 0
+    current_width = 0
+
+    # Store text extents for speed
+    widths = []
+    for line in text_list:
+        current_width, char_height = dc.GetTextExtent(line.GetText())
+        widths.append(current_width)
+        if current_width>max_width:
+            max_width = current_width
+
+    max_height = len(text_list) * char_height
+
+    if formatMode & FORMAT_CENTRE_VERT:
+        if max_height<height:
+            yoffset = ypos - height / 2 + (height - max_height) / 2
+        else:
+            yoffset = ypos - height / 2
+        yOffset = ypos
+    else:
+        yoffset = 0.0
+        yOffset = 0.0
+
+    if formatMode & FORMAT_CENTRE_HORIZ:
+        xoffset = xpos - width / 2
+        xOffset = xpos
+    else:
+        xoffset = 0.0
+        xOffset = 0.0
+
+    for i, line in enumerate(text_list):
+        if formatMode & FORMAT_CENTRE_HORIZ and widths[i]<width:
+            x = (width - widths[i]) / 2 + xoffset
+        else:
+            x = xoffset
+        y = i * char_height + yoffset
+
+        line.SetX(x - xOffset)
+        line.SetY(y - yOffset)
+        
+
+
+def DrawFormattedText(dc, text_list, xpos, ypos, width, height, formatMode):
+    if formatMode & FORMAT_CENTRE_HORIZ:
+        xoffset = xpos
+    else:
+        xoffset = xpos - width / 2
+
+    if formatMode & FORMAT_CENTRE_VERT:
+        yoffset = ypos
+    else:
+        yoffset = ypos - height / 2
+
+    # +1 to allow for rounding errors
+    dc.SetClippingRegion(xpos - width / 2, ypos - height / 2, width + 1, height + 1)
+
+    for line in text_list:
+        dc.DrawText(line.GetText(), xoffset + line.GetX(), yoffset + line.GetY())
+
+    dc.DestroyClippingRegion()
+
+
+
+def RoughlyEqual(val1, val2, tol = 0.00001):
+    return val1<(val2 + tol) and val1>(val2 - tol) and \
+           val2<(val1 + tol) and val2>(val1 - tol)
+
+
+
+def FindEndForBox(width, height, x1, y1, x2, y2):
+    xvec = [x1 - width / 2, x1 - width / 2, x1 + width / 2, x1 + width / 2, x1 - width / 2]
+    yvec = [y1 - height / 2, y1 + height / 2, y1 + height / 2, y1 - height / 2, y1 - height / 2]
+
+    return FindEndForPolyline(xvec, yvec, x2, y2, x1, y1)
+
+
+
+def CheckLineIntersection(x1, y1, x2, y2, x3, y3, x4, y4):
+    denominator_term = (y4 - y3) * (x2 - x1) - (y2 - y1) * (x4 - x3)
+    numerator_term = (x3 - x1) * (y4 - y3) + (x4 - x3) * (y1 - y3)
+
+    length_ratio = 1.0
+    k_line = 1.0
+
+    # Check for parallel lines
+    if denominator_term<0.005 and denominator_term>-0.005:
+        line_constant=-1.0
+    else:
+        line_constant = float(numerator_term) / denominator_term
+
+    # Check for intersection
+    if line_constant<1.0 and line_constant>0.0:
+        # Now must check that other line hits
+        if (y4 - y3)<0.005 and (y4 - y3)>-0.005:
+            k_line = (x1 - x3 + line_constant * (x2 - x1)) / (x4 - x3)
+        else:
+            k_line = (y1 - y3 + line_constant * (y2 - y1)) / (y4 - y3)
+        if k_line >= 0 and k_line<1:
+            length_ratio = line_constant
+        else:
+            k_line = 1
+
+    return length_ratio, k_line
+
+
+
+def FindEndForPolyline(xvec, yvec, x1, y1, x2, y2):
+    lastx = xvec[0]
+    lasty = yvec[0]
+
+    min_ratio = 1.0
+
+    for i in range(1, len(xvec)):
+        line_ratio, other_ratio = CheckLineIntersection(x1, y1, x2, y2, lastx, lasty, xvec[i], yvec[i])
+        lastx = xvec[i]
+        lasty = yvec[i]
+
+        if line_ratio<min_ratio:
+            min_ratio = line_ratio
+
+    # Do last (implicit) line if last and first doubles are not identical
+    if not (xvec[0] == lastx and yvec[0] == lasty):
+        line_ratio, other_ratio = CheckLineIntersection(x1, y1, x2, y2, lastx, lasty, xvec[0], yvec[0])
+        if line_ratio<min_ratio:
+            min_ratio = line_ratio
+
+    return x1 + (x2 - x1) * min_ratio, y1 + (y2 - y1) * min_ratio
+
+
+
+def PolylineHitTest(xvec, yvec, x1, y1, x2, y2):
+    isAHit = False
+    lastx = xvec[0]
+    lasty = yvec[0]
+
+    min_ratio = 1.0
+
+    for i in range(1, len(xvec)):
+        line_ratio, other_ratio = CheckLineIntersection(x1, y1, x2, y2, lastx, lasty, xvec[i], yvec[i])
+        if line_ratio != 1.0:
+            isAHit = True
+        lastx = xvec[i]
+        lasty = yvec[i]
+
+        if line_ratio<min_ratio:
+            min_ratio = line_ratio
+
+    # Do last (implicit) line if last and first doubles are not identical
+    if not (xvec[0] == lastx and yvec[0] == lasty):
+        line_ratio, other_ratio = CheckLineIntersection(x1, y1, x2, y2, lastx, lasty, xvec[0], yvec[0])
+        if line_ratio != 1.0:
+            isAHit = True
+
+    return isAHit
+
+
+
+def GraphicsStraightenLine(point1, point2):
+    dx = point2[0] - point1[0]
+    dy = point2[1] - point1[1]
+
+    if dx == 0:
+        return
+    elif abs(dy / dx)>1:
+        point2[0] = point1[0]
+    else:
+        point2[1] = point1[0]
+
+
+
+def GetPointOnLine(x1, y1, x2, y2, length):
+    l = sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1))
+    if l<0.01:
+        l = 0.01
+
+    i_bar = (x2 - x1) / l
+    j_bar = (y2 - y1) / l
+
+    return -length * i_bar + x2,-length * j_bar + y2
+
+
+
+def GetArrowPoints(x1, y1, x2, y2, length, width):
+    l = sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1))
+
+    if l<0.01:
+        l = 0.01
+
+    i_bar = (x2 - x1) / l
+    j_bar = (y2 - y1) / l
+    
+    x3=-length * i_bar + x2
+    y3=-length * j_bar + y2
+
+    return x2, y2, width*-j_bar + x3, width * i_bar + y3,-width*-j_bar + x3,-width * i_bar + y3
+
+
+
+def DrawArcToEllipse(x1, y1, width1, height1, x2, y2, x3, y3):
+    a1 = width1 / 2
+    b1 = height1 / 2
+
+    # Check that x2 != x3
+    if abs(x2 - x3)<0.05:
+        x4 = x2
+        if y3>y2:
+            y4 = y1 - sqrt((b1 * b1 - (((x2 - x1) * (x2 - x1)) * (b1 * b1) / (a1 * a1))))
+        else:
+            y4 = y1 + sqrt((b1 * b1 - (((x2 - x1) * (x2 - x1)) * (b1 * b1) / (a1 * a1))))
+        return x4, y4
+
+    # Calculate the x and y coordinates of the point where arc intersects ellipse
+    A = (1 / (a1 * a1))
+    B = ((y3 - y2) * (y3 - y2)) / ((x3 - x2) * (x3 - x2) * b1 * b1)
+    C = (2 * (y3 - y2) * (y2 - y1)) / ((x3 - x2) * b1 * b1)
+    D = ((y2 - y1) * (y2 - y1)) / (b1 * b1)
+    E = (A + B)
+    F = (C - (2 * A * x1) - (2 * B * x2))
+    G = ((A * x1 * x1) + (B * x2 * x2) - (C * x2) + D - 1)
+    H = ((y3 - y2) / (x32 - x2))
+    K = ((F * F) - (4 * E * G))
+
+    if K >= 0:
+        # In this case the line intersects the ellipse, so calculate intersection
+        if x2 >= x1:
+            ellipse1_x = ((F*-1) + sqrt(K)) / (2 * E)
+            ellipse1_y = ((H * (ellipse1_x - x2)) + y2)
+        else:
+            ellipse1_x = (((F*-1) - sqrt(K)) / (2 * E))
+            ellipse1_y = ((H * (ellipse1_x - x2)) + y2)
+    else:
+        # in this case, arc does not intersect ellipse, so just draw arc
+        ellipse1_x = x3
+        ellipse1_y = y3
+
+    return ellipse1_x, ellipse1_y
+
+
+
+def FindEndForCircle(radius, x1, y1, x2, y2):
+    H = sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1))
+
+    if H == 0:
+        return x1, y1
+    else:
+        return radius * (x2 - x1) / H + x1, radius * (y2 - y1) / H + y1