File size: 7,752 Bytes
6bcb42f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10b9827
6bcb42f
 
 
 
10b9827
6bcb42f
10b9827
6bcb42f
 
10b9827
6bcb42f
 
10b9827
 
 
 
 
6bcb42f
 
10b9827
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6bcb42f
 
 
10b9827
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
// TODO: access `BlockType` and `ArgumentType` without reaching into VM
// Should we move these into a new extension support module or something?
import ArgumentType from 'scratch-vm/src/extension-support/argument-type';
import BlockType from 'scratch-vm/src/extension-support/block-type';

/**
 * Define a block using extension info which has the ability to dynamically determine (and update) its layout.
 * This functionality is used for extension blocks which can change its properties based on different state
 * information. For example, the `control_stop` block changes its shape based on which menu item is selected
 * and a variable block changes its text to reflect the variable name without using an editable field.
 * @param {object} ScratchBlocks - The ScratchBlocks name space.
 * @param {object} categoryInfo - Information about this block's extension category, including any menus and icons.
 * @param {object} staticBlockInfo - The base block information before any dynamic changes.
 * @param {string} extendedOpcode - The opcode for the block (including the extension ID).
 */
// TODO: grow this until it can fully replace `_convertForScratchBlocks` in the VM runtime
const defineDynamicBlock = (ScratchBlocks, categoryInfo, staticBlockInfo, extendedOpcode) => ({
    init: function () {
        const blockJson = {
            type: extendedOpcode,
            inputsInline: true,
            category: categoryInfo.name,
            colour: categoryInfo.color1,
            colourSecondary: categoryInfo.color2,
            colourTertiary: categoryInfo.color3
        };
        // There is a scratch-blocks / Blockly extension called "scratch_extension" which adjusts the styling of
        // blocks to allow for an icon, a feature of Scratch extension blocks. However, Scratch "core" extension
        // blocks don't have icons and so they should not use 'scratch_extension'. Adding a scratch-blocks / Blockly
        // extension after `jsonInit` isn't fully supported (?), so we decide now whether there will be an icon.
        if (staticBlockInfo.blockIconURI || categoryInfo.blockIconURI) {
            blockJson.extensions = ['scratch_extension'];
        }
        // initialize the basics of the block, to be overridden & extended later by `domToMutation`
        this.jsonInit(blockJson);
        // initialize the cached block info used to carry block info from `domToMutation` to `mutationToDom`
        this.blockInfoText = '{}';
        // we need a block info update (through `domToMutation`) before we have a completely initialized block
        this.needsBlockInfoUpdate = true;
    },
    mutationToDom: function () {
        const container = document.createElement('mutation');
        container.setAttribute('blockInfo', this.blockInfoText);
        return container;
    },
    domToMutation: function (xmlElement) {
        const blockInfoText = xmlElement.getAttribute('blockInfo');
        if (!blockInfoText) return;
        if (!this.needsBlockInfoUpdate) {
            throw new Error('Attempted to update block info twice');
        }
        delete this.needsBlockInfoUpdate;
        this.blockInfoText = blockInfoText;
        const blockInfo = JSON.parse(blockInfoText);

        switch (blockInfo.blockType) {
        case BlockType.COMMAND:
        case BlockType.CONDITIONAL:
        case BlockType.LOOP:
            this.setOutputShape(ScratchBlocks.OUTPUT_SHAPE_SQUARE);
            this.setPreviousStatement(true);
            this.setNextStatement(!blockInfo.isTerminal);
            break;
        case BlockType.REPORTER:
            this.setOutput(true);
            this.setOutputShape(ScratchBlocks.OUTPUT_SHAPE_ROUND);
            if (!blockInfo.disableMonitor) {
                this.setCheckboxInFlyout(true);
            }
            break;
        case BlockType.BOOLEAN:
            this.setOutput(true);
            this.setOutputShape(ScratchBlocks.OUTPUT_SHAPE_HEXAGONAL);
            break;
        case BlockType.HAT:
        case BlockType.EVENT:
            this.setOutputShape(ScratchBlocks.OUTPUT_SHAPE_SQUARE);
            this.setNextStatement(true);
            break;
        }

        if (blockInfo.color1 || blockInfo.color2 || blockInfo.color3) {
            // `setColour` handles undefined parameters by adjusting defined colors
            this.setColour(blockInfo.color1, blockInfo.color2, blockInfo.color3);
        }

        // Layout block arguments
        // TODO handle E/C Blocks
        const blockText = blockInfo.text;
        const args = {};
        let argCount = 0;
        const scratchBlocksStyleText = blockText.replace(/\[(.+?)]/g, (match, argName) => {
            const arg = blockInfo.arguments[argName];
            switch (arg.type) {
            default: // bruh
            case ArgumentType.STRING:
                args[argName] = { type: 'input_value', name: argName };
                break;
            case ArgumentType.BOOLEAN:
                args[argName] = { type: 'input_value', name: argName, check: 'Boolean' };
                break;
            }
            if (arg.menu && !categoryInfo.menuInfo[arg.menu].acceptsReporters) {
                args[argName].type = 'field_dropdown';
                args[argName].options = categoryInfo.menuInfo[arg.menu].items;
                args[argName].value = categoryInfo.menuInfo[arg.menu].items[0][1];
            }
            return `%${++argCount}`;
        });
        this.interpolate_(scratchBlocksStyleText, Object.values(args));
        if (this.isInsertionMarker()) return;
        for (const name in args) {
            if (args[name].type.startsWith('field_')) continue;
            const arg = blockInfo.arguments[name];
            const connection = this.getInput(name).connection;
            const curBlock = connection.targetConnection?.getParentBlock?.();
            if (curBlock && curBlock.type !== 'text' && !curBlock.type.startsWith('math_')) continue;
            if (arg.menu) {
                const fieldId = `${categoryInfo.id}_menu_${arg.menu}`;
                if (curBlock?.type === fieldId) continue;
                if (curBlock) curBlock.dispose();
                const newBlock = this.workspace.newBlock(fieldId);
                if (arg.defaultValue) newBlock.getField(arg.menu).setValue(arg.defaultValue);
                newBlock.setShadow(true);
                newBlock.initSvg();
                newBlock.render();
                continue;
            }
            switch (arg.type) {
            case ArgumentType.STRING: {
                if (curBlock?.type === 'text') break;
                if (curBlock) curBlock.dispose();
                const newBlock = this.workspace.newBlock('text');
                connection.connect(newBlock.outputConnection);
                newBlock.getField('TEXT').setValue(arg.defaultValue ?? '');
                newBlock.setShadow(true);
                newBlock.initSvg();
                newBlock.render();
                break;
            }
            case ArgumentType.NUMBER: {
                if (curBlock && !curBlock.type.startsWith('math_')) break;
                if (curBlock) curBlock.dispose();
                const newBlock = this.workspace.newBlock('math_number');
                connection.connect(newBlock.outputConnection);
                newBlock.getField('NUM').setValue(arg.defaultValue ?? '');
                newBlock.setShadow(true);
                newBlock.initSvg();
                newBlock.render();
                break;
            }
            case ArgumentType.BOOLEAN: {
                if (!curBlock) break;
                curBlock.dispose();
                break;
            }
            }
        }
    }
});

export default defineDynamicBlock;