diff -urN sketch.orig/ChangeLog sketch/ChangeLog
--- sketch.orig/ChangeLog	2005-09-04 15:32:06.564771072 +0300
+++ sketch/ChangeLog	2005-09-04 15:32:06.317808616 +0300
@@ -1,3 +1,38 @@
+2005-09-04
+
+	Add an Align tool whith its own handles and keymap.
+
+	* Sketch/Pixmaps/AlignBL.xbm, Sketch/Pixmaps/AlignBR.xbm,
+	Sketch/Pixmaps/AlignB.xbm, Sketch/Pixmaps/AlignC.xbm,
+	Sketch/Pixmaps/AlignLC.xbm, Sketch/Pixmaps/AlignL.xbm,
+	Sketch/Pixmaps/AlignR.xbm, Sketch/Pixmaps/AlignTC.xbm,
+	Sketch/Pixmaps/AlignTL.xbm, Sketch/Pixmaps/AlignTR.xbm,
+	Sketch/Pixmaps/AlignT.xbm: New. Handles for the align operations.
+
+	* Sketch/Editor/const.py: Add the align handle types.
+
+	* Sketch/UI/canvas.py (handle_info): Add the align handles.
+
+	* Sketch/UI/skpixmaps.py (_bitmap_names): Add the align handles.
+
+
+	* Sketch/Editor/builtins.py (align_selected): New. Align the selected
+	objects and also add builtin commands for common align operations.
+
+	* Sketch/Editor/__init__.py: Add new entries for the Align tool in
+	standard_menu, standard_keystrokes, toolmap and create
+	align_keystrokes.
+
+	* Sketch/Editor/aligntool.py: New. Implementation of the Align tool.
+
+	* Sketch/UI/canvas.py (SketchCanvas.lookup_key): Check if the Align
+	tool is active when looking up the command.
+
+	* Sketch/UI/skapp.py: Add the align keymap.
+
+	* Sketch/Editor/context.py: Add a builtin command that activates the
+	Align tool.
+
 2005-08-27  Bernhard Herzog  <bh@intevation.de>
 
 	* Sketch/UI/properties.py (StyleList.__init__): Remove the line
