Stencil.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651
  1. /*
  2. Copyright (c) 2004-2012, The Dojo Foundation All Rights Reserved.
  3. Available via Academic Free License >= 2.1 OR the modified BSD license.
  4. see: http://dojotoolkit.org/license for details
  5. */
  6. if(!dojo._hasResource["dojox.drawing.manager.Stencil"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
  7. dojo._hasResource["dojox.drawing.manager.Stencil"] = true;
  8. dojo.provide("dojox.drawing.manager.Stencil");
  9. (function(){
  10. var surface, surfaceNode;
  11. dojox.drawing.manager.Stencil = dojox.drawing.util.oo.declare(
  12. // summary:
  13. // The main class for tracking Stencils that are cretaed, added,
  14. // selected, or deleted. Also handles selections, multiple
  15. // selections, adding and removing from selections, and dragging
  16. // selections. It's this class that triggers the anchors to
  17. // appear on a Stencil and whther there are anchor on a multiple
  18. // select or not (currently not)
  19. //
  20. function(options){
  21. //
  22. // TODO: mixin props
  23. //
  24. surface = options.surface;
  25. this.canvas = options.canvas;
  26. this.defaults = dojox.drawing.defaults.copy();
  27. this.undo = options.undo;
  28. this.mouse = options.mouse;
  29. this.keys = options.keys;
  30. this.anchors = options.anchors;
  31. this.stencils = {};
  32. this.selectedStencils = {};
  33. this._mouseHandle = this.mouse.register(this);
  34. dojo.connect(this.keys, "onArrow", this, "onArrow");
  35. dojo.connect(this.keys, "onEsc", this, "deselect");
  36. dojo.connect(this.keys, "onDelete", this, "onDelete");
  37. },
  38. {
  39. _dragBegun: false,
  40. _wasDragged:false,
  41. _secondClick:false,
  42. _isBusy:false,
  43. setRecentStencil: function(stencil){
  44. // summary:
  45. // Keeps track of the most recent stencil interacted
  46. // with, whether created or selected.
  47. this.recent = stencil;
  48. },
  49. getRecentStencil: function(){
  50. // summary:
  51. // Returns the stencil most recently interacted
  52. // with whether it's last created or last selected
  53. return this.recent;
  54. },
  55. register: function(/*Object*/stencil){
  56. // summary:
  57. // Key method for adding Stencils. Stencils
  58. // can be added to the canvas without adding
  59. // them to this, but they won't have selection
  60. // or drag ability.
  61. //
  62. console.log("Selection.register ::::::", stencil.id);
  63. if(stencil.isText && !stencil.editMode && stencil.deleteEmptyCreate && !stencil.getText()){
  64. // created empty text field
  65. // defaults say to delete
  66. console.warn("EMPTY CREATE DELETE", stencil);
  67. stencil.destroy();
  68. return false;
  69. }
  70. this.stencils[stencil.id] = stencil;
  71. this.setRecentStencil(stencil);
  72. if(stencil.execText){
  73. if(stencil._text && !stencil.editMode){
  74. console.log("select text");
  75. this.selectItem(stencil);
  76. }
  77. stencil.connect("execText", this, function(){
  78. if(stencil.isText && stencil.deleteEmptyModify && !stencil.getText()){
  79. console.warn("EMPTY MOD DELETE", stencil);
  80. // text deleted
  81. // defaults say to delete
  82. this.deleteItem(stencil);
  83. }else if(stencil.selectOnExec){
  84. this.selectItem(stencil);
  85. }
  86. });
  87. }
  88. stencil.connect("deselect", this, function(){
  89. if(!this._isBusy && this.isSelected(stencil)){
  90. // called from within stencil. do action.
  91. this.deselectItem(stencil);
  92. }
  93. });
  94. stencil.connect("select", this, function(){
  95. if(!this._isBusy && !this.isSelected(stencil)){
  96. // called from within stencil. do action.
  97. this.selectItem(stencil);
  98. }
  99. });
  100. return stencil;
  101. },
  102. unregister: function(/*Object*/stencil){
  103. // summary:
  104. // Method for removing Stencils from the manager.
  105. // This doesn't delete them, only removes them from
  106. // the list.
  107. //
  108. console.log("Selection.unregister ::::::", stencil.id, "sel:", stencil.selected);
  109. if(stencil){
  110. stencil.selected && this.onDeselect(stencil);
  111. delete this.stencils[stencil.id];
  112. }
  113. },
  114. onArrow: function(/*Key Event*/evt){
  115. // summary:
  116. // Moves selection based on keyboard arrow keys
  117. //
  118. // FIXME: Check constraints
  119. if(this.hasSelected()){
  120. this.saveThrottledState();
  121. this.group.applyTransform({dx:evt.x, dy: evt.y});
  122. }
  123. },
  124. _throttleVrl:null,
  125. _throttle: false,
  126. throttleTime:400,
  127. _lastmxx:-1,
  128. _lastmxy:-1,
  129. saveMoveState: function(){
  130. // summary:
  131. // Internal. Used for the prototype undo stack.
  132. // Saves selection position.
  133. //
  134. var mx = this.group.getTransform();
  135. if(mx.dx == this._lastmxx && mx.dy == this._lastmxy){ return; }
  136. this._lastmxx = mx.dx;
  137. this._lastmxy = mx.dy;
  138. //console.warn("SAVE MOVE!", mx.dx, mx.dy);
  139. this.undo.add({
  140. before:dojo.hitch(this.group, "setTransform", mx)
  141. });
  142. },
  143. saveThrottledState: function(){
  144. // summary:
  145. // Internal. Used for the prototype undo stack.
  146. // Prevents an undo point on every mouse move.
  147. // Only does a point when the mouse hesitates.
  148. //
  149. clearTimeout(this._throttleVrl);
  150. clearInterval(this._throttleVrl);
  151. this._throttleVrl = setTimeout(dojo.hitch(this, function(){
  152. this._throttle = false;
  153. this.saveMoveState();
  154. }), this.throttleTime);
  155. if(this._throttle){ return; }
  156. this._throttle = true;
  157. this.saveMoveState();
  158. },
  159. unDelete: function(/*Array*/stencils){
  160. // summary:
  161. // Undeletes a stencil. Used in undo stack.
  162. //
  163. console.log("unDelete:", stencils);
  164. for(var s in stencils){
  165. stencils[s].render();
  166. this.onSelect(stencils[s]);
  167. }
  168. },
  169. onDelete: function(/*Boolean*/noundo){
  170. // summary:
  171. // Event fired on deletion of a stencil
  172. //
  173. console.log("Stencil onDelete", noundo);
  174. if(noundo!==true){
  175. this.undo.add({
  176. before:dojo.hitch(this, "unDelete", this.selectedStencils),
  177. after:dojo.hitch(this, "onDelete", true)
  178. });
  179. }
  180. this.withSelected(function(m){
  181. this.anchors.remove(m);
  182. var id = m.id;
  183. console.log("delete:", m);
  184. m.destroy();
  185. delete this.stencils[id];
  186. });
  187. this.selectedStencils = {};
  188. },
  189. deleteItem: function(/*Object*/stencil){
  190. // summary:
  191. // Deletes a stencil.
  192. // NOTE: supports limited undo.
  193. //
  194. // manipulating the selection to fire onDelete properly
  195. if(this.hasSelected()){
  196. // there is a selection
  197. var sids = [];
  198. for(var m in this.selectedStencils){
  199. if(this.selectedStencils.id == stencil.id){
  200. if(this.hasSelected()==1){
  201. // the deleting stencil is the only one selected
  202. this.onDelete();
  203. return;
  204. }
  205. }else{
  206. sids.push(this.selectedStencils.id);
  207. }
  208. }
  209. // remove selection, delete, restore selection
  210. this.deselect();
  211. this.selectItem(stencil);
  212. this.onDelete();
  213. dojo.forEach(sids, function(id){
  214. this.selectItem(id);
  215. }, this);
  216. }else{
  217. // there is not a selection. select it, delete it
  218. this.selectItem(stencil);
  219. // now delete selection
  220. this.onDelete();
  221. }
  222. },
  223. removeAll: function(){
  224. // summary:
  225. // Deletes all Stencils on the canvas.
  226. this.selectAll();
  227. this._isBusy = true;
  228. this.onDelete();
  229. this.stencils = {};
  230. this._isBusy = false;
  231. },
  232. setSelectionGroup: function(){
  233. // summary:
  234. // Internal. Creates a new selection group
  235. // used to hold selected stencils.
  236. //
  237. this.withSelected(function(m){
  238. this.onDeselect(m, true);
  239. });
  240. if(this.group){
  241. surface.remove(this.group);
  242. this.group.removeShape();
  243. }
  244. this.group = surface.createGroup();
  245. this.group.setTransform({dx:0, dy: 0});
  246. this.withSelected(function(m){
  247. this.group.add(m.container);
  248. m.select();
  249. });
  250. },
  251. setConstraint: function(){
  252. // summary:
  253. // Internal. Gets all selected stencils' coordinates
  254. // and determines how far left and up the selection
  255. // can go without going below zero
  256. //
  257. var t = Infinity, l = Infinity;
  258. this.withSelected(function(m){
  259. var o = m.getBounds();
  260. t = Math.min(o.y1, t);
  261. l = Math.min(o.x1, l);
  262. });
  263. this.constrain = {l:-l, t:-t};
  264. },
  265. onDeselect: function(stencil, keepObject){
  266. // summary:
  267. // Event fired on deselection of a stencil
  268. //
  269. if(!keepObject){
  270. delete this.selectedStencils[stencil.id];
  271. }
  272. //console.log('onDeselect, keep:', keepObject, "stencil:", stencil.type)
  273. this.anchors.remove(stencil);
  274. surface.add(stencil.container);
  275. stencil.selected && stencil.deselect();
  276. stencil.applyTransform(this.group.getTransform());
  277. },
  278. deselectItem: function(/*Object*/stencil){
  279. // summary:
  280. // Deselect passed stencil
  281. //
  282. // note: just keeping with standardized methods
  283. this.onDeselect(stencil);
  284. },
  285. deselect: function(){ // all stencils
  286. // summary:
  287. // Deselect all stencils
  288. //
  289. this.withSelected(function(m){
  290. this.onDeselect(m);
  291. });
  292. this._dragBegun = false;
  293. this._wasDragged = false;
  294. },
  295. onSelect: function(/*Object*/stencil){
  296. // summary:
  297. // Event fired on selection of a stencil
  298. //
  299. //console.log("stencil.onSelect", stencil);
  300. if(!stencil){
  301. console.error("null stencil is not selected:", this.stencils)
  302. }
  303. if(this.selectedStencils[stencil.id]){ return; }
  304. this.selectedStencils[stencil.id] = stencil;
  305. this.group.add(stencil.container);
  306. stencil.select();
  307. if(this.hasSelected()==1){
  308. this.anchors.add(stencil, this.group);
  309. }
  310. },
  311. selectAll: function(){
  312. // summary:
  313. // Selects all items
  314. this._isBusy = true;
  315. for(var m in this.stencils){
  316. //if(!this.stencils[m].selected){
  317. this.selectItem(m);
  318. //}
  319. }
  320. this._isBusy = false;
  321. },
  322. selectItem: function(/*String|Object*/ idOrItem){
  323. // summary:
  324. // Method used to select a stencil.
  325. //
  326. var id = typeof(idOrItem)=="string" ? idOrItem : idOrItem.id;
  327. var stencil = this.stencils[id];
  328. this.setSelectionGroup();
  329. this.onSelect(stencil);
  330. this.group.moveToFront();
  331. this.setConstraint();
  332. },
  333. onLabelDoubleClick: function(/*EventObject*/obj){
  334. // summary:
  335. // Event to connect a textbox to
  336. // for label edits
  337. console.info("mgr.onLabelDoubleClick:", obj);
  338. if(this.selectedStencils[obj.id]){
  339. this.deselect();
  340. }
  341. },
  342. onStencilDoubleClick: function(/*EventObject*/obj){
  343. // summary:
  344. // Event fired on the double-click of a stencil
  345. //
  346. console.info("mgr.onStencilDoubleClick:", obj);
  347. if(this.selectedStencils[obj.id]){
  348. if(this.selectedStencils[obj.id].edit){
  349. console.info("Mgr Stencil Edit -> ", this.selectedStencils[obj.id]);
  350. var m = this.selectedStencils[obj.id];
  351. // deselect must happen first to set the transform
  352. // then edit knows where to set the text box
  353. m.editMode = true;
  354. this.deselect();
  355. m.edit();
  356. }
  357. }
  358. },
  359. onAnchorUp: function(){
  360. // summary:
  361. // Event fire on mouseup off of an anchor point
  362. this.setConstraint();
  363. },
  364. onStencilDown: function(/*EventObject*/obj, evt){
  365. // summary:
  366. // Event fired on mousedown on a stencil
  367. //
  368. console.info(" >>> onStencilDown:", obj.id, this.keys.meta);
  369. if(!this.stencils[obj.id]){ return; }
  370. this.setRecentStencil(this.stencils[obj.id]);
  371. this._isBusy = true;
  372. if(this.selectedStencils[obj.id] && this.keys.meta){
  373. if(dojo.isMac && this.keys.cmmd){
  374. // block context menu
  375. }
  376. console.log(" shift remove");
  377. this.onDeselect(this.selectedStencils[obj.id]);
  378. if(this.hasSelected()==1){
  379. this.withSelected(function(m){
  380. this.anchors.add(m, this.group);
  381. });
  382. }
  383. this.group.moveToFront();
  384. this.setConstraint();
  385. return;
  386. }else if(this.selectedStencils[obj.id]){
  387. console.log(" clicked on selected");
  388. // clicking on same selected item(s)
  389. // RESET OFFSETS
  390. var mx = this.group.getTransform();
  391. this._offx = obj.x - mx.dx;
  392. this._offy = obj.y - mx.dy;
  393. return;
  394. }else if(!this.keys.meta){
  395. console.log(" deselect all");
  396. this.deselect();
  397. }else{
  398. // meta-key add
  399. //console.log("reset sel and add stencil")
  400. }
  401. console.log(" add stencil to selection");
  402. // add a stencil
  403. this.selectItem(obj.id);
  404. mx = this.group.getTransform();
  405. this._offx = obj.x - mx.dx;
  406. this._offy = obj.y - mx.dx;
  407. this.orgx = obj.x;
  408. this.orgy = obj.y;
  409. this._isBusy = false;
  410. // TODO:
  411. // dojo.style(surfaceNode, "cursor", "pointer");
  412. // TODO:
  413. this.undo.add({
  414. before:function(){
  415. },
  416. after: function(){
  417. }
  418. });
  419. },
  420. onLabelDown: function(/*EventObject*/obj, evt){
  421. // summary:
  422. // Event fired on mousedown of a stencil's label
  423. // Because it's an annotation the id will be the
  424. // master stencil.
  425. //console.info("===============>>>Label click: ",obj, " evt: ",evt);
  426. this.onStencilDown(obj,evt);
  427. },
  428. onStencilUp: function(/*EventObject*/obj){
  429. // summary:
  430. // Event fired on mouseup off of a stencil
  431. //
  432. },
  433. onLabelUp: function(/*EventObject*/obj){
  434. this.onStencilUp(obj);
  435. },
  436. onStencilDrag: function(/*EventObject*/obj){
  437. // summary:
  438. // Event fired on every mousemove of a stencil drag
  439. //
  440. if(!this._dragBegun){
  441. // bug, in FF anyway - first mouse move shows x=0
  442. // the 'else' fixes it
  443. this.onBeginDrag(obj);
  444. this._dragBegun = true;
  445. }else{
  446. this.saveThrottledState();
  447. var x = obj.x - obj.last.x,
  448. y = obj.y - obj.last.y,
  449. c = this.constrain,
  450. mz = this.defaults.anchors.marginZero;
  451. x = obj.x - this._offx;
  452. y = obj.y - this._offy;
  453. if(x < c.l + mz){
  454. x = c.l + mz;
  455. }
  456. if(y < c.t + mz){
  457. y = c.t + mz;
  458. }
  459. this.group.setTransform({
  460. dx: x,
  461. dy: y
  462. });
  463. }
  464. },
  465. onLabelDrag: function(/*EventObject*/obj){
  466. this.onStencilDrag(obj);
  467. },
  468. onDragEnd: function(/*EventObject*/obj){
  469. // summary:
  470. // Event fired at the end of a stencil drag
  471. //
  472. this._dragBegun = false;
  473. },
  474. onBeginDrag: function(/*EventObject*/obj){
  475. // summary:
  476. // Event fired at the beginning of a stencil drag
  477. //
  478. this._wasDragged = true;
  479. },
  480. onDown: function(/*EventObject*/obj){
  481. // summary:
  482. // Event fired on mousedown on the canvas
  483. //
  484. this.deselect();
  485. },
  486. onStencilOver: function(obj){
  487. // summary:
  488. // This changes the cursor when hovering over
  489. // a selectable stencil.
  490. //console.log("OVER")
  491. dojo.style(obj.id, "cursor", "move");
  492. },
  493. onStencilOut: function(obj){
  494. // summary:
  495. // This restores the cursor.
  496. //console.log("OUT")
  497. dojo.style(obj.id, "cursor", "crosshair");
  498. },
  499. exporter: function(){
  500. // summary:
  501. // Collects all Stencil data and returns an
  502. // Array of objects.
  503. var items = [];
  504. for(var m in this.stencils){
  505. this.stencils[m].enabled && items.push(this.stencils[m].exporter());
  506. }
  507. return items; // Array
  508. },
  509. listStencils: function(){
  510. return this.stencils;
  511. },
  512. toSelected: function(/*String*/func){
  513. // summary:
  514. // Convenience function calls function *within*
  515. // all selected stencils
  516. var args = Array.prototype.slice.call(arguments).splice(1);
  517. for(var m in this.selectedStencils){
  518. var item = this.selectedStencils[m];
  519. item[func].apply(item, args);
  520. }
  521. },
  522. withSelected: function(/*Function*/func){
  523. // summary:
  524. // Convenience function calls function on
  525. // all selected stencils
  526. var f = dojo.hitch(this, func);
  527. for(var m in this.selectedStencils){
  528. f(this.selectedStencils[m]);
  529. }
  530. },
  531. withUnselected: function(/*Function*/func){
  532. // summary:
  533. // Convenience function calls function on
  534. // all stencils that are not selected
  535. var f = dojo.hitch(this, func);
  536. for(var m in this.stencils){
  537. !this.stencils[m].selected && f(this.stencils[m]);
  538. }
  539. },
  540. withStencils: function(/*Function*/func){
  541. // summary:
  542. // Convenience function calls function on
  543. // all stencils
  544. var f = dojo.hitch(this, func);
  545. for(var m in this.stencils){
  546. f(this.stencils[m]);
  547. }
  548. },
  549. hasSelected: function(){
  550. // summary:
  551. // Returns number of selected (generally used
  552. // as truthy or falsey)
  553. //
  554. // FIXME: should be areSelected?
  555. var ln = 0;
  556. for(var m in this.selectedStencils){ ln++; }
  557. return ln; // Number
  558. },
  559. isSelected: function(/*Object*/stencil){
  560. // summary:
  561. // Returns if passed stencil is selected or not
  562. // based on internal collection, not on stencil
  563. // boolean
  564. return !!this.selectedStencils[stencil.id]; // Boolean
  565. }
  566. }
  567. );
  568. })();
  569. }