Skip to content

[Google Blockly] Add support for Behavior blocks#53154

Merged
ebeastlake merged 50 commits into
stagingfrom
mike/behavior-blocks
Sep 5, 2023
Merged

[Google Blockly] Add support for Behavior blocks#53154
ebeastlake merged 50 commits into
stagingfrom
mike/behavior-blocks

Conversation

@mikeharv

@mikeharv mikeharv commented Aug 3, 2023

Copy link
Copy Markdown
Contributor

This builds on the work to implement Blockly's shareable procedures blocks plugin. This adds support for behavior blocks. Last week, we merged support for function blocks, which were a bit more straightforward:

Behavior blocks have some additional peculiarities.

Note: The current Blockly labs are Dance, Poetry, Flappy, and Bounce. Since behaviors are not used in any of these labs, most of the changes in this PR are not discoverable unless you use the ?blocklyVersion=google URL param to force a non-Blockly lab (e.g., Sprite Lab) to use Google Blockly.

Expand here for more information about how behavior blocks work as of this PR

The screenshots below compare the behavior in Sprite Lab on studio.code.org using CDO Blockly to Sprite Lab on localhost using Google Blockly. Note: The following screenshots assume that useModalFunctionEditor is true (the default for Sprite Lab projects) but that the modalFunctionEditor experiment is disabled. There are two control levels because Dance Party levels use functions, and some Dance Party levels are configured to have useModalFunctionEditor set to true. This means that, on the function side, we could expose the unfinished function editor on any level that accidentally has that flag set incorrectly. There were too many levels with this flag set to true to change this on the level-file side, so instead, Molly added an experiment flag so that useModalFunctionEditor has to be true and the experiment has to be enabled for the editor to be rendered. The following tables describe the interactions of those two flags and their effect on functionality.

For Functions

useModalFunctionEditor flag modalFunctionEditor experiment Result
T Enabled “Create New Function” button opens a working but unfinished modal editor; the edit button appears, and functions can be edited through the modal
T Disabled “Create New Function” button opens the function definition block in the main workspace; the edit button appears, and clicking it centers on the relevant function definition block
F Enabled Blank function definition block appears in the functions category in the toolbox; there is no edit button, but the function can be renamed and edited in the main workspace
F Disabled Blank function definition block appears in the functions category in the toolbox; there is no edit button, but the function can be renamed and edited in the main workspace

This PR does not cover enabling the “behavior editor” (i.e., the function editor for behaviors). Therefore, the following behavior is expected:

For Behaviors

# useModalFunctionEditor flag modalFunctionEditor experiment Result
1 T Enabled “Create a Behavior” button creates a behavior definition block in the main workspace; edit button appears on the call block and clicking it opens the modal function editor. The modal function editor appears to work as expected for functions, but unnamed blocks might still occasionally appear. Behavior definitions appear and are edited on the main workspace because moving them to the hidden workspace has not been implemented yet.
2 T Disabled “Create a Behavior” button creates a behavior definition block in the main workspace; the edit button appears, and clicking it centers on the relevant behavior definition block. Behavior definitions appear and are edited on the main workspace because moving them to the hidden workspace has not been implemented yet.
3 F Enabled Behavior definitions appear on the main workspace because moving them to the hidden workspace has not been implemented yet.
4 F Disabled Behavior definitions appear on the main workspace because moving them to the hidden workspace has not been implemented yet.

The screenshots in the "Testing" section focus specifically on case 2 above.

Links

Jira ticket: https://codedotorg.atlassian.net/browse/SL-641

Testing story

Before

Screenshot 2023-09-05 at 2 23 27 PM

In the previous Blockly version, behavior definition blocks were invisible on the main workspace.

After

Screenshot 2023-09-05 at 2 23 32 PM

Mainline/Google Blockly no longer supports invisible blocks. For now, the definition blocks for default behaviors (wobbling, growing, etc.) will be displayed on the main workspace, where they can also be edited. A follow-up PR will add these to the hidden definition workspace. At that point, the behavior definition blocks will no longer be visible here.