diff -urN sketch.orig/Sketch/Editor/aligntool.py sketch/Sketch/Editor/aligntool.py
--- sketch.orig/Sketch/Editor/aligntool.py	1970-01-01 02:00:00.000000000 +0200
+++ sketch/Sketch/Editor/aligntool.py	2005-09-04 15:32:06.405795240 +0300
@@ -0,0 +1,224 @@
+# Sketch - A Python-based interactive drawing program
+# Copyright (C) 2005 by Bernhard Herzog
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Library General Public
+# License as published by the Free Software Foundation; either
+# version 2 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Library General Public License for more details.
+#
+# You should have received a copy of the GNU Library General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+from selectiontool import SelectionRectangle, selection_type
+from tools import ToolInstance, ToolInfo
+from handle import Handle
+from Sketch.Base.const import SELECTION, EDITED, SelectAdd, SelectSubtract
+from Sketch import Point, Rect
+from builtins import align_selected
+from Sketch.Editor import HandleAlignTL, HandleAlignT, HandleAlignTC, \
+     HandleAlignTR, HandleAlignL, HandleAlignLC, HandleAlignR, HandleAlignBL, \
+     HandleAlignB, HandleAlignBR, HandleAlignC, HandleLine
+
+from Sketch.Editor import AddEditorBuiltin
+
+class AlignRectangle(SelectionRectangle):
+    def GetHandles(self):
+        sx = self.start.x
+        sy = self.start.y
+        ex = self.end.x
+        ey = self.end.y
+        x2 = (sx + ex) / 2
+        y2 = (sy + ey) / 2
+        return [Handle(HandleAlignTL, Point(sx, ey)),
+                Handle(HandleAlignT, Point(x2, ey)),
+                Handle(HandleAlignTC, Point(x2, ey)),
+                Handle(HandleAlignTR, Point(ex, ey)),
+                Handle(HandleAlignL, Point(sx, y2)),
+                Handle(HandleAlignLC, Point(sx, y2)),
+                Handle(HandleAlignR, Point(ex, y2)),
+                Handle(HandleAlignBL, Point(sx, sy)),
+                Handle(HandleAlignB, Point(x2, sy)),
+                Handle(HandleAlignBR, Point(ex, sy)),
+                Handle(HandleAlignC, Point(x2, y2)),
+                ]
+
+class AlignToolInstance(ToolInstance):
+    """A tool that allows to align the selected objects.
+
+    USAGE:
+        Click on the various handles to align the objects.
+        The implicit reference is the coord_rect of the last selected
+        objects.
+        Click on any selected object to make it the active reference.
+        Click on the page border to make the page the active reference.
+
+    KEYBOARD SHORTCUTS:
+        a: activate the align tool
+        t, b, l, r: align to top, bottom, left, right respectively
+        v, h: align centers vertically / horizontally
+        c: align to center
+
+        space: switch to selection tool
+
+        p: set the reference to page.
+        s: set the reference to the whole selection.
+        z: set the reference to the last selected objects.
+    """
+
+    title = ''"Align"
+
+    def __init__(self, editor):
+        ToolInstance.__init__(self, editor)
+        self.ref_rect = None
+        self.rectangle = None
+        self.SetReference('last')
+        self.sides = {HandleAlignTL: ('top', 'left'),
+                      HandleAlignT: ('top',),
+                      HandleAlignTC: ('center_x',),
+                      HandleAlignTR: ('top', 'right'),
+                      HandleAlignL: ('left',),
+                      HandleAlignLC: ('center_y',),
+                      HandleAlignR: ('right',),
+                      HandleAlignBL: ('bottom', 'left'),
+                      HandleAlignB: ('bottom',),
+                      HandleAlignBR: ('bottom', 'right'),
+                      HandleAlignC: ('center_x', 'center_y'),
+                      }
+        self.editor.Subscribe(SELECTION, self.selection_changed)
+        self.editor.Subscribe(EDITED, self.doc_was_edited)
+
+    def SetReference(self, reference):
+        """Sets the reference for the align operation.
+
+        reference should be 'last', 'page' or 'selection'.
+        """
+        if reference not in ('last', 'page', 'selection'):
+            raise ValueError("reference must be 'last', 'page' or 'selection'")
+
+        self.reference = reference
+        editor = self.editor
+        if reference == 'page':
+            self.ref_rect = editor.document.PageRect()
+        elif reference == 'selection':
+            self.ref_rect = editor.Selection().coord_rect
+        elif reference == 'last':
+            self.ref_rect = editor.Selection().last_coord_rect
+        self.rectangle = AlignRectangle(self.ref_rect)
+        self.editor.update_handles()
+
+    def End(self):
+        self.editor.Unsubscribe(SELECTION, self.selection_changed)
+        self.editor.Unsubscribe(EDITED, self.doc_was_edited)
+        ToolInstance.End(self)
+
+    def selection_changed(self):
+        self.SetReference(self.reference)
+
+    def doc_was_edited(self, *args):
+        self.selection_changed()
+
+    def ButtonClick(self, context, p, snapped, button, state, handle = None):
+        ToolInstance.ButtonClick(self, context, p, snapped, button, state,
+                                 handle = handle)
+        type = selection_type(state)
+        test = context.test_device()
+        # Calculate the page rect to detect clicks on the page border.
+        d2w = context.gc.DocToWin
+        page_w, page_h = self.editor.document.PageSize()
+        left, bottom = d2w(0, 0)
+        right, top = d2w(page_w, page_h)
+        win_page_rect = Rect(left, bottom, right, top)
+
+        if handle is not None:
+            # If a handle is clicked align the selected objects
+            sides = self.sides[handle.type]
+            context.editor.BeginTransaction('Align Objects')
+            for side in sides:
+                align_selected(context, side,
+                               ref_rect = self.rectangle.coord_rect)
+            context.editor.EndTransaction()
+        elif self.editor.SelectionHit(p, test) and type != SelectSubtract:
+            # Make the clicked object be the last selected by adding it to
+            # the selection (even if shift is not pressed)
+            self.editor.SelectPoint(p, test, SelectAdd)
+        elif win_page_rect.grown(10).contains_point(d2w(p)) and \
+                    not win_page_rect.grown(-10).contains_point(d2w(p)):
+            # A click near the page border sets the reference to the
+            # whole page rect.
+            self.SetReference('page')
+        else:
+            rect = test.HitRectAroundPoint(p)
+            hit_obj = self.editor.selection_from_point(p, rect, test, ())
+            if hit_obj:
+                self.editor.SelectPoint(p, test, type)
+            else:
+                # A click outside the selection switches to Selection tool
+                self.canvas.application.SetTool('SelectionTool')
+
+    def ButtonPress(self, context, p, snapped, button, state, handle = None):
+        ToolInstance.ButtonPress(self, context, p, snapped, button, state,
+                                 handle = handle)
+        test = context.test_device()
+        # check, whether a guide line is hit:
+        pick = self.editor.PickActiveObject(test, p)
+        if pick is not None and pick.is_GuideLine:
+            # edit guide line
+            object = Sketch.Editor.GuideEditor(pick)
+        else:
+            # Select by rubberbanding
+            object = SelectionRectangle(p)
+        self.begin_edit_object(context, object, snapped, button, state)
+
+    def ButtonRelease(self, context, p, snapped, button, state):
+        ToolInstance.ButtonRelease(self, context, p, snapped, button, state)
+        object = self.end_edit_object(context, snapped, button, state)
+        self.editor.SelectRect(object.bounding_rect, selection_type(state))
+
+    def Handles(self):
+        if self.editor.HasSelection():
+            # Get the Align handles
+            handles = self.rectangle.GetHandles()
+            # Get the small indicator handles
+            handles.append(self.editor.Selection().GetHandles()[0])
+            # I found that it helps to see the coor_rect of the selection
+            # when there are more objects selected
+            if self.editor.CountSelected() > 1:
+                sx, sy, ex, ey = self.editor.Selection().coord_rect
+                handles += [Handle(HandleLine, Point(sx, sy), Point(sx, ey)),
+                            Handle(HandleLine, Point(sx, ey), Point(ex, ey)),
+                            Handle(HandleLine, Point(ex, ey), Point(ex, sy)),
+                            Handle(HandleLine, Point(ex, sy), Point(sx, sy)),
+                            ]
+        else:
+            handles = []
+        return handles
+
+
+AlignTool = ToolInfo("AlignTool", AlignToolInstance, active_cursor = 1)
+
+def align_tool_has_selection(context):
+    editor = context.editor
+    return isinstance(editor.tool, AlignToolInstance) and editor.HasSelection()
+
+def set_align_reference(context, reference):
+    if align_tool_has_selection(context):
+        context.editor.tool.SetReference(reference)
+
+AddEditorBuiltin('set_align_reference_page', ''"Set align reference to page",
+                 set_align_reference, args = ('page',),
+                 sensitive = align_tool_has_selection)
+
+AddEditorBuiltin('set_align_reference_last', ''"Set align reference to last selected",
+                 set_align_reference, args = ('last',),
+                 sensitive = align_tool_has_selection)
+
+AddEditorBuiltin('set_align_reference_selection', ''"Set align reference to whole selection",
+                 set_align_reference, args = ('selection',),
+                 sensitive = align_tool_has_selection)
+
diff -urN sketch.orig/Sketch/Editor/builtins.py sketch/Sketch/Editor/builtins.py
--- sketch.orig/Sketch/Editor/builtins.py	2005-09-04 15:32:06.646758608 +0300
+++ sketch/Sketch/Editor/builtins.py	2005-09-04 15:32:06.396796608 +0300
@@ -86,3 +86,39 @@
 AddEditorBuiltin('flip_vertical', ''"Flip Vertical", flip_selected,
                  args = (0, 1), channels = (SELECTION,),
                  sensitive = lambda context: context.editor.HasSelection())
