Shadow Blocks Pt. 2: Creating and Customizing

Published 7/17/19

In the last part of this series we talked about fields, inputs, and shadows plus when and why you might want to use shadows. In this part we’re going to start creating and customizing them!


Go to the previous post: Shadow Blocks Pt 1

How to create a shadow

Creating a shadow block is pretty simple, just take your normal non-shadowy XML and change the <block> tags to <shadow> tags.

Old xml:

<block type="math_arithmetic">
  <value name="A">
    <block type="math_number"/>
  </value>
  <value name="B">
    <block type="math_number"/>
  </value>
</block>

Fancy shadow xml:

<block type="math_arithmetic">
  <value name="A">
    <shadow type="math_number"/>
  </value>
  <value name="B">
    <shadow type="math_number"/>
  </value>
</block>

Tangent: Why XML?

Now you may be wondering why shadows are defined in the XML rather than in the block definition. This is because there’s actually no way to define them programmatically! (at least not without some hacking). I think this decision was probably made for simplicities sake, since I can't think of a design reason why this is superior.


Overall I think it's probably worth it to keep the consistency (block shape happens in the definition, connections happen in the XML) even if it can be inconvenient.

Why you should customize shadows

Say you’ve just finished creating a custom block, it may be tempting to go to the XML and plug in whatever default blocks Blockly provides. But that is a bad decision! You should (almost) always take the extra minute to customize your shadows because it does two things for you:

  1. It helps the user understand your block.
  2. Take this list from text block for example:

    The second block is obviously more understandable, and it only took a second to add that customization.


  3. It creates a better user experience.
  4. Let’s compare these two blocks:

    The first block uses the default number block as its shadow, while the second uses a custom angle block as its shadow.


    The first option is probably sufficient for code generation, but it is severely lacking in the user experience department:


    Taking the extra minute to provide a custom shadow block can seriously aid your users, and help them quickly understand your interface.

Customizing shadows

Set default values

Setting the default value is the easiest way to customize a shadow block. All you have to do is add a <field> node to the XML.

Old xml:

<block type="math_random_int">
  <value name="FROM">
    <shadow type="math_number"/>
  </value>
  <value name="TO">
    <shadow type="math_number"/>
  </value>
</block>

New xml:

<block type="math_random_int">
  <value name="FROM">
    <shadow type="math_number">
      <field name="NUM">1</field>
    </shadow>
  </value>
  <value name="TO">
    <shadow type="math_number">
      <field name="NUM">100</field>
    </shadow>
  </value>
</block>

Add tooltips to shadows

Tooltips can be used to give the user details about a block. Most shadows simply display the tooltip of their parent (because that makes them more reusable) but I feel that creating a custom tooltip can really help your users.


Let's say you're creating a block for configuring a ball:

Blockly.defineBlocksWithJsonArray([
  {
    "type": "ball_definition",
    "message0": "create ball %1 speed %2 direction %3 bounce %4",
    "args0": [
      {
        "type": "input_dummy"
      },
      {
        "type": "input_value",
        "name": "SPEED",
        "align": "RIGHT"
      },
      {
        "type": "input_value",
        "name": "DIRECTION",
        "align": "RIGHT"
      },
      {
        "type": "input_value",
        "name": "BOUNCE",
        "align": "RIGHT"
      }
    ],
    "previousStatement": null,
    "nextStatement": null,
    "style": "example_blocks",
    "tooltip": "Create a ball with the given values."
  },
  {
    "type": "ball_param_speed",
    "message0": "%1",
    "args0": [
    {
      "type": "field_number",
      "name": "VALUE",
      "value": 0
    }
    ],
    "output": null,
    "style": "example_blocks",
    "tooltip": "The pixels per second the ball will move.",
  },
  {
    "type": "ball_param_direction",
    "message0": "%1",
    "args0": [
    {
      "type": "field_angle",
      "name": "VALUE"
    }
    ],
    "output": null,
    "style": "example_blocks",
    "tooltip": "The initial direction the ball will move in.",
  },
  {
    "type": "ball_param_bounce",
    "message0": "%1",
    "args0": [
    {
      "type": "field_number",
      "name": "VALUE",
      "value": 0
    }
    ],
    "output": null,
    "style": "example_blocks",
    "tooltip": "The number of times the ball will bounce before stopping.",
  }
]);
<block type="ball_definition">
  <value name="SPEED">
    <shadow type="ball_param_speed"></shadow>
  </value>
  <value name="DIRECTION">
    <shadow type="ball_param_direction"></shadow>
  </value>
  <value name="BOUNCE">
    <shadow type="ball_param_bounce"></shadow>
  </value>
</block>

You could either add a very long tooltip explaining every input (which could get crazy if you ever decide to add more), or you could give tooltips to the individual shadows.


