Changing Toolbox Dynamically

1,614 views
Skip to first unread message

Rupesh Kumar

unread,
Oct 7, 2021, 5:32:19 PM10/7/21
to Blockly
I am using blockly in webview android and calling a method I have defined - setCatagoryByData(arrayViewType, arrayViewID)
with javascript native interface.

I have defined various methods which are working great like clearing workspace, resetting existing workspace with blocks data, etc.

Following is the code where I think I am missing something-

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Blockly My Workspace</title>
<script src="../blockly_compressed.js"></script>
<script src="../blocks_compressed.js"></script>
<script src="../javascript_compressed.js"></script>
<script src="../msg/js/en.js"></script>
<div id="blocklyDiv" style="height: 700px; width: 1000px;"></div>

<xml xmlns="https://developers.google.com/blockly/xml" id="toolbox" style="display: none">

<!-- <category name="LN" >-->
<!-- <block type="onclick_block"></block>-->
<!-- </category>-->

<!-- <category name="ED" >-->
<!-- <block type="controls_if"></block>-->
<!-- </category>-->

</xml>

<script>
var demoWorkspace = Blockly.inject('blocklyDiv',
{
media: '../media/',
toolbox: document.getElementById('toolbox')
});
Blockly.Xml.domToWorkspace(document.getElementById('startBlocks'),
demoWorkspace);

 // Some other methods here

function setCatagoryByData(arrayViewType, arrayViewID) {
var value = JSON.parse(arrayViewType);
<!-- var category = toolbox.getToolboxItems()[0];-->
<!-- category.hide();-->
<!-- if (value.includes("LN")) {-->
<!-- category.show();-->
<!-- }else{-->
<!-- category.hide();-->
<!-- }-->
<!-- var category2 = toolbox.getToolboxItems()[1];-->
<!-- category2.hide();-->
<!-- if (value.includes("ED")) {-->
<!-- category2.show();-->
<!-- }else{-->
<!-- category2.hide();-->
<!-- }-->


var toolboxText = '<xml>';
if (value.includes("LN")) {
toolboxText += ' <category name="LN">' +
' <block type="controls_if"></block>' +
' </category>';
}
if (value.includes("ED")) {
toolboxText += ' <category name="ED">' +
' <block type="controls_if"></block>' +
' </category>';
}

toolboxText += '</xml>';
var toolboxXml = Blockly.Xml.textToDom(toolboxText);
demoWorkspace.updateToolbox(toolboxXml);
}

</script>

</body>
</html>



Please ignore the comments, for now. When I call the method I am trying to change the categories here based on the arrays passed as the method arguments. When I run the method I see an error in Android Studios Logcat

Uncaught Error: Existing toolbox has no categories.  Can't change mode.

I tried another way to hide and show predefined categories in XML for example uncomment the categories in the above code.
Comment the uncomment code and uncomment the comments inside the method.
Now It again shows an error - 

toolbox.getToolboxItems is not a function

Maybe, the toolbox is not recognizable here.
Thanks,



Message has been deleted

Jason Schanker

unread,
Oct 8, 2021, 1:47:42 AM10/8/21
to Blockly
Hi,

There are two types of toolboxes: one that just has blocks and no flyouts and one which has clickable categories with flyouts of blocks.  If the toolbox is empty, then it's considered to be of the first type and so the first error reflects that you can't change between the two types as mentioned here: https://developers.google.com/blockly/guides/configure/web/toolbox#changing_the_toolbox .  The specific error is coming from https://github.com/google/blockly/blob/64188ae27297b1ca23a3244cc987dfbfc6e98f34/core/workspace_svg.js#L1928 because a workspace's toolbox_ property will be null in this case as documented here: https://github.com/google/blockly/blob/64188ae27297b1ca23a3244cc987dfbfc6e98f34/core/workspace_svg.js#L1078 .

The second error seems to be as you suggested due to toolbox being undefined as the Blockly library does not define toolbox as a global variable.  To get a reference to the toolbox, you can use Blockly.getMainWorkspace().getToolbox(), but as noted earlier, this will be null if you have no categories in the XML.

So, assuming there are a reasonable number of possible categories, I think the cleanest way to handle what you want to accomplish is to populate the XML with all categories that you're allowed to include and then define your setCategoryByData function to show/hide these categories by determining whether each possible category name appears in the Array as follows:

