@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