Before

Screenshot 2023-09-05 at 2 23 39 PM

After

Screenshot 2023-09-05 at 2 23 44 PM

Some of the styling (ex. gray “Create a Behavior” button) still needs to be updated, but the current Behavior category toolbox has parity with the CDO toolbox. (Note: Even though the edit buttons do not currently appear on the call blocks while they are in the toolbox, they appear once dragged to the main workspace.)

Before

Screenshot 2023-09-05 at 2 23 53 PM

After

Screenshot 2023-09-05 at 2 23 59 PM

Clicking the “Create a Behavior” button creates a new, editable behavior definition block on the main workspace. The corresponding call block is added to the toolbox.

Testing was done in English, Spanish, and Arabic. Reach out to @ebeastlake for a spreadsheet that documents manual testing.

Follow-up work

See: https://codedotorg.atlassian.net/browse/CT-13

PR Checklist:

  • Tests provide adequate coverage
  • Privacy and Security impacts have been assessed
  • Code is well-commented
  • New features are translatable or updates will not break translations
  • Relevant documentation has been added or updated
  • User impact is well-understood and desirable
  • Pull Request is labeled appropriately
  • Follow-up work items (including potential tech debt) are tracked and linked

Comment thread apps/src/blockly/customBlocks/googleBlockly/behaviorBlocks.js Outdated
Comment thread apps/src/blockly/addons/cdoSerializationHelpers.js Outdated
Comment thread apps/src/blockly/addons/functionEditor.js
},

installBehaviorBlocks(behaviorEditor) {
const generator = Blockly.getGenerator();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we extract this logic out into another file? It's a bunch of code and this file will be more readable if it doesn't contains super-specific logic.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Which logic do you think it makes the most sense to move? The code for installBehaviorBlocks?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah we could just pull all the code out (and pass in a generator?) or pull out functions/objects that get assigned to various fields on generator. Basically I want to avoid making this another mega-file if we can avoid it, but I don't have a super strong opinion on the specific refactoring here.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assuming you're okay with tracking this as a follow-up, I added it here: https://codedotorg.atlassian.net/browse/CT-13

Comment thread apps/src/blockly/customBlocks/googleBlockly/behaviorBlocks.js Outdated
Comment thread apps/src/blockly/customBlocks/googleBlockly/behaviorBlocks.js
Comment thread apps/src/blockly/customBlocks/googleBlockly/mutators/behaviorDefMutator.js Outdated
Comment thread apps/src/blockly/customBlocks/googleBlockly/proceduresBlocks.js Outdated
Comment thread apps/src/blockly/customBlocks/googleBlockly/proceduresBlocks.js Outdated
Comment thread apps/src/blockly/googleBlocklyWrapper.js
Comment thread apps/src/blockly/customBlocks/googleBlockly/mutators/behaviorDefMutator.js Outdated
Comment thread apps/src/blockly/customBlocks/googleBlockly/proceduresBlocks.js Outdated
Comment thread pegasus/sites.v3/code.org/public/js/page/educate_csd.js
* remove unused import

* remove unnecessary whitespace diff

* expand definition in function names and convert to arrow function

* refactor to use partitionBlocksByType

* add comment to behaviorDefMutator

* remove unused function

* fix comment that indicates returning xml

* add missing argument

* fix flyout category callbacks

* remove console log

* add id to call block and sort by name

* remove console.logs

* convert to arrow function

* update comment
@ebeastlake

Copy link
Copy Markdown
Contributor

@maribethb My edits are in if you'd like to give this a once-over! I understand it's a large PR with lots of Code.org-specific logic, so your relevant parts are limited.

@molly-moen molly-moen left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A couple nits/questions for my information but overall this looks good!


// In Lab2, the level properties are in Redux, not appOptions. To make this work in Lab2,
// we would need to send that property from the backend and save it in lab2Redux.
const useModalFunctionEditor = window.appOptions?.level?.useModalFunctionEditor;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can use Blockly.useModalFunctionEditor once this gets merged (can be done as a follow up)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


// In Lab2, the level properties are in Redux, not appOptions. To make this work in Lab2,
// we would need to send that property from the backend and save it in lab2Redux.
const useModalFunctionEditor = window.appOptions?.level?.useModalFunctionEditor;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we also incorporate the experiment flag here?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure we need it here until the behavior editor exists. I think the implications are:

  1. If the modal function editor is enabled for the level, but the experiment isn't on, the names of the behaviors aren't editable anyway (doesn't seem like a big deal?). Option: We could require both the modal function editor to be enabled and the experiment to be on to keep the names non-editable.
     {
        type: useModalFunctionEditor ? 'field_label' : 'field_input',
        name: 'NAME',
        text: '',
        spellcheck: false,
      },
  1. If the modal function editor is enabled for a level, we don't render the SVG frame for the behavior definition blocks, even though those definition blocks get defined on the main workspace until the editor works. Option: We could render the SVG frame for the behavior definition blocks even when the modal function editor is enabled as long as the experiment is off.
  if (!useModalFunctionEditor && !this.workspace.noFunctionBlockFrame) {
    this.functionalSvg_ = new BlockSvgFrame(
      this,
      msg.behaviorEditorHeader(),
      'blocklyFunctionalFrame'
    );

    this.setOnChange(function () {
      if (!this.isInFlyout) {
        this.functionalSvg_.render(this.svgGroup_, this.RTL);
      }
    });
  }
  1. Because there's no alternate callback to open the behavior editor yet, the code below doesn't need to be modified.
  } else if (useModalFunctionEditor) {
    workspace.registerButtonCallback('createNewBehavior', createNewBehavior);
    blockList.push(newBehaviorButton);

Thoughts?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that all seems fine!

lowestBlockBottom = blockBottom;
}
});
return lowestBlockBottom + 16;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we add 16 here? Can you add a comment explaining this?