function setCatagoryByData(arrayViewType, arrayViewID) {
  var value = JSON.parse(arrayViewType);
  var toolbox = Blockly.getMainWorkspace().getToolbox();
  toolbox.getToolboxItems()
      .forEach(category => value.includes(category.getName()) ?
      category.show() : category.hide());
}

Best,
Jason

Rupesh Kumar

unread,
Oct 8, 2021, 1:28:53 PM10/8/21
to Blockly
This is helpful Jason, One more thing I wanted to ask is that there is a block that is added in a category.
Blockly.Blocks['onclickcard'] = { init: function() { this.appendDummyInput() .appendField("On Click Card Background ID") .appendField(new Blockly.FieldDropdown([["cardbackgroundid","OPTIONNAME"]]), "onclickoptions"); this.appendStatementInput("OnClickInputStatement") .setCheck(null); this.setColour(230); this.setTooltip(""); this.setHelpUrl(""); } };

<category name="Card" >
<block type="onclickcard"></block>
</category>

Can I modify/add/remove the existing dropdown options in the block inside the category programmatically? and Can I add the newly created blocks to an existing category programmatically? Are there any appropriate javascript methods(similar to suggested Blockly.getMainWorkspace().getToolbox();) to do that?

Thanks,
Rupesh

Jason Schanker

unread,
Oct 9, 2021, 5:24:04 AM10/9/21
to Blockly
Hi Rupesh,

I don't know of any Blockly methods that explicitly add blocks to a category, but you can simulate this by using Dynamic categories.  With dynamic categories, you can register functions to be called every time the category flyout is opened.  So, you can maintain a list of block types you want the category to have, appending to it whenever you want to add one and have the function that generates the blocks do so from this Array.  For example:

XML Definition:
...
<category name="Card" custom = "CARD"></category>
...

JavaScript:

const cardBlockTypes = [];
Blockly.getMainWorkspace().registerToolboxCategoryCallback("Card", cardFlyoutCategory);

function addBlock(blockType) {
  cardBlockTypes.push(blockType);
}

function getElementForBlockType(blockType) {
  const block = Blockly.utils.xml.createElement('block');
  block.setAttribute('type', blockType);
  return block;
}

function cardFlyoutCategory() {
  return cardBlockTypes.map(blockType => getElementForBlockType(blockType));
}

addBlock("onclickcard");

For reference, the definition for this WorkspaceSvg registerToolboxCategoryCallback method can be found here: https://github.com/google/blockly/blob/master/core/workspace_svg.js#L2491.

As for modifying/adding/removing options, if you replace .appendField(new Blockly.FieldDropdown([["cardbackgroundid","OPTIONNAME"]]) with .appendField(new Blockly.FieldDropdown(cardOptions)) where cardOptions is a variable initialized to [["cardbackgroundid","OPTIONNAME"]] and then modified as you see fit, the dropdown menu of these types of blocks will change to reflect the updated options.  Alternatively, you could use a dynamically generated dropdown

Also, since the Variables category essentially has the behavior you're describing (e.g., additional blocks as variables are added and modified dropdown menus to account for added/removed/renamed variables), you may find it helpful to see how it's handled for them (e.g., Variables flyoutCategory and associated helper flyoutCategoryBlocks methods responsible for creating the Variable category buttons and blocks can be found at https://github.com/google/blockly/blob/master/core/variables.js#L115 and at https://github.com/google/blockly/blob/master/core/variables.js#L138).

Finally, I wanted to correct something I wrote before.  I was wondering why your call to toolbox.getToolboxItems resulted in a "is not a function" error instead of a ReferenceError that I would've expected if toolbox were indeed undefined.  But I realized that you have an <xml> element with an id of toolbox so there will be a toolbox property of the window referring to this element, effectively making toolbox a global variable.

Best,
Jason

Jason Schanker

unread,
Oct 9, 2021, 1:13:46 PM10/9/21
to Blockly
Blockly.getMainWorkspace().registerToolboxCategoryCallback("Card", cardFlyoutCategory) should be  Blockly.getMainWorkspace().registerToolboxCategoryCallback("CARD", cardFlyoutCategory) as it needs to match the value of the custom attribute for the category.  Sorry for any confusion this may have caused.

Rupesh Kumar

unread,
Oct 9, 2021, 4:49:09 PM10/9/21
to Blockly
Thanks for taking out time to provide a detailed answer. Using variables for appending fields is sufficient and does exactly what I was trying to do. It is helpful to know about Dynamic categories and why toolbox.getToolboxItems resulted in a "is not a function" error instead of a ReferenceError. I am able to change the fields of the block. One last thing for my app's Blockly part I am left with is finding and deleting all the blocks dropped on the workspace with selected field values. I am checking official documentation for this and would like to know your suggestions as you have saved a lot of my time.

Thanks again,
Rupesh

Rupesh Kumar

unread,
Oct 9, 2021, 5:03:28 PM10/9/21
to Blockly
Found this getAllBlocks() for the find and delete part, which returns an array of all the blocks in the workspace https://developers.google.com/blockly/reference/js/Blockly.Workspace#getAllBlocks. Now, I will be able to complete that part.

Thanks,
Rupesh

Jason Schanker

unread,
Oct 10, 2021, 2:57:46 AM10/10/21
to Blockly
Happy to help and glad the information was useful.  Yes, you could filter by the specified selected field values on the Array returned by getAllBlocks or if all the possible candidate blocks for deletion are of the same type, you could use getBlocksByType instead.  One final note is that if the block has a next connection, calling dispose on it without passing a (truthy first) argument will cause the stack of blocks below it to be removed.

Rupesh Kumar

unread,
Oct 10, 2021, 4:27:38 PM10/10/21
to Blockly
 I am able to delete the blocks and their child by filtering through the field value(selected dropdown option) with the help of dispose(true) as you suggested. But facing an issue while removing the same option(unselected option on blocks) from the dropdown field of the blocks inside the workspace.
I am setting an option variable to the block field as suggested and when I change the variable array the block in the categories gets updated with new options but the blocks which were dropped earlier with the previous options remain the same. I tried using the dynamic category function, but an unknown error was faced(unknown block type and textToDom unable to parse)
However, I have the block instances by getAllBlocks() or getBlocksByType() on which I need to remove the options from the block. Can I use these instances to change/update the dropdown options of the blocks which are inside the workspace?
Following is my block definition.
Blockly.Blocks['onclick_type'] = {
init: function() {
this.appendDummyInput()
.appendField("On Click");
this.appendDummyInput()
.appendField("ID:")
.appendField(new Blockly.FieldDropdown(option), "onclickoptions");

this.appendStatementInput("OnClickInputStatement")
.setCheck(null);
this.setColour(230);
this.setTooltip("");
this.setHelpUrl("");
}
};


Sorry about the conversation getting longer, This is the only issue left which I am trying to resolve.
Thanks again,
Rupesh

Jason Schanker

unread,
Oct 11, 2021, 12:37:18 AM10/11/21
to Blockly
How are you removing options from the option Array?  If you're using slice, then you're creating a (shallow) copy of part of the Array so the dropdown menus' options will be generated from different Arrays.  To avoid this, you'll need to remove options in place (e.g., using splice).  If all the blocks of a given type are sharing references to the same option Array for the dropdown menu, I'm not sure why this wouldn't work.  

That being written, there are a couple of issues you can still have with modifying the dropdown:

(1) If the dropdown menu in the workspace is open at the time the options are being changed, then the options won't update until the dropdown is closed and reopened.  But you can fix this by calling Blockly.hideChaff after making changes, which will close the dropdown (along with context menus, tooltips, etc.) so that the user would have to click the dropdown again and would see the updated options upon doing so.  Alternatively, you could reopen it after hiding it by calling showEditor on the field. 
(2) If a selected option were removed from option, then the now nonexistent option would still be selected.  You can fix this by calling setValue(option[0][1]) on the field to reset to the option list's new 0th (top) option. 

As for an unknown block type, I'd have to see more of the code to know what's causing the problem, but I can think of a few possibilities off the top of my head:

(1) There is a misspelling (or incorrect case) of the string of a block definition type (e.g., Blockly.Blocks["foo"] = ... but <block type = "Foo"></block> in the custom category)
(2) The block definition were somehow loaded after the callback function generating the category of blocks were called.
(3) Some other error occurred before the block definition making it so that the block definition assignment never ran.

Best,
Jason

Rupesh Kumar

unread,
Oct 11, 2021, 9:06:26 PM10/11/21
to Blockly
Hi Jason, I figured out the block definitions were not getting loaded due to a syntax error. It is helpful to know it won't update if the menu will be opened and for that, I can use  Blockly.hideChaff as suggested. I can modify dropdown options dynamically with an option generating function.

Thanks again,
Rupesh

Reply all
Reply to author
Forward
0 new messages