+
+def align_selected(context, side, ref_rect = None):
+    selection = context.editor.Selection()
+    objects = selection.GetObjects()
+    AddUndo = context.document.AddUndo
+    if selection:
+        if ref_rect is None:
+            ref_rect = selection.last_coord_rect
+        for obj in objects:
+            r = obj.coord_rect
+            xoff = yoff = 0
+            if side == 'left':
+                xoff = ref_rect.left - r.left
+            elif side == 'center_x':
+                xoff = ref_rect.center().x - r.center().x
+            elif side == 'right':
+                xoff = ref_rect.right - r.right
+            elif side == 'top':
+                yoff = ref_rect.top - r.top
+            elif side == 'center_y':
+                yoff = ref_rect.center().y - r.center().y
+            elif side == 'bottom':
+                yoff = ref_rect.bottom - r.bottom
+            elif side == 'center':
+                xoff = ref_rect.center().x - r.center().x
+                yoff = ref_rect.center().y - r.center().y
+
+            if xoff or yoff:
+                AddUndo(obj.Translate(Point(xoff, yoff)))
+
+sides = ('left', 'top', 'right', 'bottom', 'center_x', 'center_y', 'center')
+for side in sides:
+    AddEditorBuiltin("align_%s" % side, ''"Align Objects",
+                     align_selected,
+                     args = (side,), channels = (SELECTION,),
+                     sensitive = lambda context: context.editor.HasSelection())
diff -urN sketch.orig/Sketch/Editor/const.py sketch/Sketch/Editor/const.py
--- sketch.orig/Sketch/Editor/const.py	2005-09-04 15:32:06.647758456 +0300
+++ sketch/Sketch/Editor/const.py	2005-09-04 15:32:06.396796608 +0300
@@ -63,8 +63,17 @@
 
 HandleIndicatorList = 'IndicatorList'
 
-
-
+HandleAlignTL = 'AlignTL'
+HandleAlignTC = 'AlignTC'
+HandleAlignT = 'AlignT'
+HandleAlignTR = 'AlignTR'
+HandleAlignL = 'AlignL'
+HandleAlignLC = 'AlignLC'
+HandleAlignR = 'AlignR'
+HandleAlignBL = 'AlignBL'
+HandleAlignB = 'AlignB'
+HandleAlignBR = 'AlignBR'
+HandleAlignC = 'AlignC'
 
 #
 # Modifier Keys
diff -urN sketch.orig/Sketch/Editor/context.py sketch/Sketch/Editor/context.py
--- sketch.orig/Sketch/Editor/context.py	2005-09-04 15:32:06.647758456 +0300
+++ sketch/Sketch/Editor/context.py	2005-09-04 15:32:06.397796456 +0300
@@ -114,6 +114,7 @@
 AddBuiltin(switch_tool, ''"Switch Tool")
 
 AddBuiltin('edit_tool', ''"Edit Tool", set_tool, args = 'EditTool')
+AddBuiltin('align_tool', ''"Align Tool", set_tool, args = 'AlignTool')
 AddBuiltin('selection_tool', ''"Selection Tool", set_tool,
            args = 'SelectionTool')
 AddBuiltin('zoom_tool', ''"Zoom Tool", set_tool, args = 'ZoomTool')
diff -urN sketch.orig/Sketch/Editor/__init__.py sketch/Sketch/Editor/__init__.py
--- sketch.orig/Sketch/Editor/__init__.py	2005-09-04 15:32:06.646758608 +0300
+++ sketch/Sketch/Editor/__init__.py	2005-09-04 15:32:06.396796608 +0300
@@ -70,7 +70,7 @@
 from texttool import SimpleTextCreator, CommonTextEditor, SimpleTextEditor, \
      InternalPathTextEditor, PathTextCreator
 
-import tools, selectiontool, edittool, texttool
+import tools, selectiontool, edittool, texttool, aligntool
 import builtins
 
 from smoothcurve import smoothen_freehand_curve
