creating dynamic menu Block

507 views
Skip to first unread message

Hannes

unread,
Aug 30, 2022, 6:39:50 AM8/30/22
to Blockly
Hello,

I am new to Blockly and try to create a Block which can be used to create a custom "Menu" Block.

The Block I want to create starts with just a Dropdown Menu.
The Dropdown Menu has an Option "+ add Main Menu".
Selecting this Option will create a first DummyInput "Main Menu 1" and adding "Delete Main Menu 1" to the dropdown string.
This Dropdown Option ("Delete Main Menu 1") shall obviously delete the newly created DummyInput "Main Menu 1" and remove it from the Dropdown String.

The newly added DummyInput (Main Menu 1) shall contain:
- a dropdown menu with ("+ add submenu" and "+ add option")

Selecting "+add submenu" shall create a new DummyInput, which would be "Submenu 1" listed below Main Menu 1 and adding "delete Submenu 1" to the Dropdown string of (Main Menu 1).
Selecting "+ add option" shall create a new DummyInput, which would be "Option 1"  listed below Main Menu 1  and adding "delete Option 1" to the Dropdown string of (Main Menu 1).


Adding Menus shall count upwards, e.g.

Main Menu 1
    Submenu 1
    ...
Main Menu 2
    Submenu 1
        Option 1
        ...
    Submenu 2
        Option 1
        Option 2
        ...
    ...
Main Menu 3
...



I am facing some problems here:
- Creating the dynamic dropdown string.
- Linking the Dropdown option e.g. "Delete Main Menu 1" to the right DummyInput (Main Menu 1), to delete it.
- Sometimes moving the edited Block results in an error (I was reading about mutators, which shall be used, but to be honest I dont understand those at the moment)


I´d appreciate any kind of help.

Best regard
Hannes

Hannes

unread,
Aug 30, 2022, 7:46:27 AM8/30/22
to Blockly
edit: When I was talking about the Dropdown string, I actually ment the array.

Maribeth Bottorff

unread,
Aug 30, 2022, 8:41:04 PM8/30/22
to Blockly
Hello,

First, I have a few questions about this hypothetical design. Are you trying to use blocks to create a menu, or are you sure you want the blocks to modify themselves? Remember that you can use blocks to generate a menu without the block having to mirror the menu itself in appearance. As an example, think about the Block Factory demo: https://blockly-demo.appspot.com/static/demos/blockfactory/index.html here we are using blocks to create blocks. But we are using several different custom-made blocks that can be composed together, and the end result is a block, rather than trying to make the block itself on the fly using one giant block that constantly changes shape. Does that distinction make sense? Knowing admittedly little about your use case, I wonder if a design more like the Block Factory might make more sense and be easier to implement. For example, you would have blocks that "add submenu" and "add option", and you'd attach those blocks to some parent "create menu" block.

Second, in your current design, keep in mind that dropdown fields are dropdown selection fields, but it sounds like you want to use them more like a context menu. When a user selects the "Delete submenu" option, you certainly can program it to do something in response to that selection, but then what? The dropdown selection will stay as "Delete submenu" even though there is nothing else to delete and that's confusing. or what if they want to delete multiple submenus?

So before you go too much deeper I'd encourage you to draw out some example blocks and states, and think about what is the end goal of the block? What kind of code will the block generate? If the end goal of the block is to create a menu, then this may not be the right design. If on the other hand you need this menu in order to use the block for its real purpose, then maybe it is.

If you want to keep your current design, there are a couple pieces you would need to know to make this work.
- When blocks are moved around the workspace, we serialize and deserialize them to create a copy of the block for dragging purposes. That means your block needs to be able to serialize itself correctly (of course, you also need that in order to properly save your workspace). To dynamically change the inputs and fields on a block, this means you do need to create a mutator so that the current shape of the block is saved. Think about a mutator as saving some data about your block in text-form (either xml or json), then later using that text to recreate the block. What information would you need to save? The mutator would need to know how many menus, and the submenus and options for each one. Then, it needs to do the reverse: given that data, how does it recreate the block (by adding the appropriate inputs and fields)?
- I would carefully study this page: https://developers.google.com/blockly/guides/create-custom-blocks/define-blocks paying special attention to the bits about using appendInput and appendField.
- For the dropdown, there are two ways to create a dropdown. You can either pass an array of options (static) or pass a function that will be called to whenever the dropdown is opened to generate the options (dynamic). Read more here: https://developers.google.com/blockly/guides/create-custom-blocks/fields/built-in-fields/dropdown#dynamic_dropdowns and remember you will need to pass a function that can generate the appropriate options given the current block, so you'll need to figure out how to calculate what options should be shown.
- For changing the block in response to selecting a dropdown option you can use a block change listener, or use a validator on the dropdown field which is already called whenever the dropdown is changed. As an example, check out the math_property block extension https://github.com/google/blockly/blob/55a707662409a8829416e38e7655bb5d009aca39/blocks/math.js#L504 which uses the validator to update the shape of the block when a new option is chosen.

