12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670 |
- /**
- * @license
- * Visual Blocks Editor
- *
- * Copyright 2014 Google Inc.
- * https://developers.google.com/blockly/
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /**
- * @fileoverview Object representing a workspace rendered as SVG.
- * @author fraser@google.com (Neil Fraser)
- */
- 'use strict';
- goog.provide('Blockly.WorkspaceSvg');
- // TODO(scr): Fix circular dependencies
- //goog.require('Blockly.BlockSvg');
- goog.require('Blockly.ConnectionDB');
- goog.require('Blockly.constants');
- goog.require('Blockly.Options');
- goog.require('Blockly.ScrollbarPair');
- goog.require('Blockly.Touch');
- goog.require('Blockly.Trashcan');
- goog.require('Blockly.Workspace');
- goog.require('Blockly.Xml');
- goog.require('Blockly.ZoomControls');
- goog.require('goog.array');
- goog.require('goog.dom');
- goog.require('goog.math.Coordinate');
- goog.require('goog.userAgent');
- /**
- * Class for a workspace. This is an onscreen area with optional trashcan,
- * scrollbars, bubbles, and dragging.
- * @param {!Blockly.Options} options Dictionary of options.
- * @extends {Blockly.Workspace}
- * @constructor
- */
- Blockly.WorkspaceSvg = function(options) {
- Blockly.WorkspaceSvg.superClass_.constructor.call(this, options);
- this.getMetrics =
- options.getMetrics || Blockly.WorkspaceSvg.getTopLevelWorkspaceMetrics_;
- this.setMetrics =
- options.setMetrics || Blockly.WorkspaceSvg.setTopLevelWorkspaceMetrics_;
-
- this.undo_stack = [];
- this.redo_stack = [];
- this.last_change_by_undo = false;
- this.save_undo_flag = true;
- Blockly.ConnectionDB.init(this);
- /**
- * Database of pre-loaded sounds.
- * @private
- * @const
- */
- this.SOUNDS_ = Object.create(null);
- /**
- * List of currently highlighted blocks.
- * @type !Array.<!Blockly.BlockSvg>
- * @private
- */
- this.highlightedBlocks_ = [];
- };
- goog.inherits(Blockly.WorkspaceSvg, Blockly.Workspace);
- /**
- * A wrapper function called when a resize event occurs. You can pass the result to `unbindEvent_`.
- * @type {Array.<!Array>}
- */
- Blockly.WorkspaceSvg.prototype.resizeHandlerWrapper_ = null;
- /**
- * The render status of an SVG workspace.
- * Returns `true` for visible workspaces and `false` for non-visible, or headless, workspaces.
- * @type {boolean}
- */
- Blockly.WorkspaceSvg.prototype.rendered = true;
- /**
- * Is this workspace the surface for a flyout?
- * @type {boolean}
- */
- Blockly.WorkspaceSvg.prototype.isFlyout = false;
- /**
- * Is this workspace the surface for a mutator?
- * @type {boolean}
- * @package
- */
- Blockly.WorkspaceSvg.prototype.isMutator = false;
- /**
- * Is this workspace currently being dragged around?
- * DRAG_NONE - No drag operation.
- * DRAG_BEGIN - Still inside the initial DRAG_RADIUS.
- * DRAG_FREE - Workspace has been dragged further than DRAG_RADIUS.
- * @private
- */
- Blockly.WorkspaceSvg.prototype.dragMode_ = Blockly.DRAG_NONE;
- /**
- * Whether this workspace has resizes enabled.
- * Disable during batch operations for a performance improvement.
- * @type {boolean}
- * @private
- */
- Blockly.WorkspaceSvg.prototype.resizesEnabled_ = true;
- /**
- * Current horizontal scrolling offset.
- * @type {number}
- */
- Blockly.WorkspaceSvg.prototype.scrollX = 0;
- /**
- * Current vertical scrolling offset.
- * @type {number}
- */
- Blockly.WorkspaceSvg.prototype.scrollY = 0;
- /**
- * Horizontal scroll value when scrolling started.
- * @type {number}
- */
- Blockly.WorkspaceSvg.prototype.startScrollX = 0;
- /**
- * Vertical scroll value when scrolling started.
- * @type {number}
- */
- Blockly.WorkspaceSvg.prototype.startScrollY = 0;
- /**
- * Distance from mouse to object being dragged.
- * @type {goog.math.Coordinate}
- * @private
- */
- Blockly.WorkspaceSvg.prototype.dragDeltaXY_ = null;
- /**
- * Current scale.
- * @type {number}
- */
- Blockly.WorkspaceSvg.prototype.scale = 1;
- /**
- * The workspace's trashcan (if any).
- * @type {Blockly.Trashcan}
- */
- Blockly.WorkspaceSvg.prototype.trashcan = null;
- /**
- * This workspace's scrollbars, if they exist.
- * @type {Blockly.ScrollbarPair}
- */
- Blockly.WorkspaceSvg.prototype.scrollbar = null;
- /**
- *
- *
- */
- Blockly.WorkspaceSvg.prototype.alignment_vertical_spacing = 20;
- /**
- * Time that the last sound was played.
- * @type {Date}
- * @private
- */
- Blockly.WorkspaceSvg.prototype.lastSound_ = null;
- /**
- * Last known position of the page scroll.
- * This is used to determine whether we have recalculated screen coordinate
- * stuff since the page scrolled.
- * @type {!goog.math.Coordinate}
- * @private
- */
- Blockly.WorkspaceSvg.prototype.lastRecordedPageScroll_ = null;
- /**
- * Inverted screen CTM, for use in mouseToSvg.
- * @type {SVGMatrix}
- * @private
- */
- Blockly.WorkspaceSvg.prototype.inverseScreenCTM_ = null;
- /**
- * Getter for the inverted screen CTM.
- * @return {SVGMatrix} The matrix to use in mouseToSvg
- */
- Blockly.WorkspaceSvg.prototype.getInverseScreenCTM = function() {
- return this.inverseScreenCTM_;
- };
- /**
- * Update the inverted screen CTM.
- */
- Blockly.WorkspaceSvg.prototype.updateInverseScreenCTM = function() {
- var ctm = this.getParentSvg().getScreenCTM();
- if (ctm) {
- this.inverseScreenCTM_ = ctm.inverse();
- }
- };
- /**
- * Save resize handler data so we can delete it later in dispose.
- * @param {!Array.<!Array>} handler Data that can be passed to unbindEvent_.
- */
- Blockly.WorkspaceSvg.prototype.setResizeHandlerWrapper = function(handler) {
- this.resizeHandlerWrapper_ = handler;
- };
- /**
- * Create the workspace DOM elements.
- * @param {string=} opt_backgroundClass Either 'blocklyMainBackground' or
- * 'blocklyMutatorBackground'.
- * @return {!Element} The workspace's SVG group.
- */
- Blockly.WorkspaceSvg.prototype.createDom = function(opt_backgroundClass) {
- /**
- * <g class="blocklyWorkspace">
- * <rect class="blocklyMainBackground" height="100%" width="100%"></rect>
- * [Trashcan and/or flyout may go here]
- * <g class="blocklyBlockCanvas"></g>
- * <g class="blocklyBubbleCanvas"></g>
- * [Scrollbars may go here]
- * </g>
- * @type {SVGElement}
- */
- this.svgGroup_ = Blockly.createSvgElement('g',
- {'class': 'blocklyWorkspace'}, null);
- if (opt_backgroundClass) {
- /** @type {SVGElement} */
- this.svgBackground_ = Blockly.createSvgElement('rect',
- {'height': '100%', 'width': '100%', 'class': opt_backgroundClass},
- this.svgGroup_);
- if (opt_backgroundClass == 'blocklyMainBackground') {
- this.svgBackground_.style.fill =
- 'url(#' + this.options.gridPattern.id + ')';
- }
- }
- /** @type {SVGElement} */
- this.svgBlockCanvas_ = Blockly.createSvgElement('g',
- {'class': 'blocklyBlockCanvas'}, this.svgGroup_, this);
- /** @type {SVGElement} */
- this.svgBubbleCanvas_ = Blockly.createSvgElement('g',
- {'class': 'blocklyBubbleCanvas'}, this.svgGroup_, this);
- var bottom = Blockly.Scrollbar.scrollbarThickness;
- if (this.options.hasTrashcan) {
- bottom = this.addTrashcan_(bottom);
- }
- if (this.options.zoomOptions && this.options.zoomOptions.controls) {
- bottom = this.addZoomControls_(bottom);
- }
- if (!this.isFlyout) {
- Blockly.bindEventWithChecks_(this.svgGroup_, 'mousedown', this,
- this.onMouseDown_);
- var thisWorkspace = this;
- Blockly.bindEvent_(this.svgGroup_, 'touchstart', null,
- function(e) {Blockly.longStart_(e, thisWorkspace);});
- if (this.options.zoomOptions && this.options.zoomOptions.wheel) {
- // Mouse-wheel.
- Blockly.bindEventWithChecks_(this.svgGroup_, 'wheel', this,
- this.onMouseWheel_);
- }
- }
- // Determine if there needs to be a category tree, or a simple list of
- // blocks. This cannot be changed later, since the UI is very different.
- if (this.options.hasCategories) {
- /**
- * @type {Blockly.Toolbox}
- * @private
- */
- this.toolbox_ = new Blockly.Toolbox(this);
- } else if (this.options.languageTree) {
- this.addFlyout_();
- }
- this.updateGridPattern_();
- this.recordDeleteAreas();
- return this.svgGroup_;
- };
- /**
- * Dispose of this workspace.
- * Unlink from all DOM elements to prevent memory leaks.
- */
- Blockly.WorkspaceSvg.prototype.dispose = function() {
- // Stop rerendering.
- this.rendered = false;
- Blockly.WorkspaceSvg.superClass_.dispose.call(this);
- if (this.svgGroup_) {
- goog.dom.removeNode(this.svgGroup_);
- this.svgGroup_ = null;
- }
- this.svgBlockCanvas_ = null;
- this.svgBubbleCanvas_ = null;
- if (this.toolbox_) {
- this.toolbox_.dispose();
- this.toolbox_ = null;
- }
- if (this.flyout_) {
- this.flyout_.dispose();
- this.flyout_ = null;
- }
- if (this.trashcan) {
- this.trashcan.dispose();
- this.trashcan = null;
- }
- if (this.scrollbar) {
- this.scrollbar.dispose();
- this.scrollbar = null;
- }
- if (this.zoomControls_) {
- this.zoomControls_.dispose();
- this.zoomControls_ = null;
- }
- if (!this.options.parentWorkspace) {
- // Top-most workspace. Dispose of the div that the
- // svg is injected into (i.e. injectionDiv).
- goog.dom.removeNode(this.getParentSvg().parentNode);
- }
- if (this.resizeHandlerWrapper_) {
- Blockly.unbindEvent_(this.resizeHandlerWrapper_);
- this.resizeHandlerWrapper_ = null;
- }
- };
- /**
- * Obtain a newly created block.
- * @param {?string} prototypeName Name of the language object containing
- * type-specific functions for this block.
- * @param {string=} opt_id Optional ID. Use this ID if provided, otherwise
- * create a new ID.
- * @return {!Blockly.BlockSvg} The created block.
- */
- Blockly.WorkspaceSvg.prototype.newBlock = function(prototypeName, opt_id) {
- return new Blockly.BlockSvg(this, prototypeName, opt_id);
- };
- /**
- * Add a trashcan.
- * @param {number} bottom Distance from workspace bottom to bottom of trashcan.
- * @return {number} Distance from workspace bottom to the top of trashcan.
- * @private
- */
- Blockly.WorkspaceSvg.prototype.addTrashcan_ = function(bottom) {
- /** @type {Blockly.Trashcan} */
- this.trashcan = new Blockly.Trashcan(this);
- var svgTrashcan = this.trashcan.createDom();
- this.svgGroup_.insertBefore(svgTrashcan, this.svgBlockCanvas_);
- return this.trashcan.init(bottom);
- };
- /**
- * Add zoom controls.
- * @param {number} bottom Distance from workspace bottom to bottom of controls.
- * @return {number} Distance from workspace bottom to the top of controls.
- * @private
- */
- Blockly.WorkspaceSvg.prototype.addZoomControls_ = function(bottom) {
- /** @type {Blockly.ZoomControls} */
- this.zoomControls_ = new Blockly.ZoomControls(this);
- var svgZoomControls = this.zoomControls_.createDom();
- this.svgGroup_.appendChild(svgZoomControls);
- return this.zoomControls_.init(bottom);
- };
- /**
- * Add a flyout.
- * @private
- */
- Blockly.WorkspaceSvg.prototype.addFlyout_ = function() {
- var workspaceOptions = {
- disabledPatternId: this.options.disabledPatternId,
- parentWorkspace: this,
- RTL: this.RTL,
- oneBasedIndex: this.options.oneBasedIndex,
- horizontalLayout: this.horizontalLayout,
- toolboxPosition: this.options.toolboxPosition
- };
- /** @type {Blockly.Flyout} */
- this.flyout_ = new Blockly.Flyout(workspaceOptions);
- this.flyout_.autoClose = false;
- var svgFlyout = this.flyout_.createDom();
- this.svgGroup_.insertBefore(svgFlyout, this.svgBlockCanvas_);
- };
- /**
- * Update items that use screen coordinate calculations
- * because something has changed (e.g. scroll position, window size).
- * @private
- */
- Blockly.WorkspaceSvg.prototype.updateScreenCalculations_ = function() {
- this.updateInverseScreenCTM();
- this.recordDeleteAreas();
- };
- /**
- * If enabled, resize the parts of the workspace that change when the workspace
- * contents (e.g. block positions) change. This will also scroll the
- * workspace contents if needed.
- * @package
- */
- Blockly.WorkspaceSvg.prototype.resizeContents = function() {
- if (!this.resizesEnabled_ || !this.rendered) {
- return;
- }
- if (this.scrollbar) {
- // TODO(picklesrus): Once rachel-fenichel's scrollbar refactoring
- // is complete, call the method that only resizes scrollbar
- // based on contents.
- this.scrollbar.resize();
- }
- this.updateInverseScreenCTM();
- };
- /**
- * Resize and reposition all of the workspace chrome (toolbox,
- * trash, scrollbars etc.)
- * This should be called when something changes that
- * requires recalculating dimensions and positions of the
- * trash, zoom, toolbox, etc. (e.g. window resize).
- */
- Blockly.WorkspaceSvg.prototype.resize = function() {
- if (this.toolbox_) {
- this.toolbox_.position();
- }
- if (this.flyout_) {
- this.flyout_.position();
- }
- if (this.trashcan) {
- this.trashcan.position();
- }
- if (this.zoomControls_) {
- this.zoomControls_.position();
- }
- if (this.scrollbar) {
- this.scrollbar.resize();
- }
- this.updateScreenCalculations_();
- };
- /**
- * Resizes and repositions workspace chrome if the page has a new
- * scroll position.
- * @package
- */
- Blockly.WorkspaceSvg.prototype.updateScreenCalculationsIfScrolled
- = function() {
- /* eslint-disable indent */
- var currScroll = goog.dom.getDocumentScroll();
- if (!goog.math.Coordinate.equals(this.lastRecordedPageScroll_,
- currScroll)) {
- this.lastRecordedPageScroll_ = currScroll;
- this.updateScreenCalculations_();
- }
- }; /* eslint-enable indent */
- /**
- * Get the SVG element that forms the drawing surface.
- * @return {!Element} SVG element.
- */
- Blockly.WorkspaceSvg.prototype.getCanvas = function() {
- return this.svgBlockCanvas_;
- };
- /**
- * Get the SVG element that forms the bubble surface.
- * @return {!SVGGElement} SVG element.
- */
- Blockly.WorkspaceSvg.prototype.getBubbleCanvas = function() {
- return this.svgBubbleCanvas_;
- };
- /**
- * Get the SVG element that contains this workspace.
- * @return {!Element} SVG element.
- */
- Blockly.WorkspaceSvg.prototype.getParentSvg = function() {
- if (this.cachedParentSvg_) {
- return this.cachedParentSvg_;
- }
- var element = this.svgGroup_;
- while (element) {
- if (element.tagName == 'svg') {
- this.cachedParentSvg_ = element;
- return element;
- }
- element = element.parentNode;
- }
- return null;
- };
- /**
- * Translate this workspace to new coordinates.
- * @param {number} x Horizontal translation.
- * @param {number} y Vertical translation.
- */
- Blockly.WorkspaceSvg.prototype.translate = function(x, y) {
- var translation = 'translate(' + x + ',' + y + ') ' +
- 'scale(' + this.scale + ')';
- this.svgBlockCanvas_.setAttribute('transform', translation);
- this.svgBubbleCanvas_.setAttribute('transform', translation);
- };
- /**
- * Align the blocks in the workspace vertically.
- */
- Blockly.WorkspaceSvg.prototype.align = function() {
- var blocks = this.getTopBlocks(false);
- var y = 0;
- for (var i = 0; i < blocks.length; i++){
- // Get a block
- var block = blocks[i];
- var properties = block.getRelativeToSurfaceXY();
- block.moveBy(-properties.x, -properties.y+y);
- // Move it by its height plus a buffer
- y += block.getHeightWidth().height + this.alignment_vertical_spacing;
- }
- }
- /*
- Blockly.WorkspaceSvg.prototype.maximum_undos = 20;
- Blockly.WorkspaceSvg.prototype.enableUndo = function() {
- this.saveUndo();
- var _instance = this;
- this.addChangeListener(function() {_instance.saveUndo()});
- }
- Blockly.WorkspaceSvg.prototype.saveUndo = function() {
- if (this.save_undo_flag) {
- if (this.undo_stack.length >= this.maximum_undos) {
- this.undo_stack.shift();
- }
- var current = Blockly.Xml.domToText(Blockly.Xml.workspaceToDom(this));
- this.undo_stack.push(current);
- this.redo_stack = Array();
- this.last_change_by_undo = false;
- } else {
- this.save_undo_flag = true;
- }
- };
-
- Blockly.WorkspaceSvg.prototype.undo = function() {
- if (this.undo_stack.length > 0) {
- this.save_undo_flag = false;
- var state = this.undo_stack.pop();
- this.redo_stack.push(state);
- if (!this.last_change_by_undo &&
- this.undo_stack.length > 0) {
- state = this.undo_stack.pop();
- this.redo_stack.push(state);
- }
- this.clear();
- var xml = Blockly.Xml.textToDom(state);
- Blockly.Xml.domToWorkspace(this, xml);
- this.last_change_by_undo = true;
- }
- }
- Blockly.WorkspaceSvg.prototype.redo = function() {
- if (this.redo_stack.length > 0) {
- this.save_undo_flag = false;
- var state = this.redo_stack.pop();
- this.undo_stack.push(state);
- if (this.redo_stack.length == 1) {
- state = this.redo_stack.pop();
- this.undo_stack.push(state);
- } else {
- this.clear();
- var xml = Blockly.Xml.textToDom(state);
- Blockly.Xml.domToWorkspace(this, xml);
- }
- }
- if (this.redo_stack.length == 0) {
- this.last_change_by_undo = false;
- }
- }
- */
- /**
- * Returns the horizontal offset of the workspace.
- * Intended for LTR/RTL compatibility in XML.
- * @return {number} Width.
- */
- Blockly.WorkspaceSvg.prototype.getWidth = function() {
- var metrics = this.getMetrics();
- return metrics ? metrics.viewWidth / this.scale : 0;
- };
- /**
- * Toggles the visibility of the workspace.
- * Currently only intended for main workspace.
- * @param {boolean} isVisible True if workspace should be visible.
- */
- Blockly.WorkspaceSvg.prototype.setVisible = function(isVisible) {
- this.getParentSvg().style.display = isVisible ? 'block' : 'none';
- if (this.toolbox_) {
- // Currently does not support toolboxes in mutators.
- this.toolbox_.HtmlDiv.style.display = isVisible ? 'block' : 'none';
- }
- if (isVisible) {
- this.render();
- if (this.toolbox_) {
- this.toolbox_.position();
- }
- } else {
- Blockly.hideChaff(true);
- }
- };
- /**
- * Render all blocks in workspace.
- */
- Blockly.WorkspaceSvg.prototype.render = function() {
- // Generate list of all blocks.
- var blocks = this.getAllBlocks();
- // Render each block.
- for (var i = blocks.length - 1; i >= 0; i--) {
- blocks[i].render(false);
- }
- };
- /**
- * Was used back when block highlighting (for execution) and block selection
- * (for editing) were the same thing.
- * Any calls of this function can be deleted.
- * @deprecated October 2016
- */
- Blockly.WorkspaceSvg.prototype.traceOn = function() {
- console.warn('Deprecated call to traceOn, delete this.');
- };
- /**
- * Highlight or unhighlight a block in the workspace.
- * @param {?string} id ID of block to highlight/unhighlight,
- * or null for no block (used to unhighlight all blocks).
- * @param {boolean=} opt_state If undefined, highlight specified block and
- * automatically unhighlight all others. If true or false, manually
- * highlight/unhighlight the specified block.
- */
- Blockly.WorkspaceSvg.prototype.highlightBlock = function(id, opt_state) {
- if (opt_state === undefined) {
- // Unhighlight all blocks.
- for (var i = 0, block; block = this.highlightedBlocks_[i]; i++) {
- block.setHighlighted(false);
- }
- this.highlightedBlocks_.length = 0;
- }
- // Highlight/unhighlight the specified block.
- var block = id ? this.getBlockById(id) : null;
- if (block) {
- var state = (opt_state === undefined) || opt_state;
- // Using Set here would be great, but at the cost of IE10 support.
- if (!state) {
- goog.array.remove(this.highlightedBlocks_, block);
- } else if (this.highlightedBlocks_.indexOf(block) == -1) {
- this.highlightedBlocks_.push(block);
- }
- block.setHighlighted(state);
- }
- };
- /**
- * Paste the provided block onto the workspace.
- * @param {!Element} xmlBlock XML block element.
- */
- Blockly.WorkspaceSvg.prototype.paste = function(xmlBlock) {
- if (!this.rendered || xmlBlock.getElementsByTagName('block').length >=
- this.remainingCapacity()) {
- return;
- }
- Blockly.terminateDrag_(); // Dragging while pasting? No.
- Blockly.Events.disable();
- try {
- var block = Blockly.Xml.domToBlock(xmlBlock, this);
- // Move the duplicate to original position.
- var blockX = parseInt(xmlBlock.getAttribute('x'), 10);
- var blockY = parseInt(xmlBlock.getAttribute('y'), 10);
- if (!isNaN(blockX) && !isNaN(blockY)) {
- if (this.RTL) {
- blockX = -blockX;
- }
- // Offset block until not clobbering another block and not in connection
- // distance with neighbouring blocks.
- do {
- var collide = false;
- var allBlocks = this.getAllBlocks();
- for (var i = 0, otherBlock; otherBlock = allBlocks[i]; i++) {
- var otherXY = otherBlock.getRelativeToSurfaceXY();
- if (Math.abs(blockX - otherXY.x) <= 1 &&
- Math.abs(blockY - otherXY.y) <= 1) {
- collide = true;
- break;
- }
- }
- if (!collide) {
- // Check for blocks in snap range to any of its connections.
- var connections = block.getConnections_(false);
- for (var i = 0, connection; connection = connections[i]; i++) {
- var neighbour = connection.closest(Blockly.SNAP_RADIUS,
- new goog.math.Coordinate(blockX, blockY));
- if (neighbour.connection) {
- collide = true;
- break;
- }
- }
- }
- if (collide) {
- if (this.RTL) {
- blockX -= Blockly.SNAP_RADIUS;
- } else {
- blockX += Blockly.SNAP_RADIUS;
- }
- blockY += Blockly.SNAP_RADIUS * 2;
- }
- } while (collide);
- block.moveBy(blockX, blockY);
- }
- } finally {
- Blockly.Events.enable();
- }
- if (Blockly.Events.isEnabled() && !block.isShadow()) {
- Blockly.Events.fire(new Blockly.Events.Create(block));
- }
- block.select();
- };
- /**
- * Create a new variable with the given name. Update the flyout to show the new
- * variable immediately.
- * TODO: #468
- * @param {string} name The new variable's name.
- */
- Blockly.WorkspaceSvg.prototype.createVariable = function(name) {
- Blockly.WorkspaceSvg.superClass_.createVariable.call(this, name);
- // Don't refresh the toolbox if there's a drag in progress.
- if (this.toolbox_ && this.toolbox_.flyout_ && !Blockly.Flyout.startFlyout_) {
- this.toolbox_.refreshSelection();
- }
- };
- /**
- * Make a list of all the delete areas for this workspace.
- */
- Blockly.WorkspaceSvg.prototype.recordDeleteAreas = function() {
- if (this.trashcan) {
- this.deleteAreaTrash_ = this.trashcan.getClientRect();
- } else {
- this.deleteAreaTrash_ = null;
- }
- if (this.flyout_) {
- this.deleteAreaToolbox_ = this.flyout_.getClientRect();
- } else if (this.toolbox_) {
- this.deleteAreaToolbox_ = this.toolbox_.getClientRect();
- } else {
- this.deleteAreaToolbox_ = null;
- }
- };
- /**
- * Is the mouse event over a delete area (toolbox or non-closing flyout)?
- * Opens or closes the trashcan and sets the cursor as a side effect.
- * @param {!Event} e Mouse move event.
- * @return {boolean} True if event is in a delete area.
- */
- Blockly.WorkspaceSvg.prototype.isDeleteArea = function(e) {
- var xy = new goog.math.Coordinate(e.clientX, e.clientY);
- if (this.deleteAreaTrash_) {
- if (this.deleteAreaTrash_.contains(xy)) {
- this.trashcan.setOpen_(true);
- Blockly.Css.setCursor(Blockly.Css.Cursor.DELETE);
- return true;
- }
- this.trashcan.setOpen_(false);
- }
- if (this.deleteAreaToolbox_) {
- if (this.deleteAreaToolbox_.contains(xy)) {
- Blockly.Css.setCursor(Blockly.Css.Cursor.DELETE);
- return true;
- }
- }
- Blockly.Css.setCursor(Blockly.Css.Cursor.CLOSED);
- return false;
- };
- /**
- * Handle a mouse-down on SVG drawing surface.
- * @param {!Event} e Mouse down event.
- * @private
- */
- Blockly.WorkspaceSvg.prototype.onMouseDown_ = function(e) {
- this.markFocused();
- if (Blockly.isTargetInput_(e)) {
- Blockly.Touch.clearTouchIdentifier();
- return;
- }
- Blockly.terminateDrag_(); // In case mouse-up event was lost.
- Blockly.hideChaff();
- var isTargetWorkspace = e.target && e.target.nodeName &&
- (e.target.nodeName.toLowerCase() == 'svg' ||
- e.target == this.svgBackground_);
- if (isTargetWorkspace && Blockly.selected && !this.options.readOnly) {
- // Clicking on the document clears the selection.
- Blockly.selected.unselect();
- }
- if (Blockly.isRightButton(e)) {
- // Right-click.
- this.showContextMenu_(e);
- // Since this was a click, not a drag, end the gesture immediately.
- Blockly.Touch.clearTouchIdentifier();
- } else if (this.scrollbar) {
- this.dragMode_ = Blockly.DRAG_BEGIN;
- // Record the current mouse position.
- this.startDragMouseX = e.clientX;
- this.startDragMouseY = e.clientY;
- this.startDragMetrics = this.getMetrics();
- this.startScrollX = this.scrollX;
- this.startScrollY = this.scrollY;
- // If this is a touch event then bind to the mouseup so workspace drag mode
- // is turned off and double move events are not performed on a block.
- // See comment in inject.js Blockly.init_ as to why mouseup events are
- // bound to the document instead of the SVG's surface.
- if ('mouseup' in Blockly.Touch.TOUCH_MAP) {
- Blockly.Touch.onTouchUpWrapper_ = Blockly.Touch.onTouchUpWrapper_ || [];
- Blockly.Touch.onTouchUpWrapper_ = Blockly.Touch.onTouchUpWrapper_.concat(
- Blockly.bindEventWithChecks_(document, 'mouseup', null,
- Blockly.onMouseUp_));
- }
- Blockly.onMouseMoveWrapper_ = Blockly.onMouseMoveWrapper_ || [];
- Blockly.onMouseMoveWrapper_ = Blockly.onMouseMoveWrapper_.concat(
- Blockly.bindEventWithChecks_(document, 'mousemove', null,
- Blockly.onMouseMove_));
- } else {
- // It was a click, but the workspace isn't draggable.
- Blockly.Touch.clearTouchIdentifier();
- }
- // This event has been handled. No need to bubble up to the document.
- e.stopPropagation();
- e.preventDefault();
- };
- /**
- * Start tracking a drag of an object on this workspace.
- * @param {!Event} e Mouse down event.
- * @param {!goog.math.Coordinate} xy Starting location of object.
- */
- Blockly.WorkspaceSvg.prototype.startDrag = function(e, xy) {
- // Record the starting offset between the bubble's location and the mouse.
- var point = Blockly.mouseToSvg(e, this.getParentSvg(),
- this.getInverseScreenCTM());
- // Fix scale of mouse event.
- point.x /= this.scale;
- point.y /= this.scale;
- this.dragDeltaXY_ = goog.math.Coordinate.difference(xy, point);
- };
- /**
- * Track a drag of an object on this workspace.
- * @param {!Event} e Mouse move event.
- * @return {!goog.math.Coordinate} New location of object.
- */
- Blockly.WorkspaceSvg.prototype.moveDrag = function(e) {
- var point = Blockly.mouseToSvg(e, this.getParentSvg(),
- this.getInverseScreenCTM());
- // Fix scale of mouse event.
- point.x /= this.scale;
- point.y /= this.scale;
- return goog.math.Coordinate.sum(this.dragDeltaXY_, point);
- };
- /**
- * Is the user currently dragging a block or scrolling the flyout/workspace?
- * @return {boolean} True if currently dragging or scrolling.
- */
- Blockly.WorkspaceSvg.prototype.isDragging = function() {
- return Blockly.dragMode_ == Blockly.DRAG_FREE ||
- (Blockly.Flyout.startFlyout_ &&
- Blockly.Flyout.startFlyout_.dragMode_ == Blockly.DRAG_FREE) ||
- this.dragMode_ == Blockly.DRAG_FREE;
- };
- /**
- * Handle a mouse-wheel on SVG drawing surface.
- * @param {!Event} e Mouse wheel event.
- * @private
- */
- Blockly.WorkspaceSvg.prototype.onMouseWheel_ = function(e) {
- // TODO: Remove terminateDrag and compensate for coordinate skew during zoom.
- Blockly.terminateDrag_();
- var delta = e.deltaY > 0 ? -1 : 1;
- var position = Blockly.mouseToSvg(e, this.getParentSvg(),
- this.getInverseScreenCTM());
- this.zoom(position.x, position.y, delta);
- e.preventDefault();
- };
- /**
- * Calculate the bounding box for the blocks on the workspace.
- *
- * @return {Object} Contains the position and size of the bounding box
- * containing the blocks on the workspace.
- */
- Blockly.WorkspaceSvg.prototype.getBlocksBoundingBox = function() {
- var topBlocks = this.getTopBlocks(false);
- // There are no blocks, return empty rectangle.
- if (!topBlocks.length) {
- return {x: 0, y: 0, width: 0, height: 0};
- }
- // Initialize boundary using the first block.
- var boundary = topBlocks[0].getBoundingRectangle();
- // Start at 1 since the 0th block was used for initialization
- for (var i = 1; i < topBlocks.length; i++) {
- var blockBoundary = topBlocks[i].getBoundingRectangle();
- if (blockBoundary.topLeft.x < boundary.topLeft.x) {
- boundary.topLeft.x = blockBoundary.topLeft.x;
- }
- if (blockBoundary.bottomRight.x > boundary.bottomRight.x) {
- boundary.bottomRight.x = blockBoundary.bottomRight.x;
- }
- if (blockBoundary.topLeft.y < boundary.topLeft.y) {
- boundary.topLeft.y = blockBoundary.topLeft.y;
- }
- if (blockBoundary.bottomRight.y > boundary.bottomRight.y) {
- boundary.bottomRight.y = blockBoundary.bottomRight.y;
- }
- }
- return {
- x: boundary.topLeft.x,
- y: boundary.topLeft.y,
- width: boundary.bottomRight.x - boundary.topLeft.x,
- height: boundary.bottomRight.y - boundary.topLeft.y
- };
- };
- /**
- * Clean up the workspace by ordering all the blocks in a column.
- */
- Blockly.WorkspaceSvg.prototype.cleanUp = function() {
- Blockly.Events.setGroup(true);
- var topBlocks = this.getTopBlocks(true);
- var cursorY = 0;
- for (var i = 0, block; block = topBlocks[i]; i++) {
- var xy = block.getRelativeToSurfaceXY();
- block.moveBy(-xy.x, cursorY - xy.y);
- block.snapToGrid();
- cursorY = block.getRelativeToSurfaceXY().y +
- block.getHeightWidth().height + Blockly.BlockSvg.MIN_BLOCK_Y;
- }
- Blockly.Events.setGroup(false);
- // Fire an event to allow scrollbars to resize.
- this.resizeContents();
- };
- /**
- * Copy all the blocks on the current workspace.
- * @public
- */
- Blockly.WorkspaceSvg.prototype.copyAll = function() {
- Blockly.hideChaff();
- var xmlBlocks = Blockly.Xml.workspaceToDom(this);
- if (Blockly.dragMode_ != Blockly.DRAG_FREE) {
- Blockly.Xml.deleteNext(xmlBlocks);
- }
- // Encode start position in XML.
- //var xy = block.getRelativeToSurfaceXY();
- //xmlBlock.setAttribute('x', block.RTL ? -xy.x : xy.x);
- //xmlBlock.setAttribute('y', xy.y);
- Blockly.clipboardXml_ = xmlBlocks;
- Blockly.clipboardSource_ = this;
- localStorage.setItem('_blockly_clipboardXml_', Blockly.Xml.domToText(xmlBlocks));
- }
- /**
- * Pastea all blocks on the current workspace.
- * @public
- */
- Blockly.WorkspaceSvg.prototype.pasteFromClipboard = function() {
- var blocks;
- if (localStorage.getItem('_blockly_clipboardXml_')) {
- var domText = localStorage.getItem('_blockly_clipboardXml_');
- blocks = Blockly.Xml.textToDom(domText);
- } else if (Blockly.clipboardXml_) {
- blocks = Blockly.clipboardXml_
- }
- Blockly.Events.setGroup(true);
- if (blocks.tagName !== undefined && blocks.tagName.toLowerCase() == "xml") {
- for (var i = 0, len = blocks.children.length; i < len; i=i+1) {
- Blockly.mainWorkspace.paste(blocks.children[i]);
- }
- this.align();
- } else {
- Blockly.mainWorkspace.paste(blocks);
- }
- Blockly.Events.setGroup(false);
- }
-
- /**
- * Show the context menu for the workspace.
- * @param {!Event} e Mouse event.
- * @private
- */
- Blockly.WorkspaceSvg.prototype.showContextMenu_ = function(e) {
- if (this.options.readOnly || this.isFlyout) {
- return;
- }
- var menuOptions = [];
- var topBlocks = this.getTopBlocks(true);
- var eventGroup = Blockly.genUid();
- // Options to undo/redo previous action.
- var undoOption = {};
- undoOption.text = Blockly.Msg.UNDO;
- undoOption.enabled = this.undoStack_.length > 0;
- undoOption.callback = this.undo.bind(this, false);
- menuOptions.push(undoOption);
- var redoOption = {};
- redoOption.text = Blockly.Msg.REDO;
- redoOption.enabled = this.redoStack_.length > 0;
- redoOption.callback = this.undo.bind(this, true);
- menuOptions.push(redoOption);
-
- // Options to copy all/paste previous action.
- var copyAllOption = {};
- copyAllOption.text = "Copy All Blocks";
- copyAllOption.enabled = true;
- copyAllOption.callback = this.copyAll.bind(this);
- menuOptions.push(copyAllOption);
- var pasteOption = {};
- pasteOption.text = "Paste Blocks";
- var ls_xml = localStorage.getItem('_blockly_clipboardXml_'),
- cp_xml = Blockly.clipboardXml_;
- pasteOption.enabled = false;
- if (ls_xml) {
- cp_xml = ls_xml;
- }
- if (cp_xml) {
- if (typeof cp_xml == "string") {
- cp_xml = Blockly.Xml.textToDom(cp_xml);
- }
- if (cp_xml.tagName != undefined) {
- if (cp_xml.tagName.toLowerCase() == "xml") {
- pasteOption.enabled = cp_xml.children;
- } else {
- pasteOption.enabled = cp_xml.tagName.toLowerCase() == "block";
- }
- }
- }
- pasteOption.callback = this.pasteFromClipboard.bind(this);
- menuOptions.push(pasteOption);
- // Option to clean up blocks.
- if (this.scrollbar) {
- var cleanOption = {};
- cleanOption.text = Blockly.Msg.CLEAN_UP;
- cleanOption.enabled = topBlocks.length > 1;
- cleanOption.callback = this.cleanUp.bind(this);
- //menuOptions.push(cleanOption);
- }
- // Add a little animation to collapsing and expanding.
- var DELAY = 10;
- if (this.options.collapse) {
- var hasCollapsedBlocks = false;
- var hasExpandedBlocks = false;
- for (var i = 0; i < topBlocks.length; i++) {
- var block = topBlocks[i];
- while (block) {
- if (block.isCollapsed()) {
- hasCollapsedBlocks = true;
- } else {
- hasExpandedBlocks = true;
- }
- block = block.getNextBlock();
- }
- }
- /**
- * Option to collapse or expand top blocks.
- * @param {boolean} shouldCollapse Whether a block should collapse.
- * @private
- */
- var toggleOption = function(shouldCollapse) {
- var ms = 0;
- for (var i = 0; i < topBlocks.length; i++) {
- var block = topBlocks[i];
- while (block) {
- setTimeout(block.setCollapsed.bind(block, shouldCollapse), ms);
- block = block.getNextBlock();
- ms += DELAY;
- }
- }
- };
- // Option to collapse top blocks.
- var collapseOption = {enabled: hasExpandedBlocks};
- collapseOption.text = Blockly.Msg.COLLAPSE_ALL;
- collapseOption.callback = function() {
- toggleOption(true);
- };
- menuOptions.push(collapseOption);
- // Option to expand top blocks.
- var expandOption = {enabled: hasCollapsedBlocks};
- expandOption.text = Blockly.Msg.EXPAND_ALL;
- expandOption.callback = function() {
- toggleOption(false);
- };
- menuOptions.push(expandOption);
- }
-
- var _instance = this;
-
- // Option to align the blocks
- menuOptions.push({enabled: true,
- text: "Align Blocks",
- callback: function() {
- _instance.align();
- }});
- /*
- menuOptions.push({enabled: true,
- text: "Undo",
- callback: function() {
- _instance.undo();
- }});
- menuOptions.push({enabled: true,
- text: "Redo",
- callback: function() {
- _instance.redo();
- }});
- */
-
-
- /*
- // Option to clear all the blocks.
- var clearOption = {enabled: true};
- clearOption.text = "Clear Blocks";
- var _instance = this;
- clearOption.callback = function() {
- _instance.clear();
- };
- menuOptions.push(clearOption);
- */
- // Option to delete all blocks.
- // Count the number of blocks that are deletable.
- var deleteList = [];
- function addDeletableBlocks(block) {
- if (block.isDeletable()) {
- deleteList = deleteList.concat(block.getDescendants());
- } else {
- var children = block.getChildren();
- for (var i = 0; i < children.length; i++) {
- addDeletableBlocks(children[i]);
- }
- }
- }
- for (var i = 0; i < topBlocks.length; i++) {
- addDeletableBlocks(topBlocks[i]);
- }
- function deleteNext() {
- Blockly.Events.setGroup(eventGroup);
- var block = deleteList.shift();
- if (block) {
- if (block.workspace) {
- block.dispose(false, true);
- setTimeout(deleteNext, DELAY);
- } else {
- deleteNext();
- }
- }
- Blockly.Events.setGroup(false);
- }
- var deleteOption = {
- text: deleteList.length == 1 ? Blockly.Msg.DELETE_BLOCK :
- Blockly.Msg.DELETE_X_BLOCKS.replace('%1', String(deleteList.length)),
- enabled: deleteList.length > 0,
- callback: function() {
- if (deleteList.length < 2 ) {
- deleteNext();
- } else {
- Blockly.confirm(Blockly.Msg.DELETE_ALL_BLOCKS.
- replace('%1', deleteList.length),
- function(ok) {
- if (ok) {
- deleteNext();
- }
- });
- }
- }
- };
- menuOptions.push(deleteOption);
-
- if (Blockly.captureDialog_ != undefined) {
- var captureOption = {
- text: 'Screenshot',
- enabled: true,
- callback: function() {
- Blockly.captureDialog_();
- }
- };
- menuOptions.push(captureOption);
- }
- Blockly.ContextMenu.show(e, menuOptions, this.RTL);
- };
- /**
- * Load an audio file. Cache it, ready for instantaneous playing.
- * @param {!Array.<string>} filenames List of file types in decreasing order of
- * preference (i.e. increasing size). E.g. ['media/go.mp3', 'media/go.wav']
- * Filenames include path from Blockly's root. File extensions matter.
- * @param {string} name Name of sound.
- * @private
- */
- Blockly.WorkspaceSvg.prototype.loadAudio_ = function(filenames, name) {
- if (!filenames.length) {
- return;
- }
- try {
- var audioTest = new window['Audio']();
- } catch (e) {
- // No browser support for Audio.
- // IE can throw an error even if the Audio object exists.
- return;
- }
- var sound;
- for (var i = 0; i < filenames.length; i++) {
- var filename = filenames[i];
- var ext = filename.match(/\.(\w+)$/);
- if (ext && audioTest.canPlayType('audio/' + ext[1])) {
- // Found an audio format we can play.
- sound = new window['Audio'](filename);
- break;
- }
- }
- if (sound && sound.play) {
- this.SOUNDS_[name] = sound;
- }
- };
- /**
- * Preload all the audio files so that they play quickly when asked for.
- * @private
- */
- Blockly.WorkspaceSvg.prototype.preloadAudio_ = function() {
- for (var name in this.SOUNDS_) {
- var sound = this.SOUNDS_[name];
- sound.volume = .01;
- sound.play();
- sound.pause();
- // iOS can only process one sound at a time. Trying to load more than one
- // corrupts the earlier ones. Just load one and leave the others uncached.
- if (goog.userAgent.IPAD || goog.userAgent.IPHONE) {
- break;
- }
- }
- };
- /**
- * Play a named sound at specified volume. If volume is not specified,
- * use full volume (1).
- * @param {string} name Name of sound.
- * @param {number=} opt_volume Volume of sound (0-1).
- */
- Blockly.WorkspaceSvg.prototype.playAudio = function(name, opt_volume) {
- var sound = this.SOUNDS_[name];
- if (sound) {
- // Don't play one sound on top of another.
- var now = new Date;
- if (now - this.lastSound_ < Blockly.SOUND_LIMIT) {
- return;
- }
- this.lastSound_ = now;
- var mySound;
- var ie9 = goog.userAgent.DOCUMENT_MODE &&
- goog.userAgent.DOCUMENT_MODE === 9;
- if (ie9 || goog.userAgent.IPAD || goog.userAgent.ANDROID) {
- // Creating a new audio node causes lag in IE9, Android and iPad. Android
- // and IE9 refetch the file from the server, iPad uses a singleton audio
- // node which must be deleted and recreated for each new audio tag.
- mySound = sound;
- } else {
- mySound = sound.cloneNode();
- }
- mySound.volume = (opt_volume === undefined ? 1 : opt_volume);
- mySound.play();
- } else if (this.options.parentWorkspace) {
- // Maybe a workspace on a lower level knows about this sound.
- this.options.parentWorkspace.playAudio(name, opt_volume);
- }
- };
- /**
- * Modify the block tree on the existing toolbox.
- * @param {Node|string} tree DOM tree of blocks, or text representation of same.
- */
- Blockly.WorkspaceSvg.prototype.updateToolbox = function(tree) {
- tree = Blockly.Options.parseToolboxTree(tree);
- if (!tree) {
- if (this.options.languageTree) {
- throw 'Can\'t nullify an existing toolbox.';
- }
- return; // No change (null to null).
- }
- if (!this.options.languageTree) {
- throw 'Existing toolbox is null. Can\'t create new toolbox.';
- }
- if (tree.getElementsByTagName('category').length) {
- if (!this.toolbox_) {
- throw 'Existing toolbox has no categories. Can\'t change mode.';
- }
- this.options.languageTree = tree;
- this.toolbox_.populate_(tree);
- this.toolbox_.addColour_();
- } else {
- if (!this.flyout_) {
- throw 'Existing toolbox has categories. Can\'t change mode.';
- }
- this.options.languageTree = tree;
- this.flyout_.show(tree.childNodes);
- }
- };
- /**
- * Mark this workspace as the currently focused main workspace.
- */
- Blockly.WorkspaceSvg.prototype.markFocused = function() {
- if (this.options.parentWorkspace) {
- this.options.parentWorkspace.markFocused();
- } else {
- Blockly.mainWorkspace = this;
- }
- };
- /**
- * Zooming the blocks centered in (x, y) coordinate with zooming in or out.
- * @param {number} x X coordinate of center.
- * @param {number} y Y coordinate of center.
- * @param {number} type Type of zooming (-1 zooming out and 1 zooming in).
- */
- Blockly.WorkspaceSvg.prototype.zoom = function(x, y, type) {
- var speed = this.options.zoomOptions.scaleSpeed;
- var metrics = this.getMetrics();
- var center = this.getParentSvg().createSVGPoint();
- center.x = x;
- center.y = y;
- center = center.matrixTransform(this.getCanvas().getCTM().inverse());
- x = center.x;
- y = center.y;
- var canvas = this.getCanvas();
- // Scale factor.
- var scaleChange = (type == 1) ? speed : 1 / speed;
- // Clamp scale within valid range.
- var newScale = this.scale * scaleChange;
- if (newScale > this.options.zoomOptions.maxScale) {
- scaleChange = this.options.zoomOptions.maxScale / this.scale;
- } else if (newScale < this.options.zoomOptions.minScale) {
- scaleChange = this.options.zoomOptions.minScale / this.scale;
- }
- if (this.scale == newScale) {
- return; // No change in zoom.
- }
- if (this.scrollbar) {
- var matrix = canvas.getCTM()
- .translate(x * (1 - scaleChange), y * (1 - scaleChange))
- .scale(scaleChange);
- // newScale and matrix.a should be identical (within a rounding error).
- this.scrollX = matrix.e - metrics.absoluteLeft;
- this.scrollY = matrix.f - metrics.absoluteTop;
- }
- this.setScale(newScale);
- };
- /**
- * Zooming the blocks centered in the center of view with zooming in or out.
- * @param {number} type Type of zooming (-1 zooming out and 1 zooming in).
- */
- Blockly.WorkspaceSvg.prototype.zoomCenter = function(type) {
- var metrics = this.getMetrics();
- var x = metrics.viewWidth / 2;
- var y = metrics.viewHeight / 2;
- this.zoom(x, y, type);
- };
- /**
- * Zoom the blocks to fit in the workspace if possible.
- */
- Blockly.WorkspaceSvg.prototype.zoomToFit = function() {
- var metrics = this.getMetrics();
- var blocksBox = this.getBlocksBoundingBox();
- var blocksWidth = blocksBox.width;
- var blocksHeight = blocksBox.height;
- if (!blocksWidth) {
- return; // Prevents zooming to infinity.
- }
- var workspaceWidth = metrics.viewWidth;
- var workspaceHeight = metrics.viewHeight;
- if (this.flyout_) {
- workspaceWidth -= this.flyout_.width_;
- }
- if (!this.scrollbar) {
- // Orgin point of 0,0 is fixed, blocks will not scroll to center.
- blocksWidth += metrics.contentLeft;
- blocksHeight += metrics.contentTop;
- }
- var ratioX = workspaceWidth / blocksWidth;
- var ratioY = workspaceHeight / blocksHeight;
- this.setScale(Math.min(ratioX, ratioY));
- this.scrollCenter();
- };
- /**
- * Center the workspace.
- */
- Blockly.WorkspaceSvg.prototype.scrollCenter = function() {
- if (!this.scrollbar) {
- // Can't center a non-scrolling workspace.
- return;
- }
- var metrics = this.getMetrics();
- var x = (metrics.contentWidth - metrics.viewWidth) / 2;
- if (this.flyout_) {
- x -= this.flyout_.width_ / 2;
- }
- var y = (metrics.contentHeight - metrics.viewHeight) / 2;
- this.scrollbar.set(x, y);
- };
- /**
- * Set the workspace's zoom factor.
- * @param {number} newScale Zoom factor.
- */
- Blockly.WorkspaceSvg.prototype.setScale = function(newScale) {
- if (this.options.zoomOptions.maxScale &&
- newScale > this.options.zoomOptions.maxScale) {
- newScale = this.options.zoomOptions.maxScale;
- } else if (this.options.zoomOptions.minScale &&
- newScale < this.options.zoomOptions.minScale) {
- newScale = this.options.zoomOptions.minScale;
- }
- this.scale = newScale;
- this.updateGridPattern_();
- if (this.scrollbar) {
- this.scrollbar.resize();
- } else {
- this.translate(this.scrollX, this.scrollY);
- }
- Blockly.hideChaff(false);
- if (this.flyout_) {
- // No toolbox, resize flyout.
- this.flyout_.reflow();
- }
- };
- /**
- * Updates the grid pattern.
- * @private
- */
- Blockly.WorkspaceSvg.prototype.updateGridPattern_ = function() {
- if (!this.options.gridPattern) {
- return; // No grid.
- }
- // MSIE freaks if it sees a 0x0 pattern, so set empty patterns to 100x100.
- var safeSpacing = (this.options.gridOptions['spacing'] * this.scale) || 100;
- this.options.gridPattern.setAttribute('width', safeSpacing);
- this.options.gridPattern.setAttribute('height', safeSpacing);
- var half = Math.floor(this.options.gridOptions['spacing'] / 2) + 0.5;
- var start = half - this.options.gridOptions['length'] / 2;
- var end = half + this.options.gridOptions['length'] / 2;
- var line1 = this.options.gridPattern.firstChild;
- var line2 = line1 && line1.nextSibling;
- half *= this.scale;
- start *= this.scale;
- end *= this.scale;
- if (line1) {
- line1.setAttribute('stroke-width', this.scale);
- line1.setAttribute('x1', start);
- line1.setAttribute('y1', half);
- line1.setAttribute('x2', end);
- line1.setAttribute('y2', half);
- }
- if (line2) {
- line2.setAttribute('stroke-width', this.scale);
- line2.setAttribute('x1', half);
- line2.setAttribute('y1', start);
- line2.setAttribute('x2', half);
- line2.setAttribute('y2', end);
- }
- };
- /**
- * Return an object with all the metrics required to size scrollbars for a
- * top level workspace. The following properties are computed:
- * .viewHeight: Height of the visible rectangle,
- * .viewWidth: Width of the visible rectangle,
- * .contentHeight: Height of the contents,
- * .contentWidth: Width of the content,
- * .viewTop: Offset of top edge of visible rectangle from parent,
- * .viewLeft: Offset of left edge of visible rectangle from parent,
- * .contentTop: Offset of the top-most content from the y=0 coordinate,
- * .contentLeft: Offset of the left-most content from the x=0 coordinate.
- * .absoluteTop: Top-edge of view.
- * .absoluteLeft: Left-edge of view.
- * .toolboxWidth: Width of toolbox, if it exists. Otherwise zero.
- * .toolboxHeight: Height of toolbox, if it exists. Otherwise zero.
- * .flyoutWidth: Width of the flyout if it is always open. Otherwise zero.
- * .flyoutHeight: Height of flyout if it is always open. Otherwise zero.
- * .toolboxPosition: Top, bottom, left or right.
- * @return {!Object} Contains size and position metrics of a top level
- * workspace.
- * @private
- * @this Blockly.WorkspaceSvg
- */
- Blockly.WorkspaceSvg.getTopLevelWorkspaceMetrics_ = function() {
- var svgSize = Blockly.svgSize(this.getParentSvg());
- if (this.toolbox_) {
- if (this.toolboxPosition == Blockly.TOOLBOX_AT_TOP ||
- this.toolboxPosition == Blockly.TOOLBOX_AT_BOTTOM) {
- svgSize.height -= this.toolbox_.getHeight();
- } else if (this.toolboxPosition == Blockly.TOOLBOX_AT_LEFT ||
- this.toolboxPosition == Blockly.TOOLBOX_AT_RIGHT) {
- svgSize.width -= this.toolbox_.getWidth();
- }
- }
- // Set the margin to match the flyout's margin so that the workspace does
- // not jump as blocks are added.
- var MARGIN = Blockly.Flyout.prototype.CORNER_RADIUS - 1;
- var viewWidth = svgSize.width - MARGIN;
- var viewHeight = svgSize.height - MARGIN;
- var blockBox = this.getBlocksBoundingBox();
- // Fix scale.
- var contentWidth = blockBox.width * this.scale;
- var contentHeight = blockBox.height * this.scale;
- var contentX = blockBox.x * this.scale;
- var contentY = blockBox.y * this.scale;
- if (this.scrollbar) {
- // Add a border around the content that is at least half a screenful wide.
- // Ensure border is wide enough that blocks can scroll over entire screen.
- var leftEdge = Math.min(contentX - viewWidth / 2,
- contentX + contentWidth - viewWidth);
- var rightEdge = Math.max(contentX + contentWidth + viewWidth / 2,
- contentX + viewWidth);
- var topEdge = Math.min(contentY - viewHeight / 2,
- contentY + contentHeight - viewHeight);
- var bottomEdge = Math.max(contentY + contentHeight + viewHeight / 2,
- contentY + viewHeight);
- } else {
- var leftEdge = blockBox.x;
- var rightEdge = leftEdge + blockBox.width;
- var topEdge = blockBox.y;
- var bottomEdge = topEdge + blockBox.height;
- }
- var absoluteLeft = 0;
- if (this.toolbox_ && this.toolboxPosition == Blockly.TOOLBOX_AT_LEFT) {
- absoluteLeft = this.toolbox_.getWidth();
- }
- var absoluteTop = 0;
- if (this.toolbox_ && this.toolboxPosition == Blockly.TOOLBOX_AT_TOP) {
- absoluteTop = this.toolbox_.getHeight();
- }
- var metrics = {
- viewHeight: svgSize.height,
- viewWidth: svgSize.width,
- contentHeight: bottomEdge - topEdge,
- contentWidth: rightEdge - leftEdge,
- viewTop: -this.scrollY,
- viewLeft: -this.scrollX,
- contentTop: topEdge,
- contentLeft: leftEdge,
- absoluteTop: absoluteTop,
- absoluteLeft: absoluteLeft,
- toolboxWidth: this.toolbox_ ? this.toolbox_.getWidth() : 0,
- toolboxHeight: this.toolbox_ ? this.toolbox_.getHeight() : 0,
- flyoutWidth: this.flyout_ ? this.flyout_.getWidth() : 0,
- flyoutHeight: this.flyout_ ? this.flyout_.getHeight() : 0,
- toolboxPosition: this.toolboxPosition
- };
- return metrics;
- };
- /**
- * Sets the X/Y translations of a top level workspace to match the scrollbars.
- * @param {!Object} xyRatio Contains an x and/or y property which is a float
- * between 0 and 1 specifying the degree of scrolling.
- * @private
- * @this Blockly.WorkspaceSvg
- */
- Blockly.WorkspaceSvg.setTopLevelWorkspaceMetrics_ = function(xyRatio) {
- if (!this.scrollbar) {
- throw 'Attempt to set top level workspace scroll without scrollbars.';
- }
- var metrics = this.getMetrics();
- if (goog.isNumber(xyRatio.x)) {
- this.scrollX = -metrics.contentWidth * xyRatio.x - metrics.contentLeft;
- }
- if (goog.isNumber(xyRatio.y)) {
- this.scrollY = -metrics.contentHeight * xyRatio.y - metrics.contentTop;
- }
- var x = this.scrollX + metrics.absoluteLeft;
- var y = this.scrollY + metrics.absoluteTop;
- this.translate(x, y);
- if (this.options.gridPattern) {
- this.options.gridPattern.setAttribute('x', x);
- this.options.gridPattern.setAttribute('y', y);
- if (goog.userAgent.IE) {
- // IE doesn't notice that the x/y offsets have changed. Force an update.
- this.updateGridPattern_();
- }
- }
- };
- /**
- * Update whether this workspace has resizes enabled.
- * If enabled, workspace will resize when appropriate.
- * If disabled, workspace will not resize until re-enabled.
- * Use to avoid resizing during a batch operation, for performance.
- * @param {boolean} enabled Whether resizes should be enabled.
- */
- Blockly.WorkspaceSvg.prototype.setResizesEnabled = function(enabled) {
- var reenabled = (!this.resizesEnabled_ && enabled);
- this.resizesEnabled_ = enabled;
- if (reenabled) {
- // Newly enabled. Trigger a resize.
- this.resizeContents();
- }
- };
- /**
- * Dispose of all blocks in workspace, with an optimization to prevent resizes.
- */
- Blockly.WorkspaceSvg.prototype.clear = function() {
- this.setResizesEnabled(false);
- Blockly.WorkspaceSvg.superClass_.clear.call(this);
- this.setResizesEnabled(true);
- };
- // Export symbols that would otherwise be renamed by Closure compiler.
- Blockly.WorkspaceSvg.prototype['setVisible'] =
- Blockly.WorkspaceSvg.prototype.setVisible;
|