@@ -202,6 +202,7 @@
 
     (''"Tools", ['selection_tool',
                  'edit_tool',
+                 'align_tool',
                  'zoom_tool',
                  'ellipse_tool',
                  'rectangle_tool',
@@ -232,6 +233,7 @@
 
     ('selection_tool', 's'),
     ('edit_tool', 'e'),
+    ('align_tool', 'a'),
     ('rectangle_tool', 'r'),
     ('ellipse_tool', 'c'),      # c = circle
     ('curve_tool', 'b'),        # b = bezier
@@ -250,7 +252,7 @@
     ('view_fit_selection_to_window', 'C-F4'),
     ('view_fit_page_to_window', 'F4'),
 
-    ('cont_angle', 'a'),
+    #('cont_angle', 'a'),
     ('cont_smooth', 's'),
     ('cont_symmetrical', 'y'),
     ('segments_to_lines', 'l'),
@@ -273,10 +275,24 @@
     ('delete_char_backward', 'BackSpace'),
     ]
 
+align_keystrokes = [
+    ('align_left', 'l'),
+    ('align_top', 't'),
+    ('align_right', 'r'),
+    ('align_bottom', 'b'),
+    ('align_center_x', 'v'),
+    ('align_center_y', 'h'),
+    ('align_center', 'c'),
+    ('set_align_reference_page', 'p'),
+    ('set_align_reference_last', 'z'),
+    ('set_align_reference_selection', 's'),
+    ]
+
 # text command not listed: insert_char
 
 toolmap = {'SelectionTool': selectiontool.SelectionTool,
            'EditTool': edittool.EditTool,
+           'AlignTool': aligntool.AlignTool,
            'TextTool': texttool.TextTool,
            'ZoomTool': tools.ZoomTool,
            'CreateRect': tools.RectangleTool,
diff -urN sketch.orig/Sketch/Pixmaps/AlignBL.xbm sketch/Sketch/Pixmaps/AlignBL.xbm
--- sketch.orig/Sketch/Pixmaps/AlignBL.xbm	1970-01-01 02:00:00.000000000 +0200
+++ sketch/Sketch/Pixmaps/AlignBL.xbm	2005-09-04 15:32:06.504780192 +0300
@@ -0,0 +1,7 @@
+#define im_width 16
+#define im_height 16
+static char im_bits[] = {
+0x03,0x00,0x03,0x00,0x03,0x00,0x03,0x00,0x03,0x00,0xfb,0x01,0x0b,0x01,0xfb,
+0x3f,0x0b,0x21,0x0b,0x21,0x0b,0x21,0x0b,0x21,0xfb,0x3f,0x03,0x00,0xff,0xff,
+0xff,0xff
+};
diff -urN sketch.orig/Sketch/Pixmaps/AlignBR.xbm sketch/Sketch/Pixmaps/AlignBR.xbm
--- sketch.orig/Sketch/Pixmaps/AlignBR.xbm	1970-01-01 02:00:00.000000000 +0200
+++ sketch/Sketch/Pixmaps/AlignBR.xbm	2005-09-04 15:32:06.504780192 +0300
@@ -0,0 +1,7 @@
+#define im_width 16
+#define im_height 16
+static char im_bits[] = {
+0x00,0xc0,0x00,0xc0,0x00,0xc0,0x00,0xc0,0x00,0xc0,0x80,0xdf,0x80,0xd0,0xfc,
+0xdf,0x84,0xd0,0x84,0xd0,0x84,0xd0,0x84,0xd0,0xfc,0xdf,0x00,0xc0,0xff,0xff,
+0xff,0xff
+};
diff -urN sketch.orig/Sketch/Pixmaps/AlignB.xbm sketch/Sketch/Pixmaps/AlignB.xbm
--- sketch.orig/Sketch/Pixmaps/AlignB.xbm	1970-01-01 02:00:00.000000000 +0200
+++ sketch/Sketch/Pixmaps/AlignB.xbm	2005-09-04 15:32:06.505780040 +0300
@@ -0,0 +1,7 @@
+#define im_width 16
+#define im_height 16
+static char im_bits[] = {
+0x00,0x00,0x00,0x00,0x7e,0x00,0x42,0x00,0x42,0x00,0x42,0x00,0x42,0x00,0x42,
+0x7e,0x42,0x42,0x42,0x42,0x42,0x42,0x42,0x42,0x7e,0x7e,0x00,0x00,0xff,0xff,
+0xff,0xff
+};
diff -urN sketch.orig/Sketch/Pixmaps/AlignC.xbm sketch/Sketch/Pixmaps/AlignC.xbm
--- sketch.orig/Sketch/Pixmaps/AlignC.xbm	1970-01-01 02:00:00.000000000 +0200
+++ sketch/Sketch/Pixmaps/AlignC.xbm	2005-09-04 15:32:06.505780040 +0300
@@ -0,0 +1,7 @@
+#define im_width 16
+#define im_height 16
+static char im_bits[] = {
+0x80,0x01,0x80,0x01,0x80,0x01,0xf8,0x1f,0x88,0x11,0x88,0x11,0x88,0x11,0xff,
+0xff,0xff,0xff,0x88,0x11,0x88,0x11,0x88,0x11,0xf8,0x1f,0x80,0x01,0x80,0x01,
+0x80,0x01
+};
diff -urN sketch.orig/Sketch/Pixmaps/AlignLC.xbm sketch/Sketch/Pixmaps/AlignLC.xbm
--- sketch.orig/Sketch/Pixmaps/AlignLC.xbm	1970-01-01 02:00:00.000000000 +0200
+++ sketch/Sketch/Pixmaps/AlignLC.xbm	2005-09-04 15:32:06.505780040 +0300
@@ -0,0 +1,7 @@
+#define im_width 16
+#define im_height 16
+static char im_bits[] = {
+0x00,0x00,0x00,0x00,0x00,0x3e,0x00,0x22,0x00,0x22,0xfc,0x22,0x84,0x22,0xff,
+0xff,0xff,0xff,0x84,0x22,0xfc,0x22,0x00,0x22,0x00,0x22,0x00,0x3e,0x00,0x00,
+0x00,0x00
+};
diff -urN sketch.orig/Sketch/Pixmaps/AlignL.xbm sketch/Sketch/Pixmaps/AlignL.xbm
--- sketch.orig/Sketch/Pixmaps/AlignL.xbm	1970-01-01 02:00:00.000000000 +0200
+++ sketch/Sketch/Pixmaps/AlignL.xbm	2005-09-04 15:32:06.505780040 +0300
@@ -0,0 +1,7 @@
+#define im_width 16
+#define im_height 16
+static char im_bits[] = {
+0x03,0x00,0xfb,0x01,0x0b,0x01,0x0b,0x01,0x0b,0x01,0x0b,0x01,0xfb,0x01,0x03,
+0x00,0x03,0x00,0xfb,0x3f,0x0b,0x20,0x0b,0x20,0x0b,0x20,0x0b,0x20,0xfb,0x3f,
+0x03,0x00
+};
diff -urN sketch.orig/Sketch/Pixmaps/AlignR.xbm sketch/Sketch/Pixmaps/AlignR.xbm
--- sketch.orig/Sketch/Pixmaps/AlignR.xbm	1970-01-01 02:00:00.000000000 +0200
+++ sketch/Sketch/Pixmaps/AlignR.xbm	2005-09-04 15:32:06.506779888 +0300
@@ -0,0 +1,7 @@
+#define im_width 16
+#define im_height 16
+static char im_bits[] = {
+0x00,0xc0,0x80,0xdf,0x80,0xd0,0x80,0xd0,0x80,0xd0,0x80,0xd0,0x80,0xdf,0x00,
+0xc0,0x00,0xc0,0xfc,0xdf,0x04,0xd0,0x04,0xd0,0x04,0xd0,0x04,0xd0,0xfc,0xdf,
+0x00,0xc0
+};
diff -urN sketch.orig/Sketch/Pixmaps/AlignTC.xbm sketch/Sketch/Pixmaps/AlignTC.xbm
--- sketch.orig/Sketch/Pixmaps/AlignTC.xbm	1970-01-01 02:00:00.000000000 +0200
+++ sketch/Sketch/Pixmaps/AlignTC.xbm	2005-09-04 15:32:06.506779888 +0300
@@ -0,0 +1,7 @@
+#define im_width 16
+#define im_height 16
+static char im_bits[] = {
+0x80,0x01,0x80,0x01,0xe0,0x07,0xa0,0x05,0xa0,0x05,0xa0,0x05,0xa0,0x05,0xe0,
+0x07,0x80,0x01,0xfc,0x3f,0x84,0x21,0x84,0x21,0x84,0x21,0xfc,0x3f,0x80,0x01,
+0x80,0x01
+};
diff -urN sketch.orig/Sketch/Pixmaps/AlignTL.xbm sketch/Sketch/Pixmaps/AlignTL.xbm
--- sketch.orig/Sketch/Pixmaps/AlignTL.xbm	1970-01-01 02:00:00.000000000 +0200
+++ sketch/Sketch/Pixmaps/AlignTL.xbm	2005-09-04 15:32:06.506779888 +0300
@@ -0,0 +1,7 @@
+#define im_width 16
+#define im_height 16
+static char im_bits[] = {
+0xff,0xff,0xff,0xff,0x03,0x00,0xfb,0x3f,0x0b,0x21,0x0b,0x21,0x0b,0x21,0x0b,
+0x21,0xfb,0x3f,0x0b,0x01,0xfb,0x01,0x03,0x00,0x03,0x00,0x03,0x00,0x03,0x00,
+0x03,0x00
+};
diff -urN sketch.orig/Sketch/Pixmaps/AlignTR.xbm sketch/Sketch/Pixmaps/AlignTR.xbm
--- sketch.orig/Sketch/Pixmaps/AlignTR.xbm	1970-01-01 02:00:00.000000000 +0200
+++ sketch/Sketch/Pixmaps/AlignTR.xbm	2005-09-04 15:32:06.507779736 +0300
@@ -0,0 +1,7 @@
+#define im_width 16
+#define im_height 16
+static char im_bits[] = {
+0xff,0xff,0xff,0xff,0x00,0xc0,0xfc,0xdf,0x84,0xd0,0x84,0xd0,0x84,0xd0,0x84,
+0xd0,0xfc,0xdf,0x80,0xd0,0x80,0xdf,0x00,0xc0,0x00,0xc0,0x00,0xc0,0x00,0xc0,
+0x00,0xc0
+};
diff -urN sketch.orig/Sketch/Pixmaps/AlignT.xbm sketch/Sketch/Pixmaps/AlignT.xbm
--- sketch.orig/Sketch/Pixmaps/AlignT.xbm	1970-01-01 02:00:00.000000000 +0200
+++ sketch/Sketch/Pixmaps/AlignT.xbm	2005-09-04 15:32:06.507779736 +0300
@@ -0,0 +1,7 @@
+#define im_width 16
+#define im_height 16
+static char im_bits[] = {
+0xff,0xff,0xff,0xff,0x00,0x00,0x7e,0x7e,0x42,0x42,0x42,0x42,0x42,0x42,0x42,
+0x42,0x42,0x7e,0x42,0x00,0x42,0x00,0x42,0x00,0x42,0x00,0x7e,0x00,0x00,0x00,
+0x00,0x00
+};
diff -urN sketch.orig/Sketch/UI/canvas.py sketch/Sketch/UI/canvas.py
--- sketch.orig/Sketch/UI/canvas.py	2005-09-04 15:32:06.720747360 +0300
+++ sketch/Sketch/UI/canvas.py	2005-09-04 15:32:06.515778520 +0300
@@ -89,6 +89,18 @@
     e.HandleSizeBL: (Handle_FilledRect, (-1, -1), 'CurSizeBL', ''),
     e.HandleSizeB: (Handle_FilledRect, (0, -1), 'CurSizeB', ''),
     e.HandleSizeBR: (Handle_FilledRect, (+1, -1), 'CurSizeBR', ''),
+
+    e.HandleAlignTL: (Handle_Pixmap, (-2, +2), 'CurSizeTL', 'AlignTL'),
+    e.HandleAlignTC: (Handle_Pixmap, (0, +5), 'CurHGuide', 'AlignTC'),
+    e.HandleAlignT: (Handle_Pixmap, (0, 2), 'CurSizeT', 'AlignT'),
+    e.HandleAlignTR: (Handle_Pixmap, (+2, +2), 'CurSizeTR', 'AlignTR'),
+    e.HandleAlignL: (Handle_Pixmap, (-2, 0), 'CurSizeL', 'AlignL'),
+    e.HandleAlignLC: (Handle_Pixmap, (-5, 0), 'CurVGuide', 'AlignLC'),
+    e.HandleAlignR: (Handle_Pixmap, (2, 0), 'CurSizeR', 'AlignR'),
+    e.HandleAlignBL: (Handle_Pixmap, (-2, -2), 'CurSizeBL', 'AlignBL'),
+    e.HandleAlignB: (Handle_Pixmap, (0, -2), 'CurSizeB', 'AlignB'),
+    e.HandleAlignBR: (Handle_Pixmap, (+2, -2), 'CurSizeBR', 'AlignBR'),
+    e.HandleAlignC: (Handle_Pixmap, (0, 0), 'CurHandle', 'AlignC'),
     }
 
 del e
@@ -309,6 +321,10 @@
                 return command
             if self.is_normal_char(keyval, state, str):
                 return 'insert_char'
+        elif tool == 'AlignTool':
+            command = skapp.align_keymap.Command((keyval, state))
+            if command is not None:
+                return command
         return skapp.keymap.Command((keyval, state))
 
     def is_normal_char(self, keyval, state, str):
diff -urN sketch.orig/Sketch/UI/skapp.py sketch/Sketch/UI/skapp.py
--- sketch.orig/Sketch/UI/skapp.py	2005-09-04 15:32:06.729745992 +0300
+++ sketch/Sketch/UI/skapp.py	2005-09-04 15:32:06.523777304 +0300
@@ -37,6 +37,8 @@
                               special_keys = gtk.keysyms.__dict__)
 text_keymap = Sketch.Editor.Keymap(Sketch.Editor.text_keystrokes,
                                    special_keys = gtk.keysyms.__dict__)
