diff --git a/core/block_dragger.js b/core/block_dragger.js index 0ba1749f0..c6a0a3b1a 100644 --- a/core/block_dragger.js +++ b/core/block_dragger.js @@ -168,7 +168,9 @@ Blockly.BlockDragger.prototype.startBlockDrag = function(currentDragDeltaXY) { this.draggingBlock_.moveToDragSurface_(); if (this.workspace_.toolbox_) { - this.workspace_.toolbox_.addDeleteStyle(); + var style = this.draggingBlock_.isDeletable() ? 'blocklyToolboxDelete' : + 'blocklyToolboxGrab'; + this.workspace_.toolbox_.addStyle(style); } }; @@ -224,7 +226,9 @@ Blockly.BlockDragger.prototype.endBlockDrag = function(e, currentDragDeltaXY) { this.workspace_.setResizesEnabled(true); if (this.workspace_.toolbox_) { - this.workspace_.toolbox_.removeDeleteStyle(); + var style = this.draggingBlock_.isDeletable() ? 'blocklyToolboxDelete' : + 'blocklyToolboxGrab'; + this.workspace_.toolbox_.removeStyle(style); } Blockly.Events.setGroup(false); }; diff --git a/core/css.js b/core/css.js index d5074b0e3..57c244d3c 100644 --- a/core/css.js +++ b/core/css.js @@ -253,6 +253,12 @@ Blockly.Css.CONTENT = [ 'cursor: url("<<>>/handdelete.cur"), auto;', '}', + '.blocklyToolboxGrab {', + 'cursor: url("<<>>/handclosed.cur"), auto;', + 'cursor: grabbing;', + 'cursor: -webkit-grabbing;', + '}', + '.blocklyDragging>.blocklyPath,', '.blocklyDragging>.blocklyPathLight {', 'fill-opacity: .8;', @@ -499,6 +505,9 @@ Blockly.Css.CONTENT = [ '.blocklyDropdownMenu {', 'padding: 0 !important;', + /* max-height value is same as the constant + * Blockly.FieldDropdown.MAX_MENU_HEIGHT defined in field_dropdown.js. */ + 'max-height: 300px !important;', '}', /* Override the default Closure URL. */ diff --git a/core/field_dropdown.js b/core/field_dropdown.js index d70212186..2f9c11e61 100644 --- a/core/field_dropdown.js +++ b/core/field_dropdown.js @@ -68,6 +68,12 @@ goog.inherits(Blockly.FieldDropdown, Blockly.Field); */ Blockly.FieldDropdown.CHECKMARK_OVERHANG = 25; +/** + * Maximum height of the dropdown menu,it's also referenced in css.js as + * part of .blocklyDropdownMenu. + */ +Blockly.FieldDropdown.MAX_MENU_HEIGHT = 300; + /** * Android can't (in 2014) display "▾", so use "▼" instead. */ @@ -239,6 +245,10 @@ Blockly.FieldDropdown.prototype.positionMenu_ = function(menu) { this.createWidget_(menu); var menuSize = Blockly.utils.uiMenu.getSize(menu); + if (menuSize.height > Blockly.FieldDropdown.MAX_MENU_HEIGHT) { + menuSize.height = Blockly.FieldDropdown.MAX_MENU_HEIGHT; + } + if (this.sourceBlock_.RTL) { Blockly.utils.uiMenu.adjustBBoxesForRTL(viewportBBox, anchorBBox, menuSize); } diff --git a/core/field_image.js b/core/field_image.js index 8ee57bc6a..6c2c11793 100644 --- a/core/field_image.js +++ b/core/field_image.js @@ -175,6 +175,13 @@ Blockly.FieldImage.prototype.render_ = function() { // NOP }; +/** + * Images are fixed width, no need to render even if forced. + */ +Blockly.FieldImage.prototype.forceRerender = function() { + // NOP +}; + /** * Images are fixed width, no need to update. * @private diff --git a/core/flyout_base.js b/core/flyout_base.js index 1ec79c31b..c56ea6253 100644 --- a/core/flyout_base.js +++ b/core/flyout_base.js @@ -596,6 +596,33 @@ Blockly.Flyout.prototype.onMouseDown_ = function(e) { } }; +/** + * Helper function to get the list of variables that have been added to the + * workspace after adding a new block, using the given list of variables that + * were in the workspace before the new block was added. + * @param {!Array.} originalVariables The array of + * variables that existed in the workspace before adding the new block. + * @return {!Array.} The new array of variables that were + * freshly added to the workspace after creating the new block, or [] if no + * new variables were added to the workspace. + * @private + */ +Blockly.Flyout.prototype.getAddedVariables_ = function(originalVariables) { + var allCurrentVariables = this.targetWorkspace_.getAllVariables(); + var addedVariables = []; + if (originalVariables.length != allCurrentVariables.length) { + for (var i = 0; i < allCurrentVariables.length; i++) { + var variable = allCurrentVariables[i]; + // For any variable that is present in allCurrentVariables but not + // present in originalVariables, add the variable to addedVariables. + if (!originalVariables.includes(variable)) { + addedVariables.push(variable); + } + } + } + return addedVariables; +}; + /** * Create a copy of this block on the workspace. * @param {!Blockly.BlockSvg} originalBlock The block to copy from the flyout. @@ -606,6 +633,7 @@ Blockly.Flyout.prototype.onMouseDown_ = function(e) { Blockly.Flyout.prototype.createBlock = function(originalBlock) { var newBlock = null; Blockly.Events.disable(); + var variablesBeforeCreation = this.targetWorkspace_.getAllVariables(); this.targetWorkspace_.setResizesEnabled(false); try { newBlock = this.placeNewBlock_(originalBlock); @@ -615,9 +643,16 @@ Blockly.Flyout.prototype.createBlock = function(originalBlock) { Blockly.Events.enable(); } + var newVariables = this.getAddedVariables_(variablesBeforeCreation); + if (Blockly.Events.isEnabled()) { Blockly.Events.setGroup(true); Blockly.Events.fire(new Blockly.Events.Create(newBlock)); + // Fire a VarCreate event for each (if any) new variable created. + for(var i = 0; i < newVariables.length; i++) { + var thisVariable = newVariables[i]; + Blockly.Events.fire(new Blockly.Events.VarCreate(thisVariable)); + } } if (this.autoClose) { this.hide(); diff --git a/core/toolbox.js b/core/toolbox.js index 5531f1c4a..0a07fddba 100644 --- a/core/toolbox.js +++ b/core/toolbox.js @@ -422,21 +422,23 @@ Blockly.Toolbox.prototype.clearSelection = function() { }; /** - * Adds styles on the toolbox indicating blocks will be deleted. + * Adds a style on the toolbox. Usually used to change the cursor. + * @param {string} style The name of the class to add. * @package */ -Blockly.Toolbox.prototype.addDeleteStyle = function() { +Blockly.Toolbox.prototype.addStyle = function(style) { Blockly.utils.addClass(/** @type {!Element} */ (this.HtmlDiv), - 'blocklyToolboxDelete'); + style); }; /** - * Remove styles from the toolbox that indicate blocks will be deleted. + * Removes a style from the toolbox. Usually used to change the cursor. + * @param {string} style The name of the class to remove. * @package */ -Blockly.Toolbox.prototype.removeDeleteStyle = function() { +Blockly.Toolbox.prototype.removeStyle = function(style) { Blockly.utils.removeClass(/** @type {!Element} */ (this.HtmlDiv), - 'blocklyToolboxDelete'); + style); }; /** diff --git a/core/xml.js b/core/xml.js index 82e4040b4..bf0d9e321 100644 --- a/core/xml.js +++ b/core/xml.js @@ -86,6 +86,50 @@ Blockly.Xml.blockToDomWithXY = function(block, opt_noId) { return element; }; +/** + * Encode a field as XML. + * @param {!Blockly.Field} field The field to encode. + * @param {!Blockly.Workspace} workspace The workspace that the field is in. + * @return {?Element} XML element, or null if the field did not need to be + * serialized. + * @private + */ +Blockly.Xml.fieldToDom_ = function(field, workspace) { + if (field.name && field.EDITABLE) { + var container = goog.dom.createDom('field', null, field.getValue()); + container.setAttribute('name', field.name); + if (field instanceof Blockly.FieldVariable) { + var variable = workspace.getVariable(field.getValue()); + if (variable) { + container.setAttribute('id', variable.getId()); + container.setAttribute('variabletype', variable.type); + } + } + return container; + } + return null; +}; + +/** + * Encode all of a block's fields as XML and attach them to the given tree of + * XML elements. + * @param {!Blockly.Block} block A block with fields to be encoded. + * @param {!Element} element The XML element to which the field DOM should be + * attached. + * @private + */ +Blockly.Xml.allFieldsToDom_ = function(block, element) { + var workspace = block.workspace; + for (var i = 0, input; input = block.inputList[i]; i++) { + for (var j = 0, field; field = input.fieldRow[j]; j++) { + var fieldDom = Blockly.Xml.fieldToDom_(field, workspace); + if (fieldDom) { + element.appendChild(fieldDom); + } + } + } +}; + /** * Encode a block subtree as XML. * @param {!Blockly.Block} block The root block to encode. @@ -105,25 +149,8 @@ Blockly.Xml.blockToDom = function(block, opt_noId) { element.appendChild(mutation); } } - function fieldToDom(field) { - if (field.name && field.EDITABLE) { - var container = goog.dom.createDom('field', null, field.getValue()); - container.setAttribute('name', field.name); - if (field instanceof Blockly.FieldVariable) { - var variable = block.workspace.getVariable(field.getValue()); - if (variable) { - container.setAttribute('id', variable.getId()); - container.setAttribute('variabletype', variable.type); - } - } - element.appendChild(container); - } - } - for (var i = 0, input; input = block.inputList[i]; i++) { - for (var j = 0, field; field = input.fieldRow[j]; j++) { - fieldToDom(field); - } - } + + Blockly.Xml.allFieldsToDom_(block, element); var commentText = block.getCommentText(); if (commentText) { @@ -583,31 +610,7 @@ Blockly.Xml.domToBlockHeadless_ = function(xmlBlock, workspace) { // Titles were renamed to field in December 2013. // Fall through. case 'field': - var field = block.getField(name); - var text = xmlChild.textContent; - if (field instanceof Blockly.FieldVariable) { - // TODO (marisaleung): When we change setValue and getValue to - // interact with IDs instead of names, update this so that we get - // the variable based on ID instead of textContent. - var type = xmlChild.getAttribute('variabletype') || ''; - var variable = workspace.getVariable(text); - if (!variable) { - variable = workspace.createVariable(text, type, - xmlChild.getAttribute(id)); - } - if (type != null && type !== variable.type) { - throw Error('Serialized variable type with id \'' + - variable.getId() + '\' had type ' + variable.type + ', and ' + - 'does not match variable field that references it: ' + - Blockly.Xml.domToText(xmlChild) + '.'); - } - } - if (!field) { - console.warn('Ignoring non-existent field ' + name + ' in block ' + - prototypeName); - break; - } - field.setValue(text); + Blockly.Xml.domToField_(block, name, xmlChild); break; case 'value': case 'statement': @@ -695,6 +698,42 @@ Blockly.Xml.domToBlockHeadless_ = function(xmlBlock, workspace) { return block; }; +/** + * Decode an XML field tag and set the value of that field on the given block. + * @param {!Blockly.Block} block The block that is currently being deserialized. + * @param {string} fieldName The name of the field on the block. + * @param {!Element} xml The field tag to decode. + * @private + */ +Blockly.Xml.domToField_ = function(block, fieldName, xml) { + var field = block.getField(fieldName); + if (!field) { + console.warn('Ignoring non-existent field ' + fieldName + ' in block ' + + block.type); + return; + } + + var text = xml.textContent; + if (field instanceof Blockly.FieldVariable) { + // TODO (#1199): When we change setValue and getValue to + // interact with IDs instead of names, update this so that we get + // the variable based on ID instead of textContent. + var type = xml.getAttribute('variabletype') || ''; + var variable = block.workspace.getVariable(text); + if (!variable) { + variable = block.workspace.createVariable(text, type, + xml.getAttribute('id')); + } + if (type != null && type !== variable.type) { + throw Error('Serialized variable type with id \'' + + variable.getId() + '\' had type ' + variable.type + ', and ' + + 'does not match variable field that references it: ' + + Blockly.Xml.domToText(xml) + '.'); + } + } + field.setValue(text); +}; + /** * Remove any 'next' block (statements in a stack). * @param {!Element} xmlBlock XML block element.