I hope that gives you a starting place or food for thought. If you continue to face problems during your implementation then posting your code and error messages would help us give you more specific help. Good luck!

Maribeth

Hannes Wichmann

unread,
Aug 31, 2022, 5:22:15 AM8/31/22
to blo...@googlegroups.com
Hello Maribeth,

thank you for the fast reply. The goal is to have a fast and simple way to create a Menu Block with Blockly. We want to use Blockly for different Projects, which come and go. Every Project has several Menus which differ in appearance. We dont want to create the menu block everytime with JSON or Javascript. We rather want to give the user, who can not code, the opportunity to create the Menu Block he needs for his Project himself. I have an example of a finished menu block here and attached the code:
example_setting_block.pngIt shows the 3 variations of the dropdown menu, but is actually one Block, as the code shows I attached.


The Block I am trying to build, should be able to create the example block shown above. The new created Block with this method will then be implemented for the project. This would safe a lot of time in the future.
I hope you could see my intention.

Using the Block Factory and the concept of creating blocks with blocks led me to my idea. But the solution has to be even simpler and faster, for a more specific use case (here to create a menu Block).
Reading your reply gave me an idea, of using the option the standard "if-block" has, by adding "else if" or "else" to it. I think it would be even more user friendly to do it that way. I am going to start looking into it now and post my questions and problems here that will eventually come.  

Best wishes
Hannes

--
You received this message because you are subscribed to a topic in the Google Groups "Blockly" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/blockly/JHJ8J_vscEQ/unsubscribe.
To unsubscribe from this group and all its topics, send an email to blockly+u...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/blockly/5b11f010-685e-4802-8498-6873fea2d652n%40googlegroups.com.
settings_Example_Block.txt

Hannes Wichmann

unread,
Sep 5, 2022, 3:51:18 AM9/5/22
to blo...@googlegroups.com
Hello again,

the last days I learned that mutators are actually the option to add blocks like the "else if" or "else" block to the "if" condition block.
So I was looking at the code for that if condition block, trying to understand it.
I was able to start with the mutator blocks. But I´m struggling with the save and rebuild of the mutator (child) Blocks, that I put in the statement or value input (because I had no example of that in the if condition).
Settings_with_mutators_2.png

If I close the Mutator window and open it again, only the four "Main Menu" Blocks are connected, because I dont know how to safe the Submenu or Option Blocks.
The saving of the "Main Menu" block happens in the decompose function I think:

  mutationToDom: function() {
    var container = Blockly.utils.xml.createElement('mutation');
    if (this.mainMenuCount_) {
      container.setAttribute('mainMenu', this.mainMenuCount_);
    }
   return container;
  },
domToMutation: function(xmlElement) {
    this.mainMenuCount_ = parseInt(xmlElement.getAttribute('mainMenu'), 10) || 0;
this.rebuildShape_();
}
decompose: function(workspace) {
    var containerBlock = workspace.newBlock('settings_superior_main_block');
    containerBlock.initSvg();
    var connection = containerBlock.nextConnection;

    for (var i = 1; i <= this.mainMenuCount_; i++) {
      var mainMenuBlock = workspace.newBlock('settings_superior_main_menu');
      mainMenuBlock.initSvg();
      connection.connect(mainMenuBlock.previousConnection);
      connection = mainMenuBlock.nextConnection;
    }
    return containerBlock;
  },


The next Block is saved with "previousConnection" and "nextConnection", how do I access the next/previous Connection of statement inputs?

Best Regards
Hannes

On Wed, Aug 31, 2022 at 2:41 AM 'Maribeth Bottorff' via Blockly <blo...@googlegroups.com> wrote:
--

Christopher Allen

unread,
Sep 6, 2022, 10:05:04 AM9/6/22
to blo...@googlegroups.com
Hi Hannes,
 
the last days I learned that mutators are actually the option to add blocks like the "else if" or "else" block to the "if" condition block.

Having just now read some of your earlier messages in this thread, I was thinking "this sounds like exactly the use case for mutators", and I was glad to see that you have had the same idea.
 
So I was looking at the code for that if condition block, trying to understand it.
I was able to start with the mutator blocks. But I´m struggling with the save and rebuild of the mutator (child) Blocks, that I put in the statement or value input (because I had no example of that in the if condition).
Settings_with_mutators_2.png

If I close the Mutator window and open it again, only the four "Main Menu" Blocks are connected, because I dont know how to safe the Submenu or Option Blocks.