@ebeastlake ebeastlake Aug 30, 2023

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My understanding is this is an arbitrary offset for when we add a new definition block to the bottom of of the blockspace. I added a constant BLOCK_OFFSET at the top of the file since we use 16 both for x and y in createNewDefinitionBlock, so it's now:

  const newDefinitionBlock = Blockly.serialization.blocks.append(
    {...blockState, x: BLOCK_OFFSET, y: getLowestBlockBottomY() + BLOCK_OFFSET},
    Blockly.getMainWorkspace()
  );

It's worth noting that eventually, this'll get removed.

block.getFieldValue('NAME'),
Blockly.Names.NameType.PROCEDURE
);
let xfix1 = '';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what xfix1 is. Can you add a comment and/or update the variable name?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same for xfix2 and branch

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tried my best 😅

this.getFieldValue('NAME'),
'PROCEDURE'
);
return [`new Behavior(${name}, [])`, generator.ORDER_ATOMIC];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(can be a follow up) should new Behavior be translated?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know that we can just start translating this since it's the student's JavaScript code. If we translated new Behavior, the JS interpreter probably wouldn't know how to handle it.

};

const originalSetOutput = blocklyWrapper.Block.prototype.setOutput;
blocklyWrapper.Block.prototype.setOutput = function (isOutput, check) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is the purpose of this?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blockly has a setOutput method, in which null specifies any type could be returned: https://developers.google.com/blockly/reference/js/blockly.block_class.setoutput_1_method

We have a "None" type specified in our constants, which we seem to have used to mean the same thing (any/a variable type as output):

NONE: 'None', // Typically as a connection/input check means "accepts any type"

So, this code replaces the original setOutput method with a custom version that will handle the case when "None" is passed appropriately. That's my guess, anyway 😅 Thoughts?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems right! Maybe worth adding a comment?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

Comment thread apps/src/util/sort.js Outdated
Comment thread apps/src/util/sort.js
export const nameComparator = (a, b) => {
const nameA = a.name.toUpperCase(); // ignore upper and lowercase
const nameB = b.name.toUpperCase(); // ignore upper and lowercase
if (nameA < nameB) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't know you could do this with strings!

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants