<@doc hierarchy="GMLDOM"> Base aspect for all planar diagram types (c) SAP AG 2003-2006. All rights reserved. #INCLUDE[dev:defs.inc] abstract Aspect Diagram for GmlDrawing; constructor(board, rootNode) this.board = board; this.canvas = board.canvas; this.rootNode = rootNode; this.selgroup = this; this.selection = {}; this.allGraphics = {}; this.allGroups = {}; this.allLines = {}; this.allShapes = {}; this.allPins = {}; this.subShapes = {}; this.subGroups = {}; this.spriteData = {}; this.isEditable = base.isEnabled(); //used to be base.isEditable(0, but now a readOnly model can be enabled for editing. this.isReadonly= base.isEditable(); // This is used to decide if to write the string "readonly" on the storyboard header - see Caption.svg board.freeze(#[SVG_FREEZE_ALL]); this.paintLayers(); this.paintGraphics(); board.unfreeze(#[SVG_FREEZE_ALL]); end destructor for (var k in this.allGraphics) this.removeGraphic(k); SVG.clearElement(this.rootNode); end ////////////////////////////////////////////////////////////////////////////////////// // ASPECT PROPERTIES <@doc type="@Graphic![id]" scope="private">Gets the collection of all graphic primitives in this diagram (shapes, groups, and lines) virtual property allGraphics = null; <@doc type="@Group![id]" scope="private">Gets the collection of all groups in this diagram virtual property allGroups = null; <@doc type="@Line![id]" scope="private">Gets the collection of all lines in this diagram virtual property allLines = null; <@doc type="@Shape![id]" scope="private">Gets the collection of all shapes in this diagram virtual property allShapes = null; <@doc type="@Pin![id]" scope="private">Gets the collection of all pins in this diagram virtual property allPins = null; <@doc type="@Board!" scope="private">Gets the current drawing board object virtual property board = null; <@doc type="@Canvas!" scope="private">Gets the canvas object virtual property canvas = null; <@doc type="SVGNode" scope="private">Gets the diagram drawing SVG layer virtual property drawingLayer = null; <@doc type="@Graphic!" scope="private">Gets the graphic currently in focus (primary selection) virtual property focus = 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 scope="private">Indicates whether the board is in the middle of an open transaction . virtual property inTrans = false; <@doc type="SVGNode" scope="private">Gets the lines SVG layer virtual property linesLayer = null; <@doc scope="private">Gets the number of selected shapes virtual property nselected = 0; <@doc type="@Line![id]" scope="private">Gets the buffer of pending line reroutes in the current transaction. virtual property reroutesBuf = null; <@doc type="SVGNode" scope="private">Gets the root SVG node in which to create the diagram virtual property rootNode = 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="@Graphic![id]" scope="private">Gets the collection of selected graphics virtual property selection = null; <@doc type="@Group" scope="private">Gets the innermost group that contains the current selection, or the diagram in case of a top-level selection virtual property selgroup = null; <@doc type="SVGNode" scope="private">Gets the shapes SVG layer virtual property shapesLayer = null; <@doc type="Object" scope="private">Gets the sprite data object virtual property spriteData = null; <@doc type="SVGNode" scope="private">Gets the sprite SVG layer virtual property spriteLayer = null; <@doc type="@Group![id]" scope="private">Gets the sub-groups collection (i.e., the top-level groups) virtual property subGroups = null; <@doc type="@Shape![id]" scope="private">Gets the sub-shapes collection (i.e., the top-level shapes, including the top-level groups) virtual property subShapes = null; <@doc type="Window" scope="private">Gets the current tooltip window virtual property tooltipWin = null; <@doc type="SVGNode" scope="private">Gets the uniform stroke SVG layer virtual property unistrokeLayer = null; <@doc type="@Object![id]" scope="private">Gets the buffer of pending property updates in the current transaction. virtual property updatesBuf = null; <@doc type="SVGNode" scope="private">Gets the wireframes SVG layer virtual property wireframesLayer = null; ////////////////////////////////////////////////////////////////////////////////////// // PAINTING <@doc scope="private"> Paints the diagram layers virtual method paintLayers() this.drawingLayer = SVG.createElement(this.rootNode, 'g', {id:this.board.allocID()}); this.shapesLayer = SVG.createElement(this.drawingLayer, 'g', {id:this.board.allocID()}); this.linesLayer = SVG.createElement(this.drawingLayer, 'g', {id:this.board.allocID()}); this.unistrokeLayer = SVG.createElement(this.rootNode, 'g'); this.spriteLayer = SVG.createElement(this.unistrokeLayer, 'g'); this.wireframesLayer = SVG.createElement(this.unistrokeLayer, 'g'); this.rubberbandObj = SVG.createElement(this.unistrokeLayer, 'rect', {'class':'wireframe', visibility:'hidden'}); this.handlesLayer = SVG.createElement(this.unistrokeLayer, 'g'); this.handlesLayer.peer = this; this.handlesList = {}; this.adjustScale(this.canvas.scale); end <@doc scope="private"> Paints the diagram graphics virtual method paintGraphics() var C=base.children, L={}, S={}, T={}, G={}, diag=this; // organize diagram's graphics into shapes, groups, and lines for (var k in C) { var ch=C[k]; if (ch.isa('core.gml:Link')) { L[k] = ch; } else { S[k] = T[k] = ch; if (ch.members && HAS_ASPECT(ch, 'core.svg:Group')) G[k] = {}; } } for (var k in G) { var M=S[k].members; for (var k2 in M) { if (!(k2 in S)) continue; G[k][k2] = S[k2]; delete T[k2]; } } // draw shapes and groups according to nesting order for (var k in T) { var g = this.addGraphic(T[k]); if (k in G) { paintGroup(G[k], g); g.updateCoordSys(); } } // draw and reroute all lines // TODO: optimize this by storing the pins layout and rerouting only if it has changed for (var k in L) { var line=this.addGraphic(L[k]); line.reroute(); } function paintGroup(members, group) { for (var k in members) { var ch=S[k]; if (!ch || (k in T)) continue; // to avoid circles delete S[k]; var g=diag.addGraphic(ch, group); if (k in G) paintGroup(G[k], g); } } end <@doc scope="private"> Adds a new graphic for a given base object to this diagram virtual method addGraphic(baseObj, group) var g = $DOM.createAspect('#NS[GmlDrawing]', baseObj, this, group||null); if (!g) return null; this.allGraphics[g.id] = g; return g; end <@doc scope="private"> Removes a specified graphic from this diagram virtual method removeGraphic(id) var g = this.allGraphics[id]; if (!g) return false; if (g.Destructor) g.Destructor(); delete this.allGraphics[g.id]; return true; end <@doc scope="private"> Gets a specified graphic by Id virtual method getGraphic(id) return this.allGraphics[id] || null; end <@doc scope="private"> Handles a mutation event on the base object of this diagram virtual method onObjectMutate(evt) var b=this.board; switch (evt.type) { case 'onupdateobject': if (evt.name == 'notes') { b.canvas.updateDiagram(); } break; case 'oninsertelement': var child=evt.child, state=child.state; var shape = this.addGraphic(child); if (!shape) break; if (ISA(shape, 'core.svg:Shape')) { shape.updateCoordSys(); } if (state) { var group=this.allGroups[state.id] if (group) group.onAttachShape(shape); } this.selectById(shape.id); b.adjustCanvas(); break; case 'onremoveelement': if (!this.removeGraphic(evt.child.id)) break; b.adjustCanvas(); break; } end <@doc scope="private"> Performs adjustments after the diagram bounding box has changed virtual method adjustCanvas() this.canvas.repaint(#[SVG_ADJUST_CONTENT]); end <@doc scope="private"> Performs adjustments after the canvas scale has changed virtual method adjustScale(scale) this.unistrokeLayer.setAttribute('stroke-width', 1/scale); this.unistrokeLayer.setAttribute('stroke-dasharray', 3.5/scale); var H=this.handlesList; for (var k in H) H[k].setAttribute('r', 3/Math.sqrt(scale)); end <@doc scope="private"> Creates a mirror object for this diagram virtual method createMirrorObject(parent) return SVG.createFragment(parent, ''); end <@doc scope="private"> If an object is a usage and removed from the diagram, this function removes its associated diagram from the canvas diagram cache. virtual method removeFromCanvasCache(elementID) var elem = this.base.elements[elementID]; if ( ISA(elem, 'core.gml:Usage')) { var id = elem.getTarget().id; this.canvas.removeFromCache(id, true); } end ////////////////////////////////////////////////////////////////////////////////////// // GENERIC DRAWING TRANSACTION <@doc scope="private"> Begins a drawing transaction Descriptive transaction name Additional pointer information abstract virtual method begin(name, ptr) <@doc scope="private"> Commits a drawing transaction Additional pointer information abstract virtual method commit(ptr) <@doc scope="private"> Rolls back a drawing transaction abstract virtual method rollback() <@doc scope="private"> Buffers a line rerouting operation in the scope of the current transaction The line to reroute abstract virtual method bufferedReroute(line) <@doc scope="private"> Buffers a property update operation in the scope of the current transaction The graphic object to update The name of the property to update The new property value abstract virtual method bufferedUpdate(graphic, prop, value) <@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 abstract virtual method silentUpdate(graphic, prop, value) ////////////////////////////////////////////////////////////////////////////////////// // CONCRETE DRAWING TRANSACTIONS <@doc scope="private"> Copies the currently selected shapes to the clipboard abstract virtual method copyClipboard() <@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 abstract virtual method createLine(gmlclass, srcPlug, trgPlug) <@doc scope="private"> Creates a new base object and its corresponding graphic shape in one step The class of the new base object The property values for initializing the new base object The position of the new shape abstract virtual method createShape(gmlclass, values, pos, targetGroup) <@doc scope="private"> Cuts the currently selected shapes to the clipboard abstract virtual method cutClipboard() <@doc scope="private"> Deletes the currently selected shapes abstract virtual method deleteSelection() <@doc scope="private"> Drills down into the currently selected shape (primary selection) abstract virtual method drilldownSelection() <@doc scope="private"> Duplicates the currently selected shapes abstract virtual method duplicateSelection() <@doc scope="private"> Ends an annotation operation The annotation data abstract virtual method endAnnotate(data) <@doc scope="private"> Ends a rename operation The rename operation data abstract virtual method endRename(data) <@doc scope="private"> Moves the currently selected shapes The move offset Additional pointer information virtual method moveSelection(offset, ptr) try { this.begin('move shape(s)', ptr); var movedShapes={}; for (var k in this.selection) { var shape=this.selection[k]; if (!shape.isa('core.svg:Shape')) continue; if (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); shape.adjustWireframe(); movedShapes[k] = shape; } this.plowShapesList(movedShapes); this.repackSelection(); this.commit(ptr); } 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 abstract virtual method pasteClipboard(pos, force) <@doc scope="private"> Moves all shapes in the diagram away from each other virtual method plowDiagram() try { this.begin('plow diagram'); this.plowAllShapes(); this.commit(); } catch (e) { #LOG[4, 'Failed to plow diagram: '+e.description]; this.rollback(); } end <@doc scope="private"> Regroups the currently selected shapes The move offset The source group The target group Additional pointer information abstract virtual method regroupSelection(offset, srcgrp, trggrp, ptr) <@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.focus; if (!shape) return; try { var plow = (size.w > shape.w || size.h > shape.h); this.begin('resize shape', ptr); this.bufferedUpdate(shape, '@size', size.w+' '+size.h); this.bufferedUpdate(shape, '@pos', pos.x+' '+pos.y); if (plow) this.plowShape(shape); shape.adjustWireframe(); // in case the shape was not actually resized this.adjustHandles(); // in case the shape was not actually resized this.repackSelection(); this.commit(ptr); } 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)); if (this.nselected == 1) { if (typeof this.focus[func] != 'function') return; try { this.begin(func+' shape'); this.focus[func](prm); this.plowShape(this.focus); this.repackSelection(); this.commit(); } catch(e) { #LOG[4, 'Failed to '+func+' shape(s): '+e.description]; this.rollback(); } } if (this.nselected < 2) return; var sel=this.selection, nshapes=0, shapes={}, lines={}; var m=Number.MAX_VALUE, bbox={x1:m, y1:m, x2:-m, y2:-m}; for (var k in sel) { var obj=sel[k] if (!obj.isa('core.svg:Shape')) continue; nshapes++; shapes[k] = obj; bbox.x1 = Math.min(bbox.x1, obj.x1); bbox.y1 = Math.min(bbox.y1, obj.y1); bbox.x2 = Math.max(bbox.x2, obj.x2); bbox.y2 = Math.max(bbox.y2, obj.y2); } 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.plowShapesList(shapes); this.repackSelection(); this.commit(); } catch(e) { #LOG[4, 'Failed to '+func+' shape(s): '+e.description]; this.rollback(); } end ////////////////////////////////////////////////////////////////////////////////////// // SELECTION HANDLER <@doc scope="private"> Selects a graphic by a specified Id virtual method selectById(id, additive) if (this.board.checkFreeze(#[SVG_FREEZE_CANVAS])) return; var g = this.allGraphics[id]; if (!g) return; var focus=null, sel=this.selection; var group=g.getSelGroup(); if (group !== this.selgroup) { // invariant: all selected shapes must be contained by exactly the same group for (var k in sel) { sel[k].unselect(); delete sel[k]; } this.nselected = 0; this.selgroup = group; } if (this.focus == g) { if (!additive) return; g.unselect(); delete sel[id]; this.nselected--; for (var k in sel) { focus = sel[k]; break; } } else { if (!g.selected) { if (!additive) { for (var k in sel) { sel[k].unselect(); delete sel[k]; } this.nselected = 0; } sel[id] = g; if (g.select()) this.nselected++; } focus = g; } this.setFocus(focus); end <@doc scope="private"> Selects a collection of graphics by a given list of Ids virtual method selectByIdList(idList, focusId) if (this.board.checkFreeze(#[SVG_FREEZE_CANVAS])) return; var sel=this.selection; for (var k in sel) { sel[k].unselect(); delete sel[k]; } this.nselected = 0; var G = this.allGraphics; var focus = focusId && G[focusId] || null; if (idList) { for (var i=0, len=idList.length; i Selects all graphics in a given area virtual method selectByArea(rect, anchor) 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; for (var k in sel) { sel[k].unselect(); delete sel[k]; } this.nselected = 0; this.selgroup = this; // find all matching shapes, and find the topmost group that encloses matching shapes and is closest to the anchor point var G=this.allGraphics, matches={}, mindepth=Number.MAX_VALUE, mindist=Number.MAX_VALUE; var method=(GETVAR('PartialSelect') ? 'intersects' : 'enclosedIn' ); for (var k in G) { var g=G[k], group=g.getSelGroup(); if (!g[method](rect)) continue; matches[k] = g; for (var depth=0; g && g != this; depth++, g=g.getSelGroup()); if (depth > mindepth) continue; var dist = (group != this) ? Math.sqrt(SQ(group.ox+group.cx*group.os-ax)+SQ(group.oy+group.cy*group.os-ay)) : 0; if (depth < mindepth) { mindepth = depth; mindist = dist; this.selgroup = group; continue; } if (group && dist Clears the current selection virtual method clearSelection() var sel=this.selection; for (var k in sel) { sel[k].unselect(); delete sel[k]; } this.nselected = 0; this.selgroup = this; this.setFocus(); end <@doc scope="private"> Expands the current selection to all shapes in the containing group virtual method expandSelection() if (this.board.checkFreeze(#[SVG_FREEZE_CANVAS])) return; var focus=this.focus, selgroup=this.selgroup, sel=this.selection, G=this.allGraphics; for (var k in sel) { sel[k].unselect(); delete sel[k]; } this.nselected = 0; for (var k in G) { var g=G[k], group=g.getSelGroup(); if (group !== selgroup) continue; sel[k] = g; if (!focus) focus = g; if (g.select()) this.nselected++; } this.setFocus(focus); end <@doc scope="private"> Repacks the group containing the current selection virtual method repackSelection() var S=this.selection, G=this.selgroup; if (G == this) return; if (!G.repack(GETVAR('DndPlow'))) return; for (var k in S) S[k].adjustWireframe(); this.adjustHandles(); end <@doc scope="private"> Captures the current selection into a given snapshot object virtual method captureSelection(snapshot) snapshot.focus = this.focus && this.focus.id || null; if (this.nselected > 1) { snapshot.selection = []; for (var k in this.selection) 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"> Sets the selection focus virtual method setFocus(focus) if (!focus) focus=null; if (this.focus === focus) return; this.focus = focus; this.setupHandles(); $ENV.context = focus ? focus.base : this.base; end <@doc scope="private"> Gets the table of selected objects (the base objects of the current selection), plus internal group objects virtual method getSelectedObjects() var A={}; var S=this.selection; var empty=true; for (var k in S) { A[k] = S[k].base; empty=false; if (S[k].isa('core.svg:Group')) { var nested = S[k].getNestedShapes(true, true); for (x in nested) { A[x] = nested[x].base; } } } return empty ? null : 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 method = (GETVAR('PartialSelect') ? 'intersects' : 'enclosedIn'); for (var k in this.allGraphics) { var g=this.allGraphics[k]; if (g[method](rect)) list.push(g.base); } return list; end ////////////////////////////////////////////////////////////////////////////////////// // CONTROL HANDLES virtual method setupPointer(evt) if (evt.target.parentNode == this.handlesLayer && this.focus && this.isEditable) { return new SvgPointer('onHandleMove', evt, this.focus, 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 <@doc scope="private"> Prepares the control handles for a new focus virtual method setupHandles() if (!this.focus || !this.isEditable) { SVG.display(this.handlesLayer, false); return; } var H=this.handlesList, D=this.focus.getHandlesData(), scale=this.canvas.scale; SVG.display(this.handlesLayer, true); for (var k in D) { if (!(k in H)) { H[k] = SVG.createElement(this.handlesLayer, 'circle', {handle:k, r:3/Math.sqrt(scale), 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.display(H[k], true); } for (var k in H) { if (!D || !(k in D)) SVG.display(H[k], false); } end <@doc scope="private"> Adjusts the control handles virtual method adjustHandles() if (!this.focus) return; var H=this.handlesList, D=this.focus.getHandlesData(); 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('DndSmooth') ? 1 : #[SVG_SNAPUNIT]; ptr.snapshot = this.board.captureSnapshot(); ptr.isLocked = !this.isEditable; break; case #[PTR_FIRSTMOVE]: if (ptr.isLocked) break; SVG.display(this.handlesLayer, false); ptr.targetGroup = this.selgroup; if (ptr.targetGroup != this) ptr.targetGroup.hilight(); var sel=this.selection; ptr.shapes = {}; for (var k in sel) { if (!sel[k].isa('core.svg:Shape')) continue; if (sel[k].base.@protect & #[SVG_PROTECT_MOVE]) continue; ptr.shapes[k] = sel[k]; } //TODO: raise event to get valid target groups var G=this.allGroups, L=this.allLines; for (var k in G) { var g = G[k]; for (var g2=g; g2 && !g2.selected && g2 != this; g2=g2.group); if (g2 == this) g.setAttachMode(true); } for (var k in L) L[k].setAttachMode(true); break; case #[PTR_MOVE]: if (ptr.isLocked) break; var off=ptr.offset, u=ptr.snapUnit, S=ptr.shapes; for (var k in S) { var shape=S[k], scale=shape.os; var cx=shape.cx+off.x/scale, cy=shape.cy+off.y/scale; if (u != 1) cx=Math.round(cx/u)*u, cy=Math.round(cy/u)*u; shape.adjustWireframe(cx,cy); } break; case #[PTR_LASTMOVE]: if (ptr.isLocked) break; SVG.display(this.handlesLayer, true); if (ptr.targetGroup != this) ptr.targetGroup.unlight(); var G=this.allGroups, L=this.allLines; for (var k in G) G[k].setAttachMode(false); for (var k in L) L[k].setAttachMode(false); break; case #[PTR_FINISH]: if (!ptr.moved) { if (!(ptr.button & #[PTR_RIGHT])) break; var evt = SVG.createModelEvent('onShapeMenu'); var obj = (this.focus || this).base; // Yariv - Fix for CSN 3112180 2006 //evt.object = this.focus.base; evt.object = obj; evt.selection = this.getSelectedObjects(); evt.pos = ptr.pos; evt.menu = $ENV.defineMenubar(); evt.callback = null; evt.cancel = false; $ENV.fireModelEvent(evt); this.openMenu(evt.cancel ? null : evt.menu, ptr.pos, evt.callback); break; } var srcgrp=this.selgroup||this, trggrp=ptr.targetGroup||this; if (srcgrp === trggrp) { this.moveSelection(ptr.offset, ptr); } else { this.regroupSelection(ptr.offset, srcgrp, trggrp, ptr); } break; case #[PTR_CANCEL]: if (!ptr.moved) break; var S=ptr.shapes; for (var k in S) S[k].repaint('@shape'); 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; var evt = SVG.createModelEvent('onDragEnter'); evt.object = base; evt.dnd = dnd; evt.cancel = false; $ENV.fireModelEvent(evt); if(evt.cancel) { 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[GmlDrawing]'); if (!baseClass || !aspectClass) return; if (this.isEditable && GETVAR('DndSprite')) { if (sprite.node && sprite.baseName != baseName) { SVG.display(sprite.node, false); sprite.node = null; } if (!sprite.node) { var bp = baseClass.prototype, geo=bp.@geometry; var dw = bp.@spriteWidth || geo && geo.defWidth || 80; var dh = bp.@spriteHeight || geo && geo.defHeight || 60; sprite.width = dw; sprite.height = dh; sprite.node = sprite.allnodes[baseName]; sprite.baseName = baseName; } if (!sprite.node) { sprite.node = aspectClass.prototype.createSprite(this.spriteLayer, aspectClass, baseClass, dw, dh); sprite.allnodes[baseName] = sprite.node; } SVG.display(sprite.node, true); } else { if (sprite.node) { SVG.display(sprite.node, false); sprite.node = null; } } sprite.targetGroup = null; sprite.targetTimer = 0; sprite.snapUnit = GETVAR('DndSmooth') ? 1 : #[SVG_SNAPUNIT]; sprite.dragging = true; } 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.display(sprite.node, false); if (sprite.targetGroup) sprite.targetGroup.unlight(); end virtual method dragover(dnd, pos, evt) var sprite=this.spriteData; if (!sprite.dragging) return; if (!this.isEditable || dnd.source == this.base) { $CTL.setDropEffect(evt, #[DND_NONE]); return; } $CTL.setDropEffect(evt, dnd.effect || #[DND_COPY]); var timestamp=(new Date()).getTime(); if (timestamp > sprite.targetTimer+150) { sprite.targetTimer = timestamp; var oldgrp = sprite.targetGroup; var newgrp = this.findGroupAt(pos, oldgrp); if (newgrp !== oldgrp) { if (oldgrp) oldgrp.unlight(); if (newgrp) newgrp.hilight(); sprite.targetGroup = newgrp; } } 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) if (!this.isEditable) return; var sprite=this.spriteData, grp=sprite.targetGroup; if (!sprite.dragging) return; var evt = SVG.createModelEvent('onDropMenu'); evt.object = grp && grp.base || base; evt.dnd = dnd; evt.pos = pos; evt.menu = $ENV.defineMenubar(); evt.callback = null; evt.cancel = false; $ENV.fireModelEvent(evt); if (evt.cancel) return; if (evt.menu.length > 0) { this.openMenu(evt.menu, pos, evt.callback); return; } this.hideMenus(); dnd.point = {x:pos.x, y:pos.y}; dnd.target = base; dnd.targetSite = '#NS[GmlDrawing]'; dnd.targetGroup = grp && grp.base || null; this.dragleave(dnd); end <@doc scope="private"> Creates the diagram's sprite object virtual method createSprite(parentNode, aspectClass, baseClass, w, h) return SVG.createElement(parentNode, 'rect', {x:0, y:0, rx:10, width:w, height:h, 'class':'spriteframe'}); end ////////////////////////////////////////////////////////////////////////////////////// // CONTEXT MENUS <@doc scope="private"> 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]: this.clearSelection(); SVG.visible(this.rubberbandObj, true); SVG.setRect(this.rubberbandObj, 0, 0, 0, 0); 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]); if (!ptr.moved) { if (rflag) { var evt = SVG.createModelEvent('onDiagramMenu'); evt.object = this.base; evt.pos = ptr.pos; evt.menu = $ENV.defineMenubar(); evt.callback = null; evt.cancel = false; $ENV.fireModelEvent(evt); this.openMenu(evt.cancel ? null : evt.menu, ptr.pos, evt.callback); } } else { var rect=SVG.normalizeRect({x:org.x, y:org.y, w:off.x, h:off.y}); if (rflag) { var list = this.getObjectsInArea(rect); if (list.length > 0) { var evt = SVG.createModelEvent('onRegionMenu'); evt.object = this.base; evt.selection = list; evt.rect = rect; evt.menu = $ENV.defineMenubar(); } else { var evt = SVG.createModelEvent('onQuickCreateMenu'); evt.object = this.base; evt.rect = rect; evt.menu = $ENV.defineMenubar(); } evt.callback = null; evt.cancel = false; $ENV.fireModelEvent(evt); this.openMenu(evt.cancel ? null : evt.menu, ptr.pos, evt.callback); } else { this.selectByArea(rect, org); this.hideMenus(); } } break; case #[PTR_CANCEL]: SVG.visible(this.rubberbandObj, false); 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) { var cb = menu[index].callback || callback || null; if (cb) cb(signal); else SIGNAL(signal); diagram.hideMenus(); } end <@doc scope="private"> Hides any open menus virtual method hideMenus() this.dragleave(); SVG.visible(this.rubberbandObj, false); if (this.rubberwireObj) { SVG.removeElement(this.rubberwireObj); this.rubberwireObj = null; } HIDE_POPUP(); end <@doc scope="private"> Shows the tooltip of a specified peer element virtual method showTooltip(peer) if (!GETVAR('ShowTooltips')) return; // When dealing with sahpes that their names were shortened (their original name was too long) // We define the scale as 0 in order to show the complete name in the tooltip. if((peer.isa('core.svg:Pin') && peer.name.length > peer.shape.w/20) || (!peer.isa('core.svg:Pin') && peer.base.name.length > $ENV.maxNameLength)) { scale = 0; } else { scale = ((peer.shape ? peer.shape.os*0.9 : peer.os) || 1)*this.canvas.scale; } var text = peer.base.getTooltip(scale); if (!text) return this.hideTooltip(); // measure tooltip size var extBody = this.board.extBody; var div = extBody.children.item('TOOLTIP_MEASURE'); if (!div) { div = extBody.document.createElement('DIV'); div.style.cssText = 'position:absolute; top:-1000; width:auto; height:auto;'; div.id = 'TOOLTIP_MEASURE'; extBody.appendChild(div); } div.style.width = 'auto'; div.innerHTML = text; var w=div.scrollWidth, h=div.scrollHeight; if (w > 250) { div.style.width = 250; div.innerHTML = text; var w=div.scrollWidth, h=div.scrollHeight; } w += 6; h += 2; // compute tooltip position var data = peer.getTooltipData(); var pclient = this.board.canvas.canvasToClient(data.x, data.y); var pscreen = this.board.clientToScreen(pclient); var x=pscreen.x, y=pscreen.y; var align = data.align || 'center middle'; var offset = data.offset || 4; if (align.search(/\bbefore\b/i) >= 0) { x -= w+offset; } else if (align.search(/\bafter\b/i) >= 0) { x += offset; } else { x -= w/2; } if (align.search(/\babove\b/i) >= 0) { y -= h+offset; } else if (align.search(/\bbelow\b/i) >= 0) { y += offset; } else { y -= h/2; } // change tooltip sides in case it goes out of the screen area if (x<0) x = pscreen.x + 10; if (x+w>screen.width) x = pscreen.x - 10 - w; // display the tooltip this.tooltipWin = POPUP_HTML('
'+text+'
', 'DIV{ background:infobackground; color:#424142; border:solid #424142 1; width:100%; height:100%; padding:0 3; font:normal 10px tahoma; overflow:hidden; }', x, y, w, h); end <@doc scope="private"> Hides the currently displayed tooltip, if any virtual method hideTooltip() if (this.tooltipWin) { this.tooltipWin.hide(); this.tooltipWin = null; } end ////////////////////////////////////////////////////////////////////////////////////// // MISCELLANEOUS <@doc scope="private"> Opens a printer-friendly version of the diagram in a new window virtual method printSVGImage() SUBWIN('#URL[drawing.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() return printNode(this.rootNode); end <@doc scope="private"> Starts a diagram annotate operation virtual method startAnnotate(pos) if (!this.isEditable) return; // We must know where to place the annotation ==> pos must hold vaild values // ==> if no pos, place the annotation at top left of the viewport + (10,10) if (!pos) pos = {x:this.canvas.viewport.x+10 , y:this.canvas.viewport.y+10}; var cbox = this.canvas.canvasToClient(pos.x, pos.y, 200, 75); var cpos = this.board.clientToBrowser({x:cbox.x, y:cbox.y}); var data = { multi: true, bgcolor: '#FFFFE1', fgcolor: '#[BLK]', callback: this.endAnnotate, delegate: this, x: cpos.x, y: cpos.y, w: MAX(cbox.w,200), h: MAX(cbox.h,75), pos: pos }; this.board.textedit.show(data); end <@doc scope="private"> Starts a diagram rename operation virtual method startRename() if (!this.isEditable) return; if (this.focus) { this.focus.startRename(); } else { var board=this.board; var cbox = board.caption.outerbox; var cpos = board.clientToBrowser({x:cbox.x+16,y:cbox.y}); var data = {x:cpos.x, y:cpos.y, w:cbox.w-14, h:cbox.h+1, object:base, attr:'name', centerX:false, bgcolor:'#[BG1]'}; var data = { object: this.base, attr: 'name', bgcolor: '#[BG1]', x: cpos.x, y: cpos.y, w: cbox.w-14, h: cbox.h+1 }; board.textedit.show(data); } end ////////////////////////////////////////////////////////////////////////////////////// // LAYOUT METHODS <@doc scope="private"> Gets the diagram bounding box virtual method getBBox() return this.drawingLayer.getBBox(); end <@doc scope="private"> Gets the innermost group that contains the specified point virtual method findGroupAt(pos, candidate) // test candidate group and its contained subb-groups, if given if (candidate) { var g=getgrp(candidate); if (g) return g; } // otherwise test all diagram groups, in nesting order for (var k in this.subGroups) { var g=getgrp(this.subGroups[k]); if (g) return g; } return null; function getgrp(g) { var gx=pos.x-g.ox, gy=pos.y-g.oy, s=g.os, M=g.subGroups; if (gxg.x2*s || gyg.y2*s) return null; for (var k in M) { var g2 = getgrp(M[k]); if (g2) return g2; } return g; } end <@doc scope="private"> Moves all shapes in the diagram away from each other virtual method plowAllShapes() var shapes=[]; scangrp(this); for (var i=0, len=shapes.length; i Moves other shapes away from a given list of shapes (the given shapes must belong to the same group) virtual method plowShapesList(shapes) for (var k in shapes) shapes[k].noplow = true; for (var k in shapes) this.plowShape(shapes[k]); for (var k in shapes) delete shapes[k].noplow; end <@doc scope="private"> Moves other shapes away from the given shape virtual method plowShape(shape) if (!shape) return; if (!GETVAR('DndPlow')) return; var sp0 = #[SVG_SPACING]; // Minimum space between shapes (0 <= SP0 <= SP1) var sp1 = #[SVG_SPACING]; // Plow space for move on drop // scan all placeable child elements into: P=neighbor elements, Q=remaining elements var P={}, Q={}, G=shape.group.subShapes; var x1=shape.x1-sp0, y1=shape.y1-sp0, x2=shape.x2+sp0, y2=shape.y2+sp0; for (var k in G) { var g = G[k]; if (g == shape || g.noplow) continue; if (Math.max(g.x1,x1) <= Math.min(g.x2,x2) && Math.max(g.y1,y1) <= Math.min(g.y2,y2)) P[k]=g; else Q[k]=g; } // for each neighbor element, calculate the dx,dy shift required to plow it away and then recursively // apply the same shift on any remaining elements that become touched as a result var w0=(shape.x2-shape.x1)/2+sp1, h0=(shape.y2-shape.y1)/2+sp1; for (var k in P) { // find the line and angle connecting the centers of the shape and its neighbor var p=P[k], dx=p.cx-shape.cx, dy=p.cy-shape.cy, a=Math.atan2(dy, dx); // compute the new center of the neighbor var w1=(p.x2-p.x1)/2, h1=(p.y2-p.y1)/2; if (Math.abs(dy) <= Math.abs(dx)) { var nx = (dx < 0 ? -1 : 1) * (w0 + w1); var ny = nx * Math.tan(a); } else { var ny = (dy < 0 ? -1 : 1) * (h0 + h1); var nx = ny / Math.tan(a); } dx = nx-dx; dy = ny-dy; // do the actual plow p.moveby(dx, dy); qplow(p); } // recursively detect and shift any remaining shapes function qplow(p) { var R={}, x1=p.x1-sp0, y1=p.y1-sp0, x2=p.x2+sp0, y2=p.y2+sp0; for (var k in Q) { var q=Q[k]; if (Math.max(q.x1,x1) > Math.min(q.x2,x2) || Math.max(q.y1,y1) > Math.min(q.y2,y2)) continue; q.moveby(dx, dy); delete Q[k]; R[k] = q; } for (var k in R) qplow(R[k]); } end ////////////////////////////////////////////////////////////////////////////////////// // EVENTS <@doc> Fires when the user right-clicks the diagram background. Use this event to customize the diagram's context menu. The unit base object of the diagram The pointer position, in logical units relative to the diagram origin The context menu definition object. Use the MENU_BAR methods to customize the menu items. DEPRECATED: Use this parameter to set a callback function that will be invoked when a menu item is selected. If omitted, then the SIGNAL macro will be invoked. Set this flag to ~true to cancel the event event onDiagramMenu(object, pos, menu, callback, cancel); <@doc> Fires when the user right-drags a non-empty region on the diagram. Use this event to customize the region's context menu. The containing diagram The full list of objects encompassed by the region The region bounding box, in logical units relative to the diagram origin The context menu definition object. Use the MENU_BAR methods to customize the menu items. DEPRECATED: Use this parameter to set a callback function that will be invoked when a menu item is selected. If omitted, then the SIGNAL macro will be invoked. Set this flag to ~true to cancel the event event onRegionMenu(object, selection, rect, menu, callback, cancel); <@doc> Fires when the user right-clicks a shape. Use this event to customize the shape's context menu. The base object of the shape in focus The full list of currently selected objects The pointer position, in logical units relative to the diagram origin The context menu definition object. Use the MENU_BAR methods to customize the menu items. DEPRECATED: Use this parameter to set a callback function that will be invoked when a menu item is selected. If omitted, then the SIGNAL macro will be invoked. Set this flag to ~true to cancel the event event onShapeMenu(object, selection, pos, menu, callback, cancel); <@doc> Fires when the user right-connects two shapes. Use this event to customize the connection's context menu. The element that owns the target plug The source plug The target plug The pointer position, in logical units relative to the diagram origin The context menu definition object. Use the MENU_BAR methods to customize the menu items. DEPRECATED: Use this parameter to set a callback function that will be invoked when a menu item is selected. If omitted, then the SIGNAL macro will be invoked. Set this flag to ~true to cancel the event event onConnectMenu(object, source, target, pos, menu, callback, cancel); <@doc> Fires when the user creates a dangling connection (i.e., connects a shape to nothing). Use this event to customize the quick-connect context menu. The element that owns the source plug The source plug The pointer position, in logical units relative to the diagram origin The context menu definition object. Use the MENU_BAR methods to customize the menu items. DEPRECATED: Use this parameter to set a callback function that will be invoked when a menu item is selected. If omitted, then the SIGNAL macro will be invoked. Set this flag to ~true to cancel the event

The following is a sample listener for the ~onQuickConnectMenu event:

listen onQuickConnectMenu for #NS[MyScenarioUsage] var unit=object.unit, cp= pos.x + ' ' + pos.y, srcplug=source; menu.append({signal:'FormView', callback: quickConnect , text:'Add Form', icon:'HEX23URL[ur~skin:symbols.formview16.gif]'}); menu.append({signal:'GridView', callback: quickConnect , text:'Add Grid', icon:'HEX23URL[ur~skin:symbols.gridview16.gif]'}); menu.append({signal:'GridView', callback: quickConnect , text:'Add List', icon:'HEX23URL[ur~skin:symbols.listview16.gif]'}); menu.append(); menu.append({signal:'cancel',callback: quickConnect, text:'Cancel'}); function quickConnect(signal) { if (signal == 'cancel') return; BEGIN(); var trgelem, trgplug; if (srcplug.dir == #[DIR_IN]) { trgelem = unit.createElement('com.sap.vcsample:'+signal,'elements', {'#ASPECT[g:pos]':(cp.x-40)+' '+(cp.y+10)}); trgplug = srcplug; srcplug = trgelem.getSinglePlug(#[DIR_OUT]); trgelem = trgplug.parent; } else { trgelem = unit.createElement('com.sap.vcsample:'+signal ,'elements' , {'#ASPECT[g:pos]':(cp.x+40)+' '+(cp.y+10)}); trgplug = trgelem.getSinglePlug(#[DIR_IN]); } var linktype = trgelem.canConnect(srcplug, trgplug); var A = SPLIT(TRIM(linktype)); if (!A[0]) { ROLLBACK(); return; } unit.createElement(A[0], 'elements', {source:srcplug, target:trgplug}); COMMIT(); } end
event onQuickConnectMenu(object, source, pos, menu, callback, cancel); <@doc> Fires when the user right-drags an empty region on the diagram. Use this event to customize the quick-create context menu. The containing diagram The region bounding box, in logical units relative to the diagram origin The context menu definition object. Use the MENU_BAR methods to customize the menu items. DEPRECATED: Use this parameter to set a callback function that will be invoked when a menu item is selected. If omitted, then the SIGNAL macro will be invoked. Set this flag to ~true to cancel the event

The following is a sample listener for the ~onQuickCreateMenu event:

listen onQuickCreateMenu var diag=object, cp=(rect.x+rect.w/2)+' '+(rect.y+rect.h/2); menu.append({signal:'FormView', callback:quickCreate , text:'Add Form', icon:'HEX23URL[ur~skin:symbols.formview16.gif]'}); menu.append({signal:'GridView', callback:quickCreate ,text:'Add Grid', icon:'HEX23URL[ur~skin:symbols.gridview16.gif]'}); menu.append({signal:'ListView', callback:quickCreate ,text:'Add List', icon:'HEX23URL[ur~skin:symbols.listview16.gif]'}); menu.append(); menu.append({signal:'cancel', callback:quickCreate ,text:'Cancel'}); function quickCreate(signal) { if (signal == 'cancel') return; diag.createElement('com.sap.vcsample:'+signal, 'elements', {'HEX40g:pos':cp}); } end
event onQuickCreateMenu(object, rect, menu, callback, cancel); <@doc> Fires when the user drops a shape sprite on the diagram. Use this event to customize the drag and drop context menu. The base object of the graphic element on which the sprite has been dropped The drag and drop data object The pointer position, in logical units relative to the diagram origin The context menu definition object. Use the MENU_BAR methods to customize the menu items. DEPRECATED: Use this parameter to set a callback function that will be invoked when a menu item is selected. If omitted, then the SIGNAL macro will be invoked. Set this flag to ~true to cancel the event

The default drop behavior will be cancelled if either the ~cancel parameter is set to ~true, or the ~menu parameter is populated with menu items. In the latter case, the menu will be opened instead of the default drop behavior.

The following is a sample listener for the ~onDropMenu event:

listen onDropMenu var diag=object, cp=(pos.x)+' '+(pos.y); menu.append({signal:'FormView',callback:quickCreate , text:'Add Form', icon:'HEX23URL[ur~skin:symbols.formview16.gif]'}); menu.append({signal:'GridView',callback:quickCreate , text:'Add Grid', icon:'HEX23URL[ur~skin:symbols.gridview16.gif]'}); menu.append({signal:'ListView',callback:quickCreate , text:'Add List', icon:'HEX23URL[ur~skin:symbols.listview16.gif]'}); menu.append(); menu.append({signal:'cancel', text:'Cancel'}); function quickCreate(signal) { if (signal == 'cancel') return; diag.createElement('com.sap.vcsample:'+signal, 'elements', {'HEX40g:pos':cp}); } end
event onDropMenu(object, dnd, pos, menu, callback, cancel); <@doc> Fires when the user triggers a drawing-related action. The base object of the drawing element that was activated The action code (SVG_ACTIVATE, SVG_CONNECT, or SVG_ATTACH) event onDrawingAction(object, detail); <@doc> Fires when the user enters the drawing board The containing Unit The drag and drop data object Set this flag to ~true to cancel the event event onDragEnter(object, dnd, cancel); <@doc> Fires when the user triggers a paste action The element to be pasted The containing Unit The full list of currently selected objects Set this flag to ~true to cancel the event event canPasteElement(object, unit ,elements ,cancel);