I think you have already figured most of how this works out, but just to make sure that we are on the same page I began by checking the documentation for mutators and particularly the compose/decompose hooks; there I see that there are two phases to the saving/loading process:

When saving:
  1. First the compose hook updates the shape/state of the menu block (the green one on the main workspace) based on the arrangement of blocks in the pop-up.  Those blocks can then be thrown away since all the relevant information has been saved by modifying the menu block.
  2. Then, later, the saveExtraState hook (or, if you use the old XML serialisation, the mutationToDom hook) saves the information about the shape/state of the menu block.
When loading:
  1. First the loadExtraState (or domToMutation) hook configures the menu block back to the shape/state it was before it was saved.
  2. Then, when the user clicks on the gear button, the decompose hook recreates the arrangement of blocks to be shown in the pop-up based on the current shape/state of the menu block.
The surprising (to me) consequence of this is that the arrangement of blocks in the pop-up is not itself directly saved.  Those blocks are ephemeral: they are created when the pop-up is opened and will be re-created again later when needed.  Only the state of the block being mutated is actually saved.

So from your screenshot above it looks like at least part of the problem may be that your compose hook has not modified the state of the menu block to reflect the arrangement of Submenu and Option blocks.
 
The saving of the "Main Menu" block happens in the decompose function I think: 

As noted, the decompose hook is more about recreating the state of the blocks in the pop-up from the block in the main workspace; I'd have called this "loading" rather than "saving".
 
  mutationToDom: function() {
    var container = Blockly.utils.xml.createElement('mutation');
    if (this.mainMenuCount_) {
      container.setAttribute('mainMenu', this.mainMenuCount_);
    }
   return container;
  },
domToMutation: function(xmlElement) {
    this.mainMenuCount_ = parseInt(xmlElement.getAttribute('mainMenu'), 10) || 0;
this.rebuildShape_();
}
decompose: function(workspace) {
    var containerBlock = workspace.newBlock('settings_superior_main_block');
    containerBlock.initSvg();
    var connection = containerBlock.nextConnection;

    for (var i = 1; i <= this.mainMenuCount_; i++) {
      var mainMenuBlock = workspace.newBlock('settings_superior_main_menu');
      mainMenuBlock.initSvg();
      connection.connect(mainMenuBlock.previousConnection);
      connection = mainMenuBlock.nextConnection;
    }
    return containerBlock;
  },



 Your code here looks like a good start but is obviously incomplete: you'll need to add code to create Submenu and Option blocks and attach them to the Main Menu (settings_superior_main_menu) blocks.  
 
The next Block is saved with "previousConnection" and "nextConnection", how do I access the next/previous Connection of statement inputs?

I believe the best way to do this is to call (e.g.) mainMenuBlock.getInput(<name>).connection.connect(<Submenu Block>) to attach the newly-created Submenu block (or stack thereof) to the appropriate input of the  the Main Menu block; in the compose hook, you'll need to do the reverse: use .getInput to get the Input object, follow its .connection to the attached Submenu block(s), and then modify this block accordingly.


Hope this helps!

Christopher

Hannes Wichmann

unread,
Sep 7, 2022, 3:36:16 AM9/7/22
to blo...@googlegroups.com
Hello Christopher, 

thank you for the answer. I am still struggling with the code. 
I added these Lines to the decompose function:
    for (var i = 1; i <= this.submenuCount_; i++) {
      var mainMenuBlock = workspace.newBlock('settings_superior_main_menu');
      var submenuBlock = workspace.newBlock('settings_superior_submenu');
      mainMenuBlock.initSvg();
      if (i == 1){
        mainMenuBlock.getInput('MAIN_MENU_STATEMENT').connection.connect(submenuBlock.previousConnection);
        connection = mainMenuBlock.nextConnection;
      } else if (i > 1) {
        submenuBlock.getInput('SUBMENU_STATEMENT').connection.connect(submenuBlock.previousConnection);
        connection = submenuBlock.nextConnection;
      }
    }

and this to the compose function:
    var clauseBlockStatement = containerBlock.getInputTargetBlock('MAIN_MENU_STATEMENT');
    var submenuStatementConnections = [null];
    while (clauseBlockStatement && !clauseBlockStatement.isInsertionMarker()) {
      submenuStatementConnections.push(clauseBlockStatement.valueConnection_);
      clauseBlockStatement = clauseBlockStatement.nextConnection &&
          clauseBlockStatement.nextConnection.targetBlock();
    }

The submenu is still not saved. I dont get any errors, but I dont see the code making a difference either.
Do you maybe have a code example for me? Of a block with Mutators, that connect with statement or value inputs?

And another thing I am asking myself: When I try to save the numbers of submenus to a specif Main Menu do I have to identify the different Main Menus, like counting them e.g. from i = 1 upwards, to be able to reconnect the right amount of submenus the each specific main menu?