Now adding a tooltip to a shadow works. But in older versions of Blockly (1.20190419.0 and earlier) most fields “block” tooltips, which can be a bit of a problem if your shadow just contains a field. So if you’re running 1.20190419.0 or earlier, you’ll have to do a few little core edits:

  1. Add field tooltip support.
  2. Navigate to your core > field.js file and then change this:

    Blockly.Field.prototype.setTooltip = function(_newTip) {
      // Non-abstract sub-classes may wish to implement this.  See FieldLabel.
    };

    To this:

    Blockly.Field.prototype.setTooltip = function(newTip) {
      var clickTarget = this.getClickTarget_();
      if (!clickTarget) {
        // Field has not been initialized yet.
        this.tooltip_ = newTip;
        return;
      }
    
      if (!newTip && newTip !== '') {  // If null or undefined.
        clickTarget.tooltip = this.sourceBlock_;
      } else {
        clickTarget.tooltip = newTip;
      }
    };

  3. Add the getClickTarget function.
  4. Inside the field.js file add the following wherever you like:

    Blockly.Field.prototype.getClickTarget_ = function() {
      return this.clickTarget_ || this.getSvgRoot();
    };

    this.clickTarget_ isn’t defined as of 1.20190419.0, but it will be defined once the new rendering gets added, so we might as well put it in.


  5. Connect the tooltip to the field
  6. Add the following calls to the end of the init method (inside field.js):

    this.setTooltip(this.tooltip_);
    Blockly.Tooltip.bindMouseEvents(this.getClickTarget_());

  7. Fix a little tooltip bug.
  8. Navigate to core > tooltip.js and then inside the onMouseOver_ function change:

    var element = e.target;

    To:

    var element = e.currentTarget;

  9. Rebuild the core.
  10. If you are running in compressed mode (if you followed the Fixed-size Workspace tutorial you’re probably running in compressed mode) you will need to rebuild the core. Just navigate to your root blockly directory through the command line and then run python build.py core.

Add help URLS to shadows

I think that help urls can be useful in much the same way tooltips are.


Take this lists_split block for example:

Blockly.defineBlocksWithJsonArray([
  {
    "type": "text_delimiter",
    "message0": "%1",
    "args0": [{
      "type": "field_input",
      "name": "TEXT",
      "text": ","
    }],
    "output": "String",
    "style": "text_blocks",
    "helpUrl": "https://en.wikipedia.org/wiki/Delimiter",
    "tooltip": "The text used to separate items.",
    "extensions": [
      "text_quotes"
    ]
  },
]);
<block type="lists_split">
  <mutation mode="SPLIT"></mutation>
  <field name="MODE">SPLIT</field>
  <value name="INPUT">
    <shadow type="text">
      <field name="TEXT">item1, item2, item3</field>
    </shadow>
  </value>
  <value name="DELIM">
    <shadow type="text_delimiter">
      <field name="TEXT">,</field>
    </shadow>
  </value>
</block>

Many users may want to know what "delimiter" means (either because they’re confused or curious) so why don’t we add a help URL to point them in the right direction?


Now while tooltips do sort of work, help URLs do not. This is because (as of 1.20190419.0) Blockly’s gesture system completely ignores all shadow blocks (the reason for this is lost to time). To get shadow help working we’ll have to do the following core edits:

  1. Don't ignore shadows.
  2. This is pretty simple, just navigate to the core > gesture.js file then change the setTargetBlock_ function from this:

    Blockly.Gesture.prototype.setTargetBlock_ = function(block) {
      if (block.isShadow()) {
        this.setTargetBlock_(block.getParent());
      } else {
        this.targetBlock_ = block;
      }
    };

    To this:

    Blockly.Gesture.prototype.setTargetBlock_ = function(block) {
      this.targetBlock_ = block;
    };

  3. Fix the context menu.
  4. You probably don’t want users to disable your shadow blocks, so we should remove that option. Navigate to the core > block_svg.js file and find the place where the disable/enable option is added (showContextMenu_ in older versions, generateContextMenu_ in newer versions). Then change the add disabling check from this:

    if (this.workspace.options.disable && this.isEditable()) {
      // Add the thing
    }

    To this:

    if (this.workspace.options.disable && this.isEditable() && !this.isShadow()) {
      // Add the thing
    }

  5. Rebuild the core.
  6. Just like with the tooltips, if you are running in compressed mode (if you followed the Fixed-size Workspace tutorial you’re probably running in compressed mode) you will need to rebuild the core. Navigate to your root Blockly directory through the command line and then run python build.py core.

Now we can create our custom shadows!

Conclusion

I hope that these suggestions got you thinking about ways to take advantage of your shadow blocks. In the next post we’ll get into an even more advanced technique: customized fields on customized shadows.

Go to the next post: Shadow Blocks Pt 3