mutator.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388
  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.svgDialog_.appendChild(
  123. this.workspace_.createDom('blocklyMutatorBackground'));
  124. return this.svgDialog_;
  125. };
  126. /**
  127. * Add or remove the UI indicating if this icon may be clicked or not.
  128. */
  129. Blockly.Mutator.prototype.updateEditable = function() {
  130. if (!this.block_.isInFlyout) {
  131. if (this.block_.isEditable()) {
  132. if (this.iconGroup_) {
  133. Blockly.removeClass_(/** @type {!Element} */ (this.iconGroup_),
  134. 'blocklyIconGroupReadonly');
  135. }
  136. } else {
  137. // Close any mutator bubble. Icon is not clickable.
  138. this.setVisible(false);
  139. if (this.iconGroup_) {
  140. Blockly.addClass_(/** @type {!Element} */ (this.iconGroup_),
  141. 'blocklyIconGroupReadonly');
  142. }
  143. }
  144. }
  145. // Default behaviour for an icon.
  146. Blockly.Icon.prototype.updateEditable.call(this);
  147. };
  148. /**
  149. * Callback function triggered when the bubble has resized.
  150. * Resize the workspace accordingly.
  151. * @private
  152. */
  153. Blockly.Mutator.prototype.resizeBubble_ = function() {
  154. var doubleBorderWidth = 2 * Blockly.Bubble.BORDER_WIDTH;
  155. var workspaceSize = this.workspace_.getCanvas().getBBox();
  156. var width;
  157. if (this.block_.RTL) {
  158. width = -workspaceSize.x;
  159. } else {
  160. width = workspaceSize.width + workspaceSize.x;
  161. }
  162. var height = workspaceSize.height + doubleBorderWidth * 3;
  163. if (this.workspace_.flyout_) {
  164. var flyoutMetrics = this.workspace_.flyout_.getMetrics_();
  165. height = Math.max(height, flyoutMetrics.contentHeight + 20);
  166. }
  167. width += doubleBorderWidth * 3;
  168. // Only resize if the size difference is significant. Eliminates shuddering.
  169. if (Math.abs(this.workspaceWidth_ - width) > doubleBorderWidth ||
  170. Math.abs(this.workspaceHeight_ - height) > doubleBorderWidth) {
  171. // Record some layout information for getFlyoutMetrics_.
  172. this.workspaceWidth_ = width;
  173. this.workspaceHeight_ = height;
  174. // Resize the bubble.
  175. this.bubble_.setBubbleSize(width + doubleBorderWidth,
  176. height + doubleBorderWidth);
  177. this.svgDialog_.setAttribute('width', this.workspaceWidth_);
  178. this.svgDialog_.setAttribute('height', this.workspaceHeight_);
  179. }
  180. if (this.block_.RTL) {
  181. // Scroll the workspace to always left-align.
  182. var translation = 'translate(' + this.workspaceWidth_ + ',0)';
  183. this.workspace_.getCanvas().setAttribute('transform', translation);
  184. }
  185. this.workspace_.resize();
  186. };
  187. /**
  188. * Show or hide the mutator bubble.
  189. * @param {boolean} visible True if the bubble should be visible.
  190. */
  191. Blockly.Mutator.prototype.setVisible = function(visible) {
  192. if (visible == this.isVisible()) {
  193. // No change.
  194. return;
  195. }
  196. Blockly.Events.fire(
  197. new Blockly.Events.Ui(this.block_, 'mutatorOpen', !visible, visible));
  198. if (visible) {
  199. // Create the bubble.
  200. this.bubble_ = new Blockly.Bubble(
  201. /** @type {!Blockly.WorkspaceSvg} */ (this.block_.workspace),
  202. this.createEditor_(), this.block_.svgPath_, this.iconXY_, null, null);
  203. var tree = this.workspace_.options.languageTree;
  204. if (tree) {
  205. this.workspace_.flyout_.init(this.workspace_);
  206. this.workspace_.flyout_.show(tree.childNodes);
  207. }
  208. this.rootBlock_ = this.block_.decompose(this.workspace_);
  209. var blocks = this.rootBlock_.getDescendants();
  210. for (var i = 0, child; child = blocks[i]; i++) {
  211. child.render();
  212. }
  213. // The root block should not be dragable or deletable.
  214. this.rootBlock_.setMovable(false);
  215. this.rootBlock_.setDeletable(false);
  216. if (this.workspace_.flyout_) {
  217. var margin = this.workspace_.flyout_.CORNER_RADIUS * 2;
  218. var x = this.workspace_.flyout_.width_ + margin;
  219. } else {
  220. var margin = 16;
  221. var x = margin;
  222. }
  223. if (this.block_.RTL) {
  224. x = -x;
  225. }
  226. this.rootBlock_.moveBy(x, margin);
  227. // Save the initial connections, then listen for further changes.
  228. if (this.block_.saveConnections) {
  229. var thisMutator = this;
  230. this.block_.saveConnections(this.rootBlock_);
  231. this.sourceListener_ = function() {
  232. thisMutator.block_.saveConnections(thisMutator.rootBlock_);
  233. };
  234. this.block_.workspace.addChangeListener(this.sourceListener_);
  235. }
  236. this.resizeBubble_();
  237. // When the mutator's workspace changes, update the source block.
  238. this.workspace_.addChangeListener(this.workspaceChanged_.bind(this));
  239. this.updateColour();
  240. } else {
  241. // Dispose of the bubble.
  242. this.svgDialog_ = null;
  243. this.workspace_.dispose();
  244. this.workspace_ = null;
  245. this.rootBlock_ = null;
  246. this.bubble_.dispose();
  247. this.bubble_ = null;
  248. this.workspaceWidth_ = 0;
  249. this.workspaceHeight_ = 0;
  250. if (this.sourceListener_) {
  251. this.block_.workspace.removeChangeListener(this.sourceListener_);
  252. this.sourceListener_ = null;
  253. }
  254. }
  255. };
  256. /**
  257. * Update the source block when the mutator's blocks are changed.
  258. * Bump down any block that's too high.
  259. * Fired whenever a change is made to the mutator's workspace.
  260. * @private
  261. */
  262. Blockly.Mutator.prototype.workspaceChanged_ = function() {
  263. if (Blockly.dragMode_ == Blockly.DRAG_NONE) {
  264. var blocks = this.workspace_.getTopBlocks(false);
  265. var MARGIN = 20;
  266. for (var b = 0, block; block = blocks[b]; b++) {
  267. var blockXY = block.getRelativeToSurfaceXY();
  268. var blockHW = block.getHeightWidth();
  269. if (blockXY.y + blockHW.height < MARGIN) {
  270. // Bump any block that's above the top back inside.
  271. block.moveBy(0, MARGIN - blockHW.height - blockXY.y);
  272. }
  273. }
  274. }
  275. // When the mutator's workspace changes, update the source block.
  276. if (this.rootBlock_.workspace == this.workspace_) {
  277. Blockly.Events.setGroup(true);
  278. var block = this.block_;
  279. var oldMutationDom = block.mutationToDom();
  280. var oldMutation = oldMutationDom && Blockly.Xml.domToText(oldMutationDom);
  281. // Switch off rendering while the source block is rebuilt.
  282. var savedRendered = block.rendered;
  283. block.rendered = false;
  284. // Allow the source block to rebuild itself.
  285. block.compose(this.rootBlock_);
  286. // Restore rendering and show the changes.
  287. block.rendered = savedRendered;
  288. // Mutation may have added some elements that need initalizing.
  289. block.initSvg();
  290. var newMutationDom = block.mutationToDom();
  291. var newMutation = newMutationDom && Blockly.Xml.domToText(newMutationDom);
  292. if (oldMutation != newMutation) {
  293. Blockly.Events.fire(new Blockly.Events.Change(
  294. block, 'mutation', null, oldMutation, newMutation));
  295. // Ensure that any bump is part of this mutation's event group.
  296. var group = Blockly.Events.getGroup();
  297. setTimeout(function() {
  298. Blockly.Events.setGroup(group);
  299. block.bumpNeighbours_();
  300. Blockly.Events.setGroup(false);
  301. }, Blockly.BUMP_DELAY);
  302. }
  303. if (block.rendered) {
  304. block.render();
  305. }
  306. this.resizeBubble_();
  307. Blockly.Events.setGroup(false);
  308. }
  309. };
  310. /**
  311. * Return an object with all the metrics required to size scrollbars for the
  312. * mutator flyout. The following properties are computed:
  313. * .viewHeight: Height of the visible rectangle,
  314. * .viewWidth: Width of the visible rectangle,
  315. * .absoluteTop: Top-edge of view.
  316. * .absoluteLeft: Left-edge of view.
  317. * @return {!Object} Contains size and position metrics of mutator dialog's
  318. * workspace.
  319. * @private
  320. */
  321. Blockly.Mutator.prototype.getFlyoutMetrics_ = function() {
  322. return {
  323. viewHeight: this.workspaceHeight_,
  324. viewWidth: this.workspaceWidth_,
  325. absoluteTop: 0,
  326. absoluteLeft: 0
  327. };
  328. };
  329. /**
  330. * Dispose of this mutator.
  331. */
  332. Blockly.Mutator.prototype.dispose = function() {
  333. this.block_.mutator = null;
  334. Blockly.Icon.prototype.dispose.call(this);
  335. };
  336. /**
  337. * Reconnect an block to a mutated input.
  338. * @param {Blockly.Connection} connectionChild Connection on child block.
  339. * @param {!Blockly.Block} block Parent block.
  340. * @param {string} inputName Name of input on parent block.
  341. * @return {boolean} True iff a reconnection was made, false otherwise.
  342. */
  343. Blockly.Mutator.reconnect = function(connectionChild, block, inputName) {
  344. if (!connectionChild || !connectionChild.getSourceBlock().workspace) {
  345. return false; // No connection or block has been deleted.
  346. }
  347. var connectionParent = block.getInput(inputName).connection;
  348. var currentParent = connectionChild.targetBlock();
  349. if ((!currentParent || currentParent == block) &&
  350. connectionParent.targetConnection != connectionChild) {
  351. if (connectionParent.isConnected()) {
  352. // There's already something connected here. Get rid of it.
  353. connectionParent.disconnect();
  354. }
  355. connectionParent.connect(connectionChild);
  356. return true;
  357. }
  358. return false;
  359. };
  360. // Export symbols that would otherwise be renamed by Closure compiler.
  361. if (!goog.global['Blockly']) {
  362. goog.global['Blockly'] = {};
  363. }
  364. if (!goog.global['Blockly']['Mutator']) {
  365. goog.global['Blockly']['Mutator'] = {};
  366. }
  367. goog.global['Blockly']['Mutator']['reconnect'] = Blockly.Mutator.reconnect;