+align_keymap = Sketch.Editor.Keymap(Sketch.Editor.align_keystrokes,
+                                   special_keys = gtk.keysyms.__dict__)
 # XXX the keymaps should be maintained by the Editor sub-package
 
 from mainwindow import SketchMainWindow
diff -urN sketch.orig/Sketch/UI/skpixmaps.py sketch/Sketch/UI/skpixmaps.py
--- sketch.orig/Sketch/UI/skpixmaps.py	2005-09-04 15:32:06.729745992 +0300
+++ sketch/Sketch/UI/skpixmaps.py	2005-09-04 15:32:06.523777304 +0300
@@ -43,7 +43,10 @@
     return bitmap, x_hot, y_hot
 
 _bitmap_names = ['TurnTL', 'TurnTR', 'TurnBL', 'TurnBR', 'Center',
-                 'ShearLR', 'ShearUD']
+                 'ShearLR', 'ShearUD',
+                 'AlignTL', 'AlignTC', 'AlignT', 'AlignTR', 'AlignL',
+                 'AlignLC', 'AlignR', 'AlignBL', 'AlignB', 'AlignBR',
+                 'AlignC']
 
 class SketchBitmaps:
 
diff -urN sketch.orig/test/ChangeLog sketch/test/ChangeLog
--- sketch.orig/test/ChangeLog	2005-09-04 15:32:06.739744472 +0300
+++ sketch/test/ChangeLog	2005-09-04 15:32:06.542774416 +0300
@@ -1,3 +1,7 @@
+2005-09-04
+
+	* test_aligntool.py: New. Tests for the Align tool.
+
 2005-08-14  Bernhard Herzog  <bh@intevation.de>
 
 	Added tests for a selection that also remembers the last selected
