workspace_svg.js 51 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670
  1. /**
  2. * @license
  3. * Visual Blocks Editor
  4. *
  5. * Copyright 2014 Google Inc.
  6. * https://developers.google.com/blockly/
  7. *
  8. * Licensed under the Apache License, Version 2.0 (the "License");
  9. * you may not use this file except in compliance with the License.
  10. * You may obtain a copy of the License at
  11. *
  12. * http://www.apache.org/licenses/LICENSE-2.0
  13. *
  14. * Unless required by applicable law or agreed to in writing, software
  15. * distributed under the License is distributed on an "AS IS" BASIS,
  16. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  17. * See the License for the specific language governing permissions and
  18. * limitations under the License.
  19. */
  20. /**
  21. * @fileoverview Object representing a workspace rendered as SVG.
  22. * @author fraser@google.com (Neil Fraser)
  23. */
  24. 'use strict';
  25. goog.provide('Blockly.WorkspaceSvg');
  26. // TODO(scr): Fix circular dependencies
  27. //goog.require('Blockly.BlockSvg');
  28. goog.require('Blockly.ConnectionDB');
  29. goog.require('Blockly.constants');
  30. goog.require('Blockly.Options');
  31. goog.require('Blockly.ScrollbarPair');
  32. goog.require('Blockly.Touch');
  33. goog.require('Blockly.Trashcan');
  34. goog.require('Blockly.Workspace');
  35. goog.require('Blockly.Xml');
  36. goog.require('Blockly.ZoomControls');
  37. goog.require('goog.array');
  38. goog.require('goog.dom');
  39. goog.require('goog.math.Coordinate');
  40. goog.require('goog.userAgent');
  41. /**
  42. * Class for a workspace. This is an onscreen area with optional trashcan,
  43. * scrollbars, bubbles, and dragging.
  44. * @param {!Blockly.Options} options Dictionary of options.
  45. * @extends {Blockly.Workspace}
  46. * @constructor
  47. */
  48. Blockly.WorkspaceSvg = function(options) {
  49. Blockly.WorkspaceSvg.superClass_.constructor.call(this, options);
  50. this.getMetrics =
  51. options.getMetrics || Blockly.WorkspaceSvg.getTopLevelWorkspaceMetrics_;
  52. this.setMetrics =
  53. options.setMetrics || Blockly.WorkspaceSvg.setTopLevelWorkspaceMetrics_;
  54. this.undo_stack = [];
  55. this.redo_stack = [];
  56. this.last_change_by_undo = false;
  57. this.save_undo_flag = true;
  58. Blockly.ConnectionDB.init(this);
  59. /**
  60. * Database of pre-loaded sounds.
  61. * @private
  62. * @const
  63. */
  64. this.SOUNDS_ = Object.create(null);
  65. /**
  66. * List of currently highlighted blocks.
  67. * @type !Array.<!Blockly.BlockSvg>
  68. * @private
  69. */
  70. this.highlightedBlocks_ = [];
  71. };
  72. goog.inherits(Blockly.WorkspaceSvg, Blockly.Workspace);
  73. /**
  74. * A wrapper function called when a resize event occurs. You can pass the result to `unbindEvent_`.
  75. * @type {Array.<!Array>}
  76. */
  77. Blockly.WorkspaceSvg.prototype.resizeHandlerWrapper_ = null;
  78. /**
  79. * The render status of an SVG workspace.
  80. * Returns `true` for visible workspaces and `false` for non-visible, or headless, workspaces.
  81. * @type {boolean}
  82. */
  83. Blockly.WorkspaceSvg.prototype.rendered = true;
  84. /**
  85. * Is this workspace the surface for a flyout?
  86. * @type {boolean}
  87. */
  88. Blockly.WorkspaceSvg.prototype.isFlyout = false;
  89. /**
  90. * Is this workspace the surface for a mutator?
  91. * @type {boolean}
  92. * @package
  93. */
  94. Blockly.WorkspaceSvg.prototype.isMutator = false;
  95. /**
  96. * Is this workspace currently being dragged around?
  97. * DRAG_NONE - No drag operation.
  98. * DRAG_BEGIN - Still inside the initial DRAG_RADIUS.
  99. * DRAG_FREE - Workspace has been dragged further than DRAG_RADIUS.
  100. * @private
  101. */
  102. Blockly.WorkspaceSvg.prototype.dragMode_ = Blockly.DRAG_NONE;
  103. /**
  104. * Whether this workspace has resizes enabled.
  105. * Disable during batch operations for a performance improvement.
  106. * @type {boolean}
  107. * @private
  108. */
  109. Blockly.WorkspaceSvg.prototype.resizesEnabled_ = true;
  110. /**
  111. * Current horizontal scrolling offset.
  112. * @type {number}
  113. */
  114. Blockly.WorkspaceSvg.prototype.scrollX = 0;
  115. /**
  116. * Current vertical scrolling offset.
  117. * @type {number}
  118. */
  119. Blockly.WorkspaceSvg.prototype.scrollY = 0;
  120. /**
  121. * Horizontal scroll value when scrolling started.
  122. * @type {number}
  123. */
  124. Blockly.WorkspaceSvg.prototype.startScrollX = 0;
  125. /**
  126. * Vertical scroll value when scrolling started.
  127. * @type {number}
  128. */
  129. Blockly.WorkspaceSvg.prototype.startScrollY = 0;
  130. /**
  131. * Distance from mouse to object being dragged.
  132. * @type {goog.math.Coordinate}
  133. * @private
  134. */
  135. Blockly.WorkspaceSvg.prototype.dragDeltaXY_ = null;
  136. /**
  137. * Current scale.
  138. * @type {number}
  139. */
  140. Blockly.WorkspaceSvg.prototype.scale = 1;
  141. /**
  142. * The workspace's trashcan (if any).
  143. * @type {Blockly.Trashcan}
  144. */
  145. Blockly.WorkspaceSvg.prototype.trashcan = null;
  146. /**
  147. * This workspace's scrollbars, if they exist.
  148. * @type {Blockly.ScrollbarPair}
  149. */
  150. Blockly.WorkspaceSvg.prototype.scrollbar = null;
  151. /**
  152. *
  153. *
  154. */
  155. Blockly.WorkspaceSvg.prototype.alignment_vertical_spacing = 20;
  156. /**
  157. * Time that the last sound was played.
  158. * @type {Date}
  159. * @private
  160. */
  161. Blockly.WorkspaceSvg.prototype.lastSound_ = null;
  162. /**
  163. * Last known position of the page scroll.
  164. * This is used to determine whether we have recalculated screen coordinate
  165. * stuff since the page scrolled.
  166. * @type {!goog.math.Coordinate}
  167. * @private
  168. */
  169. Blockly.WorkspaceSvg.prototype.lastRecordedPageScroll_ = null;
  170. /**
  171. * Inverted screen CTM, for use in mouseToSvg.
  172. * @type {SVGMatrix}
  173. * @private
  174. */
  175. Blockly.WorkspaceSvg.prototype.inverseScreenCTM_ = null;
  176. /**
  177. * Getter for the inverted screen CTM.
  178. * @return {SVGMatrix} The matrix to use in mouseToSvg
  179. */
  180. Blockly.WorkspaceSvg.prototype.getInverseScreenCTM = function() {
  181. return this.inverseScreenCTM_;
  182. };
  183. /**
  184. * Update the inverted screen CTM.
  185. */
  186. Blockly.WorkspaceSvg.prototype.updateInverseScreenCTM = function() {
  187. var ctm = this.getParentSvg().getScreenCTM();
  188. if (ctm) {
  189. this.inverseScreenCTM_ = ctm.inverse();
  190. }
  191. };
  192. /**
  193. * Save resize handler data so we can delete it later in dispose.
  194. * @param {!Array.<!Array>} handler Data that can be passed to unbindEvent_.
  195. */
  196. Blockly.WorkspaceSvg.prototype.setResizeHandlerWrapper = function(handler) {
  197. this.resizeHandlerWrapper_ = handler;
  198. };
  199. /**
  200. * Create the workspace DOM elements.
  201. * @param {string=} opt_backgroundClass Either 'blocklyMainBackground' or
  202. * 'blocklyMutatorBackground'.
  203. * @return {!Element} The workspace's SVG group.
  204. */
  205. Blockly.WorkspaceSvg.prototype.createDom = function(opt_backgroundClass) {
  206. /**
  207. * <g class="blocklyWorkspace">
  208. * <rect class="blocklyMainBackground" height="100%" width="100%"></rect>
  209. * [Trashcan and/or flyout may go here]
  210. * <g class="blocklyBlockCanvas"></g>
  211. * <g class="blocklyBubbleCanvas"></g>
  212. * [Scrollbars may go here]
  213. * </g>
  214. * @type {SVGElement}
  215. */
  216. this.svgGroup_ = Blockly.createSvgElement('g',
  217. {'class': 'blocklyWorkspace'}, null);
  218. if (opt_backgroundClass) {
  219. /** @type {SVGElement} */
  220. this.svgBackground_ = Blockly.createSvgElement('rect',
  221. {'height': '100%', 'width': '100%', 'class': opt_backgroundClass},
  222. this.svgGroup_);
  223. if (opt_backgroundClass == 'blocklyMainBackground') {
  224. this.svgBackground_.style.fill =
  225. 'url(#' + this.options.gridPattern.id + ')';
  226. }
  227. }
  228. /** @type {SVGElement} */
  229. this.svgBlockCanvas_ = Blockly.createSvgElement('g',
  230. {'class': 'blocklyBlockCanvas'}, this.svgGroup_, this);
  231. /** @type {SVGElement} */
  232. this.svgBubbleCanvas_ = Blockly.createSvgElement('g',
  233. {'class': 'blocklyBubbleCanvas'}, this.svgGroup_, this);
  234. var bottom = Blockly.Scrollbar.scrollbarThickness;
  235. if (this.options.hasTrashcan) {
  236. bottom = this.addTrashcan_(bottom);
  237. }
  238. if (this.options.zoomOptions && this.options.zoomOptions.controls) {
  239. bottom = this.addZoomControls_(bottom);
  240. }
  241. if (!this.isFlyout) {
  242. Blockly.bindEventWithChecks_(this.svgGroup_, 'mousedown', this,
  243. this.onMouseDown_);
  244. var thisWorkspace = this;
  245. Blockly.bindEvent_(this.svgGroup_, 'touchstart', null,
  246. function(e) {Blockly.longStart_(e, thisWorkspace);});
  247. if (this.options.zoomOptions && this.options.zoomOptions.wheel) {
  248. // Mouse-wheel.
  249. Blockly.bindEventWithChecks_(this.svgGroup_, 'wheel', this,
  250. this.onMouseWheel_);
  251. }
  252. }
  253. // Determine if there needs to be a category tree, or a simple list of
  254. // blocks. This cannot be changed later, since the UI is very different.
  255. if (this.options.hasCategories) {
  256. /**
  257. * @type {Blockly.Toolbox}
  258. * @private
  259. */
  260. this.toolbox_ = new Blockly.Toolbox(this);
  261. } else if (this.options.languageTree) {
  262. this.addFlyout_();
  263. }
  264. this.updateGridPattern_();
  265. this.recordDeleteAreas();
  266. return this.svgGroup_;
  267. };
  268. /**
  269. * Dispose of this workspace.
  270. * Unlink from all DOM elements to prevent memory leaks.
  271. */
  272. Blockly.WorkspaceSvg.prototype.dispose = function() {
  273. // Stop rerendering.
  274. this.rendered = false;
  275. Blockly.WorkspaceSvg.superClass_.dispose.call(this);
  276. if (this.svgGroup_) {
  277. goog.dom.removeNode(this.svgGroup_);
  278. this.svgGroup_ = null;
  279. }
  280. this.svgBlockCanvas_ = null;
  281. this.svgBubbleCanvas_ = null;
  282. if (this.toolbox_) {
  283. this.toolbox_.dispose();
  284. this.toolbox_ = null;
  285. }
  286. if (this.flyout_) {
  287. this.flyout_.dispose();
  288. this.flyout_ = null;
  289. }
  290. if (this.trashcan) {
  291. this.trashcan.dispose();
  292. this.trashcan = null;
  293. }
  294. if (this.scrollbar) {
  295. this.scrollbar.dispose();
  296. this.scrollbar = null;
  297. }
  298. if (this.zoomControls_) {
  299. this.zoomControls_.dispose();
  300. this.zoomControls_ = null;
  301. }
  302. if (!this.options.parentWorkspace) {
  303. // Top-most workspace. Dispose of the div that the
  304. // svg is injected into (i.e. injectionDiv).
  305. goog.dom.removeNode(this.getParentSvg().parentNode);
  306. }
  307. if (this.resizeHandlerWrapper_) {
  308. Blockly.unbindEvent_(this.resizeHandlerWrapper_);
  309. this.resizeHandlerWrapper_ = null;
  310. }
  311. };
  312. /**
  313. * Obtain a newly created block.
  314. * @param {?string} prototypeName Name of the language object containing
  315. * type-specific functions for this block.
  316. * @param {string=} opt_id Optional ID. Use this ID if provided, otherwise
  317. * create a new ID.
  318. * @return {!Blockly.BlockSvg} The created block.
  319. */
  320. Blockly.WorkspaceSvg.prototype.newBlock = function(prototypeName, opt_id) {
  321. return new Blockly.BlockSvg(this, prototypeName, opt_id);
  322. };
  323. /**
  324. * Add a trashcan.
  325. * @param {number} bottom Distance from workspace bottom to bottom of trashcan.
  326. * @return {number} Distance from workspace bottom to the top of trashcan.
  327. * @private
  328. */
  329. Blockly.WorkspaceSvg.prototype.addTrashcan_ = function(bottom) {
  330. /** @type {Blockly.Trashcan} */
  331. this.trashcan = new Blockly.Trashcan(this);
  332. var svgTrashcan = this.trashcan.createDom();
  333. this.svgGroup_.insertBefore(svgTrashcan, this.svgBlockCanvas_);
  334. return this.trashcan.init(bottom);
  335. };
  336. /**
  337. * Add zoom controls.
  338. * @param {number} bottom Distance from workspace bottom to bottom of controls.
  339. * @return {number} Distance from workspace bottom to the top of controls.
  340. * @private
  341. */
  342. Blockly.WorkspaceSvg.prototype.addZoomControls_ = function(bottom) {
  343. /** @type {Blockly.ZoomControls} */
  344. this.zoomControls_ = new Blockly.ZoomControls(this);
  345. var svgZoomControls = this.zoomControls_.createDom();
  346. this.svgGroup_.appendChild(svgZoomControls);
  347. return this.zoomControls_.init(bottom);
  348. };
  349. /**
  350. * Add a flyout.
  351. * @private
  352. */
  353. Blockly.WorkspaceSvg.prototype.addFlyout_ = function() {
  354. var workspaceOptions = {
  355. disabledPatternId: this.options.disabledPatternId,
  356. parentWorkspace: this,
  357. RTL: this.RTL,
  358. oneBasedIndex: this.options.oneBasedIndex,
  359. horizontalLayout: this.horizontalLayout,
  360. toolboxPosition: this.options.toolboxPosition
  361. };
  362. /** @type {Blockly.Flyout} */
  363. this.flyout_ = new Blockly.Flyout(workspaceOptions);
  364. this.flyout_.autoClose = false;
  365. var svgFlyout = this.flyout_.createDom();
  366. this.svgGroup_.insertBefore(svgFlyout, this.svgBlockCanvas_);
  367. };
  368. /**
  369. * Update items that use screen coordinate calculations
  370. * because something has changed (e.g. scroll position, window size).
  371. * @private
  372. */
  373. Blockly.WorkspaceSvg.prototype.updateScreenCalculations_ = function() {
  374. this.updateInverseScreenCTM();
  375. this.recordDeleteAreas();
  376. };
  377. /**
  378. * If enabled, resize the parts of the workspace that change when the workspace
  379. * contents (e.g. block positions) change. This will also scroll the
  380. * workspace contents if needed.
  381. * @package
  382. */
  383. Blockly.WorkspaceSvg.prototype.resizeContents = function() {
  384. if (!this.resizesEnabled_ || !this.rendered) {
  385. return;
  386. }
  387. if (this.scrollbar) {
  388. // TODO(picklesrus): Once rachel-fenichel's scrollbar refactoring
  389. // is complete, call the method that only resizes scrollbar
  390. // based on contents.
  391. this.scrollbar.resize();
  392. }
  393. this.updateInverseScreenCTM();
  394. };
  395. /**
  396. * Resize and reposition all of the workspace chrome (toolbox,
  397. * trash, scrollbars etc.)
  398. * This should be called when something changes that
  399. * requires recalculating dimensions and positions of the
  400. * trash, zoom, toolbox, etc. (e.g. window resize).
  401. */
  402. Blockly.WorkspaceSvg.prototype.resize = function() {
  403. if (this.toolbox_) {
  404. this.toolbox_.position();
  405. }
  406. if (this.flyout_) {
  407. this.flyout_.position();
  408. }
  409. if (this.trashcan) {
  410. this.trashcan.position();
  411. }
  412. if (this.zoomControls_) {
  413. this.zoomControls_.position();
  414. }
  415. if (this.scrollbar) {
  416. this.scrollbar.resize();
  417. }
  418. this.updateScreenCalculations_();
  419. };
  420. /**
  421. * Resizes and repositions workspace chrome if the page has a new
  422. * scroll position.
  423. * @package
  424. */
  425. Blockly.WorkspaceSvg.prototype.updateScreenCalculationsIfScrolled
  426. = function() {
  427. /* eslint-disable indent */
  428. var currScroll = goog.dom.getDocumentScroll();
  429. if (!goog.math.Coordinate.equals(this.lastRecordedPageScroll_,
  430. currScroll)) {
  431. this.lastRecordedPageScroll_ = currScroll;
  432. this.updateScreenCalculations_();
  433. }
  434. }; /* eslint-enable indent */
  435. /**
  436. * Get the SVG element that forms the drawing surface.
  437. * @return {!Element} SVG element.
  438. */
  439. Blockly.WorkspaceSvg.prototype.getCanvas = function() {
  440. return this.svgBlockCanvas_;
  441. };
  442. /**
  443. * Get the SVG element that forms the bubble surface.
  444. * @return {!SVGGElement} SVG element.
  445. */
  446. Blockly.WorkspaceSvg.prototype.getBubbleCanvas = function() {
  447. return this.svgBubbleCanvas_;
  448. };
  449. /**
  450. * Get the SVG element that contains this workspace.
  451. * @return {!Element} SVG element.
  452. */
  453. Blockly.WorkspaceSvg.prototype.getParentSvg = function() {
  454. if (this.cachedParentSvg_) {
  455. return this.cachedParentSvg_;
  456. }
  457. var element = this.svgGroup_;
  458. while (element) {
  459. if (element.tagName == 'svg') {
  460. this.cachedParentSvg_ = element;
  461. return element;
  462. }
  463. element = element.parentNode;
  464. }
  465. return null;
  466. };
  467. /**
  468. * Translate this workspace to new coordinates.
  469. * @param {number} x Horizontal translation.
  470. * @param {number} y Vertical translation.
  471. */
  472. Blockly.WorkspaceSvg.prototype.translate = function(x, y) {
  473. var translation = 'translate(' + x + ',' + y + ') ' +
  474. 'scale(' + this.scale + ')';
  475. this.svgBlockCanvas_.setAttribute('transform', translation);
  476. this.svgBubbleCanvas_.setAttribute('transform', translation);
  477. };
  478. /**
  479. * Align the blocks in the workspace vertically.
  480. */
  481. Blockly.WorkspaceSvg.prototype.align = function() {
  482. var blocks = this.getTopBlocks(false);
  483. var y = 0;
  484. for (var i = 0; i < blocks.length; i++){
  485. // Get a block
  486. var block = blocks[i];
  487. var properties = block.getRelativeToSurfaceXY();
  488. block.moveBy(-properties.x, -properties.y+y);
  489. // Move it by its height plus a buffer
  490. y += block.getHeightWidth().height + this.alignment_vertical_spacing;
  491. }
  492. }
  493. /*
  494. Blockly.WorkspaceSvg.prototype.maximum_undos = 20;
  495. Blockly.WorkspaceSvg.prototype.enableUndo = function() {
  496. this.saveUndo();
  497. var _instance = this;
  498. this.addChangeListener(function() {_instance.saveUndo()});
  499. }
  500. Blockly.WorkspaceSvg.prototype.saveUndo = function() {
  501. if (this.save_undo_flag) {
  502. if (this.undo_stack.length >= this.maximum_undos) {
  503. this.undo_stack.shift();
  504. }
  505. var current = Blockly.Xml.domToText(Blockly.Xml.workspaceToDom(this));
  506. this.undo_stack.push(current);
  507. this.redo_stack = Array();
  508. this.last_change_by_undo = false;
  509. } else {
  510. this.save_undo_flag = true;
  511. }
  512. };
  513. Blockly.WorkspaceSvg.prototype.undo = function() {
  514. if (this.undo_stack.length > 0) {
  515. this.save_undo_flag = false;
  516. var state = this.undo_stack.pop();
  517. this.redo_stack.push(state);
  518. if (!this.last_change_by_undo &&
  519. this.undo_stack.length > 0) {
  520. state = this.undo_stack.pop();
  521. this.redo_stack.push(state);
  522. }
  523. this.clear();
  524. var xml = Blockly.Xml.textToDom(state);
  525. Blockly.Xml.domToWorkspace(this, xml);
  526. this.last_change_by_undo = true;
  527. }
  528. }
  529. Blockly.WorkspaceSvg.prototype.redo = function() {
  530. if (this.redo_stack.length > 0) {
  531. this.save_undo_flag = false;
  532. var state = this.redo_stack.pop();
  533. this.undo_stack.push(state);
  534. if (this.redo_stack.length == 1) {
  535. state = this.redo_stack.pop();
  536. this.undo_stack.push(state);
  537. } else {
  538. this.clear();
  539. var xml = Blockly.Xml.textToDom(state);
  540. Blockly.Xml.domToWorkspace(this, xml);
  541. }
  542. }
  543. if (this.redo_stack.length == 0) {
  544. this.last_change_by_undo = false;
  545. }
  546. }
  547. */
  548. /**
  549. * Returns the horizontal offset of the workspace.
  550. * Intended for LTR/RTL compatibility in XML.
  551. * @return {number} Width.
  552. */
  553. Blockly.WorkspaceSvg.prototype.getWidth = function() {
  554. var metrics = this.getMetrics();
  555. return metrics ? metrics.viewWidth / this.scale : 0;
  556. };
  557. /**
  558. * Toggles the visibility of the workspace.
  559. * Currently only intended for main workspace.
  560. * @param {boolean} isVisible True if workspace should be visible.
  561. */
  562. Blockly.WorkspaceSvg.prototype.setVisible = function(isVisible) {
  563. this.getParentSvg().style.display = isVisible ? 'block' : 'none';
  564. if (this.toolbox_) {
  565. // Currently does not support toolboxes in mutators.
  566. this.toolbox_.HtmlDiv.style.display = isVisible ? 'block' : 'none';
  567. }
  568. if (isVisible) {
  569. this.render();
  570. if (this.toolbox_) {
  571. this.toolbox_.position();
  572. }
  573. } else {
  574. Blockly.hideChaff(true);
  575. }
  576. };
  577. /**
  578. * Render all blocks in workspace.
  579. */
  580. Blockly.WorkspaceSvg.prototype.render = function() {
  581. // Generate list of all blocks.
  582. var blocks = this.getAllBlocks();
  583. // Render each block.
  584. for (var i = blocks.length - 1; i >= 0; i--) {
  585. blocks[i].render(false);
  586. }
  587. };
  588. /**
  589. * Was used back when block highlighting (for execution) and block selection
  590. * (for editing) were the same thing.
  591. * Any calls of this function can be deleted.
  592. * @deprecated October 2016
  593. */
  594. Blockly.WorkspaceSvg.prototype.traceOn = function() {
  595. console.warn('Deprecated call to traceOn, delete this.');
  596. };
  597. /**
  598. * Highlight or unhighlight a block in the workspace.
  599. * @param {?string} id ID of block to highlight/unhighlight,
  600. * or null for no block (used to unhighlight all blocks).
  601. * @param {boolean=} opt_state If undefined, highlight specified block and
  602. * automatically unhighlight all others. If true or false, manually
  603. * highlight/unhighlight the specified block.
  604. */
  605. Blockly.WorkspaceSvg.prototype.highlightBlock = function(id, opt_state) {
  606. if (opt_state === undefined) {
  607. // Unhighlight all blocks.
  608. for (var i = 0, block; block = this.highlightedBlocks_[i]; i++) {
  609. block.setHighlighted(false);
  610. }
  611. this.highlightedBlocks_.length = 0;
  612. }
  613. // Highlight/unhighlight the specified block.
  614. var block = id ? this.getBlockById(id) : null;
  615. if (block) {
  616. var state = (opt_state === undefined) || opt_state;
  617. // Using Set here would be great, but at the cost of IE10 support.
  618. if (!state) {
  619. goog.array.remove(this.highlightedBlocks_, block);
  620. } else if (this.highlightedBlocks_.indexOf(block) == -1) {
  621. this.highlightedBlocks_.push(block);
  622. }
  623. block.setHighlighted(state);
  624. }
  625. };
  626. /**
  627. * Paste the provided block onto the workspace.
  628. * @param {!Element} xmlBlock XML block element.
  629. */
  630. Blockly.WorkspaceSvg.prototype.paste = function(xmlBlock) {
  631. if (!this.rendered || xmlBlock.getElementsByTagName('block').length >=
  632. this.remainingCapacity()) {
  633. return;
  634. }
  635. Blockly.terminateDrag_(); // Dragging while pasting? No.
  636. Blockly.Events.disable();
  637. try {
  638. var block = Blockly.Xml.domToBlock(xmlBlock, this);
  639. // Move the duplicate to original position.
  640. var blockX = parseInt(xmlBlock.getAttribute('x'), 10);
  641. var blockY = parseInt(xmlBlock.getAttribute('y'), 10);
  642. if (!isNaN(blockX) && !isNaN(blockY)) {
  643. if (this.RTL) {
  644. blockX = -blockX;
  645. }
  646. // Offset block until not clobbering another block and not in connection
  647. // distance with neighbouring blocks.
  648. do {
  649. var collide = false;
  650. var allBlocks = this.getAllBlocks();
  651. for (var i = 0, otherBlock; otherBlock = allBlocks[i]; i++) {
  652. var otherXY = otherBlock.getRelativeToSurfaceXY();
  653. if (Math.abs(blockX - otherXY.x) <= 1 &&
  654. Math.abs(blockY - otherXY.y) <= 1) {
  655. collide = true;
  656. break;
  657. }
  658. }
  659. if (!collide) {
  660. // Check for blocks in snap range to any of its connections.
  661. var connections = block.getConnections_(false);
  662. for (var i = 0, connection; connection = connections[i]; i++) {
  663. var neighbour = connection.closest(Blockly.SNAP_RADIUS,
  664. new goog.math.Coordinate(blockX, blockY));
  665. if (neighbour.connection) {
  666. collide = true;
  667. break;
  668. }
  669. }
  670. }
  671. if (collide) {
  672. if (this.RTL) {
  673. blockX -= Blockly.SNAP_RADIUS;
  674. } else {
  675. blockX += Blockly.SNAP_RADIUS;
  676. }
  677. blockY += Blockly.SNAP_RADIUS * 2;
  678. }
  679. } while (collide);
  680. block.moveBy(blockX, blockY);
  681. }
  682. } finally {
  683. Blockly.Events.enable();
  684. }
  685. if (Blockly.Events.isEnabled() && !block.isShadow()) {
  686. Blockly.Events.fire(new Blockly.Events.Create(block));
  687. }
  688. block.select();
  689. };
  690. /**
  691. * Create a new variable with the given name. Update the flyout to show the new
  692. * variable immediately.
  693. * TODO: #468
  694. * @param {string} name The new variable's name.
  695. */
  696. Blockly.WorkspaceSvg.prototype.createVariable = function(name) {
  697. Blockly.WorkspaceSvg.superClass_.createVariable.call(this, name);
  698. // Don't refresh the toolbox if there's a drag in progress.
  699. if (this.toolbox_ && this.toolbox_.flyout_ && !Blockly.Flyout.startFlyout_) {
  700. this.toolbox_.refreshSelection();
  701. }
  702. };
  703. /**
  704. * Make a list of all the delete areas for this workspace.
  705. */
  706. Blockly.WorkspaceSvg.prototype.recordDeleteAreas = function() {
  707. if (this.trashcan) {
  708. this.deleteAreaTrash_ = this.trashcan.getClientRect();
  709. } else {
  710. this.deleteAreaTrash_ = null;
  711. }
  712. if (this.flyout_) {
  713. this.deleteAreaToolbox_ = this.flyout_.getClientRect();
  714. } else if (this.toolbox_) {
  715. this.deleteAreaToolbox_ = this.toolbox_.getClientRect();
  716. } else {
  717. this.deleteAreaToolbox_ = null;
  718. }
  719. };
  720. /**
  721. * Is the mouse event over a delete area (toolbox or non-closing flyout)?
  722. * Opens or closes the trashcan and sets the cursor as a side effect.
  723. * @param {!Event} e Mouse move event.
  724. * @return {boolean} True if event is in a delete area.
  725. */
  726. Blockly.WorkspaceSvg.prototype.isDeleteArea = function(e) {
  727. var xy = new goog.math.Coordinate(e.clientX, e.clientY);
  728. if (this.deleteAreaTrash_) {
  729. if (this.deleteAreaTrash_.contains(xy)) {
  730. this.trashcan.setOpen_(true);
  731. Blockly.Css.setCursor(Blockly.Css.Cursor.DELETE);
  732. return true;
  733. }
  734. this.trashcan.setOpen_(false);
  735. }
  736. if (this.deleteAreaToolbox_) {
  737. if (this.deleteAreaToolbox_.contains(xy)) {
  738. Blockly.Css.setCursor(Blockly.Css.Cursor.DELETE);
  739. return true;
  740. }
  741. }
  742. Blockly.Css.setCursor(Blockly.Css.Cursor.CLOSED);
  743. return false;
  744. };
  745. /**
  746. * Handle a mouse-down on SVG drawing surface.
  747. * @param {!Event} e Mouse down event.
  748. * @private
  749. */
  750. Blockly.WorkspaceSvg.prototype.onMouseDown_ = function(e) {
  751. this.markFocused();
  752. if (Blockly.isTargetInput_(e)) {
  753. Blockly.Touch.clearTouchIdentifier();
  754. return;
  755. }
  756. Blockly.terminateDrag_(); // In case mouse-up event was lost.
  757. Blockly.hideChaff();
  758. var isTargetWorkspace = e.target && e.target.nodeName &&
  759. (e.target.nodeName.toLowerCase() == 'svg' ||
  760. e.target == this.svgBackground_);
  761. if (isTargetWorkspace && Blockly.selected && !this.options.readOnly) {
  762. // Clicking on the document clears the selection.
  763. Blockly.selected.unselect();
  764. }
  765. if (Blockly.isRightButton(e)) {
  766. // Right-click.
  767. this.showContextMenu_(e);
  768. // Since this was a click, not a drag, end the gesture immediately.
  769. Blockly.Touch.clearTouchIdentifier();
  770. } else if (this.scrollbar) {
  771. this.dragMode_ = Blockly.DRAG_BEGIN;
  772. // Record the current mouse position.
  773. this.startDragMouseX = e.clientX;
  774. this.startDragMouseY = e.clientY;
  775. this.startDragMetrics = this.getMetrics();
  776. this.startScrollX = this.scrollX;
  777. this.startScrollY = this.scrollY;
  778. // If this is a touch event then bind to the mouseup so workspace drag mode
  779. // is turned off and double move events are not performed on a block.
  780. // See comment in inject.js Blockly.init_ as to why mouseup events are
  781. // bound to the document instead of the SVG's surface.
  782. if ('mouseup' in Blockly.Touch.TOUCH_MAP) {
  783. Blockly.Touch.onTouchUpWrapper_ = Blockly.Touch.onTouchUpWrapper_ || [];
  784. Blockly.Touch.onTouchUpWrapper_ = Blockly.Touch.onTouchUpWrapper_.concat(
  785. Blockly.bindEventWithChecks_(document, 'mouseup', null,
  786. Blockly.onMouseUp_));
  787. }
  788. Blockly.onMouseMoveWrapper_ = Blockly.onMouseMoveWrapper_ || [];
  789. Blockly.onMouseMoveWrapper_ = Blockly.onMouseMoveWrapper_.concat(
  790. Blockly.bindEventWithChecks_(document, 'mousemove', null,
  791. Blockly.onMouseMove_));
  792. } else {
  793. // It was a click, but the workspace isn't draggable.
  794. Blockly.Touch.clearTouchIdentifier();
  795. }
  796. // This event has been handled. No need to bubble up to the document.
  797. e.stopPropagation();
  798. e.preventDefault();
  799. };
  800. /**
  801. * Start tracking a drag of an object on this workspace.
  802. * @param {!Event} e Mouse down event.
  803. * @param {!goog.math.Coordinate} xy Starting location of object.
  804. */
  805. Blockly.WorkspaceSvg.prototype.startDrag = function(e, xy) {
  806. // Record the starting offset between the bubble's location and the mouse.
  807. var point = Blockly.mouseToSvg(e, this.getParentSvg(),
  808. this.getInverseScreenCTM());
  809. // Fix scale of mouse event.
  810. point.x /= this.scale;
  811. point.y /= this.scale;
  812. this.dragDeltaXY_ = goog.math.Coordinate.difference(xy, point);
  813. };
  814. /**
  815. * Track a drag of an object on this workspace.
  816. * @param {!Event} e Mouse move event.
  817. * @return {!goog.math.Coordinate} New location of object.
  818. */
  819. Blockly.WorkspaceSvg.prototype.moveDrag = function(e) {
  820. var point = Blockly.mouseToSvg(e, this.getParentSvg(),
  821. this.getInverseScreenCTM());
  822. // Fix scale of mouse event.
  823. point.x /= this.scale;
  824. point.y /= this.scale;
  825. return goog.math.Coordinate.sum(this.dragDeltaXY_, point);
  826. };
  827. /**
  828. * Is the user currently dragging a block or scrolling the flyout/workspace?
  829. * @return {boolean} True if currently dragging or scrolling.
  830. */
  831. Blockly.WorkspaceSvg.prototype.isDragging = function() {
  832. return Blockly.dragMode_ == Blockly.DRAG_FREE ||
  833. (Blockly.Flyout.startFlyout_ &&
  834. Blockly.Flyout.startFlyout_.dragMode_ == Blockly.DRAG_FREE) ||
  835. this.dragMode_ == Blockly.DRAG_FREE;
  836. };
  837. /**
  838. * Handle a mouse-wheel on SVG drawing surface.
  839. * @param {!Event} e Mouse wheel event.
  840. * @private
  841. */
  842. Blockly.WorkspaceSvg.prototype.onMouseWheel_ = function(e) {
  843. // TODO: Remove terminateDrag and compensate for coordinate skew during zoom.
  844. Blockly.terminateDrag_();
  845. var delta = e.deltaY > 0 ? -1 : 1;
  846. var position = Blockly.mouseToSvg(e, this.getParentSvg(),
  847. this.getInverseScreenCTM());
  848. this.zoom(position.x, position.y, delta);
  849. e.preventDefault();
  850. };
  851. /**
  852. * Calculate the bounding box for the blocks on the workspace.
  853. *
  854. * @return {Object} Contains the position and size of the bounding box
  855. * containing the blocks on the workspace.
  856. */
  857. Blockly.WorkspaceSvg.prototype.getBlocksBoundingBox = function() {
  858. var topBlocks = this.getTopBlocks(false);
  859. // There are no blocks, return empty rectangle.
  860. if (!topBlocks.length) {
  861. return {x: 0, y: 0, width: 0, height: 0};
  862. }
  863. // Initialize boundary using the first block.
  864. var boundary = topBlocks[0].getBoundingRectangle();
  865. // Start at 1 since the 0th block was used for initialization
  866. for (var i = 1; i < topBlocks.length; i++) {
  867. var blockBoundary = topBlocks[i].getBoundingRectangle();
  868. if (blockBoundary.topLeft.x < boundary.topLeft.x) {
  869. boundary.topLeft.x = blockBoundary.topLeft.x;
  870. }
  871. if (blockBoundary.bottomRight.x > boundary.bottomRight.x) {
  872. boundary.bottomRight.x = blockBoundary.bottomRight.x;
  873. }
  874. if (blockBoundary.topLeft.y < boundary.topLeft.y) {
  875. boundary.topLeft.y = blockBoundary.topLeft.y;
  876. }
  877. if (blockBoundary.bottomRight.y > boundary.bottomRight.y) {
  878. boundary.bottomRight.y = blockBoundary.bottomRight.y;
  879. }
  880. }
  881. return {
  882. x: boundary.topLeft.x,
  883. y: boundary.topLeft.y,
  884. width: boundary.bottomRight.x - boundary.topLeft.x,
  885. height: boundary.bottomRight.y - boundary.topLeft.y
  886. };
  887. };
  888. /**
  889. * Clean up the workspace by ordering all the blocks in a column.
  890. */
  891. Blockly.WorkspaceSvg.prototype.cleanUp = function() {
  892. Blockly.Events.setGroup(true);
  893. var topBlocks = this.getTopBlocks(true);
  894. var cursorY = 0;
  895. for (var i = 0, block; block = topBlocks[i]; i++) {
  896. var xy = block.getRelativeToSurfaceXY();
  897. block.moveBy(-xy.x, cursorY - xy.y);
  898. block.snapToGrid();
  899. cursorY = block.getRelativeToSurfaceXY().y +
  900. block.getHeightWidth().height + Blockly.BlockSvg.MIN_BLOCK_Y;
  901. }
  902. Blockly.Events.setGroup(false);
  903. // Fire an event to allow scrollbars to resize.
  904. this.resizeContents();
  905. };
  906. /**
  907. * Copy all the blocks on the current workspace.
  908. * @public
  909. */
  910. Blockly.WorkspaceSvg.prototype.copyAll = function() {
  911. Blockly.hideChaff();
  912. var xmlBlocks = Blockly.Xml.workspaceToDom(this);
  913. if (Blockly.dragMode_ != Blockly.DRAG_FREE) {
  914. Blockly.Xml.deleteNext(xmlBlocks);
  915. }
  916. // Encode start position in XML.
  917. //var xy = block.getRelativeToSurfaceXY();
  918. //xmlBlock.setAttribute('x', block.RTL ? -xy.x : xy.x);
  919. //xmlBlock.setAttribute('y', xy.y);
  920. Blockly.clipboardXml_ = xmlBlocks;
  921. Blockly.clipboardSource_ = this;
  922. localStorage.setItem('_blockly_clipboardXml_', Blockly.Xml.domToText(xmlBlocks));
  923. }
  924. /**
  925. * Pastea all blocks on the current workspace.
  926. * @public
  927. */
  928. Blockly.WorkspaceSvg.prototype.pasteFromClipboard = function() {
  929. var blocks;
  930. if (localStorage.getItem('_blockly_clipboardXml_')) {
  931. var domText = localStorage.getItem('_blockly_clipboardXml_');
  932. blocks = Blockly.Xml.textToDom(domText);
  933. } else if (Blockly.clipboardXml_) {
  934. blocks = Blockly.clipboardXml_
  935. }
  936. Blockly.Events.setGroup(true);
  937. if (blocks.tagName !== undefined && blocks.tagName.toLowerCase() == "xml") {
  938. for (var i = 0, len = blocks.children.length; i < len; i=i+1) {
  939. Blockly.mainWorkspace.paste(blocks.children[i]);
  940. }
  941. this.align();
  942. } else {
  943. Blockly.mainWorkspace.paste(blocks);
  944. }
  945. Blockly.Events.setGroup(false);
  946. }
  947. /**
  948. * Show the context menu for the workspace.
  949. * @param {!Event} e Mouse event.
  950. * @private
  951. */
  952. Blockly.WorkspaceSvg.prototype.showContextMenu_ = function(e) {
  953. if (this.options.readOnly || this.isFlyout) {
  954. return;
  955. }
  956. var menuOptions = [];
  957. var topBlocks = this.getTopBlocks(true);
  958. var eventGroup = Blockly.genUid();
  959. // Options to undo/redo previous action.
  960. var undoOption = {};
  961. undoOption.text = Blockly.Msg.UNDO;
  962. undoOption.enabled = this.undoStack_.length > 0;
  963. undoOption.callback = this.undo.bind(this, false);
  964. menuOptions.push(undoOption);
  965. var redoOption = {};
  966. redoOption.text = Blockly.Msg.REDO;
  967. redoOption.enabled = this.redoStack_.length > 0;
  968. redoOption.callback = this.undo.bind(this, true);
  969. menuOptions.push(redoOption);
  970. // Options to copy all/paste previous action.
  971. var copyAllOption = {};
  972. copyAllOption.text = "Copy All Blocks";
  973. copyAllOption.enabled = true;
  974. copyAllOption.callback = this.copyAll.bind(this);
  975. menuOptions.push(copyAllOption);
  976. var pasteOption = {};
  977. pasteOption.text = "Paste Blocks";
  978. var ls_xml = localStorage.getItem('_blockly_clipboardXml_'),
  979. cp_xml = Blockly.clipboardXml_;
  980. pasteOption.enabled = false;
  981. if (ls_xml) {
  982. cp_xml = ls_xml;
  983. }
  984. if (cp_xml) {
  985. if (typeof cp_xml == "string") {
  986. cp_xml = Blockly.Xml.textToDom(cp_xml);
  987. }
  988. if (cp_xml.tagName != undefined) {
  989. if (cp_xml.tagName.toLowerCase() == "xml") {
  990. pasteOption.enabled = cp_xml.children;
  991. } else {
  992. pasteOption.enabled = cp_xml.tagName.toLowerCase() == "block";
  993. }
  994. }
  995. }
  996. pasteOption.callback = this.pasteFromClipboard.bind(this);
  997. menuOptions.push(pasteOption);
  998. // Option to clean up blocks.
  999. if (this.scrollbar) {
  1000. var cleanOption = {};
  1001. cleanOption.text = Blockly.Msg.CLEAN_UP;
  1002. cleanOption.enabled = topBlocks.length > 1;
  1003. cleanOption.callback = this.cleanUp.bind(this);
  1004. //menuOptions.push(cleanOption);
  1005. }
  1006. // Add a little animation to collapsing and expanding.
  1007. var DELAY = 10;
  1008. if (this.options.collapse) {
  1009. var hasCollapsedBlocks = false;
  1010. var hasExpandedBlocks = false;
  1011. for (var i = 0; i < topBlocks.length; i++) {
  1012. var block = topBlocks[i];
  1013. while (block) {
  1014. if (block.isCollapsed()) {
  1015. hasCollapsedBlocks = true;
  1016. } else {
  1017. hasExpandedBlocks = true;
  1018. }
  1019. block = block.getNextBlock();
  1020. }
  1021. }
  1022. /**
  1023. * Option to collapse or expand top blocks.
  1024. * @param {boolean} shouldCollapse Whether a block should collapse.
  1025. * @private
  1026. */
  1027. var toggleOption = function(shouldCollapse) {
  1028. var ms = 0;
  1029. for (var i = 0; i < topBlocks.length; i++) {
  1030. var block = topBlocks[i];
  1031. while (block) {
  1032. setTimeout(block.setCollapsed.bind(block, shouldCollapse), ms);
  1033. block = block.getNextBlock();
  1034. ms += DELAY;
  1035. }
  1036. }
  1037. };
  1038. // Option to collapse top blocks.
  1039. var collapseOption = {enabled: hasExpandedBlocks};
  1040. collapseOption.text = Blockly.Msg.COLLAPSE_ALL;
  1041. collapseOption.callback = function() {
  1042. toggleOption(true);
  1043. };
  1044. menuOptions.push(collapseOption);
  1045. // Option to expand top blocks.
  1046. var expandOption = {enabled: hasCollapsedBlocks};
  1047. expandOption.text = Blockly.Msg.EXPAND_ALL;
  1048. expandOption.callback = function() {
  1049. toggleOption(false);
  1050. };
  1051. menuOptions.push(expandOption);
  1052. }
  1053. var _instance = this;
  1054. // Option to align the blocks
  1055. menuOptions.push({enabled: true,
  1056. text: "Align Blocks",
  1057. callback: function() {
  1058. _instance.align();
  1059. }});
  1060. /*
  1061. menuOptions.push({enabled: true,
  1062. text: "Undo",
  1063. callback: function() {
  1064. _instance.undo();
  1065. }});
  1066. menuOptions.push({enabled: true,
  1067. text: "Redo",
  1068. callback: function() {
  1069. _instance.redo();
  1070. }});
  1071. */
  1072. /*
  1073. // Option to clear all the blocks.
  1074. var clearOption = {enabled: true};
  1075. clearOption.text = "Clear Blocks";
  1076. var _instance = this;
  1077. clearOption.callback = function() {
  1078. _instance.clear();
  1079. };
  1080. menuOptions.push(clearOption);
  1081. */
  1082. // Option to delete all blocks.
  1083. // Count the number of blocks that are deletable.
  1084. var deleteList = [];
  1085. function addDeletableBlocks(block) {
  1086. if (block.isDeletable()) {
  1087. deleteList = deleteList.concat(block.getDescendants());
  1088. } else {
  1089. var children = block.getChildren();
  1090. for (var i = 0; i < children.length; i++) {
  1091. addDeletableBlocks(children[i]);
  1092. }
  1093. }
  1094. }
  1095. for (var i = 0; i < topBlocks.length; i++) {
  1096. addDeletableBlocks(topBlocks[i]);
  1097. }
  1098. function deleteNext() {
  1099. Blockly.Events.setGroup(eventGroup);
  1100. var block = deleteList.shift();
  1101. if (block) {
  1102. if (block.workspace) {
  1103. block.dispose(false, true);
  1104. setTimeout(deleteNext, DELAY);
  1105. } else {
  1106. deleteNext();
  1107. }
  1108. }
  1109. Blockly.Events.setGroup(false);
  1110. }
  1111. var deleteOption = {
  1112. text: deleteList.length == 1 ? Blockly.Msg.DELETE_BLOCK :
  1113. Blockly.Msg.DELETE_X_BLOCKS.replace('%1', String(deleteList.length)),
  1114. enabled: deleteList.length > 0,
  1115. callback: function() {
  1116. if (deleteList.length < 2 ) {
  1117. deleteNext();
  1118. } else {
  1119. Blockly.confirm(Blockly.Msg.DELETE_ALL_BLOCKS.
  1120. replace('%1', deleteList.length),
  1121. function(ok) {
  1122. if (ok) {
  1123. deleteNext();
  1124. }
  1125. });
  1126. }
  1127. }
  1128. };
  1129. menuOptions.push(deleteOption);
  1130. if (Blockly.captureDialog_ != undefined) {
  1131. var captureOption = {
  1132. text: 'Screenshot',
  1133. enabled: true,
  1134. callback: function() {
  1135. Blockly.captureDialog_();
  1136. }
  1137. };
  1138. menuOptions.push(captureOption);
  1139. }
  1140. Blockly.ContextMenu.show(e, menuOptions, this.RTL);
  1141. };
  1142. /**
  1143. * Load an audio file. Cache it, ready for instantaneous playing.
  1144. * @param {!Array.<string>} filenames List of file types in decreasing order of
  1145. * preference (i.e. increasing size). E.g. ['media/go.mp3', 'media/go.wav']
  1146. * Filenames include path from Blockly's root. File extensions matter.
  1147. * @param {string} name Name of sound.
  1148. * @private
  1149. */
  1150. Blockly.WorkspaceSvg.prototype.loadAudio_ = function(filenames, name) {
  1151. if (!filenames.length) {
  1152. return;
  1153. }
  1154. try {
  1155. var audioTest = new window['Audio']();
  1156. } catch (e) {
  1157. // No browser support for Audio.
  1158. // IE can throw an error even if the Audio object exists.
  1159. return;
  1160. }
  1161. var sound;
  1162. for (var i = 0; i < filenames.length; i++) {
  1163. var filename = filenames[i];
  1164. var ext = filename.match(/\.(\w+)$/);
  1165. if (ext && audioTest.canPlayType('audio/' + ext[1])) {
  1166. // Found an audio format we can play.
  1167. sound = new window['Audio'](filename);
  1168. break;
  1169. }
  1170. }
  1171. if (sound && sound.play) {
  1172. this.SOUNDS_[name] = sound;
  1173. }
  1174. };
  1175. /**
  1176. * Preload all the audio files so that they play quickly when asked for.
  1177. * @private
  1178. */
  1179. Blockly.WorkspaceSvg.prototype.preloadAudio_ = function() {
  1180. for (var name in this.SOUNDS_) {
  1181. var sound = this.SOUNDS_[name];
  1182. sound.volume = .01;
  1183. sound.play();
  1184. sound.pause();
  1185. // iOS can only process one sound at a time. Trying to load more than one
  1186. // corrupts the earlier ones. Just load one and leave the others uncached.
  1187. if (goog.userAgent.IPAD || goog.userAgent.IPHONE) {
  1188. break;
  1189. }
  1190. }
  1191. };
  1192. /**
  1193. * Play a named sound at specified volume. If volume is not specified,
  1194. * use full volume (1).
  1195. * @param {string} name Name of sound.
  1196. * @param {number=} opt_volume Volume of sound (0-1).
  1197. */
  1198. Blockly.WorkspaceSvg.prototype.playAudio = function(name, opt_volume) {
  1199. var sound = this.SOUNDS_[name];
  1200. if (sound) {
  1201. // Don't play one sound on top of another.
  1202. var now = new Date;
  1203. if (now - this.lastSound_ < Blockly.SOUND_LIMIT) {
  1204. return;
  1205. }
  1206. this.lastSound_ = now;
  1207. var mySound;
  1208. var ie9 = goog.userAgent.DOCUMENT_MODE &&
  1209. goog.userAgent.DOCUMENT_MODE === 9;
  1210. if (ie9 || goog.userAgent.IPAD || goog.userAgent.ANDROID) {
  1211. // Creating a new audio node causes lag in IE9, Android and iPad. Android
  1212. // and IE9 refetch the file from the server, iPad uses a singleton audio
  1213. // node which must be deleted and recreated for each new audio tag.
  1214. mySound = sound;
  1215. } else {
  1216. mySound = sound.cloneNode();
  1217. }
  1218. mySound.volume = (opt_volume === undefined ? 1 : opt_volume);
  1219. mySound.play();
  1220. } else if (this.options.parentWorkspace) {
  1221. // Maybe a workspace on a lower level knows about this sound.
  1222. this.options.parentWorkspace.playAudio(name, opt_volume);
  1223. }
  1224. };
  1225. /**
  1226. * Modify the block tree on the existing toolbox.
  1227. * @param {Node|string} tree DOM tree of blocks, or text representation of same.
  1228. */
  1229. Blockly.WorkspaceSvg.prototype.updateToolbox = function(tree) {
  1230. tree = Blockly.Options.parseToolboxTree(tree);
  1231. if (!tree) {
  1232. if (this.options.languageTree) {
  1233. throw 'Can\'t nullify an existing toolbox.';
  1234. }
  1235. return; // No change (null to null).
  1236. }
  1237. if (!this.options.languageTree) {
  1238. throw 'Existing toolbox is null. Can\'t create new toolbox.';
  1239. }
  1240. if (tree.getElementsByTagName('category').length) {
  1241. if (!this.toolbox_) {
  1242. throw 'Existing toolbox has no categories. Can\'t change mode.';
  1243. }
  1244. this.options.languageTree = tree;
  1245. this.toolbox_.populate_(tree);
  1246. this.toolbox_.addColour_();
  1247. } else {
  1248. if (!this.flyout_) {
  1249. throw 'Existing toolbox has categories. Can\'t change mode.';
  1250. }
  1251. this.options.languageTree = tree;
  1252. this.flyout_.show(tree.childNodes);
  1253. }
  1254. };
  1255. /**
  1256. * Mark this workspace as the currently focused main workspace.
  1257. */
  1258. Blockly.WorkspaceSvg.prototype.markFocused = function() {
  1259. if (this.options.parentWorkspace) {
  1260. this.options.parentWorkspace.markFocused();
  1261. } else {
  1262. Blockly.mainWorkspace = this;
  1263. }
  1264. };
  1265. /**
  1266. * Zooming the blocks centered in (x, y) coordinate with zooming in or out.
  1267. * @param {number} x X coordinate of center.
  1268. * @param {number} y Y coordinate of center.
  1269. * @param {number} type Type of zooming (-1 zooming out and 1 zooming in).
  1270. */
  1271. Blockly.WorkspaceSvg.prototype.zoom = function(x, y, type) {
  1272. var speed = this.options.zoomOptions.scaleSpeed;
  1273. var metrics = this.getMetrics();
  1274. var center = this.getParentSvg().createSVGPoint();
  1275. center.x = x;
  1276. center.y = y;
  1277. center = center.matrixTransform(this.getCanvas().getCTM().inverse());
  1278. x = center.x;
  1279. y = center.y;
  1280. var canvas = this.getCanvas();
  1281. // Scale factor.
  1282. var scaleChange = (type == 1) ? speed : 1 / speed;
  1283. // Clamp scale within valid range.
  1284. var newScale = this.scale * scaleChange;
  1285. if (newScale > this.options.zoomOptions.maxScale) {
  1286. scaleChange = this.options.zoomOptions.maxScale / this.scale;
  1287. } else if (newScale < this.options.zoomOptions.minScale) {
  1288. scaleChange = this.options.zoomOptions.minScale / this.scale;
  1289. }
  1290. if (this.scale == newScale) {
  1291. return; // No change in zoom.
  1292. }
  1293. if (this.scrollbar) {
  1294. var matrix = canvas.getCTM()
  1295. .translate(x * (1 - scaleChange), y * (1 - scaleChange))
  1296. .scale(scaleChange);
  1297. // newScale and matrix.a should be identical (within a rounding error).
  1298. this.scrollX = matrix.e - metrics.absoluteLeft;
  1299. this.scrollY = matrix.f - metrics.absoluteTop;
  1300. }
  1301. this.setScale(newScale);
  1302. };
  1303. /**
  1304. * Zooming the blocks centered in the center of view with zooming in or out.
  1305. * @param {number} type Type of zooming (-1 zooming out and 1 zooming in).
  1306. */
  1307. Blockly.WorkspaceSvg.prototype.zoomCenter = function(type) {
  1308. var metrics = this.getMetrics();
  1309. var x = metrics.viewWidth / 2;
  1310. var y = metrics.viewHeight / 2;
  1311. this.zoom(x, y, type);
  1312. };
  1313. /**
  1314. * Zoom the blocks to fit in the workspace if possible.
  1315. */
  1316. Blockly.WorkspaceSvg.prototype.zoomToFit = function() {
  1317. var metrics = this.getMetrics();
  1318. var blocksBox = this.getBlocksBoundingBox();
  1319. var blocksWidth = blocksBox.width;
  1320. var blocksHeight = blocksBox.height;
  1321. if (!blocksWidth) {
  1322. return; // Prevents zooming to infinity.
  1323. }
  1324. var workspaceWidth = metrics.viewWidth;
  1325. var workspaceHeight = metrics.viewHeight;
  1326. if (this.flyout_) {
  1327. workspaceWidth -= this.flyout_.width_;
  1328. }
  1329. if (!this.scrollbar) {
  1330. // Orgin point of 0,0 is fixed, blocks will not scroll to center.
  1331. blocksWidth += metrics.contentLeft;
  1332. blocksHeight += metrics.contentTop;
  1333. }
  1334. var ratioX = workspaceWidth / blocksWidth;
  1335. var ratioY = workspaceHeight / blocksHeight;
  1336. this.setScale(Math.min(ratioX, ratioY));
  1337. this.scrollCenter();
  1338. };
  1339. /**
  1340. * Center the workspace.
  1341. */
  1342. Blockly.WorkspaceSvg.prototype.scrollCenter = function() {
  1343. if (!this.scrollbar) {
  1344. // Can't center a non-scrolling workspace.
  1345. return;
  1346. }
  1347. var metrics = this.getMetrics();
  1348. var x = (metrics.contentWidth - metrics.viewWidth) / 2;
  1349. if (this.flyout_) {
  1350. x -= this.flyout_.width_ / 2;
  1351. }
  1352. var y = (metrics.contentHeight - metrics.viewHeight) / 2;
  1353. this.scrollbar.set(x, y);
  1354. };
  1355. /**
  1356. * Set the workspace's zoom factor.
  1357. * @param {number} newScale Zoom factor.
  1358. */
  1359. Blockly.WorkspaceSvg.prototype.setScale = function(newScale) {
  1360. if (this.options.zoomOptions.maxScale &&
  1361. newScale > this.options.zoomOptions.maxScale) {
  1362. newScale = this.options.zoomOptions.maxScale;
  1363. } else if (this.options.zoomOptions.minScale &&
  1364. newScale < this.options.zoomOptions.minScale) {
  1365. newScale = this.options.zoomOptions.minScale;
  1366. }
  1367. this.scale = newScale;
  1368. this.updateGridPattern_();
  1369. if (this.scrollbar) {
  1370. this.scrollbar.resize();
  1371. } else {
  1372. this.translate(this.scrollX, this.scrollY);
  1373. }
  1374. Blockly.hideChaff(false);
  1375. if (this.flyout_) {
  1376. // No toolbox, resize flyout.
  1377. this.flyout_.reflow();
  1378. }
  1379. };
  1380. /**
  1381. * Updates the grid pattern.
  1382. * @private
  1383. */
  1384. Blockly.WorkspaceSvg.prototype.updateGridPattern_ = function() {
  1385. if (!this.options.gridPattern) {
  1386. return; // No grid.
  1387. }
  1388. // MSIE freaks if it sees a 0x0 pattern, so set empty patterns to 100x100.
  1389. var safeSpacing = (this.options.gridOptions['spacing'] * this.scale) || 100;
  1390. this.options.gridPattern.setAttribute('width', safeSpacing);
  1391. this.options.gridPattern.setAttribute('height', safeSpacing);
  1392. var half = Math.floor(this.options.gridOptions['spacing'] / 2) + 0.5;
  1393. var start = half - this.options.gridOptions['length'] / 2;
  1394. var end = half + this.options.gridOptions['length'] / 2;
  1395. var line1 = this.options.gridPattern.firstChild;
  1396. var line2 = line1 && line1.nextSibling;
  1397. half *= this.scale;
  1398. start *= this.scale;
  1399. end *= this.scale;
  1400. if (line1) {
  1401. line1.setAttribute('stroke-width', this.scale);
  1402. line1.setAttribute('x1', start);
  1403. line1.setAttribute('y1', half);
  1404. line1.setAttribute('x2', end);
  1405. line1.setAttribute('y2', half);
  1406. }
  1407. if (line2) {
  1408. line2.setAttribute('stroke-width', this.scale);
  1409. line2.setAttribute('x1', half);
  1410. line2.setAttribute('y1', start);
  1411. line2.setAttribute('x2', half);
  1412. line2.setAttribute('y2', end);
  1413. }
  1414. };
  1415. /**
  1416. * Return an object with all the metrics required to size scrollbars for a
  1417. * top level workspace. The following properties are computed:
  1418. * .viewHeight: Height of the visible rectangle,
  1419. * .viewWidth: Width of the visible rectangle,
  1420. * .contentHeight: Height of the contents,
  1421. * .contentWidth: Width of the content,
  1422. * .viewTop: Offset of top edge of visible rectangle from parent,
  1423. * .viewLeft: Offset of left edge of visible rectangle from parent,
  1424. * .contentTop: Offset of the top-most content from the y=0 coordinate,
  1425. * .contentLeft: Offset of the left-most content from the x=0 coordinate.
  1426. * .absoluteTop: Top-edge of view.
  1427. * .absoluteLeft: Left-edge of view.
  1428. * .toolboxWidth: Width of toolbox, if it exists. Otherwise zero.
  1429. * .toolboxHeight: Height of toolbox, if it exists. Otherwise zero.
  1430. * .flyoutWidth: Width of the flyout if it is always open. Otherwise zero.
  1431. * .flyoutHeight: Height of flyout if it is always open. Otherwise zero.
  1432. * .toolboxPosition: Top, bottom, left or right.
  1433. * @return {!Object} Contains size and position metrics of a top level
  1434. * workspace.
  1435. * @private
  1436. * @this Blockly.WorkspaceSvg
  1437. */
  1438. Blockly.WorkspaceSvg.getTopLevelWorkspaceMetrics_ = function() {
  1439. var svgSize = Blockly.svgSize(this.getParentSvg());
  1440. if (this.toolbox_) {
  1441. if (this.toolboxPosition == Blockly.TOOLBOX_AT_TOP ||
  1442. this.toolboxPosition == Blockly.TOOLBOX_AT_BOTTOM) {
  1443. svgSize.height -= this.toolbox_.getHeight();
  1444. } else if (this.toolboxPosition == Blockly.TOOLBOX_AT_LEFT ||
  1445. this.toolboxPosition == Blockly.TOOLBOX_AT_RIGHT) {
  1446. svgSize.width -= this.toolbox_.getWidth();
  1447. }
  1448. }
  1449. // Set the margin to match the flyout's margin so that the workspace does
  1450. // not jump as blocks are added.
  1451. var MARGIN = Blockly.Flyout.prototype.CORNER_RADIUS - 1;
  1452. var viewWidth = svgSize.width - MARGIN;
  1453. var viewHeight = svgSize.height - MARGIN;
  1454. var blockBox = this.getBlocksBoundingBox();
  1455. // Fix scale.
  1456. var contentWidth = blockBox.width * this.scale;
  1457. var contentHeight = blockBox.height * this.scale;
  1458. var contentX = blockBox.x * this.scale;
  1459. var contentY = blockBox.y * this.scale;
  1460. if (this.scrollbar) {
  1461. // Add a border around the content that is at least half a screenful wide.
  1462. // Ensure border is wide enough that blocks can scroll over entire screen.
  1463. var leftEdge = Math.min(contentX - viewWidth / 2,
  1464. contentX + contentWidth - viewWidth);
  1465. var rightEdge = Math.max(contentX + contentWidth + viewWidth / 2,
  1466. contentX + viewWidth);
  1467. var topEdge = Math.min(contentY - viewHeight / 2,
  1468. contentY + contentHeight - viewHeight);
  1469. var bottomEdge = Math.max(contentY + contentHeight + viewHeight / 2,
  1470. contentY + viewHeight);
  1471. } else {
  1472. var leftEdge = blockBox.x;
  1473. var rightEdge = leftEdge + blockBox.width;
  1474. var topEdge = blockBox.y;
  1475. var bottomEdge = topEdge + blockBox.height;
  1476. }
  1477. var absoluteLeft = 0;
  1478. if (this.toolbox_ && this.toolboxPosition == Blockly.TOOLBOX_AT_LEFT) {
  1479. absoluteLeft = this.toolbox_.getWidth();
  1480. }
  1481. var absoluteTop = 0;
  1482. if (this.toolbox_ && this.toolboxPosition == Blockly.TOOLBOX_AT_TOP) {
  1483. absoluteTop = this.toolbox_.getHeight();
  1484. }
  1485. var metrics = {
  1486. viewHeight: svgSize.height,
  1487. viewWidth: svgSize.width,
  1488. contentHeight: bottomEdge - topEdge,
  1489. contentWidth: rightEdge - leftEdge,
  1490. viewTop: -this.scrollY,
  1491. viewLeft: -this.scrollX,
  1492. contentTop: topEdge,
  1493. contentLeft: leftEdge,
  1494. absoluteTop: absoluteTop,
  1495. absoluteLeft: absoluteLeft,
  1496. toolboxWidth: this.toolbox_ ? this.toolbox_.getWidth() : 0,
  1497. toolboxHeight: this.toolbox_ ? this.toolbox_.getHeight() : 0,
  1498. flyoutWidth: this.flyout_ ? this.flyout_.getWidth() : 0,
  1499. flyoutHeight: this.flyout_ ? this.flyout_.getHeight() : 0,
  1500. toolboxPosition: this.toolboxPosition
  1501. };
  1502. return metrics;
  1503. };
  1504. /**
  1505. * Sets the X/Y translations of a top level workspace to match the scrollbars.
  1506. * @param {!Object} xyRatio Contains an x and/or y property which is a float
  1507. * between 0 and 1 specifying the degree of scrolling.
  1508. * @private
  1509. * @this Blockly.WorkspaceSvg
  1510. */
  1511. Blockly.WorkspaceSvg.setTopLevelWorkspaceMetrics_ = function(xyRatio) {
  1512. if (!this.scrollbar) {
  1513. throw 'Attempt to set top level workspace scroll without scrollbars.';
  1514. }
  1515. var metrics = this.getMetrics();
  1516. if (goog.isNumber(xyRatio.x)) {
  1517. this.scrollX = -metrics.contentWidth * xyRatio.x - metrics.contentLeft;
  1518. }
  1519. if (goog.isNumber(xyRatio.y)) {
  1520. this.scrollY = -metrics.contentHeight * xyRatio.y - metrics.contentTop;
  1521. }
  1522. var x = this.scrollX + metrics.absoluteLeft;
  1523. var y = this.scrollY + metrics.absoluteTop;
  1524. this.translate(x, y);
  1525. if (this.options.gridPattern) {
  1526. this.options.gridPattern.setAttribute('x', x);
  1527. this.options.gridPattern.setAttribute('y', y);
  1528. if (goog.userAgent.IE) {
  1529. // IE doesn't notice that the x/y offsets have changed. Force an update.
  1530. this.updateGridPattern_();
  1531. }
  1532. }
  1533. };
  1534. /**
  1535. * Update whether this workspace has resizes enabled.
  1536. * If enabled, workspace will resize when appropriate.
  1537. * If disabled, workspace will not resize until re-enabled.
  1538. * Use to avoid resizing during a batch operation, for performance.
  1539. * @param {boolean} enabled Whether resizes should be enabled.
  1540. */
  1541. Blockly.WorkspaceSvg.prototype.setResizesEnabled = function(enabled) {
  1542. var reenabled = (!this.resizesEnabled_ && enabled);
  1543. this.resizesEnabled_ = enabled;
  1544. if (reenabled) {
  1545. // Newly enabled. Trigger a resize.
  1546. this.resizeContents();
  1547. }
  1548. };
  1549. /**
  1550. * Dispose of all blocks in workspace, with an optimization to prevent resizes.
  1551. */
  1552. Blockly.WorkspaceSvg.prototype.clear = function() {
  1553. this.setResizesEnabled(false);
  1554. Blockly.WorkspaceSvg.superClass_.clear.call(this);
  1555. this.setResizesEnabled(true);
  1556. };
  1557. // Export symbols that would otherwise be renamed by Closure compiler.
  1558. Blockly.WorkspaceSvg.prototype['setVisible'] =
  1559. Blockly.WorkspaceSvg.prototype.setVisible;