Best wishes
Hannes

--
You received this message because you are subscribed to a topic in the Google Groups "Blockly" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/blockly/JHJ8J_vscEQ/unsubscribe.
To unsubscribe from this group and all its topics, send an email to blockly+u...@googlegroups.com.

Hannes Wichmann

unread,
Sep 7, 2022, 8:31:02 AM9/7/22
to blo...@googlegroups.com
The last hours I tried to get it done, but I am still stuck.
I added the complete code here, so maybe someone can help me with it. 

I changed the submenuCount_ to an array, where submenuCount_[i-1] represents the submenu counter for Main Menu i.
Getting the right numbers for submenuCount_[] is my problem atm. This happens in the compose function.

First the Blocks and mutator Blocks I am using, they work fine:
Blockly.defineBlocksWithJsonArray([
  {
    "type": "ba_hw_settings_superior_block",
    "message0": "%1",
    "args0": [
      {
        "type": "input_dummy",
        "name": "DUMMY_INPUT_SETTINGS_BLOCK"
      },
    ],    
    "previousStatement": null,
    "nextStatement": null,
    "colour": "120",
    "helpUrl": "",
    "mutator": "ba_hw_settings_superior_block_mutator",
    "tooltip": "Design your own Block",  // TO DO: Tooltip selbst über mutator function setzen
    "extensions": ["add_lines_to_dummy_input"]
  }
])

Blockly.defineBlocksWithJsonArray([ // Mutator blocks. Do not extract.
  // Block representing the Main Settings Block in the ba_hw_settings_superior_block mutator.
  {
    "type": "settings_superior_main_block",
    "message0": "Settings",
    "nextStatement": "Mainmenu",    
    "enableContextMenu": false,
    "colour": "15",
    "tooltip": "Build your own Settings Block"
  },
  // Block representing the Submenus in the ba_hw_settings_superior_block mutator.
  {
    "type": "settings_superior_submenu",
    "message0": "%1 %2",
    "args0": [
      {
        "type": "field_label_serializable",
        "name": "SUBMENU",
        "text": "Submenu"
      },
      {
        "type": "input_statement",
        "name": "SUBMENU_STATEMENT"
      }    
    ],  
    "previousStatement": "Submenu",
    "nextStatement": "Submenu",
    "enableContextMenu": false,
    "colour": "300",
    "tooltip": "add a Submenu to the Settings Block"
  },
  // Block representing the Main Menu Dropdown in the ba_hw_settings_superior_block mutator.
  {
    "type": "settings_superior_main_menu",
    "message0": "%1 %2",
    "args0": [
      {
        "type": "field_label_serializable",
        "name": "MAIN_MENU",
        "text": "Main Menu"
      },
      {
        "type": "input_statement",
        "name": "MAIN_MENU_STATEMENT",
        "check": "Submenu"
      }    
    ],
    "previousStatement": [
      "Start",
      "Mainmenu"
    ],    
    "nextStatement": "Mainmenu",
    "enableContextMenu": true,
    "colour": "0",
    "tooltip": "add a Main Menu to the Settings Block",
  }
]);
/**
 * Mutator methods added to ba_hw_settings_superior_block.
 * @mixin
 * @augments Blockly.Block
 * @package
 * @readonly
 */