diff -urN sketch.orig/test/test_aligntool.py sketch/test/test_aligntool.py
--- sketch.orig/test/test_aligntool.py	1970-01-01 02:00:00.000000000 +0200
+++ sketch/test/test_aligntool.py	2005-09-04 15:32:06.554772592 +0300
@@ -0,0 +1,444 @@
+# Skencil - A Python-based interactive drawing program
+# Copyright (C) 2005 by Bernhard Herzog
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Library General Public
+# License as published by the Free Software Foundation; either
+# version 2 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Library General Public License for more details.
+#
+# You should have received a copy of the GNU Library General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+"""
+Tests for the Align tool.
+
+Test mouse interaction:
+    Test selecting and deselecting with mouse clicks.
+    Test clicking outside the selection but not on an object.
+    Test selecting and deselecting with press-drag-release cycles.
+    Test press-drag-release on an already selected object.
+    Test clicking on the page border.
+    Test clicking on handles.
+
+Test commands:
+    Test setting the reference rectangle.
+    Test the align left command.
+
+
+There is also a setup for testing the changing of tool, because there are
+circumstances in which the Selection tool is activated automatically.
+Currently this happens when clicking outside the selection.
+
+Changing the tool is accomplished by calling SketchApplication.SetTool.
+For this call to eventually cause a tool change in the editor we need a
+main window. This window has a canvas and an editor is bound to the canvas.
+In short, the mechanism is as follows:
+
+    1. The application calls the SetTool method of its context attribute.
+    2. The context sets its tool attribute to the name provided by the
+       call and issues a TOOL message.
+    3. All windows containing canvases receive the TOOL message and as a
+       result call the SetTool method of their canvas.
+    4. The canvas (a SketchCanvas) calls the SetTool method of its
+       associated editor.
+
+To simulate this chain of events we need:
+   * A MockApplication with a SetTool method and a context attribute.
+   * An AppContext. For our needs a simple Context is sufficient.
+   * A MockMainWindow which has a canvas attribute and is subscribed to the
+     TOOL channel.
+   * An MinimalCanvas, with an editor attribute and a SetTool method.
+"""
+
+import unittest
+
+import support
+support.add_sketch_dir_to_path()
+
+import Sketch
+from Sketch import Trafo, Identity, Point, Rectangle, UnionRects, Rect
+from Sketch.Editor import EditorWithSelection, InteractiveDocument, Context, \
+     Button1Mask, ShiftMask, ControlMask, HandleAlignL, Registry
+from Sketch.Editor.aligntool import AlignToolInstance
+from Sketch.Editor.selectiontool import SelectionToolInstance
+from Sketch.Base.const import SelectSet, SelectAdd, TOOL, SHAPE
+
+from test_doceditor import MockCanvas, MockGC, RecordingMockGC, \
+     EditorTransactionTest
+
+#
+# Setup for testing tool changing
+#
+class MockApplication:
+    context = None
+    def SetTool(self, tool):
+        self.context.SetTool(tool)
+
+class MockMainWindow:
+    def __init__(self, application, editor):
+        self.canvas = MinimalCanvas(application, editor)
+        application.context.Subscribe(TOOL, self.tool_changed)
+
+    def tool_changed(self, toolname):
+        self.canvas.SetTool(toolname)
+
+class MinimalCanvas(MockCanvas):
+
+    def __init__(self, application, editor):
+        self.application = application
+        self.editor = editor
+        self.document = editor.Document()
+        # To detect a click on the page border the Align tool needs to know
+        # the dimensions of the PageRect in window coordinates.
+        self.gc = MockGC()
+        self.gc.DocToWin = Identity
+
+    def SetTool(self, toolname):
+        self.editor.SetTool(toolname)
+
+    def PageRect(self):
+        # This provides a page border to TestAlignTool.test_click_page_border
+        return Rect(0, 0, 1000, 1000)
+
+
+#
+# Align tool tests
+#
+class AlignTest(unittest.TestCase):
+
+    """Base class for the AlignTool tests.
+    """
+
+    def setUp(self):
+        self.document = InteractiveDocument(create_layer = 1)
+        self.layer = self.document.AppendLayer("test")
+        self.editor = EditorWithSelection(self.document)
+
+    def set_objects(self):
+        # Insert some objects into the document
+        self.r1 = Rectangle(Trafo(100, 0, 0, 100, 10, 10))
+        self.r2 = Rectangle(Trafo(100, 0, 0, 100, 120, 240))
+        self.r3 = Rectangle(Trafo(100, 0, 0, 100, 240, 120))
+        self.r4 = Rectangle(Trafo(100, 0, 0, 100, 300, 300))
+
+        self.document.Insert(self.r1, self.layer)
+        self.document.Insert(self.r2, self.layer)
+        self.document.Insert(self.r3, self.layer)
+        self.document.Insert(self.r4, self.layer)
+
+        # Select three rectangles, leave one (r4) unselected
+        self.editor.SelectObject([self.r1, self.r2, self.r3])
+
+    def set_application(self):
+        # Set an application context
+        self.application = MockApplication()
+        # Normally an AppContext should be used here, but we can't
+        # import AppContext without introducing a dependency on gtk which
+        # would make the test unable to run without a working X connection.
+        self.appcontext = Context(self.application)
+        self.appcontext.set_editor(self.editor)
+        self.application.context = self.appcontext
+        self.main_window = MockMainWindow(self.application, self.editor)
+
+        # The canvas to use with clicks
+        self.canvas = self.main_window.canvas
+
+        #self.canvas.inverting_device = RecordingMockGC('DrawPixmapHandle',
+        #                                               'DrawRectangle',
+        #                                               'DrawHandle')
+        self.application.SetTool('AlignTool')
+
+    def tearDown(self):
+        if self.editor is not None:
+            self.editor.Destroy()
+        self.editor = self.document = self.layer = self.canvas = None
+        self.appcontext = self.application = self.main_window = None
+
+class TestAlignToolInteraction(AlignTest):
+
+    """Tests for the Align tool interaction: mouse clicks, commands.
+    """
+
+    def setUp(self):
+        AlignTest.setUp(self)
+        AlignTest.set_objects(self)
+        AlignTest.set_application(self)
+
+    def test_setup(self):
+        """Test Align tool setup.
+        """
+        editor = self.editor
+        self.assertEquals(editor.SelectedObjects(), [self.r1, self.r2,
+                                                     self.r3])
+        self.assertEquals(editor.LastSelectedObjects(), [self.r1, self.r2,
+                                                     self.r3])
+        self.assertEquals(self.layer.GetObjects(), [self.r1, self.r2,
+                                                     self.r3, self.r4])
+        self.failUnless(isinstance(editor.Tool(), AlignToolInstance))
+
+    #
+    # Test mouse interaction - selecting and deselecting with clicks
+    #
+
+    def test_click_selected_simple(self):
+        """Test Align tool mouse interaction: click on a selected object.
+
+        As a result, the clicked object should become the last selected one.
+        """
+        editor = self.editor
+        point_on_r2 = Point(150, 240)
+
+        self.editor.ButtonClick(self.canvas, point_on_r2, 1, 0)
+        self.assertEquals(editor.LastSelectedObjects(), [self.r2])
+        self.assertEquals(editor.tool.rectangle.coord_rect, self.r2.coord_rect)
+
+    def test_click_selected_add(self):
+        """Test Align tool mouse interaction: shift-click a selected object.
+
+        As a result, the clicked object should become the last selected one.
+        """
+        editor = self.editor
+        point_on_r2 = Point(150, 240)
+
+        self.editor.ButtonClick(self.canvas, point_on_r2, 1, ShiftMask)
+        self.assertEquals(editor.LastSelectedObjects(), [self.r2])
+        self.assertEquals(editor.tool.rectangle.coord_rect, self.r2.coord_rect)
+
+    def test_click_selected_remove(self):
+        """Test Align tool mouse interaction: control-click a selected object.
+
+        As a result, the clicked object should be deselected.
+        """
+        editor = self.editor
+        point_on_r2 = Point(150, 240)
+
+        self.editor.ButtonClick(self.canvas, point_on_r2, 1, ControlMask)
+        self.assertEquals(editor.LastSelectedObjects(), [self.r1, self.r3])
+        self.assertEquals(editor.tool.rectangle.coord_rect,
+                          UnionRects(self.r1.coord_rect, self.r3.coord_rect))
+
+    def test_click_unselected_simple(self):
+        """Test Align tool mouse interaction: click an unselected object.
+
+        As a result, the selection should contain only the clicked object.
+        """
+        editor = self.editor
+        point_on_r4 = Point(350, 300)
+
+        self.editor.ButtonClick(self.canvas, point_on_r4, 1, 0)
+        self.assertEquals(editor.LastSelectedObjects(), [self.r4])
+        self.assertEquals(editor.SelectedObjects(), [self.r4])
+        self.assertEquals(editor.tool.rectangle.coord_rect, self.r4.coord_rect)
+
+    def test_click_unselected_add(self):
+        """Test Align tool mouse interaction: shift-click an unselected object.
+
+        As a result, the object should be added to the selection.
+        """
+        editor = self.editor
+        point_on_r4 = Point(350, 300)
+
+        self.editor.ButtonClick(self.canvas, point_on_r4, 1, ShiftMask)
+        self.assertEquals(editor.LastSelectedObjects(), [self.r4])
+        self.assertEquals(editor.SelectedObjects(), [self.r1, self.r2,
+                                                     self.r3, self.r4])
+        self.assertEquals(editor.tool.rectangle.coord_rect, self.r4.coord_rect)
+
+    def test_click_unselected_remove(self):
+        """Test Align tool mouse interaction: ctrl-click an unselected object.
+
+        As a result, nothing should change.
+        """
+        editor = self.editor
+        point_on_r4 = Point(350, 300)
+
+        self.editor.ButtonClick(self.canvas, point_on_r4, 1, ControlMask)
+
+        self.assertEquals(editor.SelectedObjects(), [self.r1, self.r2,
+                                                     self.r3])
+        self.assertEquals(editor.LastSelectedObjects(), [self.r1, self.r2,
+                                                     self.r3])
+
+    def test_click_outside_selection(self):
+        """Test Align tool mouse interaction: click outside the selection.
+
+        As a result, the selection is unchanged and the Selection tool is
+        activated.
+        """
+        editor = self.editor
+        # Click somewhere not on an object.
+        self.editor.ButtonClick(self.canvas, Point(500,500), 1, 0)
+
+        self.assertEquals(editor.SelectedObjects(), [self.r1, self.r2,
+                                                     self.r3])
+        self.assertEquals(editor.LastSelectedObjects(), [self.r1, self.r2,
+                                                     self.r3])
+        self.failUnless(isinstance(editor.Tool(), SelectionToolInstance))
+
+    #
+    # Test mouse interaction - selecting and deselecting with
+    # press-drag-release cycles.
+    #
+
+    def test_drag_select(self):
+        """Test Align tool mouse interaction: select with press-drag-release.
+        """
+        editor = self.editor
+        # Select r1 and r2 with rubber banding.
+        editor.ButtonPress(self.canvas, Point(-10, -10), 1, 0)
+        editor.PointerMotion(self.canvas, Point(220, 340), Button1Mask)
+        editor.ButtonRelease(self.canvas, Point(250, 370), 1, Button1Mask)
+
+        self.assertEquals(editor.SelectedObjects(), [self.r1, self.r2])
+        self.assertEquals(editor.LastSelectedObjects(), [self.r1, self.r2])
+
+    def test_drag_reselect(self):
+        """Test Align tool mouse interaction: reselect with press-drag-release.
+        """
+        editor = self.editor
+        # Reselect r1 and r2 with rubber banding.
+        editor.ButtonPress(self.canvas, Point(-10, -10), 1, ShiftMask)
+        editor.PointerMotion(self.canvas, Point(220, 340), ShiftMask)
+        editor.ButtonRelease(self.canvas, Point(250, 370), 1, ShiftMask)
+
+        self.assertEquals(editor.SelectedObjects(), [self.r1, self.r2,
+                                                     self.r3])
+        # r1 and r2 should be the last selected objects
+        self.assertEquals(editor.LastSelectedObjects(), [self.r1, self.r2])
+
+    def test_drag_deselect(self):
+        """Test Align tool mouse interaction: deselect with press-drag-release.
+        """
+        editor = self.editor
+        # Deselect r1 and r2 with rubber banding.
+        editor.ButtonPress(self.canvas, Point(-10, -10), 1, ControlMask)
+        editor.PointerMotion(self.canvas, Point(220, 340), ControlMask)
+        editor.ButtonRelease(self.canvas, Point(250, 370), 1, ControlMask)
+
+        # Now the selection should contain only r3
+        self.assertEquals(editor.SelectedObjects(), [self.r3])
+        self.assertEquals(editor.LastSelectedObjects(), [self.r3])
+
+    def test_click_page_border(self):
+        """Test Align tool mouse interaction: click on page border.
+
+        This page rectangle should become the reference.
+        """
+        editor = self.editor
+        editor.ButtonClick(self.canvas, Point(0, 0), 1, 0)
+
+        self.assertEquals(editor.tool.rectangle.coord_rect,
+                          self.document.PageRect())
+
+    #
+    # Test mouse interaction - clicking on handles
+    #
+
+    def click_handle(self):
+        """Test Align tool mouse interaction: click on a handle.
+
+        The handle is the "align left" one, so the selected objects should be
+        aligned to the left of the selection.
+        """
+        editor = self.editor
+        left = editor.Selection().coord_rect.left
+        # Get the "align left" handle
+        handle = [h for h in editor.Handles() if h.type == HandleAlignL][0]
+
+        self.editor.ButtonClick(self.canvas, handle.p, 1, 0, handle = handle)
+        self.assertEquals(editor.SelectedObjects(), [self.r1, self.r2,
+                                                     self.r3])
+        self.assertEquals(editor.LastSelectedObjects(), [self.r1, self.r2,
+                                                     self.r3])
+        # The selected objects should be aligned to the left
+        self.assertEquals(self.r1.coord_rect.left, left)
+        self.assertEquals(self.r2.coord_rect.left, left)
+        self.assertEquals(self.r3.coord_rect.left, left)
+
+    def test_command_set_reference(self):
+        """Test Align tool commands: set the reference rectangle.
+        """
+        cmd = Registry.Command('set_align_reference_page')
+        cmd.Execute(self.appcontext)
+        self.assertEquals(self.editor.Tool().ref_rect,
+                          self.document.PageRect())
+
+    def test_command_align(self):
+        """Test Align tool commands: align left.
+        """
+        cmd = Registry.Command('align_left')
+        cmd.Execute(self.appcontext)
+
+        editor = self.editor
+
+        # Test whether objects are aligned:
+        left = editor.Selection().coord_rect.left
+        self.assertEquals(self.r1.coord_rect.left, left)
+        self.assertEquals(self.r2.coord_rect.left, left)
+        self.assertEquals(self.r3.coord_rect.left, left)
+
+
+class AlignToolTransactionTest(EditorTransactionTest, AlignTest):
+
+    def setUp(self):
+        EditorTransactionTest.setUp(self)
+        AlignTest.set_objects(self)
+        AlignTest.set_application(self)
+        self.editor.SelectObject([self.r2], SelectAdd)
+
+class TestAlignToolTransactionClick(AlignToolTransactionTest):
+
+    def action(self):
+        editor = self.editor
+        # Click on the "align left" handle.
+        handle = [h for h in editor.Handles() if h.type == HandleAlignL][0]
+        editor.ButtonClick(self.canvas, handle.p, 1, 0, handle = handle)
+
+    def check_before(self, after_undo):
+        editor = self.editor
+        self.failUnless(isinstance(editor.Tool(), AlignToolInstance))
+        self.assertEquals(editor.LastSelectedObjects(), [self.r2])
+        self.assertEquals(editor.SelectedObjects(), [self.r1, self.r2,
+                                                     self.r3])
+        self.assertEquals(editor.tool.rectangle.coord_rect, self.r2.coord_rect)
+
+    def check_after(self, after_redo):
+        editor = self.editor
+        self.failUnless(isinstance(editor.Tool(), AlignToolInstance))
+        self.assertEquals(editor.LastSelectedObjects(), [self.r2])
+        self.assertEquals(editor.SelectedObjects(), [self.r1, self.r2,
+                                                     self.r3])
+        self.assertEquals(editor.tool.rectangle.coord_rect, self.r2.coord_rect)
+
+class TestAlignToolTransactionCommand(AlignToolTransactionTest):
+
+    def action(self):
+        # simulate pressing l
+        cmd = Registry.Command('align_left')
+        cmd.Execute(self.appcontext)
+
+    def check_before(self, after_undo):
+        editor = self.editor
+        self.failUnless(isinstance(editor.Tool(), AlignToolInstance))
+        self.assertEquals(editor.LastSelectedObjects(), [self.r2])
+        self.assertEquals(editor.SelectedObjects(), [self.r1, self.r2,
+                                                     self.r3])
+        self.assertEquals(editor.tool.rectangle.coord_rect, self.r2.coord_rect)
+
+    def check_after(self, after_redo):
+        editor = self.editor
+        self.failUnless(isinstance(editor.Tool(), AlignToolInstance))
+        self.assertEquals(editor.LastSelectedObjects(), [self.r2])
+        self.assertEquals(editor.SelectedObjects(), [self.r1, self.r2,
+                                                     self.r3])
+        self.assertEquals(editor.tool.rectangle.coord_rect, self.r2.coord_rect)
+
+if __name__ == "__main__":
+    unittest.main()
+
