From 7c891e35207b6f561917f27739917cb84755d57c Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Fri, 2 Aug 2024 13:05:42 -0700 Subject: [PATCH] feat: reenable Scratch's FieldVariable subclass (#91) * chore: move field_variable.js into src * chore: format field_variable.js * feat: reenable Scratch's FieldVariable subclass --- core/field_variable.js | 385 ----------------------------------------- src/field_variable.js | 139 +++++++++++++++ src/index.js | 1 + 3 files changed, 140 insertions(+), 385 deletions(-) delete mode 100644 core/field_variable.js create mode 100644 src/field_variable.js diff --git a/core/field_variable.js b/core/field_variable.js deleted file mode 100644 index c188482bd6..0000000000 --- a/core/field_variable.js +++ /dev/null @@ -1,385 +0,0 @@ -/** - * @license - * Visual Blocks Editor - * - * Copyright 2012 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 Variable input field. - * @author fraser@google.com (Neil Fraser) - */ -'use strict'; - -goog.provide('Blockly.FieldVariable'); - -goog.require('Blockly.FieldDropdown'); -goog.require('Blockly.Msg'); -goog.require('Blockly.VariableModel'); -goog.require('Blockly.Variables'); -goog.require('goog.asserts'); -goog.require('goog.string'); - - -/** - * Class for a variable's dropdown field. - * @param {?string} varname The default name for the variable. If null, - * a unique variable name will be generated. - * @param {Function=} opt_validator A function that is executed when a new - * option is selected. Its sole argument is the new option value. - * @param {Array.} opt_variableTypes A list of the types of variables to - * include in the dropdown. - * @extends {Blockly.FieldDropdown} - * @constructor - */ -Blockly.FieldVariable = function(varname, opt_validator, opt_variableTypes) { - // The FieldDropdown constructor would call setValue, which might create a - // spurious variable. Just do the relevant parts of the constructor. - this.menuGenerator_ = Blockly.FieldVariable.dropdownCreate; - this.size_ = new goog.math.Size(Blockly.BlockSvg.FIELD_WIDTH, - Blockly.BlockSvg.FIELD_HEIGHT); - this.setValidator(opt_validator); - // TODO (blockly #1499): Add opt_default_type to match default value. - // If not set, ''. - this.defaultVariableName = (varname || ''); - var hasSingleVarType = opt_variableTypes && (opt_variableTypes.length == 1); - this.defaultType_ = hasSingleVarType ? opt_variableTypes[0] : ''; - this.variableTypes = opt_variableTypes; - this.addArgType('variable'); - - this.value_ = null; -}; -goog.inherits(Blockly.FieldVariable, Blockly.FieldDropdown); - -/** - * Construct a FieldVariable from a JSON arg object, - * dereferencing any string table references. - * @param {!Object} options A JSON object with options (variable, - * variableTypes, and defaultType). - * @returns {!Blockly.FieldVariable} The new field instance. - * @package - * @nocollapse - */ -Blockly.FieldVariable.fromJson = function(options) { - var varname = Blockly.utils.replaceMessageReferences(options['variable']); - var variableTypes = options['variableTypes']; - return new Blockly.FieldVariable(varname, null, variableTypes); -}; - -/** - * Initialize everything needed to render this field. This includes making sure - * that the field's value is valid. - * @public - */ -Blockly.FieldVariable.prototype.init = function() { - if (this.fieldGroup_) { - // Dropdown has already been initialized once. - return; - } - Blockly.FieldVariable.superClass_.init.call(this); - - // TODO (blockly #1010): Change from init/initModel to initView/initModel - this.initModel(); -}; - -/** - * Initialize the model for this field if it has not already been initialized. - * If the value has not been set to a variable by the first render, we make up a - * variable rather than let the value be invalid. - * @package - */ -Blockly.FieldVariable.prototype.initModel = function() { - if (this.variable_) { - return; // Initialization already happened. - } - this.workspace_ = this.sourceBlock_.workspace; - // Initialize this field if it's in a broadcast block in the flyout - var variable = this.initFlyoutBroadcast_(this.workspace_); - if (!variable) { - var variable = Blockly.Variables.getOrCreateVariablePackage( - this.workspace_, null, this.defaultVariableName, this.defaultType_); - } - // Don't fire a change event for this setValue. It would have null as the - // old value, which is not valid. - Blockly.Events.disable(); - try { - this.setValue(variable.getId()); - } finally { - Blockly.Events.enable(); - } -}; - -/** - * Initialize broadcast blocks in the flyout. - * Implicit deletion of broadcast messages from the scratch vm may cause - * broadcast blocks in the flyout to change which variable they display as the - * selected option when the workspace is refreshed. - * Re-sort the broadcast messages by name, and set the field value to the id - * of the variable that comes first in sorted order. - * @param {!Blockly.Workspace} workspace The flyout workspace containing the - * broadcast block. - * @return {string} The variable of type 'broadcast_msg' that comes - * first in sorted order. - */ -Blockly.FieldVariable.prototype.initFlyoutBroadcast_ = function(workspace) { - // Using shorter name for this constant - var broadcastMsgType = Blockly.BROADCAST_MESSAGE_VARIABLE_TYPE; - var broadcastVars = workspace.getVariablesOfType(broadcastMsgType); - if(workspace.isFlyout && this.defaultType_ == broadcastMsgType && - broadcastVars.length != 0) { - broadcastVars.sort(Blockly.VariableModel.compareByName); - return broadcastVars[0]; - } -}; - -/** - * Dispose of this field. - * @public - */ -Blockly.FieldVariable.dispose = function() { - Blockly.FieldVariable.superClass_.dispose.call(this); - this.workspace_ = null; - this.variableMap_ = null; -}; - -/** - * Attach this field to a block. - * @param {!Blockly.Block} block The block containing this field. - */ -Blockly.FieldVariable.prototype.setSourceBlock = function(block) { - goog.asserts.assert(!block.isShadow(), - 'Variable fields are not allowed to exist on shadow blocks.'); - Blockly.FieldVariable.superClass_.setSourceBlock.call(this, block); -}; - -/** - * Get the variable's ID. - * @return {string} Current variable's ID. - */ -Blockly.FieldVariable.prototype.getValue = function() { - return this.variable_ ? this.variable_.getId() : null; -}; - -/** - * Get the text from this field, which is the selected variable's name. - * @return {string} The selected variable's name, or the empty string if no - * variable is selected. - */ -Blockly.FieldVariable.prototype.getText = function() { - return this.variable_ ? this.variable_.name : ''; -}; - -/** - * Get the variable model for the selected variable. - * Not guaranteed to be in the variable map on the workspace (e.g. if accessed - * after the variable has been deleted). - * @return {?Blockly.VariableModel} the selected variable, or null if none was - * selected. - * @package - */ -Blockly.FieldVariable.prototype.getVariable = function() { - return this.variable_; -}; - -/** - * Set the variable ID. - * @param {string} id New variable ID, which must reference an existing - * variable. - */ -Blockly.FieldVariable.prototype.setValue = function(id) { - var workspace = this.sourceBlock_.workspace; - var variable = Blockly.Variables.getVariable(workspace, id); - - if (!variable) { - throw new Error('Variable id doesn\'t point to a real variable! ID was ' + - id); - } - // Type checks! - var type = variable.type; - if (!this.typeIsAllowed_(type)) { - throw new Error('Variable type doesn\'t match this field! Type was ' + - type); - } - if (this.sourceBlock_ && Blockly.Events.isEnabled()) { - var oldValue = this.variable_ ? this.variable_.getId() : null; - Blockly.Events.fire(new Blockly.Events.BlockChange( - this.sourceBlock_, 'field', this.name, oldValue, id)); - } - this.variable_ = variable; - this.value_ = id; - this.setText(variable.name); -}; - -/** - * Check whether the given variable type is allowed on this field. - * @param {string} type The type to check. - * @return {boolean} True if the type is in the list of allowed types. - * @private - */ -Blockly.FieldVariable.prototype.typeIsAllowed_ = function(type) { - var typeList = this.getVariableTypes_(); - if (!typeList) { - return true; // If it's null, all types are valid. - } - for (var i = 0; i < typeList.length; i++) { - if (type == typeList[i]) { - return true; - } - } - return false; -}; - -/** - * Return a list of variable types to include in the dropdown. - * @return {!Array.} Array of variable types. - * @throws {Error} if variableTypes is an empty array. - * @private - */ -Blockly.FieldVariable.prototype.getVariableTypes_ = function() { - // TODO (#1513): Try to avoid calling this every time the field is edited. - var variableTypes = this.variableTypes; - if (variableTypes === null) { - // If variableTypes is null, return all variable types. - if (this.sourceBlock_) { - var workspace = this.sourceBlock_.workspace; - return workspace.getVariableTypes(); - } - } - variableTypes = variableTypes || ['']; - if (variableTypes.length == 0) { - // Throw an error if variableTypes is an empty list. - var name = this.getText(); - throw new Error('\'variableTypes\' of field variable ' + - name + ' was an empty list'); - } - return variableTypes; -}; - -/** - * Return a sorted list of variable names for variable dropdown menus. - * Include a special option at the end for creating a new variable name. - * @return {!Array.} Array of variable names. - * @this {Blockly.FieldVariable} - */ -Blockly.FieldVariable.dropdownCreate = function() { - if (!this.variable_) { - throw new Error('Tried to call dropdownCreate on a variable field with no' + - ' variable selected.'); - } - var variableModelList = []; - var name = this.getText(); - var workspace = null; - if (this.sourceBlock_) { - workspace = this.sourceBlock_.workspace; - } - if (workspace) { - var variableTypes = this.getVariableTypes_(); - var variableModelList = []; - // Get a copy of the list, so that adding rename and new variable options - // doesn't modify the workspace's list. - for (var i = 0; i < variableTypes.length; i++) { - var variableType = variableTypes[i]; - var variables = workspace.getVariablesOfType(variableType); - variableModelList = variableModelList.concat(variables); - - var potentialVarMap = workspace.getPotentialVariableMap(); - if (potentialVarMap) { - var potentialVars = potentialVarMap.getVariablesOfType(variableType); - variableModelList = variableModelList.concat(potentialVars); - } - } - } - variableModelList.sort(Blockly.VariableModel.compareByName); - - var options = []; - for (var i = 0; i < variableModelList.length; i++) { - // Set the uuid as the internal representation of the variable. - options[i] = [variableModelList[i].name, variableModelList[i].getId()]; - } - if (this.defaultType_ == Blockly.BROADCAST_MESSAGE_VARIABLE_TYPE) { - options.unshift( - [Blockly.Msg.NEW_BROADCAST_MESSAGE, Blockly.NEW_BROADCAST_MESSAGE_ID]); - } else { - // Scalar variables and lists have the same backing action, but the option - // text is different. - if (this.defaultType_ == Blockly.LIST_VARIABLE_TYPE) { - var renameText = Blockly.Msg.RENAME_LIST; - var deleteText = Blockly.Msg.DELETE_LIST; - } else { - var renameText = Blockly.Msg.RENAME_VARIABLE; - var deleteText = Blockly.Msg.DELETE_VARIABLE; - } - options.push([renameText, Blockly.RENAME_VARIABLE_ID]); - if (deleteText) { - options.push( - [ - deleteText.replace('%1', name), - Blockly.DELETE_VARIABLE_ID - ]); - } - } - - return options; -}; - -/** - * Handle the selection of an item in the variable dropdown menu. - * Special case the 'Rename variable...', 'Delete variable...', - * and 'New message...' options. - * In the rename case, prompt the user for a new name. - * @param {!goog.ui.Menu} menu The Menu component clicked. - * @param {!goog.ui.MenuItem} menuItem The MenuItem selected within menu. - */ -Blockly.FieldVariable.prototype.onItemSelected = function(menu, menuItem) { - var id = menuItem.getValue(); - if (this.sourceBlock_ && this.sourceBlock_.workspace) { - var workspace = this.sourceBlock_.workspace; - if (id == Blockly.RENAME_VARIABLE_ID) { - // Rename variable. - Blockly.Variables.renameVariable(workspace, this.variable_); - return; - } else if (id == Blockly.DELETE_VARIABLE_ID) { - // Delete variable. - workspace.deleteVariableById(this.variable_.getId()); - return; - } else if (id == Blockly.NEW_BROADCAST_MESSAGE_ID) { - var thisField = this; - var updateField = function(varId) { - if (varId) { - thisField.setValue(varId); - } - }; - Blockly.Variables.createVariable(workspace, updateField, - Blockly.BROADCAST_MESSAGE_VARIABLE_TYPE); - return; - } - - // TODO (blockly #1529): Call any validation function, and allow it to override. - } - this.setValue(id); -}; - -/** - * Overrides referencesVariables(), indicating this field refers to a variable. - * @return {boolean} True. - * @package - * @override - */ -Blockly.FieldVariable.prototype.referencesVariables = function() { - return true; -}; - -Blockly.Field.register('field_variable', Blockly.FieldVariable); diff --git a/src/field_variable.js b/src/field_variable.js new file mode 100644 index 0000000000..0617fd688b --- /dev/null +++ b/src/field_variable.js @@ -0,0 +1,139 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2012 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 Variable input field. + * @author fraser@google.com (Neil Fraser) + */ +import * as Blockly from "blockly/core"; +import * as Constants from "./constants.js"; +import { ScratchMsgs } from "../msg/scratch_msgs.js"; +import { createVariable } from "./variables.js"; + +class FieldVariable extends Blockly.FieldVariable { + constructor(varName, validator, variableTypes, defaultType, config) { + super(varName, validator, variableTypes, defaultType, config); + this.menuGenerator_ = FieldVariable.dropdownCreate; + } + + initModel() { + if (!this.variable) { + const sourceBlock = this.getSourceBlock(); + if (sourceBlock) { + const broadcastVariable = this.initFlyoutBroadcast( + sourceBlock.workspace + ); + if (broadcastVariable) { + this.doValueUpdate_(broadcastVariable.getId()); + return; + } + } + } + + super.initModel(); + } + + /** + * Initialize broadcast blocks in the flyout. + * Implicit deletion of broadcast messages from the scratch vm may cause + * broadcast blocks in the flyout to change which variable they display as the + * selected option when the workspace is refreshed. + * Re-sort the broadcast messages by name, and set the field value to the id + * of the variable that comes first in sorted order. + * @param {!Blockly.Workspace} workspace The flyout workspace containing the + * broadcast block. + * @return {string} The variable of type 'broadcast_msg' that comes + * first in sorted order. + */ + initFlyoutBroadcast(workspace) { + const broadcastVars = workspace.getVariablesOfType( + Constants.BROADCAST_MESSAGE_VARIABLE_TYPE + ); + if ( + workspace.isFlyout && + this.getDefaultType() == Constants.BROADCAST_MESSAGE_VARIABLE_TYPE && + broadcastVars.length != 0 + ) { + broadcastVars.sort(Blockly.Variables.compareByName); + return broadcastVars[0]; + } + } + + /** + * Return a sorted list of variable names for variable dropdown menus. + * Include a special option at the end for creating a new variable name. + * @return {!Array.} Array of variable names. + * @this {Blockly.FieldVariable} + */ + static dropdownCreate() { + const options = super.dropdownCreate(); + const type = this.getDefaultType(); + if (type === Constants.BROADCAST_MESSAGE_VARIABLE_TYPE) { + options.splice(-2, 2, [ + ScratchMsgs.translate("NEW_BROADCAST_MESSAGE"), + Constants.NEW_BROADCAST_MESSAGE_ID, + ]); + } else if (type === Constants.LIST_VARIABLE_TYPE) { + for (const option of options) { + if (option[1] === Blockly.RENAME_VARIABLE_ID) { + option[0] = ScratchMsgs.translate("RENAME_LIST"); + } else if (option[1] === Blockly.DELETE_VARIABLE_ID) { + option[0] = ScratchMsgs.translate("DELETE_LIST").replace( + "%1", + this.getText() + ); + } + } + } + + return options; + } + + /** Handle the selection of an item in the variable dropdown menu. + * Special case the 'Rename variable...', 'Delete variable...', + * and 'New message...' options. + * In the rename case, prompt the user for a new name. + * @param {!Blockly.Menu} menu The Menu component clicked. + * @param {!Blockly.MenuItem} menuItem The MenuItem selected within menu. + */ + onItemSelected_(menu, menuItem) { + const sourceBlock = this.getSourceBlock(); + if ( + sourceBlock && + !sourceBlock.isDeadOrDying() && + menuItem.getValue() === Constants.NEW_BROADCAST_MESSAGE_ID + ) { + createVariable( + sourceBlock.workspace, + (varId) => { + if (varId) { + this.setValue(varId); + } + }, + Constants.BROADCAST_MESSAGE_VARIABLE_TYPE + ); + return; + } + super.onItemSelected_(menu, menuItem); + } +} + +Blockly.fieldRegistry.unregister("field_variable"); +Blockly.fieldRegistry.register("field_variable", FieldVariable); diff --git a/src/index.js b/src/index.js index 917665e6f1..da6176eb5b 100644 --- a/src/index.js +++ b/src/index.js @@ -44,6 +44,7 @@ import "./events_block_comment_delete.js"; import "./events_block_comment_move.js"; import "./events_block_comment_resize.js"; import "./events_scratch_variable_create.js"; +import "./field_variable.js"; import { buildShadowFilter } from "./shadows.js"; export * from "blockly";