<@doc hierarchy="GMLDOM"> Aspect for drawing zoomable diagrams (c) SAP AG 2003-2006. All rights reserved. /////////////////////////////////////////////////////////////////////// // ASPECT HEADER #INCLUDE[dev:defs.inc] Aspect ZDiagram for ZDrawing; constructor(board, drawingLayer) // initialize internal structures this.board = board; this.boardAspect = CLASS('#NS[ZDrawing]'); this.canvas = board.canvas; this.drawingLayer = drawingLayer; this.isEditable = base.isEditable(); this.rootID = ''; this.id = base.id; this.focusID = ''; this.pfocusID = this.id; this.selection = {}; this.graphics = {}; this.spriteData = {}; this.handlesList = {}; this.coloredMarkers = {}; this.pinSymbols = {}; this.canvas.setWidget(this); // initialize status flags this.alphaMode = GETVAR('SVG_ALPHA_MODE'); this.defaultRouting = GETVAR('SVG_ROUTING_MODE') || base.@routingMode || #[SVG_STRAIGHT_LINE]; this.isZoomable = GETVAR('SVG_ZOOMABLE'); this.hilightMode = GETVAR('SVG_HILIGHT_MODE'); this.plowMode = GETVAR('SVG_PLOW_MODE'); this.tooltipsMode = GETVAR('SVG_TOOLTIPS'); this.strokeScaling = GETVAR('SVG_STROKE_SCALING'); this.handleTextOverflow = GETVAR('SVG_HANDLE_TEXTOVERFLOW'); this.paint(); end destructor var root = this.getRoot(); if (root.base.@pos != '0 0') { try { this.begin('fix diagram origin'); root.moveto(0,0); this.commit('fix diagram origin'); } catch(e) { #LOG[4, 'Failed to destroy diagram: '+e.description]; this.rollback(); } } this.removeGraphic(this.rootID); SVG.clearElement(this.drawingLayer); this.canvas.removeWidget(this.id); //TODO: delete this['base']; end virtual method getRoot() return this.canvas.getWidget(this.rootID); end virtual method getPFocus() return this.canvas.getWidget(this.pfocusID); end virtual method getFocus() return this.canvas.getWidget(this.focusID); end virtual method getParent() return null; end ////////////////////////////////////////////////////////////////////////////////////// // STATIC PROPERTIES <@doc type="RGB" group="Aspect Properties">Defines the diagram's hilight color static readonly property hilightColor = '#FF9933'; <@doc type="RGB" group="Aspect Properties">Defines the diagram's hilighted fill color static readonly property hilightFill = '#FFEEDD'; <@doc type="RGB" group="Aspect Properties">Defines the diagram's hilighted text color static readonly property hilightText = '#BB6600'; <@doc type="@ZLine!LINE_ROUTING" default="SVG_STRAIGHT_LINE">Defines the default routing mode for the diagram's lines static readonly property routingMode = #[SVG_STRAIGHT_LINE]; <@doc type="@ZShape!SHAPE_PARTS[]"> Defines the frame parts used for drawing the diagram's sprite See @ZShape!frameParts for more details. static readonly property spriteParts = null; <@doc type="RGB">Defines the diagram's sprite stroke color static readonly property spriteColor = '#718398'; <@doc type="RGB">Defines the diagram's sprite height static readonly property spriteHeight = 70; <@doc type="RGB">Defines the diagram's sprite width static readonly property spriteWidth = 80; ////////////////////////////////////////////////////////////////////////////////////// // ASPECT PROPERTIES // Associated objects <@doc type="@Board!" scope="private">Gets the current drawing board object virtual property board = null; <@doc type="@core.dev:IBoard!" scope="private">Gets the drawing board aspect virtual property boardAspect = null; <@doc type="s" scope="private">Gets the collection of existing colored markers virtual property coloredMarkers = null; <@doc type="@Canvas!" scope="private">Gets the canvas object virtual property canvas = null; <@doc type="Boolean[id]" scope="private">Gets the collection of all graphics in this diagram (that have instantiated thus far) virtual property graphics = null; <@doc scope="private">Gets the root block virtual property rootID = ''; <@doc type="s" scope="private">Gets the collection of existing pin symbols virtual property pinSymbols = null; <@doc type="Object" scope="private">Gets the sprite data object virtual property spriteData = null; // Current selection <@doc scope="private">Gets the graphic currently in focus (primary selection) virtual property focusID = ''; <@doc scope="private">Gets the parent of the current focus virtual property pfocusID = ''; <@doc type="Boolean[id]" scope="private">Gets the collection of selected graphics virtual property selection = null; <@doc scope="private">Gets the number of selected shapes virtual property nselected = 0; // Current transaction <@doc scope="private">Indicates whether the board is in the middle of a transaction recording. virtual property inTransaction = false; <@doc type="@Object![id]" scope="private">Gets the shape (positioning) records in the current transaction. virtual property invalidShapes = null; // Status flags <@doc scope="private">Indicates whether alpha mode (transparency effects) is enabled virtual property alphaMode = false; <@doc type="RGB" scope="private">Gets the diagram's default line routing mode virtual property defaultRouting = #[SVG_STRAIGHT_LINE]; <@doc scope="private">Indicates whether the diagram is displayed in high quality virtual property highQuality = false; <@doc scope="private">Indicates whether hilighting mode is enabled virtual property hilightMode = false; <@doc scope="private">Indicates whether plow mode is enabled virtual property plowMode = false; <@doc scope="private">Indicates whether tooltips mode is enabled virtual property tooltipsMode = false; <@doc scope="private">Indicates whether stroke width is scaled with the diagram virtual property strokeScaling = true; // SVG painting objects <@doc type="SVGNode" scope="private">Gets the drawing's root SVG layer virtual property drawingLayer = null; <@doc type="SVGNode" scope="private">Gets the control handles SVG layer virtual property handlesLayer = null; <@doc type="SVGNode[id]" scope="private">Gets the control handles list virtual property handlesList = null; <@doc type="SVGNode" scope="private">Gets the selection rubberband SVG node virtual property rubberbandObj = null; <@doc type="SVGNode" scope="private">Gets the connection rubberwire SVG node virtual property rubberwireObj = null; <@doc type="SVGNode" scope="private">Gets the diagram shapes SVG layer virtual property shapesLayer = null; <@doc type="SVGNode" scope="private">Gets the wireframes SVG layer virtual property wiresLayer = null; // TODO: BlockZoom: currently zoomed gml2:Block virtual property blockID = null; virtual property isZoomable = false; virtual property inZoomableAction = false; virtual property zoomTargetID = ''; ////////////////////////////////////////////////////////////////////////////////////// // MODEL EVENTS <@doc scope="private"> Handles an update of a diagram property virtual method onModelUpdate(evt) // nothing to do (diagram has no visual properties) end <@doc scope="private"> Handles an insertion of a child graphic into this diagram virtual method onModelInsert(evt) // nothing to do (diagram always has exactly one child graphic - the root block) end <@doc scope="private"> Handles a removal of a child graphic from this diagram virtual method onModelRemove(evt) // nothing to do (diagram always has exactly one child graphic - the root block) end ////////////////////////////////////////////////////////////////////////////////////// // PAINTING METHODS <@doc scope="private"> Paints the diagram virtual method paint() try { // create the drawing layers this.shapesLayer = SVG.createElement(this.drawingLayer, 'g'); this.wiresLayer = SVG.createElement(this.drawingLayer, 'g'); this.rubberbandObj = SVG.createElement(this.drawingLayer, 'rect', {'class':'rubberband', display:'none'}); this.handlesLayer = SVG.createElement(this.drawingLayer, 'a'); this.handlesLayer.peerID = this.id; this.toggleQuality(true); // get root block var root = base.root; var aspect = root && $DOM.getAspectOf(root, '#NS[ZDrawing]') || null; if (!aspect || !aspect.prototype.isBlock) { throw new Error(-1, 'Diagram root block is bad or missing'); } // compute initial viewport that exactly fits the root block (before painting it) var size = SPLITN(root.@size), w=(size[0]||400), h=(size[1]||300); var pos = SPLITN(root.@pos), x=(pos[0]||0), y=(pos[1]||0); this.canvas.fitArea({x:x-w*5, y:y-h*5, w:w*10, h:h*10}); // paint the root block var _root = this.createGraphic(root, this); this.rootID = _root.id; if (this.isZoomable) this.setActiveBlock(_root); // TODO: BlockZoom work this.repaintBorder(); } catch(e) { #LOG[4, 'Failed to paint diagram: '+e.description]; } end <@doc scope="private"> Repaints the diagram border virtual method repaintBorder() var flag = GETVAR('SVG_SHOW_BORDER') var root = this.getRoot(); SVG.display(root.frameNode, flag); SVG.setProperty(root.frameNode, 'pointer-events', flag ? 'visiblePainted' : 'none'); if (!flag && root.isSelected) this.clearSelection(); end <@doc scope="private"> Repaints the diagram background virtual method repaintBackground() var canvas = this.canvas; bgcolor(this.getRoot()); function bgcolor(block) { block.repaintBackground(); var shapes = block.base[block.base.@shapesKey]; for (var k in shapes) { var shape=canvas.getWidget(k); if (shape && shape.isBlock) bgcolor(shape); } } end <@doc scope="private"> Performs adjustments after the canvas scale has changed virtual method adjustScale() // The SVG stroke is scaled by default, the following is needed if we want to compensate the scale to create a non-scaling stroke effect if (!this.strokeScaling) { var scale=this.canvas.scale; this.shapesLayer.setAttribute('stroke-width', 2/scale); this.handlesLayer.setAttribute('stroke-width', 1.5/scale); var H=this.handlesList, r=3/Math.sqrt(scale); for (var k in H) { H[k].setAttribute('r',r); } } end <@doc scope="private"> Performs adjustments to the wireframes layer based on canvas scale and alpha mode virtual method adjustWireframes() SVG.setProperty(this.wiresLayer, '$stroke-width', 2/this.canvas.scale); end <@doc scope="private"> Toggles the diagram display quality virtual method toggleQuality(flag) if (this.highQuality === flag) return; this.highQuality = flag; var rendering = flag ? 'auto' : 'optimizeSpeed'; this.shapesLayer.setAttribute('shape-rendering', rendering); this.shapesLayer.setAttribute('image-rendering', rendering); this.shapesLayer.setAttribute('text-rendering', rendering); end <@doc scope="private"> Flies the diagram into view virtual method flyIntoView() var b=this.getBBox(); return this.canvas.flyToArea({x:b.x, y:b.y, w:b.width, h:b.height}); end <@doc scope="private"> Updates the diagram after a settings change virtual method onsettingschange(evt) var flag = BOOL(evt.value); switch (UPPER(evt.name)) { case 'SVG_ALPHA_MODE': this.clearSelection(); this.alphaMode = flag; break; case 'SVG_COLOR_NESTING': this.repaintBackground(); break; case 'SVG_HILIGHT_MODE': this.hilightMode = flag; break; case 'SVG_PLOW_MODE': this.plowMode = flag; if (flag) this.applyGlobalLayout(); break; case 'SVG_ROUTING_MODE': this.applyGlobalRouting(); break; case 'SVG_SHOW_BORDER': this.repaintBorder(); break; case 'SVG_TOOLTIPS': this.tooltipsMode = flag; break; } end ////////////////////////////////////////////////////////////////////////////////////// // GENERIC EDITING TRANSACTION <@doc scope="private"> Begins a drawing transaction Descriptive transaction name Additional pointer information virtual method begin(name, ptr) BEGIN(name); if (this.inTransaction) return; this.inTransaction = true; this.board.freeze(#[SVG_FREEZE_CANVAS]); end <@doc scope="private"> Commits a drawing transaction Additional pointer information virtual method commit(name) if (!this.inTransaction) { COMMIT(name); return; } this.inTransaction = false; var S=this.invalidShapes, board=this.board, canvas=this.canvas; if (!S) { board.unfreeze(#[SVG_FREEZE_ALL]); COMMIT(name); return; } try { // freeze event handlers since repaints were already done when the invalidated shapes were buffered board.freeze(#[SVG_FREEZE_EVENTS]); // commit invalidated shape properties if (!RULE('rearrangeShapes', base, S)) raiseRuleError(); // reroute invalidated lines (derived recursively from the list of invalidated shapes) var L={}, V={}; for (var k in S) invalidateLines(this.getGraphic(k)); for (var k in L) L[k].reroute(); // unfreeze event handlers board.unfreeze(#[SVG_FREEZE_ALL]); this.invalidShapes = null; COMMIT(name); } catch(e) { #LOG[4, 'Failed to commit: '+e.description]; board.unfreeze(#[SVG_FREEZE_ALL]); ROLLBACK(); throw e; } function invalidateLines(shape) { if (!shape || !shape.isShape || (shape.id in V)) return; V[shape.id] = true; if (shape.isBlock) { var sub=shape.base[shape.base.@shapesKey]; for (var k in sub) invalidateLines(canvas.getWidget(k)); } var shapeBase = shape.base; if (!shapeBase.@pinsKey) return; var P = shapeBase[shapeBase.@pinsKey]; for (var k in P) { var pin = canvas.getWidget(P[k].id); if (!pin) continue; var lines = pin.lines; for (var k2 in lines) { var line=canvas.getWidget(k2); // All lines should be validated if (line) L[k2] = line; /* Optimization removed due to not validating correctly within same block if (!line) continue; var s1=line.getSrcpin().getShape(), s2=line.getTrgpin().getShape(); if (s1 == s2) { // if the line is from an invalid shape to itself, the line is invalid if (s1.id in S) L[k2]=line; } else { // look for closest invalidated ancestor of each line end for (var p1=s1; !p1.isRoot && !(p1.id in S); p1=p1.getParent()); for (var p2=s2; !p2.isRoot && !(p2.id in S); p2=p2.getParent()); // if the line connects two different invalidated ancestors(not from the same block), the line is invalid if (p1 != p2) L[k2]=line; }*/ } } } end <@doc scope="private"> Rolls back a drawing transaction virtual method rollback() this.inTransaction = false; this.invalidShapes = null; this.board.unfreeze(#[SVG_FREEZE_ALL]); ROLLBACK(); end <@doc scope="private"> Carries out a silent property update (to avoid raising repaint events) The graphic object to update The name of the property to update The new property value virtual method silentUpdate(graphic, prop, value) graphic.base.setProperty(prop, value, true); end ////////////////////////////////////////////////////////////////////////////////////// // CONCRETE EDITING TRANSACTIONS <@doc scope="private"> Copies the currently selected shapes to the clipboard virtual method copyClipboard() var elements = {}; var sel = this.selection, canvas = this.canvas; for (var k in sel) { var widget = canvas.getWidget(k); if (!widget) continue; elements[k] = widget.base; } RULE('doCopy', base, this.boardAspect, base, elements); end <@doc scope="private"> Connects two base objects and creates the corresponding graphic line in one step The class of the new link The source plug The target plug virtual method createLine(linktype, srcplug, trgplug) try { this.begin('#TEXT[XMIT_TRANS_CONNECT]'.replace('{0}', "'" + srcplug.parent.name + "'") .replace('{1}', "'" + trgplug.parent.name + "'")); var link = RULE('doConnect', base, this.boardAspect, CLASS(linktype), srcplug, trgplug); if (!link) raiseRuleError(); this.commit('connect shapes'); this.selectById(link.id); this.hideTooltip(); } catch(e) { #LOG[4, 'Failed to connect shapes: '+e.description]; this.rollback(); } end <@doc scope="private"> Cuts the currently selected shapes to the clipboard virtual method cutClipboard() this.copyClipboard(); this.deleteSelection(); end <@doc scope="private"> Deletes the currently selected shapes virtual method deleteSelection() HIDE_POPUP(); if (!this.isEditable || this.nselected == 0) return; var sel = this.selection; var canvas = this.canvas; try { // collect shapes and lines to be removed var pfocus = this.getPFocus(); var S={}, L={}, selectedShapes = {}, doDeleteControls, parent=pfocus.base; for (var k in sel) selectedShapes[k] = true; // find out if there are dependent controls, and allow the user to cancel the delete operation. for (var k in sel) { var widget = canvas.getWidget(k); if (!widget) continue; var obj = widget.base; if (hasDependentControls(obj, selectedShapes, undefined, $ENV.datahlpr)) { var buttons = { '#TEXT[XBUT_DELETE_CONTROLS]' : 1, '#TEXT[XBUT_LEAVE_CONTROLS]' : -1, 'Cancel' : 0 }; doDeleteControls = CONFIRM('#TEXT[XMSG_DOM_DELETE__DEPENDED_SHAPE]', buttons); if (doDeleteControls == 0) return; break; } } for (var k in sel) { var g = canvas.getWidget(k); if (!RULE('canRemove', base, this.boardAspect, parent, g.base)) continue; if (g.isLine) { L[k] = g; } else if (g.isShape) { S[k] = g.base; this.getCrossingLines(g, g, L); } } if (ISEMPTY(S) && ISEMPTY(L)) return; var descr = '#TEXT[XMIT_TRANS_DELETE_ELEMENTS]'; if(this.nselected == 1) { var s = this.getFocus(); descr = '#TEXT[XMIT_TRANS_DELETE]'.replace('{0}',"'"+(s.base.name ||s.base.Class.metadata.title||s.base.Class.metadata.name)+"'"); } this.begin(descr); // this deletes the controls that are in interactors NOT being deleted if (doDeleteControls == 1) { for (var k in sel) { var widget = canvas.getWidget(k); if (widget) deleteControls(widget.base, undefined, $ENV.datahlpr); } } // first disconnect all lines connected to or into the selected shapes for (var k in L) { var line=L[k], srcplug=line.getSrcpin().base, trgplug=line.getTrgpin().base; if (!RULE('doDisconnect', base, this.boardAspect, line.base, srcplug, trgplug)) raiseRuleError(); } // then remove the selected shapes if (!RULE('doRemove', base, this.boardAspect, parent, S)) raiseRuleError(); this.commit('delete shape(s)'); this.clearSelection(); this.hideTooltip(); } catch(e) { #LOG[4, 'Failed to delete shape(s): '+e.description]; this.rollback(); } // this code is similar to the code in DataBoundInteractor. But here (1) we take into account the other shapes being // deleted for performance reasons and (2) we don't want the function to return true if a deleted interactor // has locally bound controls function hasDependentControls(shape, selectedShapes, path, datahlpr) { if (!path) path = {}; if (shape.id in path) return false; path[shape.id] = true; // always true if (( shape.isa('gml2:DataBoundInteractor') && !selectedShapes[shape.id]) && shape.hasLocalControls()) { return true; } // always false if (shape.isa('gml2:Note')) { return false; } var outLinks = shape.outLinks || [shape]; for (var k in outLinks){ if (ISA(outLinks[k],'gml2:DataMap')) continue; var nextShape = outLinks[k].target.parent; if (datahlpr.isValueTransform(nextShape) && hasDependentControls(nextShape, selectedShapes, path, datahlpr) ) { return true; } } return false; } function deleteControls(shape, path, datahlpr){ if (!path) path = {}; if (shape.id in path) return true; path[shape.id] = true; var outLinks = shape.outLinks || [shape]; for (var k in outLinks){ var nextShape = outLinks[k].target.parent; if (datahlpr.isValueTransform(nextShape)) deleteControls(nextShape, path, datahlpr); } if (!selectedShapes[shape.id] && shape.isa('gml2:DataBoundInteractor')) shape.removeLocalControls(); } end virtual method getCrossingLines(top, shape, L) var canvas = this.canvas; if (shape.isBlock) { var shapes = shape.base[shape.base.@shapesKey]; for (var k in shapes) { var s = canvas.getWidget(k); if (s) this.getCrossingLines(top, s, L); } } var shapeBase = shape.base; if (!shapeBase.@pinsKey) return; var P = shapeBase[shapeBase.@pinsKey]; for (var k in P) { var pin=canvas.getWidget(P[k].id); if (!pin) continue; var lines=pin.lines; for (var k2 in lines) { var line = canvas.getWidget(k2); if (!line) continue; var pin2 = (line.srcpinID == pin.id ? line.getTrgpin() : line.getSrcpin()); if (pin2 == pin) continue; for (var p=pin2.getShape(); p && p != top; p=p.getParent()); if (!p) L[k2] = line; } } end <@doc scope="private"> Drills down into the currently selected shape (primary selection) virtual method drilldownSelection() var elem = (this.getFocus()||this).base; RULE('drillIntoShape', base, elem); end <@doc scope="private"> Duplicates the currently selected shapes virtual method duplicateSelection() #LOG[4, '"Duplicate" action is not yet implemented']; end <@doc scope="private"> Ends an annotation operation The annotation data virtual method endAnnotate(data) if (!data || !data.value) return; try { this.begin('#TEXT[XMIT_TRANS_CREATE_ANNOTATION]'); var note = RULE('annotateDrawing', base, data.parent, data.value, {'@pos':data.pos}); if (!note) raiseRuleError(); var shape = this.getGraphic(note.id), u=#[SVG_SNAPUNIT]; shape.fitToBody(); shape.moveby(FLOOR(shape.w/2,u),FLOOR(shape.h/2,u)); this.adjustBlockLayout(this.getPFocus(), this.getSelectedShapes()); this.commit('create annotation'); this.selectById(note.id); this.hideTooltip(); } catch (e) { #LOG[4, 'Failed to create annotation: '+e.description]; this.rollback(); } end <@doc scope="private"> Ends a rename operation The rename operation data virtual method endRename(data) try { var elem = data.graphic.base; var name = data.value; var desc = name ? '#TEXT[XMIT_TRANS_RENAME_ELEMENT_NAME]' : '#TEXT[XMIT_TRANS_RENAME_ELEMENT]'; this.begin(desc.replace('{0}', elem.Class.metadata.title||elem.Class.metadata.name) .replace('{1}', "'" + name + "'")); if (!RULE('doRename', base, this.boardAspect, elem, name)) raiseRuleError(); if (data.graphic.isShape) data.graphic.fitToBody(); this.commit('rename shape'); this.hideTooltip(); } catch(e) { #LOG[4, 'Failed to rename shape: '+e.description]; this.rollback(); } end <@doc scope="private"> Recomputes the layout of the entire diagram virtual method applyGlobalLayout() try { this.begin('layout diagram'); this.globalDiagramLayout(); this.commit('layout diagram'); } catch (e) { #LOG[4, 'Failed to layout diagram: '+e.description]; this.rollback(); } end <@doc scope="private"> Recomputes the line routing in the entire diagram virtual method applyGlobalRouting() try { this.begin('reroute diagram'); this.globalDiagramRouting(); this.commit('reroute diagram'); } catch (e) { #LOG[4, 'Failed to reroute diagram: '+e.description]; this.rollback(); } end <@doc scope="private"> Adjusts the layout of the currently selected shapes (usually after a drop operation) virtual method layoutSelection() try { this.begin('layout shape(s)'); this.adjustBlockLayout(this.getPFocus(), this.getSelectedShapes()); BOARD.window.focus(); this.commit('layout shape(s)'); } catch(e) { this.rollback(); #LOG[4, 'Failed to layout shape(s): '+e.description]; } end <@doc scope="private"> Moves the currently selected shapes The move offset Additional pointer information virtual method moveSelection(offset, ptr) try { var descr = '#TEXT[XMIT_TRANS_MOVE_ELEMENTS]'; if(this.nselected == 1) { var shape = this.getFocus(); descr = '#TEXT[XMIT_TRANS_MOVE]'.replace('{0}', "'" + (shape.base.name ||shape.base.Class.metadata.title||shape.base.Class.metadata.name) + "'"); } this.begin(descr, ptr); var shapes = {}, sel = this.selection, canvas = this.canvas; for (var k in sel) { var shape = canvas.getWidget(k); if (!shape || !shape.isShape || shape.base.@protect & #[SVG_PROTECT_MOVE]) continue; var ox=shape.ox, oy=shape.oy, os=shape.os var cx=shape.cx+offset.x/os, cy=shape.cy+offset.y/os; shape.moveto(cx,cy); shapes[shape.id] = shape; } this.adjustBlockLayout(this.getPFocus(), shapes); this.commit('move shape(s)'); } catch(e) { this.rollback(); #LOG[4, 'Failed to move shape(s): '+e.description]; } end <@doc scope="private"> Pastes the clipboard contents into the diagram The paste position virtual method pasteClipboard(pos, force) var focus = this.getFocus(); for (var p = focus && focus.base; p && !p.isa('gml2:Block'); p = p.parent); var targetWin = p || base.root; var ppos = null; if(pos) { var parent = focus; if (!parent || parent == this) parent = this.getRoot(); if (!parent.isBlock) parent = parent.getParent(); if (!parent) return; ppos = Math.round((pos.x-parent.qx)/parent.qs) + ' ' + Math.round((pos.y-parent.qy)/parent.qs); } else { var w, h, size = targetWin['z$size']; if(size) { size = SPLIT(size); w = INT(size[0]); h = INT(size[1]);} else { var tbb = e['z$boundingBox']; w = tbb.defWidth; h = tbb.defHeight;} var bb = $ENV.getClipboard().boundingBox; if((bb.x>w || bb.y>h)) ppos = "0 0"; } if(! RULE('doPaste', base, this.boardAspect, targetWin, ppos)) return; if(!ppos) this.flyIntoView(); end <@doc scope="private"> Recomposes the currently selected shapes The move offset The source block The target block Additional pointer information virtual method recomposeSelection(offset, srcBlock, trgBlock, ptr) try { var descr = 'Move Elements'; if(this.nselected == 1) { var shape = this.getFocus(); descr = 'Move \''+ (shape.base.name ||shape.base.Class.metadata.title||shape.base.Class.metadata.name)+'\''; } this.begin(descr, ptr); var shapes = {}, sel = this.selection, canvas = this.canvas; for (var k in sel) { var shape = canvas.getWidget(k); if (!shape || !shape.isShape || shape.base.@protect & #[SVG_PROTECT_MOVE]) continue; var cx = ((shape.ox + shape.cx*shape.os + offset.x) - trgBlock.qx) / trgBlock.qs; var cy = ((shape.oy + shape.cy*shape.os + offset.y) - trgBlock.qy) / trgBlock.qs; shape.moveto(cx,cy); shapes[shape.id] = shape; } var parent=trgBlock.base, parentKey=parent.@shapesKey; if (!RULE('doInsert', base, this.boardAspect, parent, parentKey, this.getBaseObjects(shapes))) raiseRuleError(); this.pfocusID = trgBlock.id; this.adjustBlockLayout(trgBlock, shapes); this.applyGlobalLayout(); this.commit('recompose shape(s)'); } catch(e) { this.rollback(); #LOG[4, 'Failed to recompose shape(s): '+e.description]; } end <@doc scope="private"> Resizes the currently selected shape (in case of multiple selection, only the primary selection is resized) The new shape size The new shape position Additional pointer information virtual method resizeSelection(size, pos, ptr) var shape = this.getFocus(); if (!shape || !shape.isShape) return; try { this.begin('Resize '+(shape.base.name || shape.base.Class.metadata.title||shape.base.Class.metadata.name), ptr); if (shape.isBlock) { var dx = (ptr.dx<0 ? shape.cx-Math.round(pos.x) : 0); var dy = (ptr.dy<0 ? shape.cy-Math.round(pos.y) : 0); shape.shiftContents(2*dx, 2*dy); } shape.moveto(pos.x,pos.y); shape.sizeto(size.w,size.h); if (shape.isBlock) shape.resizeContents(); this.adjustBlockLayout(this.getPFocus(), shape); this.commit('resize shape'); } catch(e) { this.rollback(); #LOG[4, 'Failed to resize shape: '+e.description]; } end <@doc scope="private"> Rotates or flips the currently selected shapes +1=rotate clockwise, -1=rotate counter-clockwise, SVG_FLIPX=flip horizontal, SVG_FLIPY=flip vertical virtual method rotateSelection(dir) if (!this.isEditable) return; var func = (dir==#[SVG_FLIPX] ? 'flipX' : dir==#[SVG_FLIPY] ? 'flipY' : 'rotate'); var prm = (func == 'rotate' ? dir : void(0)); var nsel = this.nselected; if (nsel <= 1) { var focus = this.getFocus(); if (nsel==0 || !focus.isShape) return; try { var transLabel = (dir==#[SVG_FLIPX] || dir==#[SVG_FLIPY] ? '#TEXT[XMIT_TRANS_FLIP_ELEMENT]' : '#TEXT[XMIT_TRANS_ROTATE_ELEMENT]'); this.begin(transLabel); focus[func](prm); this.adjustBlockLayout(this.getPFocus(), focus); this.commit(func+' Element'); } catch(e) { #LOG[4, 'Failed to '+func+' shape(s): '+e.description]; this.rollback(); } return; } var shapes=this.getSelectedShapes(), nshapes=0; var m=Number.MAX_VALUE, bbox={x1:m, y1:m, x2:-m, y2:-m}; for (var k in shapes) { var shape = shapes[k]; bbox.x1 = Math.min(bbox.x1, shape.x1); bbox.y1 = Math.min(bbox.y1, shape.y1); bbox.x2 = Math.max(bbox.x2, shape.x2); bbox.y2 = Math.max(bbox.y2, shape.y2); nshapes++; } if (nshapes==0 || bbox.x2<=bbox.x1 || bbox.y2<=bbox.y1) return; var cx=(bbox.x2+bbox.x1)/2, cy=(bbox.y2+bbox.y1)/2, dx, dy, sx, sy, px, py; switch (dir) { case #[SVG_FLIPX]: dx=bbox.x2+bbox.x1; sx=-1; dy=0; sy=+1; px='cx'; py='cy'; break; case #[SVG_FLIPY]: dx=0, sx=+1; dy=bbox.y2+bbox.y1; sy=-1; px='cx'; py='cy'; break; default: dx=cx+dir*cy; sx=-dir; dy=cy-dir*cx; sy=dir; px='cy'; py='cx'; break; } try { this.begin(func+' shapes'); for (var k in shapes) { var shape=shapes[k]; shape.moveto(dx+sx*shape[px],dy+sy*shape[py]); shape[func](prm); } this.adjustBlockLayout(this.getPFocus(), shapes); this.commit(func+' shape'); } catch(e) { #LOG[4, 'Failed to '+func+' shape(s): '+e.description]; this.rollback(); } end ////////////////////////////////////////////////////////////////////////////////////// // SELECTION MANAGER // invariant: all selected shapes must be directly contained by the same block <@doc scope="private"> Sets the selection focus virtual method setFocus(focus, quiet, force) if (focus) focus.showFocusEffect(); else focus=null; var oldFocus = this.getFocus(); if (oldFocus === focus && !force) return; if (oldFocus) oldFocus.hideFocusEffect(); this.focusID = focus && focus.id || ''; this.setupHandles(); if (!quiet) $ENV.context = focus ? focus.base : base; end <@doc scope="private"> Selects a graphic by a specified Id virtual method selectById(id, additive, quiet, doFocus) // Setting doFocus default value to TRUE doFocus = (doFocus === undefined) ? true : doFocus; if (this.board.checkFreeze(#[SVG_FREEZE_CANVAS])) return; var g = this.getGraphic(id); if (!g) return false; var focus=null, sel=this.selection, parent=g.getParent(); var canvas = this.canvas; if (parent.id !== this.pfocusID) { for (var k in sel) { var widget = canvas.getWidget(k); if (widget) widget.hideSelectionEffect(); delete sel[k]; } this.nselected = 0; this.pfocusID = parent.id; } if (this.getFocus() == g) { if (!additive) return; g.hideSelectionEffect(); delete sel[id]; this.nselected--; for (var k in sel) { focus = canvas.getWidget(k); break; } } else { if (!g.isSelected) { if (!additive) { for (var k in sel) { canvas.getWidget(k).hideSelectionEffect(); delete sel[k]; } this.nselected = 0; } sel[id] = true; g.showSelectionEffect(); this.nselected++; this.addAttachedElement2Selection(g); } focus = g; } if (doFocus) this.setFocus(focus, quiet); return true; end <@doc scope="private"> Selects a collection of graphics by a given list of Ids virtual method selectByIdList(idList, focusId, quiet) var force = false; if (this.board.checkFreeze(#[SVG_FREEZE_CANVAS])) return; var sel = this.selection, canvas = this.canvas; for (var k in sel) { var widget = canvas.getWidget(k); if (widget) widget.hideSelectionEffect(); delete sel[k]; } this.nselected = 0; var focus = focusId && this.getGraphic(focusId) || null; if (idList) { for (var i=0, len=idList.length; i Selects all graphics in a given area virtual method selectByArea(rect, anchor, quiet) if (this.board.checkFreeze(#[SVG_FREEZE_CANVAS])) return; var rect = SVG.normalizeRect(rect); var ax = (anchor ? anchor.x : rect.x); var ay = (anchor ? anchor.y : rect.y); // clear the current selection var focus = null, sel = this.selection, canvas = this.canvas; for (var k in sel) { var widget = canvas.getWidget(k); if (widget) widget.hideSelectionEffect(); delete sel[k]; } this.nselected = 0; this.pfocusID = this.id; // find all matching shapes, and find the topmost block that encloses matching shapes and is closest to the anchor point var G=this.graphics, matches={}, mindepth=Number.MAX_VALUE, mindist=Number.MAX_VALUE; var method=(GETVAR('SVG_PARTIAL_SELECT') ? 'intersects' : 'enclosedIn'); for (var k in G) { var g=this.getGraphic(k), parent=g.getParent(); if (parent==this || !g[method](rect)) continue; matches[k] = g; for (var depth=0; g && g != this; depth++, g=g.getParent()); if (depth > mindepth) continue; var dist = (parent != this) ? Math.sqrt(SQ(parent.ox+parent.cx*parent.os-ax)+SQ(parent.oy+parent.cy*parent.os-ay)) : 0; if (depth < mindepth) { mindepth = depth; mindist = dist; this.pfocusID = parent.id; continue; } if (parent && dist Selects all shapes in the block containing the current selection virtual method selectAll(quiet) if (this.board.checkFreeze(#[SVG_FREEZE_CANVAS])) return; var focus=this.getFocus(), pfocus=this.getPFocus(), sel=this.selection; var canvas = this.canvas; for (var k in sel) { var widget = canvas.getWidget(k); if (widget) widget.hideSelectionEffect(); delete sel[k]; } this.nselected = 0; if (pfocus == this) pfocus = this.getRoot(); var shapes = pfocus.base[pfocus.base.@shapesKey]; for (var k in shapes) { var g = canvas.getWidget(k); if (!g) continue; sel[k] = true; if (!focus) focus = g; g.showSelectionEffect(); this.nselected++; } var lines = pfocus.lines ? pfocus.lines : pfocus.base[pfocus.base.@linesKey]; for (var k in lines) { var g = canvas.getWidget(k); if (!g) continue; sel[k] = true; if (!focus) focus = g; g.showSelectionEffect(); this.nselected++; } this.setFocus(focus, quiet); end <@doc scope="private"> Clears the current selection virtual method clearSelection(quiet) var sel = this.selection, canvas = this.canvas; for (var k in sel) { var widget = canvas.getWidget(k); if (widget) widget.hideSelectionEffect(); delete sel[k]; } this.nselected = 0; this.pfocusID = this.id; this.setFocus(null, quiet); end <@doc scope="private"> Adjusts the wireframes and handles after the current selection has been rearranged virtual method adjustSelection() var sel = this.selection, canvas = this.canvas; for (var k in sel) { var shape = canvas.getWidget(k); if (shape && shape.isShape) shape.adjustWireframe(); } this.adjustHandles(); end <@doc scope="private"> Captures the current selection into a given snapshot object virtual method captureSelection(snapshot) snapshot.focus = this.getFocus() && this.focusID || null; if (this.nselected > 1) { snapshot.selection = []; var sel = this.selection; for (var k in sel) snapshot.selection.push(k); } end <@doc scope="private"> Restores the current selection from a given snapshot object virtual method restoreSelection(snapshot) this.selectByIdList(snapshot.selection, snapshot.focus); end <@doc scope="private"> Gets the collection of selected shapes virtual method getSelectedShapes() var A = {}, S = this.selection, canvas = this.canvas; for (var k in S) { var widget = canvas.getWidget(k); if (widget && widget.isShape) A[k] = widget; } return A; end <@doc scope="private"> Gets the collection of selected lines virtual method getSelectedLines() var A = {}, S = this.selection, canvas = this.canvas; for (var k in S) { var widget = canvas.getWidget(k); if (widget && widget.isLine) A[k] = widget; } return A; end <@doc scope="private"> Gets the base objects of the given graphics collection virtual method getBaseObjects(graphics) var A={}; for (var k in graphics) { A[k] = graphics[k].base; } return A; end <@doc scope="private"> Gets the collection of selected objects (the base objects of the current selection) virtual method getSelectedObjects() var A = {}, S = this.selection, canvas = this.canvas; for (var k in S) { var widget = canvas.getWidget(k); if (widget) A[k] = widget.base; } return A; end <@doc scope="private"> Gets the list of objects contained in the given area virtual method getObjectsInArea(rect) var rect = SVG.normalizeRect(rect), list=[]; var func = (GETVAR('SVG_PARTIAL_SELECT') ? 'intersects' : 'enclosedIn'); var G = this.graphics; for (var k in G) { var g=this.getGraphic(k); if (g[func](rect)) list.push(g.base); } return list; end ////////////////////////////////////////////////////////////////////////////////////// // MOUSE EVENTS virtual method setupPointer(evt) if (evt.target.parentNode == this.handlesLayer && this.getFocus() && this.isEditable) { return new SvgPointer('onHandleMove', evt, this.focusID, this.canvas); } end virtual method mouseover(evt, ptr) if (evt.target.parentNode == this.handlesLayer && this.isEditable) { if (!ptr) SVG.setProperty(evt.target, 'class', 'handleHilight'); if (ptr && ptr.source == evt.target) ptr.isOverSource=true; } end virtual method mouseout(evt, ptr) if (evt.target.parentNode == this.handlesLayer && this.isEditable) { if (!ptr) SVG.setProperty(evt.target, 'class', 'handleNormal'); if (ptr && ptr.source == evt.target) ptr.isOverSource=false; } end ////////////////////////////////////////////////////////////////////////////////////// // HANDLES MANAGER <@doc scope="private"> Prepares the control handles for a new focus virtual method setupHandles() if (!this.getFocus() || !this.isEditable) { SVG.hide(this.handlesLayer); return; } SVG.show(this.handlesLayer); var H = this.handlesList; var D = this.getFocus().getHandlesData(); var R = 3/Math.sqrt(this.canvas.scale); if (D) { for (var k in D) { if (!(k in H)) { H[k] = SVG.createElement(this.handlesLayer, 'circle', {handle:k, r:R, cx:D[k].cx, cy:D[k].cy, 'class':'handleNormal'}); } else { H[k].setAttribute('cx', D[k].cx); H[k].setAttribute('cy', D[k].cy); } SVG.show(H[k]); } } for (var k in H) { if (!D || !(k in D)) SVG.hide(H[k]); } end <@doc scope="private"> Adjusts the control handles virtual method adjustHandles() if (!this.getFocus()) return; var H=this.handlesList; var D=this.getFocus().getHandlesData(); if (!D) return; for (var k in D) { if (!(k in H)) continue; H[k].setAttribute('cx', D[k].cx); H[k].setAttribute('cy', D[k].cy); } end <@doc scope="private"> Moves a specified control handle virtual method moveHandle(id, cx, cy) var h=this.handlesList[id]; if (!h) return; h.setAttribute('cx', cx); h.setAttribute('cy', cy); end ////////////////////////////////////////////////////////////////////////////////////// // MOVE HANDLER virtual method moveHandler(ptr) switch (ptr.phase) { case #[PTR_START]: ptr.scrollMode = #[PTR_AUTOSCROLL]; ptr.snapUnit = GETVAR('SVG_SMOOTH_DRAG') ? 1 : #[SVG_SNAPUNIT]; ptr.snapshot = this.board.captureSnapshot(); ptr.isLocked = !this.isEditable; ptr.canRecompose = (ptr.button == #[PTR_LEFT] && ptr.key != #[PTR_SHIFT]); ptr.visualCues = GETVAR('SVG_VISUAL_CUES'); break; case #[PTR_FIRSTMOVE]: if (ptr.isLocked) break; SVG.hide(this.handlesLayer); ptr.dropTargetID = this.pfocusID; SVG.setProperty(this.getRoot().borderNode, 'pointer-events', 'visible'); this.adjustWireframes(); ptr.shapes = {}; var sel=this.selection, canvas=this.canvas; this.hideTooltip(); for (var k in sel) { var g=canvas.getWidget(k); if (!g || !g.isShape) continue; g.showDragEffect(); if (g.base.@protect & #[SVG_PROTECT_MOVE]) continue; ptr.shapes[k] = g.base; } break; case #[PTR_MOVE]: if (ptr.isLocked) break; var off=ptr.offset, u=ptr.snapUnit; var dx=Math.round(off.x/u)*u, dy=Math.round(off.y/u)*u; SVG.setProperty(this.wiresLayer, 'transform', 'translate('+dx+' '+dy+')'); break; case #[PTR_LASTMOVE]: if (ptr.isLocked) break; SVG.show(this.handlesLayer); if (ptr.visualCues && ptr.dropTargetID != this.id) { this.canvas.getWidget(ptr.dropTargetID).hideDropEffect(); } SVG.setProperty(this.getRoot().borderNode, 'pointer-events', 'none'); var sel=this.selection, canvas=this.canvas; for (var k in sel) { var widget = canvas.getWidget(k) if (widget && widget.isShape) widget.hideDragEffect(); } break; case #[PTR_FINISH]: if (!ptr.moved) break; SVG.setProperty(this.wiresLayer, 'transform', 'translate(0 0)'); var psrc=this.getPFocus()||this, ptrg=this.canvas.getWidget(ptr.dropTargetID)||this; if (psrc === ptrg) { this.moveSelection(ptr.offset, ptr); } else { this.recomposeSelection(ptr.offset, psrc, ptrg, ptr); } break; case #[PTR_CANCEL]: if (!ptr.moved) break; SVG.setProperty(this.wiresLayer, 'transform', 'translate(0 0)'); break; } end ////////////////////////////////////////////////////////////////////////////////////// // DRAG AND DROP HANDLER virtual method dragenter(dnd, pos, evt) var sprite=this.spriteData; if (!dnd || sprite.dragging || dnd.effect == #[DND_NONE]) return; if (!sprite.allnodes) sprite.allnodes = {}; try { var baseName = dnd.gmlclass; var baseClass = CLASS(baseName); var aspectClass = $DOM.getAspectOf(baseClass, '#NS[ZDrawing]'); if (!baseClass || !aspectClass) return; if (this.isEditable) { if (sprite.node && sprite.baseName != baseName) { SVG.hide(sprite.node); sprite.node = null; } if (!sprite.node) { var bp = baseClass.prototype, BB=bp.@boundingBox; var w1 = BB && BB.defWidth || 80; var h1 = BB && BB.defHeight || 60; var w2 = bp.@spriteWidth || w1; var h2 = bp.@spriteHeight || h1; sprite.width = w2; sprite.height = h2; sprite.node = sprite.allnodes[baseName]; sprite.baseName = baseName; sprite.baseSize = Math.round(w1)+' '+Math.round(h1); } if (!sprite.node) { sprite.node = aspectClass.prototype.createSprite(this.wiresLayer, aspectClass, baseClass, w2, h2); sprite.allnodes[baseName] = sprite.node; } this.adjustWireframes(); SVG.show(sprite.node); } else { if (sprite.node) { SVG.hide(sprite.node); sprite.node = null; } } sprite.parentID = ''; sprite.lastPos = {x:Math.round(pos.x), y:Math.round(pos.y)} sprite.targetTimer = 0; sprite.dragging = true; sprite.visualCues = GETVAR('SVG_VISUAL_CUES'); sprite.snapUnit = GETVAR('SVG_SMOOTH_DRAG') ? 1 : #[SVG_SNAPUNIT]; if (dnd && dnd.spriteObj) dnd.spriteObj.style.display = 'none'; } catch(e) { #LOG[4, e.message]; } end virtual method dragleave(dnd, pos, evt) var sprite=this.spriteData; if (!sprite.dragging) return; sprite.dragging = false; if (sprite.node) SVG.hide(sprite.node); if (sprite.visualCues && sprite.parentID) { this.canvas.getWidget(sprite.parentID).hideDropEffect(); } if (dnd && dnd.spriteObj) dnd.spriteObj.style.display = 'block'; end virtual method dragover(dnd, pos, evt) var sprite=this.spriteData; if (!sprite.dragging) return; if (!this.isEditable || dnd.source == base) { $CTL.setDropEffect(evt, #[DND_NONE]); return; } $CTL.setDropEffect(evt, dnd.effect || #[DND_COPY]); var pos={x:Math.round(pos.x), y:Math.round(pos.y)}; var canReplace = dnd.canReplace; var timestamp=(new Date()).getTime(); if (timestamp > sprite.targetTimer+100) { sprite.targetTimer = timestamp; var oldp = this.canvas.getWidget(sprite.parentID); var newp = null; var handleComponenet = true; var baseClass = CLASS(dnd.gmlclass); // TODO : remove this logic from here (Kobi Stok) // Replace usage case if (canReplace && ISA(baseClass, 'core.gml2:Component')) { var shape = this.findShapeAt(pos, 'core.gml2:PComponentUsage'); if(shape && ISA(shape.base, 'core.gml2:PComponentUsage')) { var newType = RULE("defineUsageClass", base, baseClass, null, dnd.runtimeData); var origType = shape.base.Class.fullname; if (origType == newType) newp = shape; handleComponenet = false; } } if (handleComponenet) { newp = this.findBlockAt(pos, oldp, dnd) || this.getRoot(); if (newp && !RULE('canInsert', this.base, this.boardAspect, newp.base, baseClass)) newp = null; } if (newp !== oldp) { if (sprite.visualCues) { if (oldp) oldp.hideDropEffect(); if (newp) newp.showDropEffect(); } sprite.parentID = newp ? newp.id : ''; } } if (pos.x != sprite.lastPos.x || pos.y != sprite.lastPos.y) { sprite.lastPos = pos; if (sprite.node) { var x=pos.x, y=pos.y, u=sprite.snapUnit; if (u != 1) x=Math.round(x/u)*u, y=Math.round(y/u)*u; SVG.setTransform(sprite.node, x, y); } } end virtual method drop(dnd, pos, evt) var sprite = this.spriteData; var parent = this.canvas.getWidget(sprite.parentID); if (!this.isEditable || !sprite.dragging || !parent) { this.dragleave(); return; } if (parent == this) parent = this.getRoot(); var pbase = parent.base; var baseX = Math.round((pos.x-parent.qx)*parent.qs); var baseY = Math.round((pos.y-parent.qy)*parent.qs); dnd.board = '#NS[ZDrawing]'; dnd.target = base; dnd.parent = pbase; dnd.point = pos; dnd.values = {'@pos':baseX+' '+baseY, '@size':sprite.baseSize}; this.hideMenus(); this.dragleave(dnd); $CTL.setDropEffect(evt, dnd.effect || #[DND_COPY]); end <@doc scope="private"> Creates the diagram's own sprite object virtual method createSprite(parentNode, aspectClass, baseClass, w, h) var color = baseClass.prototype.@spriteColor; var fcolor = baseClass.prototype.@fillColor; var sprite = SVG.createElement(parentNode, 'g', {'class':'spriteframe', $stroke:color, $fill:fcolor||color}); var parser = $DOM.getAspect('#NS[ZShape]').prototype.parseParts; var data = parser(aspectClass, baseClass, baseClass.prototype.@spriteParts); for (var i=0, len=data.wireParts.length; i The diagram popup menus handler virtual method menusHandler(ptr) var pos=ptr.pos, off=ptr.offset, org=ptr.origin; switch (ptr.phase) { case #[PTR_START]: SVG.show(this.rubberbandObj); SVG.setRect(this.rubberbandObj, 0, 0, 0, 0); var scale=this.canvas.scale; SVG.setProperty(this.rubberbandObj, '$stroke-width', 1/scale); SVG.setProperty(this.rubberbandObj, '$stroke-dasharray', (3/scale)+' '+(3/scale)); ptr.scrollMode = #[PTR_AUTOSCROLL]; break; case #[PTR_MOVE]: SVG.setRect(this.rubberbandObj, org.x, org.y, off.x, off.y); break; case #[PTR_FINISH]: var rflag=(ptr.button & #[PTR_RIGHT]); var rect=SVG.normalizeRect({x:org.x, y:org.y, w:off.x, h:off.y}); if (!ptr.moved || (rect.w <10 || rect.h<10)) { if (rflag) { var obj = (this.getFocus() || this).base; var menu = RULE('defineContextMenu', base, this.boardAspect, obj, ptr.pos); if (menu) this.openMenu(menu, pos, menu.callback); } } else { if (rflag) { var list = this.getObjectsInArea(rect); var menu = RULE('defineSelectionMenu', base, this.boardAspect, list, rect, ptr.pos); if (menu) this.openMenu(menu, pos, menu.callback); } else { this.selectByArea(rect, org); this.hideMenus(); } } break; case #[PTR_CANCEL]: SVG.hide(this.rubberbandObj); break; } end <@doc scope="private"> Opens a context menu at the specified location The context menu items Position of the context menu, in logical units Optional callback function to invoke when a menu item is selected. If omitted, then the SIGNAL macro will be invoked. virtual method openMenu(menu, pos, callback) if (!menu || menu.length == 0) { this.hideMenus(); return; } var diagram = this; var pclient = this.canvas.canvasToClient(pos.x, pos.y); var pscreen = this.board.clientToScreen(pclient); CONTEXT_MENU(menu, handleMenu, 'SCREEN', pscreen.x-4, pscreen.y-4); function handleMenu(signal, index, descr) { var cb = menu[index].callback || callback || null; if (cb) cb(signal, descr); else SIGNAL(signal); diagram.hideMenus(); if (diagram.isZoomable) { // TODO: BlockZoom work var nblock = (!signal || (signal == 'cancel')) ? null : this.canvas.getWidget(diagram.zoomTargetID); diagram.zoomTargetID = ''; diagram.finishZoomableAction(nblock); } } end <@doc scope="private"> Hides any open menus virtual method hideMenus() this.dragleave(); SVG.hide(this.rubberbandObj); if (this.rubberwireObj) { SVG.removeElement(this.rubberwireObj); this.rubberwireObj = null; } HIDE_POPUP(); end ////////////////////////////////////////////////////////////////////////////////////// // TOOLTIPS HANDLER <@doc scope="private"> Shows the tooltip of a specified peer element virtual method showTooltip(peer) if (!this.tooltipsMode) return; var scale = ((peer.shapeID ? peer.getShape().os*0.9 : peer.os) || 1)*this.canvas.scale; var text = peer.base.getTooltip(scale); if (text) { var data = peer.getTooltipPos(); data.fgcolor = peer.base.@strokeColor; data.bgcolor = 'white'; this.canvas.showTooltip(text, data); } else { this.hideTooltip(); } end <@doc scope="private"> Hides the currently displayed tooltip, if any virtual method hideTooltip() this.canvas.hideTooltip(); end ////////////////////////////////////////////////////////////////////////////////////// // GEOMETRIC CALCULATIONS <@doc scope="private"> Gets the diagram bounding box virtual method getBBox() if (!this.isZoomable) { // TODO: The following is the old code that was always 'deep'. // Old code uses shapesLayer.getBBox(), while new code uses ZBlock.getBBox() // The results are bit different. Try to see why, fix and remove the commented part var bbox = this.shapesLayer.getBBox(); if (GETVAR('SVG_SHOW_BORDER')) return bbox; var rbox = this.getRoot().shapesLayer.getBBox(), W=400, H=300; if (rbox.width<0 || rbox.height<0) return {x:0,y:0,width:W,height:H}; var x=bbox.x+rbox.x, y=bbox.y+rbox.y, w=rbox.width, h=rbox.height; if (w < W) x-=(W-w)/2, w=W; if (h < H) y-=(H-h)/2, h=H; return {x:x, y:y, width:w, height:h}; } else { if (GETVAR('SVG_SHOW_BORDER')) { var root = this.getRoot(); return {x: root.ox+root.x1*root.os, y: root.oy+root.y1*root.os, width: root.w*root.os, height: root.h*root.os}; } var bbox = this.shapesLayer.getBBox(); var rbox = root.getBBox(), W=400, H=300; if (rbox.width<0 || rbox.height<0) return {x:0,y:0,width:W,height:H}; var x=bbox.x+rbox.x, y=bbox.y+rbox.y, w=rbox.width, h=rbox.height; if (w < W) x-=(W-w)/2, w=W; if (h < H) y-=(H-h)/2, h=H; return {x:x, y:y, width:w, height:h}; } end <@doc scope="private"> Gets the innermost block that contains the specified point virtual method findBlockAt(pos, candidate, dnd) var canvas = this.canvas; if (candidate) { // TODO: this is an optimization but it causes incorrect result in case the // candidate is obscured at the given pos by another block at a higher z-order var block = findBlock(candidate); if (block) return block; } // otherwise test all diagram blocks, in nesting order var block = findBlock(this.getRoot()); return block || null; //TODO: need to consider also z-order and visibility function findBlock(block) { if (!block.isBlock) return null; var x=pos.x-block.ox, y=pos.y-block.oy, s=block.os; if (xblock.x2*s || yblock.y2*s) return null; var node = block.shapesLayer && block.shapesLayer.lastChild; while (node) { var cblock = findBlock(canvas.getWidget(node.peerID)); if (cblock) return cblock; node = node.previousSibling; } return block; } end virtual method findShapeAt(pos, scope) var rect=SVG.normalizeRect({x:Math.round(pos.x), y:Math.round(pos.y), w:30, h:30}); var G = this.graphics; for (var k in G) { var shape = this.getGraphic(k); if(!shape || !shape.base) continue; if(scope) { if(shape.base.isa(scope) && shape.intersects(rect)) return shape; } else if(!shape.base.isa('gml2:Window') && shape.intersects(rect)) return shape; } return null; end ////////////////////////////////////////////////////////////////////////////////////// // LAYOUT METHODS <@doc scope="private"> Adjusts the diagram layout after the contents of the given block have been rearranged to ensure all layout constraints are satisfied virtual method adjustBlockLayout(block, anchors) while (block && block != this) { block.adjustLayout(anchors); anchors = block; block = block.getParent(); } this.globalDiagramRouting(block); this.adjustSelection(); end <@doc scope="private"> Recomputes the layout of the entire diagram virtual method globalDiagramLayout() var canvas = this.canvas; layoutUpward(this.getRoot()); this.adjustSelection(); function layoutUpward(block) { var S=block.base[block.base.@shapesKey]; for (var k in S) { var s = canvas.getWidget(k); if (s && s.isBlock) layoutUpward(s); } if (block.adjustLayout) block.adjustLayout(); } end <@doc scope="private"> Recomputes the lines routing in the entire diagram virtual method globalDiagramRouting(block) var canvas = this.canvas; var rm = GETVAR('SVG_ROUTING_MODE'); var RM = rm || base.@routingMode || #[SVG_STRAIGHT_LINE]; this.defaultRouting = RM; rerouteDownward(block || this.getRoot()); this.adjustSelection(); function rerouteDownward(block) { var L=block.lines ? block.lines : block.base[block.base.@linesKey]; var S=block.base[block.base.@shapesKey]; for (var k in L) { var line = canvas.getWidget(k); if (!line) continue; line.lineRouting = (rm ? 0 : L[k].@routingMode) || RM; line.reroute(); } for (var k in S) { var s = canvas.getWidget(k); if (s && s.isBlock) rerouteDownward(s); } } end ////////////////////////////////////////////////////////////////////////////////////// // UTILITIES <@doc scope="private"> Creates a new graphic and inserts it into the diagram virtual method createGraphic(baseObj, parent) var g = $DOM.createAspect('#NS[ZDrawing]', baseObj, this, parent||this); if (g) this.graphics[g.id] = true; return g || null; end <@doc scope="private"> Removes a graphic from the diagram virtual method removeGraphic(id) var g = this.canvas.getWidget(id); if (!g) return false; if (g.Destructor) g.Destructor(); else this.canvas.removeWidget(id) delete this.graphics[g.id]; return true; end <@doc scope="private"> Gets a specified graphic by id. Returns a reference to an instance of aspect virtual method getGraphic(id) return this.graphics[id] && this.canvas.getWidget(id) || null; end <@doc scope="private"> Gets a specified pin by id virtual method getPin(id) // var plug = base.allElements[id]; // var elem = plug && plug.parent || null; // var shape = elem && this.graphics[elem.id]; // return shape && shape.pins && shape.pins[id] || null; return this.canvas.getWidget(id); end <@doc scope="private"> Gets any drawing aspect (diagram, graphic, or pin) by id virtual method getDrawingAspect(id) if (id == base.id) return this; var g=this.getGraphic(id); if (g) return g; var pin = this.getPin(id); if (pin) return pin; return null; end <@doc scope="private"> Activates/deactivates the diagram background for pointer events virtual method activateBackground(flag) { this.getRoot().borderNode.setAttribute('pointer-events', flag ? 'visible' : 'none'); } <@doc scope="private"> Gets the specified colored marker virtual method getColoredMarker(markerId, color) var coloredId = markerId + '_' + color.replace(/^\#/,''); // if marker is already cached, return it if (coloredId in this.coloredMarkers) { return this.coloredMarkers[coloredId]; } // if marker already exists, cache and return it var marker=SVG.getElement(coloredId); if (marker) { this.coloredMarkers[coloredId] = 'url(#'+coloredId+')'; return this.coloredMarkers[coloredId]; } // look for marker template var k = markerId.search(/\d+$/); var markerPref = markerId.substring(0,k); var markerCode = POS(markerId.substring(k)); var arrowCode = (markerCode & #[SVG_ALL_ARROWS]); var isFilled = (markerCode & #[SVG_FILLED]); var marker=SVG.getElement(markerPref+arrowCode); if (!marker) return ''; // create new marker based on template and given color var fragment = printNode(marker); fragment = fragment.replace(/arrowStroke/g, color); fragment = fragment.replace(/arrowFill/g, isFilled ? color : 'white'); fragment = fragment.replace(/id=\"\w+\"/g, 'id="'+coloredId+'"'); SVG.createFragment(SVG.getElement('globalDefs'), fragment); return 'url(#'+coloredId+')'; end <@doc scope="private"> Gets the pin symbol for the given pin virtual method getPinSymbol(pin) var symbolCode = INT(pin.getSymbol()); if (symbolCode & #[SVG_NOSYMBOL]) return null; var pShape = pin.getShape(); if (symbolCode & #[SVG_CUSTOM_PIN]) { symbolCode = symbolCode & 0xFF; var pinId = 'pin' + pShape.base.Class.name + symbolCode; } else { symbolCode = (symbolCode & #[SVG_ALL_PINS]) || #[SVG_CLASSIC_PIN]; var pinId = 'pinSymbol' + symbolCode; } // if symbol is already cached, return it if (pinId in this.pinSymbols) { return this.pinSymbols[pinId]; } // if symbol already exists, cache and return it var pindata = getPinData(pinId, this); if (pindata) return pindata; // look for symbol template var symbolDefs = pShape.base.@pinSymbols if (!symbolDefs) return null; for (var i=0, key='id="'+symbolCode+'"', len=symbolDefs.length; i 0) break; } if (i == len) return null; // create new symbol based on template var fragment = symbolDefs[i].replace(key, 'id="'+pinId+'"'); var x=SVG.createFragment(SVG.getElement('globalDefs'), fragment); var pindata = getPinData(pinId, this); return pindata || null; function getPinData(pinId, diag) { var pinObj = SVG.getElement(pinId); if (!pinObj) return null; var viewbox = SPLIT(pinObj.getAttribute('viewBox')); var radius = POS(pinObj.getAttribute('radius')) || (viewbox[2]/2) var pindata = {id:'#'+pinId, viewbox:viewbox, radius:radius}; diag.pinSymbols[pinId] = pindata; return pindata; } end <@doc scope="private"> Raises a rule execution error virtual method raiseRuleError() { var errors = $ENV.getKitErrors(); throw new Error(-1, JOIN(errors,' \n') || 'unknown error'); } <@doc scope="private"> Opens a printer-friendly version of the diagram in a new window virtual method printSVGImage() SUBWIN('#URL[zdrawing.board.Print.htm]', this, 'SVG_PRINT', 'menubar=yes'); end <@doc scope="private"> Returns the string representation of the diagram's SVG image virtual method captureSVGImage(maxW, maxH) var canvas = this.canvas, board = this.board; var layer = this.drawingLayer; var border = GETVAR("SVG_SHOW_BORDER"); var snapshot = board.captureSnapshot(); var obox = canvas.outerbox; var x = obox.x, y = obox.y, w = obox.w, h = obox.h; var filter = layer.getAttribute('filter'); SETVAR("SVG_SHOW_BORDER", false); canvas.toggleScrollers(); canvas.scaleTo(1); board.setBoundingBox(0, 0, maxW, maxH); canvas.fitDiagram(); if (canvas.scale > 1) { canvas.scaleTo(1); canvas.centerView(true, true); } layer.setAttribute('filter', 'url(#GrayScale)'); var svgimage = printNode(layer.parentNode); layer.setAttribute('filter', filter); canvas.toggleScrollers(); board.setBoundingBox(x, y, w, h) SETVAR("SVG_SHOW_BORDER", border); board.restoreSnapshot(snapshot); return {globals:printNode(SVG.getElement('globalDefs')), image:svgimage} end <@doc scope="private"> Starts a diagram annotate operation virtual method startAnnotate(pos) if (!this.isEditable || !pos) return; var parent = this.getFocus(); if (!parent || parent == this) parent = this.getRoot(); if (!parent.isBlock) parent = parent.getParent(); if (!parent) return; var cbox = this.canvas.canvasToClient(pos.x, pos.y, 200, 75); var cpos = this.board.clientToBrowser({x:cbox.x, y:cbox.y}); var ppos = Math.round((pos.x-parent.qx)/parent.qs) + ' ' + Math.round((pos.y-parent.qy)/parent.qs); var data = { multi: true, isArray: true, bgcolor: '#FFFFE1', fgcolor: '#[BLK]', callback: this.endAnnotate, delegate: this, parent: parent.base, pos: ppos, x: cpos.x, y: cpos.y, w: MAX(cbox.w,200), h: MAX(cbox.h,75) }; this.board.textedit.show(data); end <@doc scope="private"> Starts a diagram rename operation virtual method startRename() var focus = this.getFocus(); if (!this.isEditable || !focus) return; var elem = focus.base; if (!RULE('canRename', base, this.boardAspect, elem)) return; this.scrollIntoView(focus); focus.startRename(); end <@doc scope="private"> Scrolls to the current selection virtual method scrollToFocus() var focus = this.getFocus(); if (!this.isEditable || !focus) return; var elem = focus.base; this.scrollIntoView(focus); end <@doc scope="private"> Scroll ViewPort to element's position The SVG aspect virtual method scrollIntoView(elem) if (!this.canvas) return; var vp = this.canvas.viewport; // The main Viewport (x, y, w, h) var gap = 20; var scrollX =0, scrollY =0; var scale = elem.os; // Current scalemode if (elem.ox+elem.x1*scale < vp.x) scrollX = elem.ox+elem.x1*scale - vp.x - gap; // Move VP left else if (elem.ox+elem.x2*scale > vp.x+vp.w) scrollX = gap+elem.ox+elem.x2*scale - (vp.x+vp.w); // Move VP right if (elem.oy+elem.y1*scale < vp.y) scrollY = elem.oy+elem.y1*scale - vp.y - gap; // Move VP down else if (elem.oy+elem.y2*scale > vp.y+vp.h) scrollY = gap + elem.oy+elem.y2*scale - (vp.y+vp.h); // Move VP up if (scrollX !=0 || scrollY !=0) this.canvas.scrollBy(scrollX, scrollY); end ////////////////////////////////////////////////////////////////////////////////////// // ZOOM HANDLING virtual method setActiveBlock(block) var oblock = this.canvas.getWidget(this.blockID); if (!block || (oblock == block)) return; try { this.begin('activate block'); // if no old block - either first-time activate, or old block was deleted - unzoom to the root if (oblock) oblock.Unzoom(true); else this.getRoot().UnzoomAll(); block.Zoom(true); this.commit('activate block'); this.blockID = block.id; } catch (e) { #LOG[4, 'Failed to activate block: '+e.description]; this.rollback(); } // block.flyIntoView(); end virtual method zoomBlock(block) try { this.begin('zoom block'); block.Zoom(false); this.commit('zoom block'); } catch (e) { #LOG[4, 'Failed to zoom block: '+e.description]; this.roolback(); } end virtual method startZoomableAction() if (this.inZoomableAction) WRITE('warning: startZoomableAction() - already in zoomable action'); this.inZoomableAction = true; end virtual method finishZoomableAction(nblock) if (!this.inZoomableAction) return; this.inZoomableAction = false; var oblock = this.canvas.getWidget(this.blockID); this.blockID = ''; // this will result in unzooming all blocks up to root if (nblock && oblock) oblock.Unzoom(true); // if there was active old block, and new block is suppied - deactivate old block this.setActiveBlock(nblock ? nblock : oblock); end virtual method setColor(s, color) try { if (!s) return false; var isPort = ISA(s, 'gml2:Port'); var isLink = ISA(s, 'gml2:Link'); var isPin = ISA(s, 'gml2:Plug'); var node = isPort ? 'bodyNode' : (isLink ? 'graphicNode' : (isPin ? 'pinNode' : 'frameNode')); var prop = isPort ? 'color' : 'stroke'; var g = isPin ? this.getPin(s.id) : this.getGraphic(s.id); if (!g) return false; var oldColor = g[node].getAttribute(prop); SVG.setProperty(g[node], prop, color); if (g.labelNode) SVG.setProperty(g.labelNode, 'fill', color); if (g.labelNode2) SVG.setProperty(g.labelNode2, 'fill', color); g.marked = color ? node : false; return oldColor; } catch(e) { return false; } end //private virtual method addAttachedElement2Selection(g) if (!g) return; var sel = this.selection; if (ISA(g.base,'gml2:PAttachedElement')) { var attElem = g.base.attachedElement || null; if (attElem) { var g2 = this.getGraphic(attElem.id); if (!sel[g2.id]) { sel[g2.id] = true; g2.showSelectionEffect(); this.nselected++; } g2 = this.getGraphic(g.base.getLink().id); if (sel[g2.id]) { sel[g2.id] = true; g2.showSelectionEffect(); this.nselected++; } } } end