#---------------------------------------------------------------------------- # Name: AbstractEditor.py # Purpose: Non-text editor for DataModel and Process # # Author: Peter Yared, Morgan Hua # # Created: 7/28/04 # CVS-ID: $Id$ # Copyright: (c) 2004-2005 ActiveGrid, Inc. # License: wxWindows License #---------------------------------------------------------------------------- import wx import wx.lib.docview import wx.lib.ogl as ogl import PropertyService _ = wx.GetTranslation SELECT_BRUSH = wx.Brush("BLUE", wx.SOLID) SHAPE_BRUSH = wx.Brush("WHEAT", wx.SOLID) LINE_BRUSH = wx.BLACK_BRUSH INACTIVE_SELECT_BRUSH = wx.Brush("LIGHT BLUE", wx.SOLID) def GetRawModel(model): if hasattr(model, "GetRawModel"): rawModel = model.GetRawModel() else: rawModel = model return rawModel class CanvasView(wx.lib.docview.View): #---------------------------------------------------------------------------- # Overridden methods #---------------------------------------------------------------------------- def __init__(self, brush = SHAPE_BRUSH): wx.lib.docview.View.__init__(self) self._brush = brush self._canvas = None self._pt1 = None self._pt2 = None self._needEraseLasso = False self._propShape = None self._maxWidth = 2000 self._maxHeight = 16000 def OnDraw(self, dc): """ for Print Preview and Print """ dc.BeginDrawing() self._canvas.Redraw(dc) dc.EndDrawing() def OnCreate(self, doc, flags): frame = wx.GetApp().CreateDocumentFrame(self, doc, flags) frame.Show() sizer = wx.BoxSizer() self._CreateCanvas(frame) sizer.Add(self._canvas, 1, wx.EXPAND, 0) frame.SetSizer(sizer) frame.Layout() self.Activate() return True def OnActivateView(self, activate, activeView, deactiveView): if activate and self._canvas: # In MDI mode just calling set focus doesn't work and in SDI mode using CallAfter causes an endless loop if self.GetDocumentManager().GetFlags() & wx.lib.docview.DOC_SDI: self.SetFocus() else: wx.CallAfter(self.SetFocus) def SetFocus(self): if self._canvas: self._canvas.SetFocus() def OnFocus(self, event): self.FocusColorPropertyShape(True) event.Skip() def FocusOnClick(self, event): self.SetFocus() event.Skip() def OnKillFocus(self, event): self.FocusColorPropertyShape(False) event.Skip() def HasFocus(self): winWithFocus = wx.Window.FindFocus() if not winWithFocus: return False while winWithFocus: if winWithFocus == self._canvas: return True winWithFocus = winWithFocus.GetParent() return False def OnClose(self, deleteWindow = True): statusC = wx.GetApp().CloseChildDocuments(self.GetDocument()) statusP = wx.lib.docview.View.OnClose(self, deleteWindow = deleteWindow) if hasattr(self, "ClearOutline"): wx.CallAfter(self.ClearOutline) # need CallAfter because when closing the document, it is Activated and then Close, so need to match OnActivateView's CallAfter if not (statusC and statusP): return False self.Activate(False) if deleteWindow and self.GetFrame(): self.GetFrame().Destroy() return True def _CreateCanvas(self, parent): self._canvas = ogl.ShapeCanvas(parent) wx.EVT_LEFT_DOWN(self._canvas, self.OnLeftClick) wx.EVT_LEFT_UP(self._canvas, self.OnLeftUp) wx.EVT_MOTION(self._canvas, self.OnLeftDrag) wx.EVT_LEFT_DCLICK(self._canvas, self.OnLeftDoubleClick) wx.EVT_KEY_DOWN(self._canvas, self.OnKeyPressed) # need this otherwise mouse clicks don't set focus to this view wx.EVT_LEFT_DOWN(self._canvas, self.FocusOnClick) wx.EVT_LEFT_DCLICK(self._canvas, self.FocusOnClick) wx.EVT_RIGHT_DOWN(self._canvas, self.FocusOnClick) wx.EVT_RIGHT_DCLICK(self._canvas, self.FocusOnClick) wx.EVT_MIDDLE_DOWN(self._canvas, self.FocusOnClick) wx.EVT_MIDDLE_DCLICK(self._canvas, self.FocusOnClick) wx.EVT_KILL_FOCUS(self._canvas, self.OnKillFocus) wx.EVT_SET_FOCUS(self._canvas, self.OnFocus) self._canvas.SetScrollbars(20, 20, self._maxWidth / 20, self._maxHeight / 20) self._canvas.SetBackgroundColour(wx.WHITE) self._diagram = ogl.Diagram() self._canvas.SetDiagram(self._diagram) self._diagram.SetCanvas(self._canvas) def OnKeyPressed(self, event): key = event.KeyCode() if key == wx.WXK_DELETE: self.OnClear(event) else: event.Skip() def OnLeftClick(self, event): self.EraseRubberBand() dc = wx.ClientDC(self._canvas) self._canvas.PrepareDC(dc) # keep track of mouse down for group select self._pt1 = event.GetLogicalPosition(dc) # this takes into account scrollbar offset self._pt2 = None shape = self._canvas.FindShape(self._pt1[0], self._pt1[1])[0] if shape: self.BringToFront(shape) self._pt1 = None event.Skip() # pass on event to shape handler to take care of selection return elif event.ControlDown() or event.ShiftDown(): # extend select, don't deselect pass else: # click on empty part of canvas, deselect everything needRefresh = False for shape in self._diagram.GetShapeList(): if hasattr(shape, "GetModel"): if shape.Selected(): needRefresh = True shape.Select(False, dc) if needRefresh: self._canvas.Redraw(dc) self.SetPropertyModel(None) if len(self.GetSelection()) == 0: self.SetPropertyShape(None) def OnLeftDoubleClick(self, event): propertyService = wx.GetApp().GetService(PropertyService.PropertyService) if propertyService: propertyService.ShowWindow() def OnLeftDrag(self, event): # draw lasso for group select if self._pt1 and event.LeftIsDown(): # we are in middle of lasso selection self.EraseRubberBand() dc = wx.ClientDC(self._canvas) self._canvas.PrepareDC(dc) self._pt2 = event.GetLogicalPosition(dc) # this takes into account scrollbar offset self.DrawRubberBand() else: event.Skip() def OnLeftUp(self, event): # do group select if self._needEraseLasso: self.EraseRubberBand() dc = wx.ClientDC(self._canvas) self._canvas.PrepareDC(dc) x1, y1 = self._pt1 x2, y2 = event.GetLogicalPosition(dc) # this takes into account scrollbar offset tol = self._diagram.GetMouseTolerance() if abs(x1 - x2) > tol or abs(y1 - y2) > tol: # make sure x1 < x2 and y1 < y2 to make comparison test easier if x1 > x2: temp = x1 x1 = x2 x2 = temp if y1 > y2: temp = y1 y1 = y2 y2 = temp for shape in self._diagram.GetShapeList(): if not shape.GetParent() and hasattr(shape, "GetModel"): # if part of a composite, don't select it x, y = shape.GetX(), shape.GetY() width, height = shape.GetBoundingBoxMax() selected = x1 < x - width/2 and x2 > x + width/2 and y1 < y - height/2 and y2 > y + height/2 if event.ControlDown() or event.ShiftDown(): # extend select, don't deselect if selected: shape.Select(selected, dc) else: # select items in lasso and deselect items out of lasso shape.Select(selected, dc) self._canvas.Redraw(dc) else: event.Skip() else: event.Skip() def EraseRubberBand(self): if self._needEraseLasso: self._needEraseLasso = False dc = wx.ClientDC(self._canvas) self._canvas.PrepareDC(dc) dc.SetLogicalFunction(wx.XOR) pen = wx.Pen(wx.Colour(200, 200, 200), 1, wx.SHORT_DASH) dc.SetPen(pen) brush = wx.Brush(wx.Colour(255, 255, 255), wx.TRANSPARENT) dc.SetBrush(brush) dc.ResetBoundingBox() dc.BeginDrawing() x1, y1 = self._pt1 x2, y2 = self._pt2 # make sure x1 < x2 and y1 < y2 # this will make (x1, y1) = upper left corner if x1 > x2: temp = x1 x1 = x2 x2 = temp if y1 > y2: temp = y1 y1 = y2 y2 = temp # erase previous outline dc.SetClippingRegion(x1, y1, x2 - x1, y2 - y1) dc.DrawRectangle(x1, y1, x2 - x1, y2 - y1) dc.EndDrawing() def DrawRubberBand(self): self._needEraseLasso = True dc = wx.ClientDC(self._canvas) self._canvas.PrepareDC(dc) dc.SetLogicalFunction(wx.XOR) pen = wx.Pen(wx.Colour(200, 200, 200), 1, wx.SHORT_DASH) dc.SetPen(pen) brush = wx.Brush(wx.Colour(255, 255, 255), wx.TRANSPARENT) dc.SetBrush(brush) dc.ResetBoundingBox() dc.BeginDrawing() x1, y1 = self._pt1 x2, y2 = self._pt2 # make sure x1 < x2 and y1 < y2 # this will make (x1, y1) = upper left corner if x1 > x2: temp = x1 x1 = x2 x2 = temp if y1 > y2: temp = y1 y1 = y2 y2 = temp # draw outline dc.SetClippingRegion(x1, y1, x2 - x1, y2 - y1) dc.DrawRectangle(x1, y1, x2 - x1, y2 - y1) dc.EndDrawing() def FindParkingSpot(self, width, height): """ given a width and height, find a upper left corner where shape can be parked without overlapping other shape """ offset = 30 # space between shapes x = offset y = offset maxX = 700 # max distance to the right where we'll place tables noParkingSpot = True while noParkingSpot: point = self.isSpotOccupied(x, y, width, height) if point: x = point[0] + offset if x > maxX: x = offset y = point[1] + offset else: noParkingSpot = False return x, y def isSpotOccupied(self, x, y, width, height): """ returns None if at x,y,width,height no object occupies that rectangle, otherwise returns lower right corner of object that occupies given x,y position """ x2 = x + width y2 = y + height for shape in self._diagram.GetShapeList(): if isinstance(shape, ogl.RectangleShape) or isinstance(shape, ogl.EllipseShape): if shape.GetParent() and isinstance(shape.GetParent(), ogl.CompositeShape): # skip, part of a composite shape continue if hasattr(shape, "GetModel"): other_x, other_y, other_width, other_height = shape.GetModel().getEditorBounds() other_x2 = other_x + other_width other_y2 = other_y + other_height else: # shapes x,y are at the center of the shape, need to transform to upper left coordinate other_width, other_height = shape.GetBoundingBoxMax() other_x = shape.GetX() - other_width/2 other_y = shape.GetY() - other_height/2 other_x2 = other_x + other_width other_y2 = other_y + other_height # intersection check if ((other_x2 < other_x or other_x2 > x) and (other_y2 < other_y or other_y2 > y) and (x2 < x or x2 > other_x) and (y2 < y or y2 > other_y)): return (other_x2, other_y2) return None #---------------------------------------------------------------------------- # Canvas methods #---------------------------------------------------------------------------- def AddShape(self, shape, x = None, y = None, pen = None, brush = None, text = None, eventHandler = None): if isinstance(shape, ogl.CompositeShape): dc = wx.ClientDC(self._canvas) self._canvas.PrepareDC(dc) shape.Move(dc, x, y) else: shape.SetDraggable(True, True) shape.SetCanvas(self._canvas) if x: shape.SetX(x) if y: shape.SetY(y) shape.SetCentreResize(False) if pen: shape.SetPen(pen) if brush: shape.SetBrush(brush) if text: shape.AddText(text) shape.SetShadowMode(ogl.SHADOW_NONE) self._diagram.AddShape(shape) shape.Show(True) if not eventHandler: eventHandler = EditorCanvasShapeEvtHandler(self) eventHandler.SetShape(shape) eventHandler.SetPreviousHandler(shape.GetEventHandler()) shape.SetEventHandler(eventHandler) return shape def RemoveShape(self, model = None, shape = None): if not model and not shape: return if not shape: shape = self.GetShape(model) if shape: shape.Select(False) for line in shape.GetLines(): shape.RemoveLine(line) self._diagram.RemoveShape(line) for obj in self._diagram.GetShapeList(): for line in obj.GetLines(): if self.IsShapeContained(shape, line.GetTo()) or self.IsShapeContained(shape, line.GetFrom()): obj.RemoveLine(line) self._diagram.RemoveShape(line) if line == shape: obj.RemoveLine(line) shape.RemoveFromCanvas(self._canvas) self._diagram.RemoveShape(shape) def IsShapeContained(self, parent, shape): if parent == shape: return True elif shape.GetParent(): return self.IsShapeContained(parent, shape.GetParent()) return False def UpdateShape(self, model): for shape in self._diagram.GetShapeList(): if hasattr(shape, "GetModel") and shape.GetModel() == model: x, y, w, h = model.getEditorBounds() newX = x + w / 2 newY = y + h / 2 changed = False if isinstance(shape, ogl.CompositeShape): if shape.GetX() != newX or shape.GetY() != newY: dc = wx.ClientDC(self._canvas) self._canvas.PrepareDC(dc) shape.SetSize(w, h, True) # wxBug: SetSize must be before Move because links won't go to the right place shape.Move(dc, newX, newY) # wxBug: Move must be before SetSize because links won't go to the right place changed = True else: oldw, oldh = shape.GetBoundingBoxMax() oldx = shape.GetX() oldy = shape.GetY() if oldw != w or oldh != h or oldx != newX or oldy != newY: shape.SetSize(w, h) shape.SetX(newX) shape.SetY(newY) changed = True if changed: shape.ResetControlPoints() self._canvas.Refresh() break def GetShape(self, model): for shape in self._diagram.GetShapeList(): if hasattr(shape, "GetModel") and shape.GetModel() == model: return shape return None def GetSelection(self): return filter(lambda shape: shape.Selected(), self._diagram.GetShapeList()) def SetSelection(self, models, extendSelect = False): dc = wx.ClientDC(self._canvas) self._canvas.PrepareDC(dc) update = False if not isinstance(models, type([])) and not isinstance(models, type(())): models = [models] for shape in self._diagram.GetShapeList(): if hasattr(shape, "GetModel"): if shape.Selected() and not shape.GetModel() in models: # was selected, but not in new list, so deselect, unless extend select if not extendSelect: shape.Select(False, dc) update = True elif not shape.Selected() and shape.GetModel() in models: # was not selected and in new list, so select shape.Select(True, dc) update = True elif extendSelect and shape.Selected() and shape.GetModel() in models: # was selected, but extend select means to deselect shape.Select(False, dc) update = True if update: self._canvas.Redraw(dc) def BringToFront(self, shape): if shape.GetParent() and isinstance(shape.GetParent(), ogl.CompositeShape): self._diagram.RemoveShape(shape.GetParent()) self._diagram.AddShape(shape.GetParent()) else: self._diagram.RemoveShape(shape) self._diagram.AddShape(shape) def SendToBack(self, shape): if shape.GetParent() and isinstance(shape.GetParent(), ogl.CompositeShape): self._diagram.RemoveShape(shape.GetParent()) self._diagram.InsertShape(shape.GetParent()) else: self._diagram.RemoveShape(shape) self._diagram.InsertShape(shape) def ScrollVisible(self, shape): xUnit, yUnit = shape._canvas.GetScrollPixelsPerUnit() scrollX, scrollY = self._canvas.GetViewStart() # in scroll units scrollW, scrollH = self._canvas.GetSize() # in pixels w, h = shape.GetBoundingBoxMax() # in pixels x = shape.GetX() - w/2 # convert to upper left coordinate from center y = shape.GetY() - h/2 # convert to upper left coordinate from center if x >= scrollX*xUnit and x <= scrollX*xUnit + scrollW: # don't scroll if already visible x = -1 else: x = x/xUnit if y >= scrollY*yUnit and y <= scrollY*yUnit + scrollH: # don't scroll if already visible y = -1 else: y = y/yUnit self._canvas.Scroll(x, y) # in scroll units def SetPropertyShape(self, shape): # no need to highlight if no PropertyService is running propertyService = wx.GetApp().GetService(PropertyService.PropertyService) if not propertyService: return if shape == self._propShape: return if hasattr(shape, "GetPropertyShape"): shape = shape.GetPropertyShape() dc = wx.ClientDC(self._canvas) self._canvas.PrepareDC(dc) dc.BeginDrawing() # erase old selection if it still exists if self._propShape and self._propShape in self._diagram.GetShapeList(): self._propShape.SetBrush(self._brush) if (self._propShape._textColourName in ["BLACK", "WHITE"]): # Would use GetTextColour() but it is broken self._propShape.SetTextColour("BLACK", 0) self._propShape.Draw(dc) # set new selection self._propShape = shape # draw new selection if self._propShape and self._propShape in self._diagram.GetShapeList(): if self.HasFocus(): self._propShape.SetBrush(SELECT_BRUSH) else: self._propShape.SetBrush(INACTIVE_SELECT_BRUSH) if (self._propShape._textColourName in ["BLACK", "WHITE"]): # Would use GetTextColour() but it is broken self._propShape.SetTextColour("WHITE", 0) self._propShape.Draw(dc) dc.EndDrawing() def FocusColorPropertyShape(self, gotFocus=False): # no need to change highlight if no PropertyService is running propertyService = wx.GetApp().GetService(PropertyService.PropertyService) if not propertyService: return if not self._propShape: return dc = wx.ClientDC(self._canvas) self._canvas.PrepareDC(dc) dc.BeginDrawing() # draw deactivated selection if self._propShape and self._propShape in self._diagram.GetShapeList(): if gotFocus: self._propShape.SetBrush(SELECT_BRUSH) else: self._propShape.SetBrush(INACTIVE_SELECT_BRUSH) if (self._propShape._textColourName in ["BLACK", "WHITE"]): # Would use GetTextColour() but it is broken self._propShape.SetTextColour("WHITE", 0) self._propShape.Draw(dc) dc.EndDrawing() #---------------------------------------------------------------------------- # Property Service methods #---------------------------------------------------------------------------- def GetPropertyModel(self): if hasattr(self, "_propModel"): return self._propModel return None def SetPropertyModel(self, model): # no need to set the model if no PropertyService is running propertyService = wx.GetApp().GetService(PropertyService.PropertyService) if not propertyService: return if hasattr(self, "_propModel") and model == self._propModel: return self._propModel = model propertyService.LoadProperties(self._propModel, self.GetDocument()) class EditorCanvasShapeMixin: def GetModel(self): return self._model def SetModel(self, model): self._model = model class EditorCanvasShapeEvtHandler(ogl.ShapeEvtHandler): """ wxBug: Bug in OLG package. With wxShape.SetShadowMode() turned on, when we set the size, the width/height is larger by 6 pixels. Need to subtract this value from width and height when we resize the object. """ SHIFT_KEY = 1 CONTROL_KEY = 2 def __init__(self, view): ogl.ShapeEvtHandler.__init__(self) self._view = view def OnLeftClick(self, x, y, keys = 0, attachment = 0): shape = self.GetShape() if hasattr(shape, "GetModel"): # Workaround, on drag, we should deselect all other objects and select the clicked on object model = shape.GetModel() else: shape = shape.GetParent() if shape: model = shape.GetModel() self._view.SetSelection(model, keys == self.SHIFT_KEY or keys == self.CONTROL_KEY) self._view.SetPropertyShape(shape) self._view.SetPropertyModel(model) def OnEndDragLeft(self, x, y, keys = 0, attachment = 0): ogl.ShapeEvtHandler.OnEndDragLeft(self, x, y, keys, attachment) shape = self.GetShape() if hasattr(shape, "GetModel"): # Workaround, on drag, we should deselect all other objects and select the clicked on object model = shape.GetModel() else: parentShape = shape.GetParent() if parentShape: model = parentShape.GetModel() self._view.SetSelection(model, keys == self.SHIFT_KEY or keys == self.CONTROL_KEY) def OnMovePre(self, dc, x, y, oldX, oldY, display): """ Prevent objects from being dragged outside of viewable area """ if (x > self._view._maxWidth) or (y > self._view._maxHeight): return False return ogl.ShapeEvtHandler.OnMovePre(self, dc, x, y, oldX, oldY, display) def OnMovePost(self, dc, x, y, oldX, oldY, display): """ Update the model's record of where the shape should be. Also enable redo/undo. """ if x == oldX and y == oldY: return if not self._view.GetDocument(): return shape = self.GetShape() if isinstance(shape, EditorCanvasShapeMixin) and shape.Draggable(): model = shape.GetModel() if hasattr(model, "getEditorBounds") and model.getEditorBounds(): x, y, w, h = model.getEditorBounds() newX = shape.GetX() - shape.GetBoundingBoxMax()[0] / 2 newY = shape.GetY() - shape.GetBoundingBoxMax()[1] / 2 newWidth = shape.GetBoundingBoxMax()[0] newHeight = shape.GetBoundingBoxMax()[1] if shape._shadowMode != ogl.SHADOW_NONE: newWidth -= shape._shadowOffsetX newHeight -= shape._shadowOffsetY newbounds = (newX, newY, newWidth, newHeight) if x != newX or y != newY or w != newWidth or h != newHeight: self._view.GetDocument().GetCommandProcessor().Submit(EditorCanvasUpdateShapeBoundariesCommand(self._view.GetDocument(), model, newbounds)) def Draw(self, dc): pass class EditorCanvasUpdateShapeBoundariesCommand(wx.lib.docview.Command): def __init__(self, canvasDocument, model, newbounds): wx.lib.docview.Command.__init__(self, canUndo = True) self._canvasDocument = canvasDocument self._model = model self._oldbounds = model.getEditorBounds() self._newbounds = newbounds def GetName(self): name = self._canvasDocument.GetNameForObject(self._model) if not name: name = "" print "ERROR: AbstractEditor.EditorCanvasUpdateShapeBoundariesCommand.GetName: unable to get name for ", self._model return _("Move/Resize %s") % name def Do(self): return self._canvasDocument.UpdateEditorBoundaries(self._model, self._newbounds) def Undo(self): return self._canvasDocument.UpdateEditorBoundaries(self._model, self._oldbounds)