diff --git a/packages/app/src/blocks/python_dict.ts b/packages/app/src/blocks/python_dict.ts index 3575900..0402b49 100644 --- a/packages/app/src/blocks/python_dict.ts +++ b/packages/app/src/blocks/python_dict.ts @@ -1,77 +1,56 @@ import * as Blockly from "blockly/core"; -import type { Block } from "blockly/core/block"; import type { BlockDefinition } from "blockly/core/blocks"; -import type { Connection } from "blockly/core/connection"; import type { BlockSvg } from "blockly/core/block_svg"; -import type { Workspace } from "blockly/core/workspace"; + +import { createPlusField } from "./fields/field_plus"; +import { createMinusField } from "./fields/field_minus"; export const pythonDict: BlockDefinition[] = [ { - type: "dicts_create_with_container", - message0: "字典 %1 %2", + type: "dicts_create_with", + message0: "创建字典 %1 空字典 %2", args0: [ { type: "input_dummy", }, { - type: "input_statement", - name: "STACK", + type: "input_dummy", + name: "EMPTY", + align: "RIGHT", }, ], + output: "dict", tooltip: "", helpUrl: "", + mutator: "dict_create_with_mutator", colour: 0, // style: "dict_blocks", }, { - type: "dicts_create_with_item", - message0: "键值对", - previousStatement: null, - nextStatement: null, - tooltip: "", - helpUrl: "", - enableContextMenu: false, - colour: 0, - // style: "dict_blocks", - }, - { - type: "dicts_create_with", - message0: "创建字典 %1 KEY-0 %2 VALUE-0 %3 KEY-1 %4 VALUE-1 %5", + type: "dicts_get", + message0: "从字典 %1 中取键 %2 的值", args0: [ - { - type: "input_dummy", - }, { type: "input_value", - name: "KEY0", - align: "RIGHT", - }, - { - type: "input_value", - name: "VALUE0", - align: "RIGHT", - }, - { - type: "input_value", - name: "KEY1", - align: "RIGHT", + name: "DICT", + check: "dict", }, { - type: "input_value", - name: "VALUE1", - align: "RIGHT", + type: "field_input", + name: "KEY", + text: "key", }, ], - output: "dict", + output: null, + inputsInline: true, tooltip: "", helpUrl: "", - mutator: "dict_create_with_mutator", colour: 0, // style: "dict_blocks", }, { - type: "dicts_get", - message0: "从字典 %1 中取 KEY %2 的值", + type: "dicts_get_multi", + message0: "从字典 %1 连续取值 %2 %3", args0: [ { type: "input_value", @@ -79,21 +58,25 @@ export const pythonDict: BlockDefinition[] = [ check: "dict", }, { - type: "field_input", - name: "KEY", - text: "key", + type: "input_dummy", + name: "TOOLS", + }, + { + type: "input_dummy", + name: "KEYS", }, ], output: null, inputsInline: true, tooltip: "", helpUrl: "", + mutator: "dict_get_multi_mutator", colour: 0, // style: "dict_blocks", }, { type: "dicts_set", - message0: "设置字典 %1 中 KEY %2 的值为 %3", + message0: "设置字典 %1 键 %2 的值为 %3", args0: [ { type: "input_value", @@ -128,6 +111,7 @@ export const pythonDict: BlockDefinition[] = [ export type DictCreateWithBlock = BlockSvg & DictCreateWithMixin; interface DictCreateWithMixin extends DictCreateWithMixinType { itemCount_: number; + topInput_: Blockly.Input | undefined; } type DictCreateWithMixinType = typeof DICTS_CREATE_WITH; @@ -136,11 +120,8 @@ const DICTS_CREATE_WITH = { * Number of item inputs the block has. * @type {number} */ - itemCount_: 2, + itemCount_: 0, - /** - * Block for creating a dict with any number of key-value of any type. - */ /** * Create XML to represent list inputs. * Backwards compatible serialization implementation. @@ -150,6 +131,7 @@ const DICTS_CREATE_WITH = { container.setAttribute("items", String(this.itemCount_)); return container; }, + /** * Parse XML to restore the list inputs. * Backwards compatible serialization implementation. @@ -162,6 +144,7 @@ const DICTS_CREATE_WITH = { this.itemCount_ = parseInt(items, 10); this.updateShape_(); }, + /** * Returns the state of this block as a JSON serializable object. * @@ -172,175 +155,261 @@ const DICTS_CREATE_WITH = { itemCount: this.itemCount_, }; }, + /** * Applies the given state to this block. * * @param state The state to apply to this block, ie the item count. */ loadExtraState: function (this: DictCreateWithBlock, state: any) { - this.itemCount_ = state["itemCount"]; + const count = state["itemCount"]; + while (this.itemCount_ < count) { + this.addPart_(); + } this.updateShape_(); }, + /** - * Populate the mutator's dialog with this block's components. - * - * @param workspace Mutator's workspace. - * @returns Root block in mutator. + * Modify this block to have the correct number of inputs. + */ + updateShape_: function (this: DictCreateWithBlock) { + this.updateMinus_(); + }, + + /** + * Callback for the plus image. Adds an input to the end of the block and + * updates the state of the minus. */ - decompose: function (this: DictCreateWithBlock, workspace: Workspace): any { - const containerBlock = workspace.newBlock("dicts_create_with_container"); - (containerBlock as BlockSvg).initSvg(); - let connection = containerBlock.getInput("STACK")!.connection; - for (let i = 0; i < this.itemCount_; i++) { - const itemBlock = workspace.newBlock("dicts_create_with_item"); - (itemBlock as BlockSvg).initSvg(); - if (!itemBlock.previousConnection) { - throw new Error("itemBlock has no previousConnection"); - } - connection!.connect(itemBlock.previousConnection); - connection = itemBlock.nextConnection; + plus: function (this: DictCreateWithBlock) { + this.addPart_(); + this.updateMinus_(); + }, + + /** + * Callback for the minus image. Removes an input from the end of the block + * and updates the state of the minus. + */ + minus: function (this: DictCreateWithBlock) { + if (this.itemCount_ == 0) { + return; } - return containerBlock; + this.removePart_(); + this.updateMinus_(); }, + + // To properly keep track of indices we have to increment before/after adding + // the inputs, and decrement the opposite. + // Because we want our first input to be ARG0 (not ARG1) we increment after. + /** - * Reconfigure this block based on the mutator dialog's components. - * - * @param containerBlock Root block in mutator. + * Adds an input to the end of the block. If the block currently has no + * inputs it updates the top 'EMPTY' input to receive a block. + * @this {Blockly.Block} + * @private */ - compose: function (this: DictCreateWithBlock, containerBlock: Block) { - let itemBlock: ItemBlock | null = containerBlock.getInputTargetBlock( - "STACK", - ) as ItemBlock; - // Count number of inputs. - const connections: Connection[] = []; - while (itemBlock) { - if (itemBlock.isInsertionMarker()) { - itemBlock = itemBlock.getNextBlock() as ItemBlock | null; - continue; - } - connections.push(itemBlock.valueConnection_?.key as Connection); - connections.push(itemBlock.valueConnection_?.value as Connection); - itemBlock = itemBlock.getNextBlock() as ItemBlock | null; + addPart_: function (this: DictCreateWithBlock) { + if (this.itemCount_ == 0) { + this.removeInput("EMPTY"); + this.topInput_ = this.appendValueInput("KEY" + String(this.itemCount_)) + .setAlign(Blockly.inputs.Align.RIGHT) + .appendField(createPlusField(), "PLUS") + .appendField(`键 ${this.itemCount_}`); + this.appendValueInput("VALUE" + String(this.itemCount_)) + .setAlign(Blockly.inputs.Align.RIGHT) + .appendField(`值 ${this.itemCount_}`); + } else { + this.appendValueInput("KEY" + String(this.itemCount_)) + .setAlign(Blockly.inputs.Align.RIGHT) + .appendField(`键 ${this.itemCount_}`); + this.appendValueInput("VALUE" + String(this.itemCount_)) + .setAlign(Blockly.inputs.Align.RIGHT) + .appendField(`值 ${this.itemCount_}`); } - // Disconnect any children that don't belong. - for (let i = 0; i < this.itemCount_; i++) { - const connection_key = this.getInput("KEY" + i)!.connection! - .targetConnection; - if (connection_key && connections.indexOf(connection_key) === -1) { - connection_key.disconnect(); - } - const connection_value = this.getInput("VALUE" + i)!.connection! - .targetConnection; - if (connection_value && connections.indexOf(connection_value) === -1) { - connection_value.disconnect(); - } + this.itemCount_++; + }, + + /** + * Removes an input from the end of the block. If we are removing the last + * input this updates the block to have an 'EMPTY' top input. + * @this {Blockly.Block} + * @private + */ + removePart_: function (this: DictCreateWithBlock) { + this.itemCount_--; + this.removeInput("KEY" + String(this.itemCount_)); + this.removeInput("VALUE" + String(this.itemCount_)); + if (this.itemCount_ == 0) { + (this.topInput_ as Blockly.Input) = this.appendDummyInput("EMPTY") + .appendField(createPlusField(), "PLUS") + .setAlign(Blockly.inputs.Align.RIGHT) + .appendField("空字典"); } - this.itemCount_ = connections.length / 2; - this.updateShape_(); - // Reconnect any child blocks. - for (let i = 0; i < this.itemCount_; i++) { - connections[i]?.reconnect(this, "KEY" + i); - connections[i]?.reconnect(this, "VALUE" + i); + }, + + /** + * Makes it so the minus is visible iff there is an input available to remove. + * @private + */ + updateMinus_: function (this: DictCreateWithBlock) { + const minusField = this.getField("MINUS"); + if (!minusField && this.itemCount_ > 0) { + this.topInput_?.insertFieldAt(1, createMinusField(), "MINUS"); + } else if (minusField && this.itemCount_ < 1) { + this.topInput_?.removeField("MINUS"); } }, - saveConnections: function (this: DictCreateWithBlock, containerBlock: Block) { - // Store a pointer to any connected child blocks. - let itemBlock: ItemBlock | null = containerBlock.getInputTargetBlock( - "STACK", - ) as ItemBlock; - let i = 0; - while (itemBlock) { - if (itemBlock.isInsertionMarker()) { - itemBlock = itemBlock.getNextBlock() as ItemBlock | null; - continue; - } - const key_input = this.getInput("KEY" + i); - const value_input = this.getInput("VALUE" + i); - itemBlock.valueConnection_ = { - key: key_input?.connection!.targetConnection as Connection, - value: value_input?.connection!.targetConnection as Connection, - }; - itemBlock = itemBlock.getNextBlock() as ItemBlock | null; - i++; +}; + +const DICTS_CREATE_WITH_EXTENSION = function (this: DictCreateWithBlock) { + this.itemCount_ = 0; + this.updateShape_(); + this.getInput("EMPTY")?.insertFieldAt(0, createPlusField(), "PLUS"); +}; + +/** + * Type of a 'dicts_get_multi' block. + * + * @internal + */ +export type DictGetMultiBlock = BlockSvg & DictGetMultiMixin; +interface DictGetMultiMixin extends DictGetMultiMixinType { + itemCount_: number; +} +type DictGetMultiMixinType = typeof DICTS_GET_MULTI; + +const DICTS_GET_MULTI = { + /** + * Number of item inputs the block has. + * @type {number} + */ + itemCount_: 1, + + /** + * Create XML to represent list inputs. + * Backwards compatible serialization implementation. + */ + mutationToDom: function (this: DictGetMultiBlock): Element { + const container = Blockly.utils.xml.createElement("mutation"); + container.setAttribute("items", String(this.itemCount_)); + return container; + }, + + /** + * Parse XML to restore the list inputs. + * Backwards compatible serialization implementation. + * + * @param container XML storage element. + */ + domToMutation: function (this: DictGetMultiBlock, xmlElement: Element) { + const items = xmlElement.getAttribute("items"); + if (!items) throw new TypeError("element did not have items"); + this.itemCount_ = parseInt(items, 10); + this.updateShape_(); + }, + + /** + * Returns the state of this block as a JSON serializable object. + * + * @returns The state of this block, ie the item count. + */ + saveExtraState: function (this: DictGetMultiBlock): { itemCount: number } { + return { + itemCount: this.itemCount_, + }; + }, + + /** + * Applies the given state to this block. + * + * @param state The state to apply to this block, ie the item count. + */ + loadExtraState: function (this: DictGetMultiBlock, state: any) { + const count = state["itemCount"]; + while (this.itemCount_ < count) { + this.addPart_(); } + this.updateShape_(); }, + /** * Modify this block to have the correct number of inputs. */ - updateShape_: function (this: DictCreateWithBlock) { - if (this.itemCount_ && this.getInput("EMPTY")) { - this.removeInput("EMPTY"); - } else if (!this.itemCount_ && !this.getInput("EMPTY")) { - this.appendDummyInput("EMPTY").appendField("空字典"); - } - // Add new inputs. - for (let i = 0; i < this.itemCount_; i++) { - if (!this.getInput("KEY" + i)) { - this.appendValueInput("KEY" + i) - .setAlign(Blockly.inputs.Align.RIGHT) - .appendField("KEY-" + i); - } - if (!this.getInput("VALUE" + i)) { - this.appendValueInput("VALUE" + i) - .setAlign(Blockly.inputs.Align.RIGHT) - .appendField("VALUE-" + i); - } - } - // Remove deleted inputs. - for (let i = this.itemCount_; this.getInput("KEY" + i); i++) { - this.removeInput("KEY" + i); - } - for (let i = this.itemCount_; this.getInput("VALUE" + i); i++) { - this.removeInput("VALUE" + i); + updateShape_: function (this: DictGetMultiBlock) { + this.updateMinus_(); + }, + + /** + * Callback for the plus image. Adds an input to the end of the block and + * updates the state of the minus. + */ + plus: function (this: DictGetMultiBlock) { + this.addPart_(); + this.updateMinus_(); + }, + + /** + * Callback for the minus image. Removes an input from the end of the block + * and updates the state of the minus. + */ + minus: function (this: DictGetMultiBlock) { + if (this.itemCount_ == 1) { + return; } + this.removePart_(); + this.updateMinus_(); }, -}; -/** Type for a 'dicts_create_with_container' block. */ -type ContainerBlock = Block & ContainerMutator; -interface ContainerMutator extends ContainerMutatorType {} -type ContainerMutatorType = typeof DICTS_CREATE_WITH_CONTAINER; + // To properly keep track of indices we have to increment before/after adding + // the inputs, and decrement the opposite. + // Because we want our first input to be ARG0 (not ARG1) we increment after. -const DICTS_CREATE_WITH_CONTAINER = { /** - * Mutator block for dict container. + * Adds an input to the end of the block. If the block currently has no + * inputs it updates the top 'EMPTY' input to receive a block. + * @this {Blockly.Block} + * @private */ - init: function (this: ContainerBlock) { - this.setStyle("dict_blocks"); - this.appendStatementInput("STACK"); - this.contextMenu = false; + addPart_: function (this: DictGetMultiBlock) { + this.getInput("KEYS")?.appendField( + new Blockly.FieldTextInput("key" + String(this.itemCount_)), + "KEY" + String(this.itemCount_), + ); + this.itemCount_++; }, -}; -/** Type for a 'dicts_create_with_item' block. */ -type ItemBlock = Block & ItemMutator; -interface ItemConnection { - key: Connection; - value: Connection; -} -interface ItemMutator extends ItemMutatorType { - valueConnection_?: ItemConnection; -} -type ItemMutatorType = typeof DICTS_CREATE_WITH_ITEM; + /** + * Removes an input from the end of the block. If we are removing the last + * input this updates the block to have an 'EMPTY' top input. + * @this {Blockly.Block} + * @private + */ + removePart_: function (this: DictGetMultiBlock) { + this.itemCount_--; + this.getInput("KEYS")?.removeField("KEY" + String(this.itemCount_)); + }, -const DICTS_CREATE_WITH_ITEM = { /** - * Mutator block for adding items. + * Makes it so the minus is visible iff there is an input available to remove. + * @private */ - init: function (this: ItemBlock) {}, + updateMinus_: function (this: DictGetMultiBlock) { + const minusField = this.getField("MINUS"); + if (!minusField && this.itemCount_ > 1) { + this.getInput("TOOLS")?.insertFieldAt(1, createMinusField(), "MINUS"); + } else if (minusField && this.itemCount_ < 2) { + this.getInput("TOOLS")?.removeField("MINUS"); + } + }, }; -/** - * Performs final setup of a dicts_create_with block. - */ -const DICTS_CREATE_WITH_EXTENSION = function (this: DictCreateWithBlock) { - // Initialize the mutator values. - this.itemCount_ = 2; +const DICTS_GET_MULTI_EXTENSION = function (this: DictGetMultiBlock) { + this.itemCount_ = 1; this.updateShape_(); - // Configure the mutator UI. - this.setMutator( - new Blockly.icons.MutatorIcon(["dicts_create_with_item"], this), + this.getInput("KEYS")?.appendField( + new Blockly.FieldTextInput("key0"), + "KEY0", ); + this.getInput("TOOLS")?.insertFieldAt(0, createPlusField(), "PLUS"); }; if (Blockly.Extensions.isRegistered("dict_create_with_mutator")) { @@ -351,3 +420,12 @@ Blockly.Extensions.registerMutator( DICTS_CREATE_WITH, DICTS_CREATE_WITH_EXTENSION, ); + +if (Blockly.Extensions.isRegistered("dict_get_multi_mutator")) { + Blockly.Extensions.unregister("dict_get_multi_mutator"); +} +Blockly.Extensions.registerMutator( + "dict_get_multi_mutator", + DICTS_GET_MULTI, + DICTS_GET_MULTI_EXTENSION, +); diff --git a/packages/app/src/generators/python_dict.ts b/packages/app/src/generators/python_dict.ts index bd37b69..5d853ed 100644 --- a/packages/app/src/generators/python_dict.ts +++ b/packages/app/src/generators/python_dict.ts @@ -1,7 +1,7 @@ import { Order } from "blockly/python"; import * as Blockly from "blockly/core"; -import { DictCreateWithBlock } from "@/blocks/python_dict"; +import { DictCreateWithBlock, DictGetMultiBlock } from "@/blocks/python_dict"; export const forBlock = Object.create(null); @@ -18,6 +18,26 @@ forBlock["dicts_get"] = function ( return [code, Order.ATOMIC]; }; +forBlock["dicts_get_multi"] = function ( + block: DictGetMultiBlock, + generator: Blockly.CodeGenerator, +) { + const dict = generator.valueToCode(block, "DICT", Order.MEMBER) || "{}"; + let code = ""; + let key = block.getFieldValue("KEY0"); + if (!key) { + return ["None", Order.ATOMIC]; + } + code = `${dict}.get("${key}")`; + for (let n = 1; n < block.itemCount_; n++) { + key = block.getFieldValue("KEY" + n); + if (key) { + code = `(${code} or {}).get(${key})`; + } + } + return [code, Order.ATOMIC]; +}; + forBlock["dicts_set"] = function ( block: Blockly.Block, generator: Blockly.CodeGenerator,