mutator.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389
  1. /**
  2. * @license
  3. * Visual Blocks Editor
  4. *
  5. * Copyright 2012 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 mutator dialog. A mutator allows the
  22. * user to change the shape of a block using a nested blocks editor.
  23. * @author fraser@google.com (Neil Fraser)
  24. */
  25. 'use strict';
  26. goog.provide('Blockly.Mutator');
  27. goog.require('Blockly.Bubble');
  28. goog.require('Blockly.Icon');
  29. goog.require('Blockly.WorkspaceSvg');
  30. goog.require('goog.Timer');
  31. goog.require('goog.dom');
  32. /**
  33. * Class for a mutator dialog.
  34. * @param {!Array.<string>} quarkNames List of names of sub-blocks for flyout.
  35. * @extends {Blockly.Icon}
  36. * @constructor
  37. */
  38. Blockly.Mutator = function(quarkNames) {
  39. Blockly.Mutator.superClass_.constructor.call(this, null);
  40. this.quarkNames_ = quarkNames;
  41. };
  42. goog.inherits(Blockly.Mutator, Blockly.Icon);
  43. /**
  44. * Width of workspace.
  45. * @private
  46. */
  47. Blockly.Mutator.prototype.workspaceWidth_ = 0;
  48. /**
  49. * Height of workspace.
  50. * @private
  51. */
  52. Blockly.Mutator.prototype.workspaceHeight_ = 0;
  53. /**
  54. * Draw the mutator icon.
  55. * @param {!Element} group The icon group.
  56. * @private
  57. */
  58. Blockly.Mutator.prototype.drawIcon_ = function(group) {
  59. // Square with rounded corners.
  60. Blockly.createSvgElement('rect',
  61. {'class': 'blocklyIconShape',
  62. 'rx': '4', 'ry': '4',
  63. 'height': '16', 'width': '16'},
  64. group);
  65. // Gear teeth.
  66. Blockly.createSvgElement('path',
  67. {'class': 'blocklyIconSymbol',
  68. 'd': 'm4.203,7.296 0,1.368 -0.92,0.677 -0.11,0.41 0.9,1.559 0.41,0.11 1.043,-0.457 1.187,0.683 0.127,1.134 0.3,0.3 1.8,0 0.3,-0.299 0.127,-1.138 1.185,-0.682 1.046,0.458 0.409,-0.11 0.9,-1.559 -0.11,-0.41 -0.92,-0.677 0,-1.366 0.92,-0.677 0.11,-0.41 -0.9,-1.559 -0.409,-0.109 -1.046,0.458 -1.185,-0.682 -0.127,-1.138 -0.3,-0.299 -1.8,0 -0.3,0.3 -0.126,1.135 -1.187,0.682 -1.043,-0.457 -0.41,0.11 -0.899,1.559 0.108,0.409z'},
  69. group);
  70. // Axle hole.
  71. Blockly.createSvgElement('circle',
  72. {'class': 'blocklyIconShape', 'r': '2.7', 'cx': '8', 'cy': '8'},
  73. group);
  74. };
  75. /**
  76. * Clicking on the icon toggles if the mutator bubble is visible.
  77. * Disable if block is uneditable.
  78. * @param {!Event} e Mouse click event.
  79. * @private
  80. * @override
  81. */
  82. Blockly.Mutator.prototype.iconClick_ = function(e) {
  83. if (this.block_.isEditable()) {
  84. Blockly.Icon.prototype.iconClick_.call(this, e);
  85. }
  86. };
  87. /**
  88. * Create the editor for the mutator's bubble.
  89. * @return {!Element} The top-level node of the editor.
  90. * @private
  91. */
  92. Blockly.Mutator.prototype.createEditor_ = function() {
  93. /* Create the editor. Here's the markup that will be generated:
  94. <svg>
  95. [Workspace]
  96. </svg>
  97. */
  98. this.svgDialog_ = Blockly.createSvgElement('svg',
  99. {'x': Blockly.Bubble.BORDER_WIDTH, 'y': Blockly.Bubble.BORDER_WIDTH},
  100. null);
  101. // Convert the list of names into a list of XML objects for the flyout.
  102. if (this.quarkNames_.length) {
  103. var quarkXml = goog.dom.createDom('xml');
  104. for (var i = 0, quarkName; quarkName = this.quarkNames_[i]; i++) {
  105. quarkXml.appendChild(goog.dom.createDom('block', {'type': quarkName}));
  106. }
  107. } else {
  108. var quarkXml = null;
  109. }
  110. var workspaceOptions = {
  111. languageTree: quarkXml,
  112. parentWorkspace: this.block_.workspace,
  113. pathToMedia: this.block_.workspace.options.pathToMedia,
  114. RTL: this.block_.RTL,
  115. toolboxPosition: this.block_.RTL ? Blockly.TOOLBOX_AT_RIGHT :
  116. Blockly.TOOLBOX_AT_LEFT,
  117. horizontalLayout: false,
  118. getMetrics: this.getFlyoutMetrics_.bind(this),
  119. setMetrics: null
  120. };
  121. this.workspace_ = new Blockly.WorkspaceSvg(workspaceOptions);
  122. this.workspace_.isMutator = true;
  123. this.svgDialog_.appendChild(
  124. this.workspace_.createDom('blocklyMutatorBackground'));
  125. return this.svgDialog_;
  126. };
  127. /**
  128. * Add or remove the UI indicating if this icon may be clicked or not.
  129. */
  130. Blockly.Mutator.prototype.updateEditable = function() {
  131. if (!this.block_.isInFlyout) {
  132. if (this.block_.isEditable()) {
  133. if (this.iconGroup_) {
  134. Blockly.removeClass_(/** @type {!Element} */ (this.iconGroup_),
  135. 'blocklyIconGroupReadonly');
  136. }
  137. } else {
  138. // Close any mutator bubble. Icon is not clickable.
  139. this.setVisible(false);
  140. if (this.iconGroup_) {
  141. Blockly.addClass_(/** @type {!Element} */ (this.iconGroup_),
  142. 'blocklyIconGroupReadonly');
  143. }
  144. }
  145. }
  146. // Default behaviour for an icon.
  147. Blockly.Icon.prototype.updateEditable.call(this);
  148. };
  149. /**
  150. * Callback function triggered when the bubble has resized.
  151. * Resize the workspace accordingly.
  152. * @private
  153. */
  154. Blockly.Mutator.prototype.resizeBubble_ = function() {
  155. var doubleBorderWidth = 2 * Blockly.Bubble.BORDER_WIDTH;
  156. var workspaceSize = this.workspace_.getCanvas().getBBox();
  157. var width;
  158. if (this.block_.RTL) {
  159. width = -workspaceSize.x;
  160. } else {
  161. width = workspaceSize.width + workspaceSize.x;
  162. }
  163. var height = workspaceSize.height + doubleBorderWidth * 3;
  164. if (this.workspace_.flyout_) {
  165. var flyoutMetrics = this.workspace_.flyout_.getMetrics_();
  166. height = Math.max(height, flyoutMetrics.contentHeight + 20);
  167. }
  168. width += doubleBorderWidth * 3;
  169. // Only resize if the size difference is significant. Eliminates shuddering.
  170. if (Math.abs(this.workspaceWidth_ - width) > doubleBorderWidth ||
  171. Math.abs(this.workspaceHeight_ - height) > doubleBorderWidth) {
  172. // Record some layout information for getFlyoutMetrics_.
  173. this.workspaceWidth_ = width;
  174. this.workspaceHeight_ = height;
  175. // Resize the bubble.
  176. this.bubble_.setBubbleSize(width + doubleBorderWidth,
  177. height + doubleBorderWidth);
  178. this.svgDialog_.setAttribute('width', this.workspaceWidth_);
  179. this.svgDialog_.setAttribute('height', this.workspaceHeight_);
  180. }
  181. if (this.block_.RTL) {
  182. // Scroll the workspace to always left-align.
  183. var translation = 'translate(' + this.workspaceWidth_ + ',0)';
  184. this.workspace_.getCanvas().setAttribute('transform', translation);
  185. }
  186. this.workspace_.resize();
  187. };
  188. /**
  189. * Show or hide the mutator bubble.
  190. * @param {boolean} visible True if the bubble should be visible.
  191. */
  192. Blockly.Mutator.prototype.setVisible = function(visible) {
  193. if (visible == this.isVisible()) {
  194. // No change.
  195. return;
  196. }
  197. Blockly.Events.fire(
  198. new Blockly.Events.Ui(this.block_, 'mutatorOpen', !visible, visible));
  199. if (visible) {
  200. // Create the bubble.
  201. this.bubble_ = new Blockly.Bubble(
  202. /** @type {!Blockly.WorkspaceSvg} */ (this.block_.workspace),
  203. this.createEditor_(), this.block_.svgPath_, this.iconXY_, null, null);
  204. var tree = this.workspace_.options.languageTree;
  205. if (tree) {
  206. this.workspace_.flyout_.init(this.workspace_);
  207. this.workspace_.flyout_.show(tree.childNodes);
  208. }
  209. this.rootBlock_ = this.block_.decompose(this.workspace_);
  210. var blocks = this.rootBlock_.getDescendants();
  211. for (var i = 0, child; child = blocks[i]; i++) {
  212. child.render();
  213. }
  214. // The root block should not be dragable or deletable.
  215. this.rootBlock_.setMovable(false);
  216. this.rootBlock_.setDeletable(false);
  217. if (this.workspace_.flyout_) {
  218. var margin = this.workspace_.flyout_.CORNER_RADIUS * 2;
  219. var x = this.workspace_.flyout_.width_ + margin;
  220. } else {
  221. var margin = 16;
  222. var x = margin;
  223. }
  224. if (this.block_.RTL) {
  225. x = -x;
  226. }
  227. this.rootBlock_.moveBy(x, margin);
  228. // Save the initial connections, then listen for further changes.
  229. if (this.block_.saveConnections) {
  230. var thisMutator = this;
  231. this.block_.saveConnections(this.rootBlock_);
  232. this.sourceListener_ = function() {
  233. thisMutator.block_.saveConnections(thisMutator.rootBlock_);
  234. };
  235. this.block_.workspace.addChangeListener(this.sourceListener_);
  236. }
  237. this.resizeBubble_();
  238. // When the mutator's workspace changes, update the source block.
  239. this.workspace_.addChangeListener(this.workspaceChanged_.bind(this));
  240. this.updateColour();
  241. } else {
  242. // Dispose of the bubble.
  243. this.svgDialog_ = null;
  244. this.workspace_.dispose();
  245. this.workspace_ = null;
  246. this.rootBlock_ = null;
  247. this.bubble_.dispose();
  248. this.bubble_ = null;
  249. this.workspaceWidth_ = 0;
  250. this.workspaceHeight_ = 0;
  251. if (this.sourceListener_) {
  252. this.block_.workspace.removeChangeListener(this.sourceListener_);
  253. this.sourceListener_ = null;
  254. }
  255. }
  256. };
  257. /**
  258. * Update the source block when the mutator's blocks are changed.
  259. * Bump down any block that's too high.
  260. * Fired whenever a change is made to the mutator's workspace.
  261. * @private
  262. */
  263. Blockly.Mutator.prototype.workspaceChanged_ = function() {
  264. if (Blockly.dragMode_ == Blockly.DRAG_NONE) {
  265. var blocks = this.workspace_.getTopBlocks(false);
  266. var MARGIN = 20;
  267. for (var b = 0, block; block = blocks[b]; b++) {
  268. var blockXY = block.getRelativeToSurfaceXY();
  269. var blockHW = block.getHeightWidth();
  270. if (blockXY.y + blockHW.height < MARGIN) {
  271. // Bump any block that's above the top back inside.
  272. block.moveBy(0, MARGIN - blockHW.height - blockXY.y);
  273. }
  274. }
  275. }
  276. // When the mutator's workspace changes, update the source block.
  277. if (this.rootBlock_.workspace == this.workspace_) {
  278. Blockly.Events.setGroup(true);
  279. var block = this.block_;
  280. var oldMutationDom = block.mutationToDom();
  281. var oldMutation = oldMutationDom && Blockly.Xml.domToText(oldMutationDom);
  282. // Switch off rendering while the source block is rebuilt.
  283. var savedRendered = block.rendered;
  284. block.rendered = false;
  285. // Allow the source block to rebuild itself.
  286. block.compose(this.rootBlock_);
  287. // Restore rendering and show the changes.
  288. block.rendered = savedRendered;
  289. // Mutation may have added some elements that need initalizing.
  290. block.initSvg();
  291. var newMutationDom = block.mutationToDom();
  292. var newMutation = newMutationDom && Blockly.Xml.domToText(newMutationDom);
  293. if (oldMutation != newMutation) {
  294. Blockly.Events.fire(new Blockly.Events.Change(
  295. block, 'mutation', null, oldMutation, newMutation));
  296. // Ensure that any bump is part of this mutation's event group.
  297. var group = Blockly.Events.getGroup();
  298. setTimeout(function() {
  299. Blockly.Events.setGroup(group);
  300. block.bumpNeighbours_();
  301. Blockly.Events.setGroup(false);
  302. }, Blockly.BUMP_DELAY);
  303. }
  304. if (block.rendered) {
  305. block.render();
  306. }
  307. this.resizeBubble_();
  308. Blockly.Events.setGroup(false);
  309. }
  310. };
  311. /**
  312. * Return an object with all the metrics required to size scrollbars for the
  313. * mutator flyout. The following properties are computed:
  314. * .viewHeight: Height of the visible rectangle,
  315. * .viewWidth: Width of the visible rectangle,
  316. * .absoluteTop: Top-edge of view.
  317. * .absoluteLeft: Left-edge of view.
  318. * @return {!Object} Contains size and position metrics of mutator dialog's
  319. * workspace.
  320. * @private
  321. */
  322. Blockly.Mutator.prototype.getFlyoutMetrics_ = function() {
  323. return {
  324. viewHeight: this.workspaceHeight_,
  325. viewWidth: this.workspaceWidth_,
  326. absoluteTop: 0,
  327. absoluteLeft: 0
  328. };
  329. };
  330. /**
  331. * Dispose of this mutator.
  332. */
  333. Blockly.Mutator.prototype.dispose = function() {
  334. this.block_.mutator = null;
  335. Blockly.Icon.prototype.dispose.call(this);
  336. };
  337. /**
  338. * Reconnect an block to a mutated input.
  339. * @param {Blockly.Connection} connectionChild Connection on child block.
  340. * @param {!Blockly.Block} block Parent block.
  341. * @param {string} inputName Name of input on parent block.
  342. * @return {boolean} True iff a reconnection was made, false otherwise.
  343. */
  344. Blockly.Mutator.reconnect = function(connectionChild, block, inputName) {
  345. if (!connectionChild || !connectionChild.getSourceBlock().workspace) {
  346. return false; // No connection or block has been deleted.
  347. }
  348. var connectionParent = block.getInput(inputName).connection;
  349. var currentParent = connectionChild.targetBlock();
  350. if ((!currentParent || currentParent == block) &&
  351. connectionParent.targetConnection != connectionChild) {
  352. if (connectionParent.isConnected()) {
  353. // There's already something connected here. Get rid of it.
  354. connectionParent.disconnect();
  355. }
  356. connectionParent.connect(connectionChild);
  357. return true;
  358. }
  359. return false;
  360. };
  361. // Export symbols that would otherwise be renamed by Closure compiler.
  362. if (!goog.global['Blockly']) {
  363. goog.global['Blockly'] = {};
  364. }
  365. if (!goog.global['Blockly']['Mutator']) {
  366. goog.global['Blockly']['Mutator'] = {};
  367. }
  368. goog.global['Blockly']['Mutator']['reconnect'] = Blockly.Mutator.reconnect;