import gtk from types import ListType import Sketch from Sketch import Point, UnionRects, Rect from Sketch.Editor import Keymap, Add, AdvancedScript, HandleLine from Sketch.Editor.tools import ToolInstance, ToolInfo, selection_type from Sketch.Editor.selrect import SelectionRectangle from Sketch.Base.const import SELECTION, EDITED, CHANGED, \ SelectSet, SelectAdd, SelectSubtract from Sketch.Editor.selectiontool import SelectionToolInstance, SizeRectangle, \ TrafoRectangle from Sketch.Editor.selection import Selection from Sketch.Editor.handle import Handle import Sketch.UI from Sketch.UI import skapp from Sketch.UI.view import SketchView from Sketch.UI.mainwindow import SketchCanvas # needed for pixmap handles: import PIL from PIL.Image import ROTATE_90, ROTATE_270, FLIP_LEFT_RIGHT, FLIP_TOP_BOTTOM from Sketch.UI.skpixmaps import SketchBitmaps from Sketch.UI.canvas import Handle_Pixmap, Handle_Line, handle_info class NewSelection(Selection): """A selection that remembers which objects were selected last""" _lazy_attrs = Selection._lazy_attrs.copy() _lazy_attrs["last_rect"] = "update_last_rect" def __init__(self, document): Selection.__init__(self, document) self.last_selected = [] def SetSelection(self, objects): if objects is None: self.last_selected = [] elif type(objects) == ListType: self.last_selected = objects else: self.last_selected = [objects,] return Selection.SetSelection(self, objects) def Add(self, objects): if objects: if type(objects) == ListType: self.last_selected = objects else: self.last_selected = [objects,] return Selection.Add(self, objects) else: return 0 def Subtract(self, objects): if objects: if type(objects) == ListType: for obj in objects: if obj in self.last_selected: self.last_selected.remove(obj) elif objects in self.last_selected: self.last_selected.remove(objects) return Selection.Subtract(self, objects) else: return 0 def update_last_rect(self): if self.last_selected: self.last_rect = reduce(UnionRects, [o.coord_rect for o in self.last_selected]) else: self.last_rect = self.coord_rect def GetLastObjects(self): return self.last_selected Sketch.Editor.doceditor.Selection = NewSelection ## The Align Tool 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 c = self.handle_idx_to_sel return [Handle('AlignTL', Point(sx, ey)), Handle('AlignT', Point(x2, ey)), Handle('AlignTC', Point(x2, ey)), Handle('AlignTR', Point(ex, ey)), Handle('AlignL', Point(sx, y2)), Handle('AlignLC', Point(sx, y2)), Handle('AlignR', Point(ex, y2)), Handle('AlignBL', Point(sx, sy)), Handle('AlignB', Point(x2, sy)), Handle('AlignBR', Point(ex, sy)), Handle('AlignC', Point(x2, y2)), ] class AlignToolInstance(ToolInstance): title = "Align" def __init__(self, editor): ToolInstance.__init__(self, editor) self.rectangle = AlignRectangle(self.editor.Selection().last_rect) self.sides = {'AlignTL': ('top', 'left'), 'AlignT': ('top',), 'AlignTC': ('center_x',), 'AlignTR': ('top', 'right'), 'AlignL': ('left',), 'AlignLC': ('center_y',), 'AlignR': ('right',), 'AlignBL': ('bottom', 'left'), 'AlignB': ('bottom',), 'AlignBR': ('bottom', 'right'), 'AlignC': ('center_x', 'center_y'), } self.editor.Subscribe(SELECTION, self.selection_changed) self.editor.Subscribe(EDITED, self.doc_was_edited) # self.ref keeps track of the align reference. Can be 'page' or 'last' self.ref = 'last' 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): if self.ref == 'page': self.rectangle = AlignRectangle(self.editor.document.PageRect()) else: self.rectangle = AlignRectangle(self.editor.Selection().last_rect) # TODO: # When deselecting it would be nice to end the align tool and instantiate # the selection tool. 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, 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 # selection (even if shift is not pressed) self.ref = 'last' 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 on near the page border sets the reference to the # whole page rect. self.rectangle = AlignRectangle(self.editor.document.PageRect()) self.ref = 'page' self.editor.update_handles() else: # Otherwise behave as the Selection Tool self.ref = 'last' self.editor.SelectPoint(p, test, type) 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 contour 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) Sketch.Editor.toolmap['AlignTool'] = AlignTool Add(AdvancedScript("align_tool", "Align", lambda c: c.SetTool('AlignTool'))) Sketch.UI.skapp.main_menu.AddItem(("Tools",), "align_tool") ## Align tool shortcuts # Rebind 'a' to align_tool instead of cont_angle to make the demo easier Sketch.UI.skapp.keymap.RemoveItem((ord('a'),0)) Sketch.Editor.standard_keystrokes.append(('align_tool', 'a')) Sketch.UI.skapp.keymap = Keymap(Sketch.Editor.standard_keystrokes, special_keys = gtk.keysyms.__dict__) # Make some keybord shortcuts available while the Align tool is active 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'), ('selection_tool', ' '), ] align_keymap = Keymap(align_keystrokes, special_keys = gtk.keysyms.__dict__) lookup_key = SketchCanvas.lookup_key def my_lookup_key(self, keyval, state, str): if self.application.context.tool == 'AlignTool': command = align_keymap.Command((keyval, state)) if command is not None: return command return lookup_key(self, keyval, state, str) SketchCanvas.lookup_key = my_lookup_key ## Align function and commands 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_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: Add(AdvancedScript("align_%s" % side, "Align Objects", align_selected, (side,))) ## Various hacks to add some align pixmap handles: new_bmps = {} bmpstr = {'AlignL': """ = { 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 };""", 'AlignTL': """ = { 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 };""", 'AlignTC': """ = { 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 };""", 'AlignC': """ = { 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 };""", } for bmp in bmpstr: new_bmps[bmp] = PIL.Image.fromstring("1", (16, 16), bmpstr[bmp], "xbm") new_bmps['AlignR'] = new_bmps['AlignL'].transpose(FLIP_LEFT_RIGHT) new_bmps['AlignT'] = new_bmps['AlignL'].transpose(ROTATE_270) new_bmps['AlignB'] = new_bmps['AlignT'].transpose(FLIP_TOP_BOTTOM) new_bmps['AlignTR'] = new_bmps['AlignTL'].transpose(FLIP_LEFT_RIGHT) new_bmps['AlignBR'] = new_bmps['AlignTR'].transpose(FLIP_TOP_BOTTOM) new_bmps['AlignBL'] = new_bmps['AlignTL'].transpose(FLIP_TOP_BOTTOM) new_bmps['AlignLC'] = new_bmps['AlignTC'].transpose(ROTATE_90) def load_bitmap_from_image(window, image): data = image.tostring("raw", "1;R") return gtk.gdk.bitmap_create_from_data(window, data, 16, 16) class NewBitmaps(SketchBitmaps): def InitFromWidget(self, widget): for name in new_bmps: setattr(self, name, load_bitmap_from_image(widget.window, new_bmps[name])) SketchBitmaps.InitFromWidget(self, widget) Sketch.UI.skpixmaps.bitmaps = NewBitmaps() Sketch.UI.canvas.bitmaps = Sketch.UI.skpixmaps.bitmaps new_infos = {'AlignTL': (Handle_Pixmap, (-2, +2), 'CurSizeTL', 'AlignTL'), 'AlignTC': (Handle_Pixmap, (0, +5), 'CurHGuide', 'AlignTC'), 'AlignT': (Handle_Pixmap, (0, 2), 'CurSizeT', 'AlignT'), 'AlignTR': (Handle_Pixmap, (+2, +2), 'CurSizeTR', 'AlignTR'), 'AlignL': (Handle_Pixmap, (-2, 0), 'CurSizeL', 'AlignL'), 'AlignLC': (Handle_Pixmap, (-5, 0), 'CurVGuide', 'AlignLC'), 'AlignR': (Handle_Pixmap, (2, 0), 'CurSizeR', 'AlignR'), 'AlignBL': (Handle_Pixmap, (-2, -2), 'CurSizeBL', 'AlignBL'), 'AlignB': (Handle_Pixmap, (0, -2), 'CurSizeB', 'AlignB'), 'AlignBR': (Handle_Pixmap, (+2, -2), 'CurSizeBR', 'AlignBR'), 'AlignC': (Handle_Pixmap, (0, 0), 'CurHandle', 'AlignC'), } for info in new_infos: Sketch.UI.canvas.handle_info[info] = new_infos[info]