diff --git a/docs/defining-blocks.md b/docs/defining-blocks.md index 7f717fe75130..c3bde459992a 100644 --- a/docs/defining-blocks.md +++ b/docs/defining-blocks.md @@ -455,6 +455,14 @@ parameter like so: **Playground example**: [Enumerations](https://makecode.com/playground#basic-enums) +### Color picker shadows + +Use `color.shadow=makecode_color_picker` with a numeric color parameter. Set +`color.fieldOptions.format` to `rgb` (the default), `hsv`, `hsl`, `cmyk`, or `hex`. +`color.defl=0x7f3fbf` supplies the RGB color regardless of the initial display mode. +For example, `color.fieldOptions.format=hex` displays that default as `#7F3FBF`. +The picker always outputs a number through `colorHelpers`. + ### Creating enumerations with blocks You can have blocks themselves define an enumeration dynamically. The block will specify some initial members but additional ones are added by selecting the "Add a new <enum_name>..." option in the parameter dropdown. diff --git a/pxtblocks/loader.ts b/pxtblocks/loader.ts index 4b040a501c8f..216496aee8bc 100644 --- a/pxtblocks/loader.ts +++ b/pxtblocks/loader.ts @@ -23,6 +23,7 @@ import { initOnStart } from "./builtins/misc"; import { initContextMenu } from "./contextMenu"; import { renderCodeCard } from "./codecardRenderer"; import { FieldDropdown } from "./fields/field_dropdown"; +import { COLOR_PICKER_BLOCK_TYPE } from "./plugins/colorpicker"; import { setDraggableShadowBlocks, setDuplicateOnDrag, setDuplicateOnDragStrategy } from "./plugins/duplicateOnDrag"; import { initCopyPaste } from "./copyPaste"; export { initCopyPaste } from "./copyPaste"; @@ -110,7 +111,10 @@ export function blockSymbol(type: string): pxtc.SymbolInfo { export function injectBlocks(blockInfo: pxtc.BlocksInfo): pxtc.SymbolInfo[] { cachedBlockInfo = blockInfo; - setDraggableShadowBlocks(blockInfo.blocks.filter(fn => fn.attributes.duplicateShadowOnDrag).map(fn => fn.attributes.blockId)); + setDraggableShadowBlocks([ + COLOR_PICKER_BLOCK_TYPE, + ...blockInfo.blocks.filter(fn => fn.attributes.duplicateShadowOnDrag).map(fn => fn.attributes.blockId) + ]); setArgumentReporterLocalizeFunction((arg, block) => { return localizeArgumentReporter(blockInfo, arg, block); diff --git a/pxtblocks/plugins/colorpicker/colorPickerBlock.ts b/pxtblocks/plugins/colorpicker/colorPickerBlock.ts index 1937c9bf3256..1f246cdc6ba1 100644 --- a/pxtblocks/plugins/colorpicker/colorPickerBlock.ts +++ b/pxtblocks/plugins/colorpicker/colorPickerBlock.ts @@ -3,6 +3,7 @@ import { ColorDropdownField } from "./colorPickerDropdown"; import { ColorPickerNumberBlock, COLOR_NUMBER_BLOCK_TYPE, generateColorPickerNumberShadowDom } from "./colorPickerNumberBlock"; import { COLOR_STRING_BLOCK_TYPE, generateColorPickerStringShadowDom } from "./colorPickerStringBlock"; import { FieldColorPickerNumberType, fromFormatToHex, fromFormatToHSV, fromHexToFormat, fromHSVToFormat, getFieldTypesForFormat } from "./util"; +import { setDuplicateOnDragStrategy, updateDuplicateOnDragState } from "../duplicateOnDrag"; export interface ColorPickerBlock extends Blockly.Block { colorHSVLoaded: boolean; @@ -11,6 +12,8 @@ export interface ColorPickerBlock extends Blockly.Block { setColorHSV: (hsv: number[]) => void; setFormat: (format: string, prevFormat?: string) => void; readColorFromInputs: () => void; + updateBeforeRender: () => void; + updateColorPreview: () => void; } const HEX_INPUT_NAME = "HEX_INPUT"; @@ -31,10 +34,56 @@ export function initColorPickerBlock() { this.setInputsInline(true); this.appendDummyInput() + .appendField(new Blockly.FieldImage(previewImage("#000000"), 24, 24, lf("Choose color"), () => { + const inputs = [HEX_INPUT_NAME, "INPUT0", "INPUT1", "INPUT2", "INPUT3"]; + for (const input of inputs) { + const child = this.getInputTargetBlock(input); + if (child?.type === COLOR_STRING_BLOCK_TYPE || child?.type === COLOR_NUMBER_BLOCK_TYPE) { + this.readColorFromInputs(); + child.getField(input === HEX_INPUT_NAME ? "TEXT" : "NUM").showEditor(); + return; + } + } + }), "PREVIEW") .appendField(new ColorDropdownField("rgb"), "FORMAT"); this.updateShape(this.getFieldValue("FORMAT")); this.setColorHSV(this.colorHSV); + setDuplicateOnDragStrategy(this); + if (this instanceof Blockly.BlockSvg) updateDuplicateOnDragState(this); + this.setOnChange((event: Blockly.Events.BlockBase) => { + if (event.type !== Blockly.Events.BLOCK_CHANGE && event.type !== Blockly.Events.BLOCK_MOVE + && event.type !== Blockly.Events.BLOCK_CREATE) return; + const moved = event as Blockly.Events.BlockMove; + if (event.blockId === this.id || moved.oldParentId === this.id || moved.newParentId === this.id + || this.getChildren(false).some(child => child.id === event.blockId)) { + this.updateColorPreview(); + } + }); + }, + + updateBeforeRender: function (this: ColorPickerBlock) { + this.updateColorPreview(); + }, + + updateColorPreview: function (this: ColorPickerBlock) { + const preview = this.getField("PREVIEW") as Blockly.FieldImage; + const color = getColorPickerColor(this); + // This is derived UI, not an edit: don't add history or invalidate redo. + Blockly.Events.disable(); + try { + if (color) { + const image = previewImage(color); + if (preview.getValue() !== image) { + preview.setValue(image); + } + preview.setAlt(lf("Color {0}. Choose color", color)); + } + preview.setVisible(!!color); + } + finally { + Blockly.Events.enable(); + } }, domToMutation: function (this: ColorPickerBlock, xmlElement: Element) { @@ -134,6 +183,7 @@ export function initColorPickerBlock() { }, setColorHSV: function (this: ColorPickerBlock, hsv: number[]) { + this.colorHSV = hsv; if (this.getFieldValue("FORMAT") === "hex") { const color = fromFormatToHex("hsv", hsv); @@ -144,6 +194,7 @@ export function initColorPickerBlock() { const field = target.getField("TEXT"); field.setValue(color); } + this.updateColorPreview(); return; } @@ -159,7 +210,7 @@ export function initColorPickerBlock() { } } - this.colorHSV = hsv; + this.updateColorPreview(); }, setFormat: function (this: ColorPickerBlock, format: string, prevFormat = format) { @@ -210,4 +261,30 @@ export function initColorPickerBlock() { this.colorHSV = fromFormatToHSV(this.getFieldValue("FORMAT"), newValues); } } +} + +/** Evaluate literal inputs only; never guess a runtime value for variables or expressions. */ +export function getColorPickerColor(block: Blockly.Block): string | undefined { + const format = block.getFieldValue("FORMAT"); + if (format === "hex") { + const child = block.getInputTargetBlock(HEX_INPUT_NAME); + if (child?.type !== COLOR_STRING_BLOCK_TYPE && child?.type !== "text") return undefined; + const hex = child.getFieldValue("TEXT"); + return /^#?(?:[0-9a-f]{3}|[0-9a-f]{6})$/i.test(hex) + ? fromFormatToHex("hsv", fromHexToFormat("hsv", hex)) : undefined; + } + const values: number[] = []; + for (let i = 0; i < getFieldTypesForFormat(format).length; i++) { + const child = block.getInputTargetBlock("INPUT" + i); + if (child?.type !== COLOR_NUMBER_BLOCK_TYPE && child?.type !== "math_number") return undefined; + const value = Number(child.getFieldValue("NUM")); + if (!Number.isFinite(value)) return undefined; + values.push(value); + } + return fromFormatToHex(format, values); +} + +function previewImage(color: string): string { + const content = ``; + return "data:image/svg+xml," + encodeURIComponent(`${content}`); } \ No newline at end of file diff --git a/pxtblocks/plugins/colorpicker/colorPickerDropdown.ts b/pxtblocks/plugins/colorpicker/colorPickerDropdown.ts index 12f5da5a9543..905f21d6a341 100644 --- a/pxtblocks/plugins/colorpicker/colorPickerDropdown.ts +++ b/pxtblocks/plugins/colorpicker/colorPickerDropdown.ts @@ -1,6 +1,7 @@ import * as Blockly from "blockly"; import { FieldDropdown } from "../../fields/field_dropdown"; -import { ColorPickerBlock, COLOR_PICKER_BLOCK_TYPE } from "./colorPickerBlock"; +import { ColorPickerBlock, COLOR_PICKER_BLOCK_TYPE, getColorPickerColor } from "./colorPickerBlock"; +import { fromFormatToHex } from "./util"; const COLOR_FORMATS: [string, string][] = [ ["RGB", "rgb"], @@ -20,7 +21,7 @@ export class ColorDropdownField extends FieldDropdown { override onItemSelected_(menu: Blockly.Menu, menuItem: Blockly.MenuItem) { if (this.sourceBlock_?.type === COLOR_PICKER_BLOCK_TYPE) { const colorPicker = this.sourceBlock_ as ColorPickerBlock; - if (!colorPicker.colorHSVLoaded) { + if (!colorPicker.colorHSVLoaded || getColorPickerColor(colorPicker) !== fromFormatToHex("hsv", colorPicker.colorHSV)) { colorPicker.readColorFromInputs(); } } diff --git a/pxtblocks/plugins/duplicateOnDrag/dragStrategy.ts b/pxtblocks/plugins/duplicateOnDrag/dragStrategy.ts index 8038ff605273..f8645d42f26b 100644 --- a/pxtblocks/plugins/duplicateOnDrag/dragStrategy.ts +++ b/pxtblocks/plugins/duplicateOnDrag/dragStrategy.ts @@ -40,12 +40,13 @@ export class DuplicateOnDragStrategy extends Blockly.dragging.BlockDragStrategy let target: Blockly.Connection; let xml: Element; const isShadow = self.block.isShadow(); + const duplicate = shouldDuplicateOnDrag(self.block); if (isShadow) { self.block.setShadow(false); } - if (shouldDuplicateOnDrag(self.block)) { + if (duplicate) { const output = self.block.outputConnection; if (!output?.targetConnection) return; diff --git a/pxtblocks/toolbox.ts b/pxtblocks/toolbox.ts index eb3c3f61fe1e..f1eb54f21dea 100644 --- a/pxtblocks/toolbox.ts +++ b/pxtblocks/toolbox.ts @@ -3,6 +3,10 @@ import * as Blockly from "blockly"; import { flyoutCategory, getAllFunctionDefinitionBlocks, LOCALIZATION_NAME_MUTATION_KEY } from "./plugins/functions"; import { DRAGGABLE_PARAM_INPUT_PREFIX } from "./loader"; +import { COLOR_PICKER_BLOCK_TYPE } from "./plugins/colorpicker/colorPickerBlock"; +import { generateColorPickerStringShadowDom } from "./plugins/colorpicker/colorPickerStringBlock"; +import { generateColorPickerNumberShadowDom } from "./plugins/colorpicker/colorPickerNumberBlock"; +import { fromFormatToHex, fromHexToFormat, fromHSVToFormat, getFieldTypesForFormat } from "./plugins/colorpicker/util"; const primitiveTypeRegex = /^(string|number|boolean)$/; @@ -111,6 +115,19 @@ export function createShadowValue(info: pxtc.BlocksInfo, p: pxt.blocks.BlockPara shadow.setAttribute("type", shadowId || (isArray ? 'lists_create_with' : typeInfo && typeInfo.block || p.type)); shadow.setAttribute("colour", "#fff"); + const shadowSymbol = info.blocksById[shadowId]; + if (shadowId === COLOR_PICKER_BLOCK_TYPE || shadowSymbol?.attributes.builtinBlockId === COLOR_PICKER_BLOCK_TYPE) { + const contributor = shadowSymbol || info.blocks.find(block => block.attributes.builtinBlockId === COLOR_PICKER_BLOCK_TYPE); + const defaults = contributor && pxt.blocks.compileInfo(contributor).parameters[0]; + value.replaceChild(createColorPickerBlock( + defaultV !== undefined ? defaultV : defaults?.defaultValue, + p.fieldOptions?.format || defaults?.fieldOptions?.format, + contributor?.attributes.color, + true + ), shadow); + return value; + } + if (isArray) { // if an array of booleans, numbers, or strings if (typeInfo && !shadowId) { @@ -314,7 +331,7 @@ export function createToolboxBlock(info: pxtc.BlocksInfo, fn: pxtc.SymbolInfo, c let parentInput: HTMLElement; if (fn.attributes.builtinBlockId) { - return createBuiltinBlock(fn); + return createBuiltinBlock(fn, comp, isShadow); } if (fn.attributes.toolboxParent) { @@ -587,60 +604,50 @@ export function createFunctionsFlyoutCategory(workspace: Blockly.WorkspaceSvg) { return res; }; -function createBuiltinBlock(fn: pxtc.SymbolInfo) { +function createBuiltinBlock(fn: pxtc.SymbolInfo, comp: pxt.blocks.BlockCompileInfo, isShadow: boolean) { const id = fn.attributes.builtinBlockId; - - const blockColor = fn.attributes.color; - - if (id === "makecode_color_picker") { - // - // rgb - // - // - // 255 - // - // - // - // - // 255 - // - // - // - // - // 0 - // - // - // - const block = document.createElement("block"); - block.setAttribute("type", "makecode_color_picker"); - - const field = document.createElement("field"); - field.setAttribute("name", "FORMAT"); - field.textContent = "rgb"; - block.appendChild(field); - - for (let i = 0; i < 3; i++) { - const value = document.createElement("value"); - value.setAttribute("name", `INPUT${i}`); - const shadow = document.createElement("shadow"); - shadow.setAttribute("type", "makecode_color_picker_number"); - const numField = document.createElement("field"); - numField.setAttribute("name", "NUM"); - numField.textContent = "0"; - shadow.appendChild(numField); - value.appendChild(shadow); - block.appendChild(value); - } - - if (blockColor) { - const mutation = document.createElement("mutation"); - mutation.setAttribute("color", blockColor); - block.appendChild(mutation); - } - - return block; + if (id === COLOR_PICKER_BLOCK_TYPE) { + const defaults = comp.parameters[0]; + return createColorPickerBlock(defaults?.defaultValue, defaults?.fieldOptions?.format, fn.attributes.color, isShadow); } pxt.warn(`Unsupported builtin block id: ${id}`); return undefined; +} + +function createColorPickerBlock(defaultValue: string, requestedFormat: string, blockColor: string, isShadow: boolean): HTMLElement { + if (defaultValue?.charAt(0) === '"') defaultValue = JSON.parse(defaultValue); + const formatName = ["rgb", "hsv", "hsl", "cmyk", "hex"].indexOf(requestedFormat) >= 0 ? requestedFormat : "rgb"; + const rgb = Number(defaultValue) || 0; + const hex = typeof defaultValue === "string" && /^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i.test(defaultValue) + ? defaultValue : fromFormatToHex("rgb", [rgb >> 16 & 255, rgb >> 8 & 255, rgb & 255]); + const block = document.createElement(isShadow ? "shadow" : "block"); + block.setAttribute("type", COLOR_PICKER_BLOCK_TYPE); + const mutation = document.createElement("mutation"); + if (blockColor) mutation.setAttribute("color", blockColor); + const hsv = fromHexToFormat("hsv", hex); + ["hue", "saturation", "value"].forEach((name, index) => mutation.setAttribute(name, hsv[index].toString())); + block.appendChild(mutation); + + const format = document.createElement("field"); + format.setAttribute("name", "FORMAT"); + format.textContent = formatName; + block.appendChild(format); + + if (formatName === "hex") { + const input = document.createElement("value"); + input.setAttribute("name", "HEX_INPUT"); + input.appendChild(generateColorPickerStringShadowDom(hex)); + block.appendChild(input); + } + else { + const types = getFieldTypesForFormat(formatName); + fromHSVToFormat(formatName, hsv).forEach((channel, index) => { + const input = document.createElement("value"); + input.setAttribute("name", "INPUT" + index); + input.appendChild(generateColorPickerNumberShadowDom(types[index], channel)); + block.appendChild(input); + }); + } + return block; } \ No newline at end of file diff --git a/pxtcompiler/emitter/decompiler.ts b/pxtcompiler/emitter/decompiler.ts index ecb86c56cba0..bef8aec96cfb 100644 --- a/pxtcompiler/emitter/decompiler.ts +++ b/pxtcompiler/emitter/decompiler.ts @@ -686,6 +686,13 @@ ${output}`; } function mkValue(name: string, value: ExpressionNode | TextNode, shadowType?: string, shadowMutation?: pxt.Map): ValueNode { + const contributor = blocksInfo.blocksById[shadowType]; + const builtinBlockId = contributor?.attributes.builtinBlockId; + // Keep the contributor ID so a fallback shadow can be rebuilt with its configured defaults. + if (builtinBlockId === "makecode_color_picker" && value.kind === "expr" && value.type === builtinBlockId) { + value.mutation = value.mutation || {}; + if (contributor?.attributes.color) value.mutation.color = contributor.attributes.color; + } if ((!shadowType || shadowType === numberType) && shadowMutation && shadowMutation['min'] && shadowMutation['max']) { // Convert a number to a number with a slider (math_number_minmax) if min and max shadow options are defined shadowType = minmaxNumberType; @@ -4004,8 +4011,10 @@ ${output}`; let emitShadowOnly = false; if (n.value.kind === "expr") { - if (n.value.type !== n.shadowType) { - const shadowBlockInfo = blocksInfo.blocksById[n.shadowType]; + const shadowBlockInfo = blocksInfo.blocksById[n.shadowType]; + const normalizedShadowType = shadowBlockInfo?.attributes?.builtinBlockId || n.shadowType; + + if (n.value.type !== normalizedShadowType) { let shadowBlockShimType: string; let shadowFieldName: string; @@ -4043,7 +4052,7 @@ ${output}`; value.type = colorPickerString; } - emitShadowOnly = value.type === n.shadowType; + emitShadowOnly = value.type === normalizedShadowType; if (!emitShadowOnly) { if (isNumberBlockType(value.type) || isBooleanBlockType(value.type) || isStringBlockType(value.type)) { emitShadowOnly = !n.shadowType diff --git a/tests/blocklycompiler-test/test.spec.ts b/tests/blocklycompiler-test/test.spec.ts index 63755d02d6c2..2c7ed6fbaa75 100644 --- a/tests/blocklycompiler-test/test.spec.ts +++ b/tests/blocklycompiler-test/test.spec.ts @@ -3,7 +3,7 @@ import * as Blockly from "blockly"; import * as pxtblockly from "../../pxtblocks"; -import { DuplicateOnDragConnectionChecker } from "../../pxtblocks/plugins/duplicateOnDrag"; +import { DuplicateOnDragConnectionChecker, shouldDuplicateOnDrag } from "../../pxtblocks/plugins/duplicateOnDrag"; import "./commentparsing.spec"; import "./fieldUserEnum.spec"; @@ -391,6 +391,107 @@ describe("blockly compiler", function () { }); describe("compiling text", () => { + it("should initialize color picker shadows in the parameter's requested mode", async () => { + const info = await getBlocksInfoAsync(); + const coloredInfo = { + ...info, + blocks: info.blocks.concat({ + attributes: { builtinBlockId: "makecode_color_picker", color: "#6554C0" } + } as pxtc.SymbolInfo) + }; + const workspace = new Blockly.Workspace(); + try { + for (const format of ["hex", "rgb", "hsv", "hsl", "cmyk", "invalid"]) { + const value = pxtblockly.createShadowValue(coloredInfo, { + definitionName: "color", actualName: "color", type: "number", + shadowBlockId: "makecode_color_picker", defaultValue: "0x7f3fbf", + fieldOptions: { format } + }); + const picker = Blockly.Xml.domToBlock(value.firstElementChild, workspace) as pxtblockly.ColorPickerBlock; + const preview = picker.getField("PREVIEW"); + picker.updateColorPreview(); + chai.assert.isTrue(picker.isShadow()); + chai.assert.isTrue(preview.isVisible()); + chai.assert.equal(picker.getColour().toLowerCase(), "#6554c0"); + chai.assert.equal(picker.getFieldValue("FORMAT"), format === "invalid" ? "rgb" : format); + chai.assert.equal(pxtblockly.getColorPickerColor(picker), "#7F3FBF"); + const mutation = value.querySelector("mutation"); + chai.assert.equal(mutation.getAttribute("color"), "#6554C0"); + chai.assert.isTrue(shouldDuplicateOnDrag(picker)); + mutation.removeAttribute("hue"); + mutation.removeAttribute("saturation"); + mutation.removeAttribute("value"); + const restored = Blockly.Xml.domToBlock(value.firstElementChild, workspace) as pxtblockly.ColorPickerBlock; + restored.updateBeforeRender(); + chai.assert.include(restored.getField("PREVIEW").getText(), "#7F3FBF"); + restored.dispose(); + picker.setFieldValue("rgb", "FORMAT"); + chai.assert.deepEqual([0, 1, 2].map(i => Math.round(Number(picker.getInputTargetBlock("INPUT" + i).getFieldValue("NUM")))), [127, 63, 191]); + picker.getInputTargetBlock("INPUT0").setFieldValue("255", "NUM"); + picker.updateColorPreview(); + chai.assert.include(picker.getField("PREVIEW").getText(), "#FF3FBF"); + // An expression has no statically known preview, even if another channel is literal. + const input = picker.getInput("INPUT0"); + input.connection.setShadowDom(null); + chai.assert.isUndefined(pxtblockly.getColorPickerColor(picker)); + picker.updateColorPreview(); + chai.assert.isFalse(preview.isVisible()); + picker.dispose(); + } + } + finally { + workspace.dispose(); + } + }); + + it("should share named picker defaults without taking another contributor's settings", async () => { + const info = await getBlocksInfoAsync(); + const contributor = (id: string, color: string, format: string, value: string): pxtc.SymbolInfo => ({ + name: id, namespace: "test", fileName: "test.ts", kind: pxtc.SymbolKind.Function, retType: "number", + attributes: { + blockId: id, builtinBlockId: "makecode_color_picker", color, + paramFieldEditorOptions: { value: { format } }, + paramDefl: { value }, callingConvention: pxtc.ir.CallingConvention.Plain + }, + parameters: [{ name: "value", type: "number", description: "", default: value }] + }); + const first = contributor("other_picker", "#008800", "rgb", "0xff0000"); + const selected = contributor("test_color_picker", "#6554C0", "hex", "0x7f3fbf"); + const namedInfo = { + ...info, blocks: info.blocks.concat(first, selected), + blocksById: { + ...info.blocksById, other_picker: first, test_color_picker: selected + } + }; + const shadow = pxtblockly.createShadowValue(namedInfo, { + definitionName: "color", actualName: "color", type: "number", shadowBlockId: "test_color_picker" + }).firstElementChild; + const otherShadow = pxtblockly.createShadowValue(namedInfo, { + definitionName: "color", actualName: "color", type: "number", shadowBlockId: "other_picker" + }).firstElementChild; + const standalone = pxtblockly.createToolboxBlock(namedInfo, selected, pxt.blocks.compileInfo(selected)); + chai.assert.equal(shadow.getAttribute("type"), "makecode_color_picker"); + chai.assert.equal(shadow.innerHTML, standalone.innerHTML); + chai.assert.equal(shadow.querySelector("mutation").getAttribute("color"), "#6554C0"); + chai.assert.equal(otherShadow.querySelector("mutation").getAttribute("color"), "#008800"); + chai.assert.equal(shadow.querySelector('field[name="FORMAT"]').textContent, "hex"); + chai.assert.equal(shadow.querySelector('field[name="TEXT"]').textContent, "#7F3FBF"); + const workspace = new Blockly.Workspace(); + try { + const picker = Blockly.Xml.domToBlock(shadow, workspace); + const unmarked = Blockly.Xml.domToBlock(otherShadow, workspace); + const standalonePicker = Blockly.Xml.domToBlock(pxtblockly.createToolboxBlock(namedInfo, selected, pxt.blocks.compileInfo(selected)), workspace); + chai.assert.isTrue(shouldDuplicateOnDrag(picker)); + chai.assert.isTrue(shouldDuplicateOnDrag(unmarked)); + chai.assert.isFalse(shouldDuplicateOnDrag(standalonePicker)); + const restored = Blockly.Xml.domToBlock(Blockly.Xml.blockToDom(picker, true) as Element, workspace); + chai.assert.isTrue(shouldDuplicateOnDrag(restored)); + } + finally { + workspace.dispose(); + } + }); + it("should handle the text blocks", (done: () => void) => { blockTestAsync("text").then(done, done); }); diff --git a/tests/decompile-test/baselines/contributed_builtin_shadows.blocks b/tests/decompile-test/baselines/contributed_builtin_shadows.blocks new file mode 100644 index 000000000000..fad148bb4dbc --- /dev/null +++ b/tests/decompile-test/baselines/contributed_builtin_shadows.blocks @@ -0,0 +1,46 @@ + + + + +selectedColor + +0 + + +hex + + +#123456 + + + + + + + + + +selectedColor + + + + + + + +hex + + +#abcdef + + + + + + + + + + + + diff --git a/tests/decompile-test/cases/contributed_builtin_shadows.ts b/tests/decompile-test/cases/contributed_builtin_shadows.ts new file mode 100644 index 000000000000..4dedb9020882 --- /dev/null +++ b/tests/decompile-test/cases/contributed_builtin_shadows.ts @@ -0,0 +1,3 @@ +let selectedColor = colorHelpers.hex("#123456") +testNamespace.colorArgument(selectedColor) +testNamespace.colorArgument(colorHelpers.hex("#abcdef")) diff --git a/tests/decompile-test/cases/testBlocks/basic.ts b/tests/decompile-test/cases/testBlocks/basic.ts index bcb69657837f..d01dffbdab5d 100644 --- a/tests/decompile-test/cases/testBlocks/basic.ts +++ b/tests/decompile-test/cases/testBlocks/basic.ts @@ -15,6 +15,19 @@ namespace testNamespace { //% block="Number arg %arg" export function numberArgument(arg: number): void {} + //% blockId=test_color_argument + //% block="Color arg %color" + //% color.shadow=test_color_picker + export function colorArgument(color: number): void {} + + //% block blockId=test_color_picker builtinBlockId=makecode_color_picker + //% blockHidden=true + //% value.fieldOptions.format=hex value.defl=0x7f3fbf + //% color="#6554C0" + export function __colorPicker(value: number): number { + return value; + } + //% blockId=test_string_argument //% block="String arg %arg" export function stringArgument(arg: string): void {} diff --git a/webapp/src/blocks.tsx b/webapp/src/blocks.tsx index 1a30d88a0aef..42da81e8c74f 100644 --- a/webapp/src/blocks.tsx +++ b/webapp/src/blocks.tsx @@ -955,13 +955,16 @@ export class Editor extends toolboxeditor.ToolboxEditor { }) this.editor.addChangeListener((e) => { - if (e.type === Blockly.Events.BLOCK_MOVE) { + if (e.type === Blockly.Events.BLOCK_MOVE && e.recordUndo) { const parent = this.editor.getBlockById((e as Blockly.Events.BlockMove).newParentId); - if (parent && parent.isShadow()) { + const child = this.editor.getBlockById((e as Blockly.Events.BlockMove).blockId); + // Nested shadows change during picker format switches; only promote real inputs. + if (parent?.isShadow() && parent.outputConnection?.targetConnection + && child && !child.isShadow() && child.getParent() === parent) { Blockly.Events.setGroup(e.group) const json = Blockly.serialization.blocks.save(parent); // Dispose of the original block so it doesn't get saved in the parent connection's shadow state. - this.editor.getBlockById((e as Blockly.Events.BlockMove).blockId).dispose(); + child.dispose(); const dupe = Blockly.serialization.blocks.append(json, this.editor, { recordUndo: true }); parent.outputConnection.targetConnection.connect(dupe.outputConnection); Blockly.Events.setGroup(false); @@ -2497,7 +2500,7 @@ export class Editor extends toolboxeditor.ToolboxEditor { const info = this.blockInfo.blocksById[type]; const shadowType = info?.attributes?.builtinBlockId || type; - let b = this.getBlockXml(builtin ? builtin : { name: type, type: type, attributes: { blockId: type } }, ignoregap, true); + let b = this.getBlockXml(builtin || info || { name: type, type: type, attributes: { blockId: type } }, ignoregap, true); // Note: we're setting one innerHTML to another if (b && b.length > 0 && b[0] && b[0].getAttribute("type") === shadowType) { // eslint-disable-next-line @microsoft/sdl/no-inner-html diff --git a/webapp/src/monaco.tsx b/webapp/src/monaco.tsx index 5a6cb1ddd7e4..8ef592422d14 100644 --- a/webapp/src/monaco.tsx +++ b/webapp/src/monaco.tsx @@ -1932,7 +1932,9 @@ export class Editor extends toolboxeditor.ToolboxEditor { } if (fn.attributes.builtinBlockId) { - res[ns].push(...snippets.getExtensionContributedBuiltinBlock(fn.attributes.builtinBlockId, fn.attributes.weight || 50)); + if (!fn.attributes.blockHidden) { + res[ns].push(...snippets.getExtensionContributedBuiltinBlock(fn.attributes.builtinBlockId, fn.attributes.weight || 50)); + } return; }