/*
Copyright (c) 2004-2011, The Dojo Foundation All Rights Reserved.
Available via Academic Free License >= 2.1 OR the modified BSD license.
see: http://dojotoolkit.org/license for details
*/
if(!dojo._hasResource["dojox.drawing.manager.Stencil"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
dojo._hasResource["dojox.drawing.manager.Stencil"] = true;
dojo.provide("dojox.drawing.manager.Stencil");
(function(){
var surface, surfaceNode;
dojox.drawing.manager.Stencil = dojox.drawing.util.oo.declare(
// summary:
// The main class for tracking Stencils that are cretaed, added,
// selected, or deleted. Also handles selections, multiple
// selections, adding and removing from selections, and dragging
// selections. It's this class that triggers the anchors to
// appear on a Stencil and whther there are anchor on a multiple
// select or not (currently not)
//
function(options){
//
// TODO: mixin props
//
surface = options.surface;
this.canvas = options.canvas;
this.defaults = dojox.drawing.defaults.copy();
this.undo = options.undo;
this.mouse = options.mouse;
this.keys = options.keys;
this.anchors = options.anchors;
this.stencils = {};
this.selectedStencils = {};
this._mouseHandle = this.mouse.register(this);
dojo.connect(this.keys, "onArrow", this, "onArrow");
dojo.connect(this.keys, "onEsc", this, "deselect");
dojo.connect(this.keys, "onDelete", this, "onDelete");
},
{
_dragBegun: false,
_wasDragged:false,
_secondClick:false,
_isBusy:false,
setRecentStencil: function(stencil){
// summary:
// Keeps track of the most recent stencil interacted
// with, whether created or selected.
this.recent = stencil;
},
getRecentStencil: function(){
// summary:
// Returns the stencil most recently interacted
// with whether it's last created or last selected
return this.recent;
},
register: function(/*Object*/stencil){
// summary:
// Key method for adding Stencils. Stencils
// can be added to the canvas without adding
// them to this, but they won't have selection
// or drag ability.
//
console.log("Selection.register ::::::", stencil.id);
if(stencil.isText && !stencil.editMode && stencil.deleteEmptyCreate && !stencil.getText()){
// created empty text field
// defaults say to delete
console.warn("EMPTY CREATE DELETE", stencil);
stencil.destroy();
return false;
}
this.stencils[stencil.id] = stencil;
this.setRecentStencil(stencil);
if(stencil.execText){
if(stencil._text && !stencil.editMode){
console.log("select text");
this.selectItem(stencil);
}
stencil.connect("execText", this, function(){
if(stencil.isText && stencil.deleteEmptyModify && !stencil.getText()){
console.warn("EMPTY MOD DELETE", stencil);
// text deleted
// defaults say to delete
this.deleteItem(stencil);
}else if(stencil.selectOnExec){
this.selectItem(stencil);
}
});
}
stencil.connect("deselect", this, function(){
if(!this._isBusy && this.isSelected(stencil)){
// called from within stencil. do action.
this.deselectItem(stencil);
}
});
stencil.connect("select", this, function(){
if(!this._isBusy && !this.isSelected(stencil)){
// called from within stencil. do action.
this.selectItem(stencil);
}
});
return stencil;
},
unregister: function(/*Object*/stencil){
// summary:
// Method for removing Stencils from the manager.
// This doesn't delete them, only removes them from
// the list.
//
console.log("Selection.unregister ::::::", stencil.id, "sel:", stencil.selected);
if(stencil){
stencil.selected && this.onDeselect(stencil);
delete this.stencils[stencil.id];
}
},
onArrow: function(/*Key Event*/evt){
// summary:
// Moves selection based on keyboard arrow keys
//
// FIXME: Check constraints
if(this.hasSelected()){
this.saveThrottledState();
this.group.applyTransform({dx:evt.x, dy: evt.y});
}
},
_throttleVrl:null,
_throttle: false,
throttleTime:400,
_lastmxx:-1,
_lastmxy:-1,
saveMoveState: function(){
// summary:
// Internal. Used for the prototype undo stack.
// Saves selection position.
//
var mx = this.group.getTransform();
if(mx.dx == this._lastmxx && mx.dy == this._lastmxy){ return; }
this._lastmxx = mx.dx;
this._lastmxy = mx.dy;
//console.warn("SAVE MOVE!", mx.dx, mx.dy);
this.undo.add({
before:dojo.hitch(this.group, "setTransform", mx)
});
},
saveThrottledState: function(){
// summary:
// Internal. Used for the prototype undo stack.
// Prevents an undo point on every mouse move.
// Only does a point when the mouse hesitates.
//
clearTimeout(this._throttleVrl);
clearInterval(this._throttleVrl);
this._throttleVrl = setTimeout(dojo.hitch(this, function(){
this._throttle = false;
this.saveMoveState();
}), this.throttleTime);
if(this._throttle){ return; }
this._throttle = true;
this.saveMoveState();
},
unDelete: function(/*Array*/stencils){
// summary:
// Undeletes a stencil. Used in undo stack.
//
console.log("unDelete:", stencils);
for(var s in stencils){
stencils[s].render();
this.onSelect(stencils[s]);
}
},
onDelete: function(/*Boolean*/noundo){
// summary:
// Event fired on deletion of a stencil
//
console.log("Stencil onDelete", noundo);
if(noundo!==true){
this.undo.add({
before:dojo.hitch(this, "unDelete", this.selectedStencils),
after:dojo.hitch(this, "onDelete", true)
});
}
this.withSelected(function(m){
this.anchors.remove(m);
var id = m.id;
console.log("delete:", m);
m.destroy();
delete this.stencils[id];
});
this.selectedStencils = {};
},
deleteItem: function(/*Object*/stencil){
// summary:
// Deletes a stencil.
// NOTE: supports limited undo.
//
// manipulating the selection to fire onDelete properly
if(this.hasSelected()){
// there is a selection
var sids = [];
for(var m in this.selectedStencils){
if(this.selectedStencils.id == stencil.id){
if(this.hasSelected()==1){
// the deleting stencil is the only one selected
this.onDelete();
return;
}
}else{
sids.push(this.selectedStencils.id);
}
}
// remove selection, delete, restore selection
this.deselect();
this.selectItem(stencil);
this.onDelete();
dojo.forEach(sids, function(id){
this.selectItem(id);
}, this);
}else{
// there is not a selection. select it, delete it
this.selectItem(stencil);
// now delete selection
this.onDelete();
}
},
removeAll: function(){
// summary:
// Deletes all Stencils on the canvas.
this.selectAll();
this._isBusy = true;
this.onDelete();
this.stencils = {};
this._isBusy = false;
},
setSelectionGroup: function(){
// summary:
// Internal. Creates a new selection group
// used to hold selected stencils.
//
this.withSelected(function(m){
this.onDeselect(m, true);
});
if(this.group){
surface.remove(this.group);
this.group.removeShape();
}
this.group = surface.createGroup();
this.group.setTransform({dx:0, dy: 0});
this.withSelected(function(m){
this.group.add(m.container);
m.select();
});
},
setConstraint: function(){
// summary:
// Internal. Gets all selected stencils' coordinates
// and determines how far left and up the selection
// can go without going below zero
//
var t = Infinity, l = Infinity;
this.withSelected(function(m){
var o = m.getBounds();
t = Math.min(o.y1, t);
l = Math.min(o.x1, l);
});
this.constrain = {l:-l, t:-t};
},
onDeselect: function(stencil, keepObject){
// summary:
// Event fired on deselection of a stencil
//
if(!keepObject){
delete this.selectedStencils[stencil.id];
}
//console.log('onDeselect, keep:', keepObject, "stencil:", stencil.type)
this.anchors.remove(stencil);
surface.add(stencil.container);
stencil.selected && stencil.deselect();
stencil.applyTransform(this.group.getTransform());
},
deselectItem: function(/*Object*/stencil){
// summary:
// Deselect passed stencil
//
// note: just keeping with standardized methods
this.onDeselect(stencil);
},
deselect: function(){ // all stencils
// summary:
// Deselect all stencils
//
this.withSelected(function(m){
this.onDeselect(m);
});
this._dragBegun = false;
this._wasDragged = false;
},
onSelect: function(/*Object*/stencil){
// summary:
// Event fired on selection of a stencil
//
//console.log("stencil.onSelect", stencil);
if(!stencil){
console.error("null stencil is not selected:", this.stencils)
}
if(this.selectedStencils[stencil.id]){ return; }
this.selectedStencils[stencil.id] = stencil;
this.group.add(stencil.container);
stencil.select();
if(this.hasSelected()==1){
this.anchors.add(stencil, this.group);
}
},
selectAll: function(){
// summary:
// Selects all items
this._isBusy = true;
for(var m in this.stencils){
//if(!this.stencils[m].selected){
this.selectItem(m);
//}
}
this._isBusy = false;
},
selectItem: function(/*String|Object*/ idOrItem){
// summary:
// Method used to select a stencil.
//
var id = typeof(idOrItem)=="string" ? idOrItem : idOrItem.id;
var stencil = this.stencils[id];
this.setSelectionGroup();
this.onSelect(stencil);
this.group.moveToFront();
this.setConstraint();
},
onLabelDoubleClick: function(/*EventObject*/obj){
// summary:
// Event to connect a textbox to
// for label edits
console.info("mgr.onLabelDoubleClick:", obj);
if(this.selectedStencils[obj.id]){
this.deselect();
}
},
onStencilDoubleClick: function(/*EventObject*/obj){
// summary:
// Event fired on the double-click of a stencil
//
console.info("mgr.onStencilDoubleClick:", obj);
if(this.selectedStencils[obj.id]){
if(this.selectedStencils[obj.id].edit){
console.info("Mgr Stencil Edit -> ", this.selectedStencils[obj.id]);
var m = this.selectedStencils[obj.id];
// deselect must happen first to set the transform
// then edit knows where to set the text box
m.editMode = true;
this.deselect();
m.edit();
}
}
},
onAnchorUp: function(){
// summary:
// Event fire on mouseup off of an anchor point
this.setConstraint();
},
onStencilDown: function(/*EventObject*/obj, evt){
// summary:
// Event fired on mousedown on a stencil
//
console.info(" >>> onStencilDown:", obj.id, this.keys.meta);
if(!this.stencils[obj.id]){ return; }
this.setRecentStencil(this.stencils[obj.id]);
this._isBusy = true;
if(this.selectedStencils[obj.id] && this.keys.meta){
if(dojo.isMac && this.keys.cmmd){
// block context menu
}
console.log(" shift remove");
this.onDeselect(this.selectedStencils[obj.id]);
if(this.hasSelected()==1){
this.withSelected(function(m){
this.anchors.add(m, this.group);
});
}
this.group.moveToFront();
this.setConstraint();
return;
}else if(this.selectedStencils[obj.id]){
console.log(" clicked on selected");
// clicking on same selected item(s)
// RESET OFFSETS
var mx = this.group.getTransform();
this._offx = obj.x - mx.dx;
this._offy = obj.y - mx.dy;
return;
}else if(!this.keys.meta){
console.log(" deselect all");
this.deselect();
}else{
// meta-key add
//console.log("reset sel and add stencil")
}
console.log(" add stencil to selection");
// add a stencil
this.selectItem(obj.id);
mx = this.group.getTransform();
this._offx = obj.x - mx.dx;
this._offy = obj.y - mx.dx;
this.orgx = obj.x;
this.orgy = obj.y;
this._isBusy = false;
// TODO:
// dojo.style(surfaceNode, "cursor", "pointer");
// TODO:
this.undo.add({
before:function(){
},
after: function(){
}
});
},
onLabelDown: function(/*EventObject*/obj, evt){
// summary:
// Event fired on mousedown of a stencil's label
// Because it's an annotation the id will be the
// master stencil.
//console.info("===============>>>Label click: ",obj, " evt: ",evt);
this.onStencilDown(obj,evt);
},
onStencilUp: function(/*EventObject*/obj){
// summary:
// Event fired on mouseup off of a stencil
//
},
onLabelUp: function(/*EventObject*/obj){
this.onStencilUp(obj);
},
onStencilDrag: function(/*EventObject*/obj){
// summary:
// Event fired on every mousemove of a stencil drag
//
if(!this._dragBegun){
// bug, in FF anyway - first mouse move shows x=0
// the 'else' fixes it
this.onBeginDrag(obj);
this._dragBegun = true;
}else{
this.saveThrottledState();
var x = obj.x - obj.last.x,
y = obj.y - obj.last.y,
c = this.constrain,
mz = this.defaults.anchors.marginZero;
x = obj.x - this._offx;
y = obj.y - this._offy;
if(x < c.l + mz){
x = c.l + mz;
}
if(y < c.t + mz){
y = c.t + mz;
}
this.group.setTransform({
dx: x,
dy: y
});
}
},
onLabelDrag: function(/*EventObject*/obj){
this.onStencilDrag(obj);
},
onDragEnd: function(/*EventObject*/obj){
// summary:
// Event fired at the end of a stencil drag
//
this._dragBegun = false;
},
onBeginDrag: function(/*EventObject*/obj){
// summary:
// Event fired at the beginning of a stencil drag
//
this._wasDragged = true;
},
onDown: function(/*EventObject*/obj){
// summary:
// Event fired on mousedown on the canvas
//
this.deselect();
},
onStencilOver: function(obj){
// summary:
// This changes the cursor when hovering over
// a selectable stencil.
//console.log("OVER")
dojo.style(obj.id, "cursor", "move");
},
onStencilOut: function(obj){
// summary:
// This restores the cursor.
//console.log("OUT")
dojo.style(obj.id, "cursor", "crosshair");
},
exporter: function(){
// summary:
// Collects all Stencil data and returns an
// Array of objects.
var items = [];
for(var m in this.stencils){
this.stencils[m].enabled && items.push(this.stencils[m].exporter());
}
return items; // Array
},
listStencils: function(){
return this.stencils;
},
toSelected: function(/*String*/func){
// summary:
// Convenience function calls function *within*
// all selected stencils
var args = Array.prototype.slice.call(arguments).splice(1);
for(var m in this.selectedStencils){
var item = this.selectedStencils[m];
item[func].apply(item, args);
}
},
withSelected: function(/*Function*/func){
// summary:
// Convenience function calls function on
// all selected stencils
var f = dojo.hitch(this, func);
for(var m in this.selectedStencils){
f(this.selectedStencils[m]);
}
},
withUnselected: function(/*Function*/func){
// summary:
// Convenience function calls function on
// all stencils that are not selected
var f = dojo.hitch(this, func);
for(var m in this.stencils){
!this.stencils[m].selected && f(this.stencils[m]);
}
},
withStencils: function(/*Function*/func){
// summary:
// Convenience function calls function on
// all stencils
var f = dojo.hitch(this, func);
for(var m in this.stencils){
f(this.stencils[m]);
}
},
hasSelected: function(){
// summary:
// Returns number of selected (generally used
// as truthy or falsey)
//
// FIXME: should be areSelected?
var ln = 0;
for(var m in this.selectedStencils){ ln++; }
return ln; // Number
},
isSelected: function(/*Object*/stencil){
// summary:
// Returns if passed stencil is selected or not
// based on internal collection, not on stencil
// boolean
return !!this.selectedStencils[stencil.id]; // Boolean
}
}
);
})();
}