Blockly.Constants.Logic.BA_HW_SETTINGS_SUPERIOR_BLOCK_MUTATOR_MIXIN = { // TO DO: optionCount_ after a successful submenuCount_
  mainMenuCount_: 0,
  submenuCount_: [null],

  /**
   * Don't automatically add STATEMENT_PREFIX and STATEMENT_SUFFIX to generated
   * code.  These will be handled manually in this block's generators.
   */
  suppressPrefixSuffix: true,


mutationToDom: I think it is fine like that
  /**
   * Create XML to represent the number of MainMenu, Submenu and Option inputs.
   * @return {Element} XML storage element.
   * @this {Blockly.Block}
   */
mutationToDom: function() {
    if (!this.mainMenuCount_) {
      return null;
    }
    var container = Blockly.utils.xml.createElement('mutation');
    if (this.mainMenuCount_) {
      container.setAttribute('mainMenu', this.mainMenuCount_);
    }
    for (let i = 1; i <= this.mainMenuCount_; i++) {
      if (this.submenuCount_[i-1]) {
        container.setAttribute('submenu_' + i.toString(), this.submenuCount_[i-1]);
      }
    }
    return container;
  },


domToMutation: I think it is fine like that
  /**
   * Parse XML to restore the inputs.
   * @param {!Element} xmlElement XML storage element.
   * @this {Blockly.Block}
   */
  domToMutation: function(xmlElement) {
    this.mainMenuCount_ = parseInt(xmlElement.getAttribute('mainMenu'), 10) || 0;
    for (let i = 1; i <= this.mainMenuCount_; i++) {
      this.submenuCount_[i-1] = parseInt(xmlElement.getAttribute('submenu_' + i.toString()), 10) || 0;
    }
    this.rebuildShape_();
  },


decompose: I am not sure about the part for the submenu Counts
  /**
   * Populate the mutator's dialog with this block's components.
   * @param {!Blockly.Workspace} workspace Mutator's workspace.
   * @return {!Blockly.Block} Root block in mutator.
   * @this {Blockly.Block}
   */
  decompose: function(workspace) {
    //"explodes" the block into smaller sub-blocks which can be moved around, added and deleted.
    // This function should return a "containerBlock" which is the main block in the mutator workspace that sub-blocks connect to.
    var containerBlock = workspace.newBlock('settings_superior_main_block');
    containerBlock.initSvg();
    var connection = containerBlock.nextConnection;

This part is wrong I guess!
//NEW!!!!!!
    var k = 1;
    while(k <= this.mainMenuCount_) {
      for (var i = 1; i <= this.submenuCount_[k-1]; i++) {
        var mainMenuBlock = workspace.newBlock('settings_superior_main_menu');
        var submenuBlock = workspace.newBlock('settings_superior_submenu');
        mainMenuBlock.initSvg();
        submenuBlock.initSvg();
        if (i == 1){
          mainMenuBlock.getInput('MAIN_MENU_STATEMENT').connection.connect(submenuBlock.previousConnection);
          connection = mainMenuBlock.nextConnection;
        } else if (i > 1) {
          submenuBlock.getInput('SUBMENU_STATEMENT').connection.connect(submenuBlock.previousConnection);
          connection = submenuBlock.nextConnection;
        }
      }
      k++;
    }
    // END!!!!!!

the mainMenuCount_ and Block works perfect.
    for (var i = 1; i <= this.mainMenuCount_; i++) {
      var mainMenuBlock = workspace.newBlock('settings_superior_main_menu');
      mainMenuBlock.initSvg();
      connection.connect(mainMenuBlock.previousConnection);
      connection = mainMenuBlock.nextConnection;
    }
    return containerBlock;
  },

compose: trouble with the submenuCount_

  /**
   * Reconfigure this block based on the mutator dialog's components.
   * @param {!Blockly.Block} containerBlock Root block in mutator.
   * @this {Blockly.Block}
   */
    compose: function(containerBlock) {
    //Interprets the configuration of the sub-blocks and uses them to modify the main block.
    //This function should accept the "containerBlock" which was returned by decompose as a parameter.
   
    var clauseBlock = containerBlock.nextConnection.targetBlock();
    var clauseBlockStatement = this.workspace.newBlock('settings_superior_submenu').getInput('SUBMENU_STATEMENT');
    //var clauseBlockStatement = containerBlock.getInput('SUBMENU_STATEMENT').getSourceBlock();
    // Count number of inputs.
    this.mainMenuCount_ = 0;
    this.submenuCount_ = [null];
   
    while (clauseBlock && !clauseBlock.isInsertionMarker()) {
      switch (clauseBlock.type) {
        case 'settings_superior_main_menu':
          this.mainMenuCount_++; // Actual counter, how many Main Menu Mutator Blocks are connected
          break;
        /*case 'settings_superior_submenu':   // OLD...
          this.submenuCount_++;
          break;
        case 'settings_superior_option':
          this.optionCount_++;
          break;*/
        default:
          throw TypeError('Unknown block type: ' + clauseBlock.type);
      }
      clauseBlock = clauseBlock.nextConnection &&
          clauseBlock.nextConnection.targetBlock();
    }


HERE the submenuCount will be done, but it is not working as it should atm. This is where I need help!! Also one Line here gives an Error as stated in the code comment.
    for ( let i = 1; i <= this.mainMenuCount_; i++) { // TO DO: add to the loop above
      this.submenuCount_.push([0]); // push the array to the size of the mainMenuCounter_
      while (clauseBlock && !clauseBlock.isInsertionMarker()) {
        switch (clauseBlock.type) {
          case 'settings_superior_submenu':
            this.submenuCount_[i-1]++;  // count the Submenus of each Main Menu
            break;
          default:
            throw TypeError('Unknown block type: ' + clauseBlock.type);
        }
        clauseBlock = clauseBlock.nextConnection &&
        clauseBlock.nextConnection.targetBlock();
      }
      clauseBlockStatement = clauseBlockStatement.nextConnection && // ERROR: Cannot read properties of undefined (reading 'nextConnection')
      clauseBlockStatement.nextConnection.targetBlock();
    }
   
    this.updateShape_();
  },

The way my Main Block shall behave. That works fine (if I set the submenuCount_ array manuelly:


  /**
   * Modify this block to have the correct number of inputs.
   * @this {Blockly.Block}
   * @private
   */
  updateShape_: function() {
    // Delete everything.
    var i = 1;
    if(this.getField('MAIN_MENU_DROPDOWN_FUNCTION')) {
      this.getInput('DUMMY_INPUT_SETTINGS_BLOCK')
      .removeField('MAIN_MENU_DROPDOWN_FUNCTION');  
    }
    // Rebuild block.
    var dropdownMainMenus = [];
    for (i = 1; i <= this.mainMenuCount_; i++) {  //
      if (i == 1) {
      dropdownMainMenus.push(["Main Menu " + i.toString(), "MAIN_MENU_" + i.toString()]);
      this.getInput('DUMMY_INPUT_SETTINGS_BLOCK')
      .appendField(new Blockly.FieldDropdown(dropdownMainMenus, this.validateMainMenu), "MAIN_MENU_DROPDOWN_FUNCTION");
      } else if (i > 1) {
        dropdownMainMenus.push(["Main Menu " + i.toString(), "MAIN_MENU_" + i.toString()])
      }
    }
  },
//EXTRA FOR DROPDOWN MENU
  validateMainMenu: function(mainMenuNumber) {  
    // validates which Main Menu is selected in the Dropdown Menu
    this.getSourceBlock().updateBlockBasedOnMainMenuSelectedInDropdown(mainMenuNumber);
    return mainMenuNumber;
  },
  updateBlockBasedOnMainMenuSelectedInDropdown: function(mainMenuNumber) {  
    // Adding Submenus / Options to specific Main Menu
    var i = 1;
    var p = 1;
    for (let k = 1; k <= this.mainMenuCount_; k++) {
      while (this.getInput('SUBMENU_' + k.toString() + "_" + p.toString())) {
        this.removeInput('SUBMENU_' + k.toString() + "_" + p.toString())
        p++;
      }
    }
    // TO DO: remove Options /
    while (i <= this.mainMenuCount_) {
      if(mainMenuNumber == 'MAIN_MENU_' + i.toString()){
        for (let k = 1; k <= this.submenuCount_[i-1]; k++) {
          this.appendDummyInput('SUBMENU_' + i.toString() + "_" + k.toString())
              .appendField('Submenu ' + i.toString() + "." + k.toString())
              .appendField(new Blockly.FieldCheckbox(true), "CHECKBOX_SUBMENU_" + i.toString() + "_" + k.toString())
              .setAlign(Blockly.ALIGN_RIGHT);
            }
        for (let j = 1; j <= this.optionCount_; j++) {
          this.appendDummyInput('OPTION_' + i.toString() + "_" + j.toString())
              .appendField('Option ' + i.toString() + "." + j.toString())
              .setAlign(Blockly.ALIGN_RIGHT);
        }
      }
      i++;
    }
  },

Blockly.Extensions.register('add_lines_to_dummy_input',
  function() {
    this.getInput('DUMMY_INPUT_SETTINGS_BLOCK')
      .appendField(new Blockly.FieldTextInput("any"), "SETTINGS_FIELD_INPUT")
      .appendField(new Blockly.FieldLabelSerializable("Settings:"), "SETTINGS_NAME")
  });
Blockly.Extensions.registerMutator('ba_hw_settings_superior_block_mutator',
    Blockly.Constants.Logic.BA_HW_SETTINGS_SUPERIOR_BLOCK_MUTATOR_MIXIN, null,
    ['settings_superior_main_menu', 'settings_superior_submenu', 'settings_superior_option', 'add_image_search_to_settings_block']);

Interesting is, that this code creates "Submenu" Blocks in the workspace.
I hope someone can help me with that compose/decompose function and writing the code for submenuCount_. 

And another question I have is, if the Mutation can be locked? What I mean by that is, if I lock the current mutation, the Block cannot be mutated again. Is it possible to do that?

Best wishes

Hannes

Hannes Wichmann

unread,
Sep 7, 2022, 12:47:38 PM9/7/22
to blo...@googlegroups.com
Hello again, 

sorry for the many posts, I am still trying to improve the code and it feels like I am really close, but still missing something:

I think the decompose is fine like this:

  decompose: function(workspace) {
    //"explodes" the block into smaller sub-blocks which can be moved around, added and deleted.
    // This function should return a "containerBlock" which is the main block in the mutator workspace that sub-blocks connect to.
    var containerBlock = workspace.newBlock('settings_superior_main_block');
    containerBlock.initSvg();
    var connection = containerBlock.nextConnection;

    for (var i = 1; i <= this.mainMenuCount_; i++) {
      var mainMenuBlock = workspace.newBlock('settings_superior_main_menu');
      mainMenuBlock.initSvg();
      connection.connect(mainMenuBlock.previousConnection);

      for (var j = 1; j <= this.submenuCount_[i-1]; j++) {
        var submenuBlock = workspace.newBlock('settings_superior_submenu');
        submenuBlock.initSvg();
        if (j == 1){
          mainMenuBlock.getInput('MAIN_MENU_STATEMENT').connection.connect(submenuBlock.previousConnection);
          connection = submenuBlock.nextConnection;
        } else if (j > 1) {
          connection.connect(submenuBlock.previousConnection);
          connection = submenuBlock.nextConnection;
        }
      }
      connection = mainMenuBlock.nextConnection;
    }
   
    return containerBlock;
  },


But the compose still does not count the submenuCount_ array right.
What am I missing?

  compose: function(containerBlock) {
    //Interprets the configuration of the sub-blocks and uses them to modify the main block.
    //This function should accept the "containerBlock" which was returned by decompose as a parameter.
   
    var clauseBlock = containerBlock.nextConnection.targetBlock();
    console.warn('clauseBlock:')
    console.warn(this.clauseBlock); console.warn(this.submenuCount_);
    // Count number of inputs.
    this.mainMenuCount_ = 0;
    this.submenuCount_ = [null];
   
    while (clauseBlock && !clauseBlock.isInsertionMarker()) {
          this.mainMenuCount_++; // Actual counter, how many Main Menu Mutator Blocks are connected
          if (this.mainMenuCount_ > 1) {
            this.submenuCount_.push(0);
          }
          var clauseBlockSub = clauseBlock;
          while(clauseBlockSub && !clauseBlockSub.isInsertionMarker()) {
            switch (clauseBlockSub.type) {
              case 'settings_superior_main_menu':
                this.submenuCount_[this.mainMenuCount_-1]++;
                  clauseBlockSub = clauseBlockSub.getInput('MAIN_MENU_STATEMENT').connection.targetBlock();
                  break;
              case 'settings_superior_submenu':
                this.submenuCount_[this.mainMenuCount_-1]++;
               break;
              //case 'settings_superior_option': //TO DO: add Options!
              default:
                throw TypeError('Unknown block type: ' + clauseBlockSub.type);
            }
            if(this.submenuCount_[this.mainMenuCount_-1] > 1) {
              clauseBlockSub = clauseBlockSub.nextConnection &&
              clauseBlockSub.nextConnection.targetBlock();
            }
          }
      clauseBlock = clauseBlock.nextConnection &&
          clauseBlock.nextConnection.targetBlock();
    }
   
    this.updateShape_();
  },
 
Thanks for the help and have a nice evening :)
Hannes

Hannes Wichmann

unread,
Sep 8, 2022, 4:03:40 AM9/8/22
to blo...@googlegroups.com
Aaand me again,

I got it working, here is the code:

compose: function(containerBlock) {

var clauseBlock = containerBlock.nextConnection.targetBlock();
// Count number of inputs.
this.mainMenuCount_ = 0;
this.submenuCount_ = [null];

while (clauseBlock && !clauseBlock.isInsertionMarker()) {
this.mainMenuCount_++; // Actual counter, how many Main Menu
Mutator Blocks are connected
if (this.mainMenuCount_ > 1) {
this.submenuCount_.push(0);
}
var clauseBlockSub = clauseBlock;
while(clauseBlockSub && !clauseBlockSub.isInsertionMarker()) {
switch (clauseBlockSub.type) {
case 'settings_superior_main_menu':
clauseBlockSub =
clauseBlockSub.getInput('MAIN_MENU_STATEMENT').connection &&

clauseBlockSub.getInput('MAIN_MENU_STATEMENT').connection.targetBlock();
break;
case 'settings_superior_submenu':
this.submenuCount_[this.mainMenuCount_-1]++; // Actual
counter, how many Submenu Mutator Blocks are connected per mainMenu
Mutator Block
clauseBlockSub = clauseBlockSub.nextConnection &&
clauseBlockSub.nextConnection.targetBlock();
break;
default:
throw TypeError('Unknown block type: ' +
clauseBlockSub.type);
}
}
clauseBlock = clauseBlock.nextConnection &&
clauseBlock.nextConnection.targetBlock();
}

this.updateShape_();
},


One Question remains: Is it possible to lock a current mutation state,
so the block can no longer mutate?

Best wishes
Hannes

Christopher Allen

unread,
Sep 12, 2022, 10:09:45 AM9/12/22
to blo...@googlegroups.com
Hello Hannes,

Sorry for the delay in replying to your messages.  I'm glad you got it working in the end, but I thought I'd attempt to answer a few of your questions along the way anyway, for the benefit of anyone reading this in the future.
 
Do you maybe have a code example for me? Of a block with Mutators, that connect with statement or value inputs?

This is a great question, because in the process of trying to answer it I discovered a bug two three four bugs that we'll need to fix before the next release, so thanks for asking!

Oh, suppose I should actually answer your question, too…

The closest example I know of is is the way mutators work for procedure blocks—see the decompose and compose methods of PROCEDURE_DEF_COMMON in blocks/procedures.js, which use fields (but alas not actually inputs, as you want to do) on the top-level blocks in the decomposition to obtain the names of the procedure's parameters.

And another thing I am asking myself: When I try to save the numbers of submenus to a specif Main Menu do I have to identify the different Main Menus, like counting them e.g. from i = 1 upwards, to be able to reconnect the right amount of submenus the each specific main menu?

Almost certainly.

On Thu, 8 Sept 2022 at 09:03, Hannes Wichmann <zeus4eve...@gmail.com> wrote:
Aaand me again,

I got it working, here is the code: [...]

Well done!  And thanks for sharing your final code with everyone; this will now make a great example to show others in the future.  I'll reproduce it here in a fixed-width font for easier reading:
Calling .setEditable(false) on the block will prevent it from being mutated.
 

Best wishes,

Christopher

Hannes Wichmann

unread,
Sep 16, 2022, 10:04:51 AM9/16/22
to blo...@googlegroups.com
Hello Christopher,

thanks for the reply.

My previous solution could only save and connect 1 column of Submenus to the Main Menus.
My goal is to be able to save and load an infinite number of Submenus and later on other Blocks, like the "Option" Block I mentioned in previous posts.

While testing my code I noticed that the compose function is run 3 times, when I click the Dialog button.
And when I have blocks in the Editor UI connected to the Start Block, the compose function is run 3 times + 2 times for every block connected, after opening the Editor UI.

compose_starts_3_times.png

My understanding of the compose function, after dealing with it for some time now, is, that it gets the containerBlock from the decompose function and setting the global Counter Variables by going through every connection of that container Block. The decompose function can then create that containerBlock with the values of the global variables set within the compose function.
Knowing this, I dont understand the reason for the compose function to be run that often, after one click on the Dialog button.

Is it suppose to behave like that and what is the purpose of it?

Best wishes
Hannes
> --
> You received this message because you are subscribed to a topic in the Google Groups "Blockly" group.
> To unsubscribe from this topic, visit https://groups.google.com/d/topic/blockly/JHJ8J_vscEQ/unsubscribe.
> To unsubscribe from this group and all its topics, send an email to blockly+u...@googlegroups.com.

Hannes Wichmann

unread,
Nov 24, 2022, 7:58:29 AM11/24/22
to blo...@googlegroups.com
Hello,

I´m still asking myself, why the compose Function runs that often, after one click on the dialog button. Does anyone have an answer for me? 🙂

Best wishes
Hannes

Christopher Allen

unread,
Nov 25, 2022, 3:42:08 PM11/25/22
to blo...@googlegroups.com
Hi Hannes,

Sorry we missed your reply on 16th September.  I wasn't oncall that week and although another colleague was and would normally have replied it seems that, alas!, your message slipped through the cracks.  Thanks for pinging us again.

My previous solution could only save and connect 1 column of Submenus to the Main Menus.
My goal is to be able to save and load an infinite number of Submenus and later on other Blocks, like the "Option" Block I mentioned in previous posts.

As I'm sure you've figured out by now this will entail your compose and decompose functions recursively walking the tree of blocks in the mutator flyout and recording the resulting information in some suitable format on the block being mutated.

While testing my code I noticed that the compose function is run 3 times, when I click the Dialog button.
And when I have blocks in the Editor UI connected to the Start Block, the compose function is run 3 times + 2 times for every block connected, after opening the Editor UI.
[...]
Is it suppose to behave like that and what is the purpose of it?

I'm not as familliar with this part of Blockly as my colleagues, but it is surprising to me that it is getting called that many times.

I did a quick check by instrumenting compose in the PROCEDURE_DEF_COMMON mixin in blocks/procedures.js, and I note that it gets called twice when the mutator flyout opens, and then once for each parameter block that I add to the procedure inputs block.  It probably shouldn't be called twice on open, but since the function is (or should be) idempotent it's not really going to cause a problem beyond wasted CPU time.  It should definitely be called at least once per change to the mutator workspace, since the mutated block needs to be updated (and may need to change shape).

It sounds like you're seeing it called at least one more time in each case, though, and that is a little odd.  What version of Blockly are you using?  I wonder if we previously called it more than necessary, and this was (at least partially) fixed in develop already.


Christopher




Reply all
Reply to author
Forward
0 new messages