From c9a5e81c53fc2148b76d3301b245e15003332888 Mon Sep 17 00:00:00 2001 From: Joey Wunderlich Date: Tue, 22 Sep 2026 14:39:08 -0700 Subject: [PATCH 01/11] changes to color picker --- docs/defining-blocks.md | 8 +++ .../plugins/colorpicker/colorPickerBlock.ts | 67 ++++++++++++++++++- .../colorpicker/colorPickerDropdown.ts | 5 +- pxtblocks/toolbox.ts | 40 +++++++++++ tests/blocklycompiler-test/test.spec.ts | 38 +++++++++++ webapp/src/blocks.tsx | 9 ++- webapp/src/monaco.tsx | 4 +- 7 files changed, 164 insertions(+), 7 deletions(-) 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/plugins/colorpicker/colorPickerBlock.ts b/pxtblocks/plugins/colorpicker/colorPickerBlock.ts index 1937c9bf3256..590034c1ff3e 100644 --- a/pxtblocks/plugins/colorpicker/colorPickerBlock.ts +++ b/pxtblocks/plugins/colorpicker/colorPickerBlock.ts @@ -11,6 +11,7 @@ export interface ColorPickerBlock extends Blockly.Block { setColorHSV: (hsv: number[]) => void; setFormat: (format: string, prevFormat?: string) => void; readColorFromInputs: () => void; + updateColorPreview: () => void; } const HEX_INPUT_NAME = "HEX_INPUT"; @@ -31,10 +32,44 @@ export function initColorPickerBlock() { this.setInputsInline(true); this.appendDummyInput() + .appendField(new Blockly.FieldImage(previewImage(), 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); + 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(); + } + }); + }, + + 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 { + preview.setValue(previewImage(color)); + preview.setAlt(color ? lf("Color {0}. Choose color", color) : lf("Color depends on input values")); + } + finally { + Blockly.Events.enable(); + } }, domToMutation: function (this: ColorPickerBlock, xmlElement: Element) { @@ -134,6 +169,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 +180,7 @@ export function initColorPickerBlock() { const field = target.getField("TEXT"); field.setValue(color); } + this.updateColorPreview(); return; } @@ -159,7 +196,7 @@ export function initColorPickerBlock() { } } - this.colorHSV = hsv; + this.updateColorPreview(); }, setFormat: function (this: ColorPickerBlock, format: string, prevFormat = format) { @@ -210,4 +247,32 @@ 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 = color + ? `` + : '?'; + 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/toolbox.ts b/pxtblocks/toolbox.ts index eb3c3f61fe1e..b880300497b3 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,42 @@ 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"); + if (shadowId === COLOR_PICKER_BLOCK_TYPE) { + const requestedFormat = p.fieldOptions?.format; + 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 mutation = document.createElement("mutation"); + const blockColor = info.blocks.find(block => block.attributes.builtinBlockId === COLOR_PICKER_BLOCK_TYPE)?.attributes.color; + if (blockColor) mutation.setAttribute("color", blockColor); + const hsv = fromHexToFormat("hsv", hex); + ["hue", "saturation", "value"].forEach((name, index) => mutation.setAttribute(name, hsv[index].toString())); + shadow.appendChild(mutation); + + const format = document.createElement("field"); + format.setAttribute("name", "FORMAT"); + format.textContent = formatName; + shadow.appendChild(format); + + if (formatName === "hex") { + const hexInput = document.createElement("value"); + hexInput.setAttribute("name", "HEX_INPUT"); + hexInput.appendChild(generateColorPickerStringShadowDom(hex)); + shadow.appendChild(hexInput); + } + 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)); + shadow.appendChild(input); + }); + } + return value; + } + if (isArray) { // if an array of booleans, numbers, or strings if (typeInfo && !shadowId) { diff --git a/tests/blocklycompiler-test/test.spec.ts b/tests/blocklycompiler-test/test.spec.ts index 63755d02d6c2..3a1ba0ed12f1 100644 --- a/tests/blocklycompiler-test/test.spec.ts +++ b/tests/blocklycompiler-test/test.spec.ts @@ -391,6 +391,44 @@ 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; + chai.assert.isTrue(picker.isShadow()); + chai.assert.equal(picker.getColour().toLowerCase(), "#6554c0"); + chai.assert.equal(picker.getFieldValue("FORMAT"), format === "invalid" ? "rgb" : format); + chai.assert.equal(pxtblockly.getColorPickerColor(picker), "#7F3FBF"); + 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.dispose(); + } + } + finally { + workspace.dispose(); + } + }); + it("should handle the text blocks", (done: () => void) => { blockTestAsync("text").then(done, done); }); diff --git a/webapp/src/blocks.tsx b/webapp/src/blocks.tsx index 1a30d88a0aef..0e93c6416c46 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); 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; } From 091c5a4c61cf6ed9aef38af69b5c5e98ebd07502 Mon Sep 17 00:00:00 2001 From: Joey Wunderlich Date: Tue, 22 Sep 2026 15:18:02 -0700 Subject: [PATCH 02/11] changes to support copy o drag / etc --- .../plugins/colorpicker/colorPickerBlock.ts | 5 + .../plugins/duplicateOnDrag/dragStrategy.ts | 3 +- .../duplicateOnDrag/duplicateOnDrag.ts | 11 ++ pxtblocks/toolbox.ts | 139 +++++++----------- pxtcompiler/emitter/decompiler.ts | 8 + tests/blocklycompiler-test/test.spec.ts | 48 +++++- 6 files changed, 127 insertions(+), 87 deletions(-) diff --git a/pxtblocks/plugins/colorpicker/colorPickerBlock.ts b/pxtblocks/plugins/colorpicker/colorPickerBlock.ts index 590034c1ff3e..d75b26d9be37 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 { hasDuplicateShadowOnDrag, setDuplicateShadowOnDrag, setDuplicateOnDragStrategy } from "../duplicateOnDrag"; export interface ColorPickerBlock extends Blockly.Block { colorHSVLoaded: boolean; @@ -73,6 +74,9 @@ export function initColorPickerBlock() { }, domToMutation: function (this: ColorPickerBlock, xmlElement: Element) { + const duplicate = xmlElement.getAttribute("duplicateondrag") === "true"; + setDuplicateShadowOnDrag(this, duplicate); + if (duplicate) setDuplicateOnDragStrategy(this); if (xmlElement.hasAttribute("hue") && xmlElement.hasAttribute("saturation") && xmlElement.hasAttribute("value")) { this.colorHSVLoaded = true; this.colorHSV = [ @@ -94,6 +98,7 @@ export function initColorPickerBlock() { mutationToDom: function () { const container = document.createElement("mutation"); + if (hasDuplicateShadowOnDrag(this)) container.setAttribute("duplicateondrag", "true"); if (this.colorHSVLoaded) { container.setAttribute("hue", this.colorHSV[0].toString()); container.setAttribute("saturation", this.colorHSV[1].toString()); 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/plugins/duplicateOnDrag/duplicateOnDrag.ts b/pxtblocks/plugins/duplicateOnDrag/duplicateOnDrag.ts index 4e92607a8635..f7efe24b1abd 100644 --- a/pxtblocks/plugins/duplicateOnDrag/duplicateOnDrag.ts +++ b/pxtblocks/plugins/duplicateOnDrag/duplicateOnDrag.ts @@ -3,6 +3,16 @@ import { PathObject } from "../renderer/pathObject"; let draggableShadowAllowlist: string[]; let duplicateRefs: DuplicateOnDragRef[]; +const draggableShadowInstances = new WeakSet(); + +export function setDuplicateShadowOnDrag(block: Blockly.Block, enabled: boolean): void { + if (enabled) draggableShadowInstances.add(block); + else draggableShadowInstances.delete(block); +} + +export function hasDuplicateShadowOnDrag(block: Blockly.Block): boolean { + return draggableShadowInstances.has(block); +} interface DuplicateOnDragRef { parentBlockType: string; @@ -42,6 +52,7 @@ export function setDuplicateOnDrag(parentBlockType: string, inputName?: string, } export function isAllowlistedShadow(block: Blockly.Block) { + if (hasDuplicateShadowOnDrag(block)) return true; if (draggableShadowAllowlist) { if (draggableShadowAllowlist.indexOf(block.type) !== -1) { return true; diff --git a/pxtblocks/toolbox.ts b/pxtblocks/toolbox.ts index b880300497b3..b71433eb6692 100644 --- a/pxtblocks/toolbox.ts +++ b/pxtblocks/toolbox.ts @@ -115,39 +115,17 @@ 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"); - if (shadowId === COLOR_PICKER_BLOCK_TYPE) { - const requestedFormat = p.fieldOptions?.format; - 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 mutation = document.createElement("mutation"); - const blockColor = info.blocks.find(block => block.attributes.builtinBlockId === COLOR_PICKER_BLOCK_TYPE)?.attributes.color; - if (blockColor) mutation.setAttribute("color", blockColor); - const hsv = fromHexToFormat("hsv", hex); - ["hue", "saturation", "value"].forEach((name, index) => mutation.setAttribute(name, hsv[index].toString())); - shadow.appendChild(mutation); - - const format = document.createElement("field"); - format.setAttribute("name", "FORMAT"); - format.textContent = formatName; - shadow.appendChild(format); - - if (formatName === "hex") { - const hexInput = document.createElement("value"); - hexInput.setAttribute("name", "HEX_INPUT"); - hexInput.appendChild(generateColorPickerStringShadowDom(hex)); - shadow.appendChild(hexInput); - } - 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)); - shadow.appendChild(input); - }); - } + 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, + contributor?.attributes.duplicateShadowOnDrag + ), shadow); return value; } @@ -354,7 +332,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) { @@ -627,60 +605,51 @@ 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, fn.attributes.duplicateShadowOnDrag); } pxt.warn(`Unsupported builtin block id: ${id}`); return undefined; +} + +function createColorPickerBlock(defaultValue: string, requestedFormat: string, blockColor: string, isShadow: boolean, duplicateShadowOnDrag = false): 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); + if (isShadow && duplicateShadowOnDrag) mutation.setAttribute("duplicateondrag", "true"); + 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..642fc9f5bdbb 100644 --- a/pxtcompiler/emitter/decompiler.ts +++ b/pxtcompiler/emitter/decompiler.ts @@ -686,6 +686,14 @@ ${output}`; } function mkValue(name: string, value: ExpressionNode | TextNode, shadowType?: string, shadowMutation?: pxt.Map): ValueNode { + // Contributed built-ins use their own ID in annotations, but the built-in type in Blockly XML. + const contributor = blocksInfo.blocksById[shadowType]; + shadowType = contributor?.attributes.builtinBlockId || shadowType; + if (shadowType === "makecode_color_picker" && value.kind === "expr" && value.type === shadowType) { + value.mutation = value.mutation || {}; + if (contributor?.attributes.color) value.mutation.color = contributor.attributes.color; + if (contributor?.attributes.duplicateShadowOnDrag) value.mutation.duplicateondrag = "true"; + } 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; diff --git a/tests/blocklycompiler-test/test.spec.ts b/tests/blocklycompiler-test/test.spec.ts index 3a1ba0ed12f1..9fb3df116f3a 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"; @@ -429,6 +429,52 @@ describe("blockly compiler", function () { } }); + 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 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(shadow.querySelector('field[name="FORMAT"]').textContent, "hex"); + chai.assert.equal(shadow.querySelector('field[name="TEXT"]').textContent, "#7F3FBF"); + selected.attributes.duplicateShadowOnDrag = true; + const marked = pxtblockly.createShadowValue(namedInfo, { + definitionName: "color", actualName: "color", type: "number", shadowBlockId: "test_color_picker" + }).firstElementChild; + const workspace = new Blockly.Workspace(); + try { + const picker = Blockly.Xml.domToBlock(marked, workspace); + const unmarked = Blockly.Xml.domToBlock(shadow, workspace); + const standalonePicker = Blockly.Xml.domToBlock(pxtblockly.createToolboxBlock(namedInfo, selected, pxt.blocks.compileInfo(selected)), workspace); + chai.assert.isTrue(shouldDuplicateOnDrag(picker)); + chai.assert.isFalse(shouldDuplicateOnDrag(unmarked)); + chai.assert.isFalse(shouldDuplicateOnDrag(standalonePicker)); + const restored = Blockly.Xml.domToBlock(Blockly.Xml.blockToDom(picker) as Element, workspace); + chai.assert.isTrue(shouldDuplicateOnDrag(restored)); + } + finally { + workspace.dispose(); + } + }); + it("should handle the text blocks", (done: () => void) => { blockTestAsync("text").then(done, done); }); From 85d5b596789d2970f6faface0816341985eaad2a Mon Sep 17 00:00:00 2001 From: Joey Wunderlich Date: Tue, 22 Sep 2026 15:41:12 -0700 Subject: [PATCH 03/11] drag state --- pxtblocks/plugins/colorpicker/colorPickerBlock.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pxtblocks/plugins/colorpicker/colorPickerBlock.ts b/pxtblocks/plugins/colorpicker/colorPickerBlock.ts index d75b26d9be37..2f787d8219a0 100644 --- a/pxtblocks/plugins/colorpicker/colorPickerBlock.ts +++ b/pxtblocks/plugins/colorpicker/colorPickerBlock.ts @@ -3,7 +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 { hasDuplicateShadowOnDrag, setDuplicateShadowOnDrag, setDuplicateOnDragStrategy } from "../duplicateOnDrag"; +import { hasDuplicateShadowOnDrag, setDuplicateShadowOnDrag, setDuplicateOnDragStrategy, updateDuplicateOnDragState } from "../duplicateOnDrag"; export interface ColorPickerBlock extends Blockly.Block { colorHSVLoaded: boolean; @@ -76,7 +76,10 @@ export function initColorPickerBlock() { domToMutation: function (this: ColorPickerBlock, xmlElement: Element) { const duplicate = xmlElement.getAttribute("duplicateondrag") === "true"; setDuplicateShadowOnDrag(this, duplicate); - if (duplicate) setDuplicateOnDragStrategy(this); + if (duplicate) { + setDuplicateOnDragStrategy(this); + if (this instanceof Blockly.BlockSvg) updateDuplicateOnDragState(this); + } if (xmlElement.hasAttribute("hue") && xmlElement.hasAttribute("saturation") && xmlElement.hasAttribute("value")) { this.colorHSVLoaded = true; this.colorHSV = [ From ff047db42d3a4cd90e4f8a77397c07140020432c Mon Sep 17 00:00:00 2001 From: Joey Wunderlich Date: Thu, 24 Sep 2026 15:32:54 -0700 Subject: [PATCH 04/11] fix color preview after block round trip Recompute the derived color preview before rendering so workspace loads with disabled Blockly events still reflect reconstructed literal inputs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- pxtblocks/plugins/colorpicker/colorPickerBlock.ts | 10 +++++++++- tests/blocklycompiler-test/test.spec.ts | 8 ++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/pxtblocks/plugins/colorpicker/colorPickerBlock.ts b/pxtblocks/plugins/colorpicker/colorPickerBlock.ts index 2f787d8219a0..b7af7bbe6a3c 100644 --- a/pxtblocks/plugins/colorpicker/colorPickerBlock.ts +++ b/pxtblocks/plugins/colorpicker/colorPickerBlock.ts @@ -12,6 +12,7 @@ export interface ColorPickerBlock extends Blockly.Block { setColorHSV: (hsv: number[]) => void; setFormat: (format: string, prevFormat?: string) => void; readColorFromInputs: () => void; + updateBeforeRender: () => void; updateColorPreview: () => void; } @@ -59,13 +60,20 @@ export function initColorPickerBlock() { }); }, + updateBeforeRender: function (this: ColorPickerBlock) { + this.updateColorPreview(); + }, + updateColorPreview: function (this: ColorPickerBlock) { const preview = this.getField("PREVIEW") as Blockly.FieldImage; const color = getColorPickerColor(this); + const image = previewImage(color); // This is derived UI, not an edit: don't add history or invalidate redo. Blockly.Events.disable(); try { - preview.setValue(previewImage(color)); + if (preview.getValue() !== image) { + preview.setValue(image); + } preview.setAlt(color ? lf("Color {0}. Choose color", color) : lf("Color depends on input values")); } finally { diff --git a/tests/blocklycompiler-test/test.spec.ts b/tests/blocklycompiler-test/test.spec.ts index 9fb3df116f3a..58d59ff7f84c 100644 --- a/tests/blocklycompiler-test/test.spec.ts +++ b/tests/blocklycompiler-test/test.spec.ts @@ -412,6 +412,14 @@ describe("blockly compiler", function () { 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"); + 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"); From 4ed549ca3c9446980878fc40b5f030200a625dee Mon Sep 17 00:00:00 2001 From: Joey Wunderlich Date: Thu, 24 Sep 2026 15:32:54 -0700 Subject: [PATCH 05/11] fix color preview after block round trip Recompute the derived color preview before rendering so workspace loads with disabled Blockly events still reflect reconstructed literal inputs. --- pxtblocks/plugins/colorpicker/colorPickerBlock.ts | 10 +++++++++- tests/blocklycompiler-test/test.spec.ts | 8 ++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/pxtblocks/plugins/colorpicker/colorPickerBlock.ts b/pxtblocks/plugins/colorpicker/colorPickerBlock.ts index 2f787d8219a0..b7af7bbe6a3c 100644 --- a/pxtblocks/plugins/colorpicker/colorPickerBlock.ts +++ b/pxtblocks/plugins/colorpicker/colorPickerBlock.ts @@ -12,6 +12,7 @@ export interface ColorPickerBlock extends Blockly.Block { setColorHSV: (hsv: number[]) => void; setFormat: (format: string, prevFormat?: string) => void; readColorFromInputs: () => void; + updateBeforeRender: () => void; updateColorPreview: () => void; } @@ -59,13 +60,20 @@ export function initColorPickerBlock() { }); }, + updateBeforeRender: function (this: ColorPickerBlock) { + this.updateColorPreview(); + }, + updateColorPreview: function (this: ColorPickerBlock) { const preview = this.getField("PREVIEW") as Blockly.FieldImage; const color = getColorPickerColor(this); + const image = previewImage(color); // This is derived UI, not an edit: don't add history or invalidate redo. Blockly.Events.disable(); try { - preview.setValue(previewImage(color)); + if (preview.getValue() !== image) { + preview.setValue(image); + } preview.setAlt(color ? lf("Color {0}. Choose color", color) : lf("Color depends on input values")); } finally { diff --git a/tests/blocklycompiler-test/test.spec.ts b/tests/blocklycompiler-test/test.spec.ts index 9fb3df116f3a..58d59ff7f84c 100644 --- a/tests/blocklycompiler-test/test.spec.ts +++ b/tests/blocklycompiler-test/test.spec.ts @@ -412,6 +412,14 @@ describe("blockly compiler", function () { 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"); + 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"); From f4e748983b7b74b46399e04ccfd11459e6b7e2a8 Mon Sep 17 00:00:00 2001 From: Joey Wunderlich Date: Thu, 24 Sep 2026 16:11:25 -0700 Subject: [PATCH 06/11] fix issue hassan noticed --- pxtcompiler/emitter/decompiler.ts | 14 +++--- .../contributed_builtin_shadows.blocks | 46 +++++++++++++++++++ .../cases/contributed_builtin_shadows.ts | 3 ++ .../decompile-test/cases/testBlocks/basic.ts | 13 ++++++ webapp/src/blocks.tsx | 2 +- 5 files changed, 71 insertions(+), 7 deletions(-) create mode 100644 tests/decompile-test/baselines/contributed_builtin_shadows.blocks create mode 100644 tests/decompile-test/cases/contributed_builtin_shadows.ts diff --git a/pxtcompiler/emitter/decompiler.ts b/pxtcompiler/emitter/decompiler.ts index 642fc9f5bdbb..edae47869d0e 100644 --- a/pxtcompiler/emitter/decompiler.ts +++ b/pxtcompiler/emitter/decompiler.ts @@ -686,10 +686,10 @@ ${output}`; } function mkValue(name: string, value: ExpressionNode | TextNode, shadowType?: string, shadowMutation?: pxt.Map): ValueNode { - // Contributed built-ins use their own ID in annotations, but the built-in type in Blockly XML. const contributor = blocksInfo.blocksById[shadowType]; - shadowType = contributor?.attributes.builtinBlockId || shadowType; - if (shadowType === "makecode_color_picker" && value.kind === "expr" && value.type === 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 (contributor?.attributes.duplicateShadowOnDrag) value.mutation.duplicateondrag = "true"; @@ -4012,8 +4012,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; @@ -4051,7 +4053,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/decompile-test/baselines/contributed_builtin_shadows.blocks b/tests/decompile-test/baselines/contributed_builtin_shadows.blocks new file mode 100644 index 000000000000..07784552682b --- /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..f13c467715a0 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 duplicateShadowOnDrag=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 0e93c6416c46..42da81e8c74f 100644 --- a/webapp/src/blocks.tsx +++ b/webapp/src/blocks.tsx @@ -2500,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 From 21446ea2a52c4ccfe70e79a26989d76d6368c504 Mon Sep 17 00:00:00 2001 From: Joey Wunderlich Date: Tue, 29 Sep 2026 10:29:33 -0700 Subject: [PATCH 07/11] just remove color preview box instead of question mark --- .../plugins/colorpicker/colorPickerBlock.ts | 19 ++++++++++--------- tests/blocklycompiler-test/test.spec.ts | 5 +++++ 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/pxtblocks/plugins/colorpicker/colorPickerBlock.ts b/pxtblocks/plugins/colorpicker/colorPickerBlock.ts index b7af7bbe6a3c..aad852f61ae7 100644 --- a/pxtblocks/plugins/colorpicker/colorPickerBlock.ts +++ b/pxtblocks/plugins/colorpicker/colorPickerBlock.ts @@ -34,7 +34,7 @@ export function initColorPickerBlock() { this.setInputsInline(true); this.appendDummyInput() - .appendField(new Blockly.FieldImage(previewImage(), 24, 24, lf("Choose color"), () => { + .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); @@ -67,14 +67,17 @@ export function initColorPickerBlock() { updateColorPreview: function (this: ColorPickerBlock) { const preview = this.getField("PREVIEW") as Blockly.FieldImage; const color = getColorPickerColor(this); - const image = previewImage(color); // This is derived UI, not an edit: don't add history or invalidate redo. Blockly.Events.disable(); try { - if (preview.getValue() !== image) { - preview.setValue(image); + if (color) { + const image = previewImage(color); + if (preview.getValue() !== image) { + preview.setValue(image); + } + preview.setAlt(lf("Color {0}. Choose color", color)); } - preview.setAlt(color ? lf("Color {0}. Choose color", color) : lf("Color depends on input values")); + preview.setVisible(!!color); } finally { Blockly.Events.enable(); @@ -286,9 +289,7 @@ export function getColorPickerColor(block: Blockly.Block): string | undefined { return fromFormatToHex(format, values); } -function previewImage(color?: string): string { - const content = color - ? `` - : '?'; +function previewImage(color: string): string { + const content = ``; return "data:image/svg+xml," + encodeURIComponent(`${content}`); } \ No newline at end of file diff --git a/tests/blocklycompiler-test/test.spec.ts b/tests/blocklycompiler-test/test.spec.ts index 58d59ff7f84c..eb73cbb5a77a 100644 --- a/tests/blocklycompiler-test/test.spec.ts +++ b/tests/blocklycompiler-test/test.spec.ts @@ -408,7 +408,10 @@ describe("blockly compiler", function () { 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"); @@ -429,6 +432,8 @@ describe("blockly compiler", function () { const input = picker.getInput("INPUT0"); input.connection.setShadowDom(null); chai.assert.isUndefined(pxtblockly.getColorPickerColor(picker)); + picker.updateColorPreview(); + chai.assert.isFalse(preview.isVisible()); picker.dispose(); } } From 96b6dca6dfb91cd00a6d338b95e3b82f40875c5c Mon Sep 17 00:00:00 2001 From: Joey Wunderlich Date: Tue, 29 Sep 2026 12:01:48 -0700 Subject: [PATCH 08/11] undo the mutation based dragging --- pxtblocks/loader.ts | 2 +- .../plugins/colorpicker/colorPickerBlock.ts | 11 ++--- .../duplicateOnDrag/duplicateOnDrag.ts | 41 +++++++++++----- pxtblocks/toolbox.ts | 8 ++- pxtcompiler/emitter/decompiler.ts | 1 - tests/blocklycompiler-test/test.spec.ts | 49 ++++++++++++++++--- .../contributed_builtin_shadows.blocks | 2 +- 7 files changed, 79 insertions(+), 35 deletions(-) diff --git a/pxtblocks/loader.ts b/pxtblocks/loader.ts index 4b040a501c8f..1fe08525006e 100644 --- a/pxtblocks/loader.ts +++ b/pxtblocks/loader.ts @@ -110,7 +110,7 @@ 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(blockInfo); setArgumentReporterLocalizeFunction((arg, block) => { return localizeArgumentReporter(blockInfo, arg, block); diff --git a/pxtblocks/plugins/colorpicker/colorPickerBlock.ts b/pxtblocks/plugins/colorpicker/colorPickerBlock.ts index aad852f61ae7..1f246cdc6ba1 100644 --- a/pxtblocks/plugins/colorpicker/colorPickerBlock.ts +++ b/pxtblocks/plugins/colorpicker/colorPickerBlock.ts @@ -3,7 +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 { hasDuplicateShadowOnDrag, setDuplicateShadowOnDrag, setDuplicateOnDragStrategy, updateDuplicateOnDragState } from "../duplicateOnDrag"; +import { setDuplicateOnDragStrategy, updateDuplicateOnDragState } from "../duplicateOnDrag"; export interface ColorPickerBlock extends Blockly.Block { colorHSVLoaded: boolean; @@ -49,6 +49,8 @@ export function initColorPickerBlock() { 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; @@ -85,12 +87,6 @@ export function initColorPickerBlock() { }, domToMutation: function (this: ColorPickerBlock, xmlElement: Element) { - const duplicate = xmlElement.getAttribute("duplicateondrag") === "true"; - setDuplicateShadowOnDrag(this, duplicate); - if (duplicate) { - setDuplicateOnDragStrategy(this); - if (this instanceof Blockly.BlockSvg) updateDuplicateOnDragState(this); - } if (xmlElement.hasAttribute("hue") && xmlElement.hasAttribute("saturation") && xmlElement.hasAttribute("value")) { this.colorHSVLoaded = true; this.colorHSV = [ @@ -112,7 +108,6 @@ export function initColorPickerBlock() { mutationToDom: function () { const container = document.createElement("mutation"); - if (hasDuplicateShadowOnDrag(this)) container.setAttribute("duplicateondrag", "true"); if (this.colorHSVLoaded) { container.setAttribute("hue", this.colorHSV[0].toString()); container.setAttribute("saturation", this.colorHSV[1].toString()); diff --git a/pxtblocks/plugins/duplicateOnDrag/duplicateOnDrag.ts b/pxtblocks/plugins/duplicateOnDrag/duplicateOnDrag.ts index f7efe24b1abd..a39d5c73ab09 100644 --- a/pxtblocks/plugins/duplicateOnDrag/duplicateOnDrag.ts +++ b/pxtblocks/plugins/duplicateOnDrag/duplicateOnDrag.ts @@ -3,16 +3,6 @@ import { PathObject } from "../renderer/pathObject"; let draggableShadowAllowlist: string[]; let duplicateRefs: DuplicateOnDragRef[]; -const draggableShadowInstances = new WeakSet(); - -export function setDuplicateShadowOnDrag(block: Blockly.Block, enabled: boolean): void { - if (enabled) draggableShadowInstances.add(block); - else draggableShadowInstances.delete(block); -} - -export function hasDuplicateShadowOnDrag(block: Blockly.Block): boolean { - return draggableShadowInstances.has(block); -} interface DuplicateOnDragRef { parentBlockType: string; @@ -20,8 +10,34 @@ interface DuplicateOnDragRef { childBlockType?: string; } -export function setDraggableShadowBlocks(ids: string[]) { - draggableShadowAllowlist = ids; +/** Registers draggable shadows, scoping contributed built-ins to inputs that reference their contributor ID. */ +export function setDraggableShadowBlocks(info: pxtc.BlocksInfo) { + const contributedBuiltins: pxt.Map = {}; + draggableShadowAllowlist = info.blocks + .filter(fn => fn.attributes.duplicateShadowOnDrag) + .map(fn => { + if (fn.attributes.builtinBlockId) { + contributedBuiltins[fn.attributes.blockId] = fn.attributes.builtinBlockId; + return undefined; + } + return fn.attributes.blockId; + }) + .filter(id => !!id); + + if (!Object.keys(contributedBuiltins).length) return; + + info.blocks.forEach(fn => { + const parentBlockType = fn.attributes.builtinBlockId || fn.attributes.blockId; + if (!parentBlockType) return; + const comp = pxt.blocks.compileInfo(fn); + const parameters = comp.thisParameter ? [comp.thisParameter, ...comp.parameters] : comp.parameters; + parameters.forEach(parameter => { + const childBlockType = contributedBuiltins[parameter.shadowBlockId]; + if (childBlockType) { + setDuplicateOnDrag(parentBlockType, parameter.definitionName, childBlockType); + } + }); + }); } /** @@ -52,7 +68,6 @@ export function setDuplicateOnDrag(parentBlockType: string, inputName?: string, } export function isAllowlistedShadow(block: Blockly.Block) { - if (hasDuplicateShadowOnDrag(block)) return true; if (draggableShadowAllowlist) { if (draggableShadowAllowlist.indexOf(block.type) !== -1) { return true; diff --git a/pxtblocks/toolbox.ts b/pxtblocks/toolbox.ts index b71433eb6692..f1eb54f21dea 100644 --- a/pxtblocks/toolbox.ts +++ b/pxtblocks/toolbox.ts @@ -123,8 +123,7 @@ export function createShadowValue(info: pxtc.BlocksInfo, p: pxt.blocks.BlockPara defaultV !== undefined ? defaultV : defaults?.defaultValue, p.fieldOptions?.format || defaults?.fieldOptions?.format, contributor?.attributes.color, - true, - contributor?.attributes.duplicateShadowOnDrag + true ), shadow); return value; } @@ -609,14 +608,14 @@ function createBuiltinBlock(fn: pxtc.SymbolInfo, comp: pxt.blocks.BlockCompileIn const id = fn.attributes.builtinBlockId; if (id === COLOR_PICKER_BLOCK_TYPE) { const defaults = comp.parameters[0]; - return createColorPickerBlock(defaults?.defaultValue, defaults?.fieldOptions?.format, fn.attributes.color, isShadow, fn.attributes.duplicateShadowOnDrag); + 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, duplicateShadowOnDrag = false): HTMLElement { +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; @@ -626,7 +625,6 @@ function createColorPickerBlock(defaultValue: string, requestedFormat: string, b block.setAttribute("type", COLOR_PICKER_BLOCK_TYPE); const mutation = document.createElement("mutation"); if (blockColor) mutation.setAttribute("color", blockColor); - if (isShadow && duplicateShadowOnDrag) mutation.setAttribute("duplicateondrag", "true"); const hsv = fromHexToFormat("hsv", hex); ["hue", "saturation", "value"].forEach((name, index) => mutation.setAttribute(name, hsv[index].toString())); block.appendChild(mutation); diff --git a/pxtcompiler/emitter/decompiler.ts b/pxtcompiler/emitter/decompiler.ts index edae47869d0e..bef8aec96cfb 100644 --- a/pxtcompiler/emitter/decompiler.ts +++ b/pxtcompiler/emitter/decompiler.ts @@ -692,7 +692,6 @@ ${output}`; 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 (contributor?.attributes.duplicateShadowOnDrag) value.mutation.duplicateondrag = "true"; } 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 diff --git a/tests/blocklycompiler-test/test.spec.ts b/tests/blocklycompiler-test/test.spec.ts index eb73cbb5a77a..a2aa1beb9585 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, shouldDuplicateOnDrag } from "../../pxtblocks/plugins/duplicateOnDrag"; +import { DuplicateOnDragConnectionChecker, setDraggableShadowBlocks, shouldDuplicateOnDrag } from "../../pxtblocks/plugins/duplicateOnDrag"; import "./commentparsing.spec"; import "./fieldUserEnum.spec"; @@ -455,36 +455,73 @@ describe("blockly compiler", function () { }); const first = contributor("other_picker", "#008800", "rgb", "0xff0000"); const selected = contributor("test_color_picker", "#6554C0", "hex", "0x7f3fbf"); + selected.attributes.duplicateShadowOnDrag = true; + const parent = (id: string, shadowId: string): pxtc.SymbolInfo => { + const block = `Use %color=${shadowId}`; + return { + name: id, qName: `test.${id}`, namespace: "test", fileName: "test.ts", + kind: pxtc.SymbolKind.Function, retType: "void", + attributes: { + block, blockId: id, _def: pxtc.parseBlockDefinition(block), + paramDefl: {}, callingConvention: pxtc.ir.CallingConvention.Plain + }, + parameters: [{ name: "color", type: "number", description: "" }] + }; + }; + const selectedParent = parent("test_selected_color_parent", "test_color_picker"); + const otherParent = parent("test_other_color_parent", "other_picker"); const namedInfo = { - ...info, blocks: info.blocks.concat(first, selected), - blocksById: { ...info.blocksById, other_picker: first, test_color_picker: selected } + ...info, blocks: info.blocks.concat(first, selected, selectedParent, otherParent), + blocksById: { + ...info.blocksById, other_picker: first, test_color_picker: selected, + test_selected_color_parent: selectedParent, test_other_color_parent: otherParent + } }; 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(shadow.querySelector('field[name="FORMAT"]').textContent, "hex"); chai.assert.equal(shadow.querySelector('field[name="TEXT"]').textContent, "#7F3FBF"); - selected.attributes.duplicateShadowOnDrag = true; + setDraggableShadowBlocks(namedInfo); const marked = pxtblockly.createShadowValue(namedInfo, { definitionName: "color", actualName: "color", type: "number", shadowBlockId: "test_color_picker" }).firstElementChild; + Blockly.Blocks[selectedParent.attributes.blockId] = { + init: function () { this.appendValueInput("color"); } + }; + Blockly.Blocks[otherParent.attributes.blockId] = { + init: function () { this.appendValueInput("color"); } + }; const workspace = new Blockly.Workspace(); try { const picker = Blockly.Xml.domToBlock(marked, workspace); - const unmarked = Blockly.Xml.domToBlock(shadow, workspace); + const selectedParentBlock = workspace.newBlock(selectedParent.attributes.blockId); + selectedParentBlock.getInput("color").connection.connect(picker.outputConnection); + const unmarked = Blockly.Xml.domToBlock(otherShadow, workspace); + const otherParentBlock = workspace.newBlock(otherParent.attributes.blockId); + otherParentBlock.getInput("color").connection.connect(unmarked.outputConnection); const standalonePicker = Blockly.Xml.domToBlock(pxtblockly.createToolboxBlock(namedInfo, selected, pxt.blocks.compileInfo(selected)), workspace); + chai.assert.isFalse(marked.querySelector("mutation").hasAttribute("duplicateondrag")); chai.assert.isTrue(shouldDuplicateOnDrag(picker)); chai.assert.isFalse(shouldDuplicateOnDrag(unmarked)); chai.assert.isFalse(shouldDuplicateOnDrag(standalonePicker)); - const restored = Blockly.Xml.domToBlock(Blockly.Xml.blockToDom(picker) as Element, workspace); + const restored = Blockly.Xml.domToBlock(Blockly.Xml.blockToDom(picker, true) as Element, workspace); + const restoredParent = workspace.newBlock(selectedParent.attributes.blockId); + restoredParent.getInput("color").connection.connect(restored.outputConnection); chai.assert.isTrue(shouldDuplicateOnDrag(restored)); } finally { workspace.dispose(); + delete Blockly.Blocks[selectedParent.attributes.blockId]; + delete Blockly.Blocks[otherParent.attributes.blockId]; + setDraggableShadowBlocks(info); } }); diff --git a/tests/decompile-test/baselines/contributed_builtin_shadows.blocks b/tests/decompile-test/baselines/contributed_builtin_shadows.blocks index 07784552682b..fad148bb4dbc 100644 --- a/tests/decompile-test/baselines/contributed_builtin_shadows.blocks +++ b/tests/decompile-test/baselines/contributed_builtin_shadows.blocks @@ -27,7 +27,7 @@ - + hex From 10901679c7708f000e56d80911c61d5584bad036 Mon Sep 17 00:00:00 2001 From: Joey Wunderlich Date: Tue, 29 Sep 2026 13:16:36 -0700 Subject: [PATCH 09/11] also remove color from mutations --- pxtblocks/loader.ts | 2 + .../plugins/colorpicker/colorPickerBlock.ts | 65 ++++++++++++++++--- pxtblocks/toolbox.ts | 6 +- pxtcompiler/emitter/decompiler.ts | 12 ---- tests/blocklycompiler-test/test.spec.ts | 61 ++++++++++++++--- .../contributed_builtin_shadows.blocks | 2 - 6 files changed, 113 insertions(+), 35 deletions(-) diff --git a/pxtblocks/loader.ts b/pxtblocks/loader.ts index 1fe08525006e..9d2978f04be8 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 { setColorPickerBlockStyles } from "./plugins/colorpicker"; import { setDraggableShadowBlocks, setDuplicateOnDrag, setDuplicateOnDragStrategy } from "./plugins/duplicateOnDrag"; import { initCopyPaste } from "./copyPaste"; export { initCopyPaste } from "./copyPaste"; @@ -110,6 +111,7 @@ export function blockSymbol(type: string): pxtc.SymbolInfo { export function injectBlocks(blockInfo: pxtc.BlocksInfo): pxtc.SymbolInfo[] { cachedBlockInfo = blockInfo; + setColorPickerBlockStyles(blockInfo); setDraggableShadowBlocks(blockInfo); setArgumentReporterLocalizeFunction((arg, block) => { diff --git a/pxtblocks/plugins/colorpicker/colorPickerBlock.ts b/pxtblocks/plugins/colorpicker/colorPickerBlock.ts index 1f246cdc6ba1..e73a11fee902 100644 --- a/pxtblocks/plugins/colorpicker/colorPickerBlock.ts +++ b/pxtblocks/plugins/colorpicker/colorPickerBlock.ts @@ -13,6 +13,7 @@ export interface ColorPickerBlock extends Blockly.Block { setFormat: (format: string, prevFormat?: string) => void; readColorFromInputs: () => void; updateBeforeRender: () => void; + updateColorPickerStyle: () => void; updateColorPreview: () => void; } @@ -20,6 +21,47 @@ const HEX_INPUT_NAME = "HEX_INPUT"; export const COLOR_PICKER_BLOCK_TYPE = "makecode_color_picker"; +interface ColorPickerStyle { + parentBlockType: string; + inputName: string; + color: string; +} + +let colorPickerStyles: ColorPickerStyle[] = []; + +/** Registers contributed color-picker styles for the parent inputs that reference them. */ +export function setColorPickerBlockStyles(info: pxtc.BlocksInfo): void { + const contributorColors: pxt.Map = {}; + let defaultColor: string; + info.blocks.forEach(fn => { + if (fn.attributes.builtinBlockId === COLOR_PICKER_BLOCK_TYPE && fn.attributes.color) { + defaultColor = defaultColor || fn.attributes.color; + if (fn.attributes.blockId) { + contributorColors[fn.attributes.blockId] = fn.attributes.color; + } + } + }); + + colorPickerStyles = []; + info.blocks.forEach(fn => { + if (fn.attributes.builtinBlockId === COLOR_PICKER_BLOCK_TYPE) return; + const parentBlockType = fn.attributes.builtinBlockId || fn.attributes.blockId; + if (!parentBlockType) return; + const comp = pxt.blocks.compileInfo(fn); + const parameters = comp.thisParameter ? [comp.thisParameter, ...comp.parameters] : comp.parameters; + parameters.forEach(parameter => { + const color = contributorColors[parameter.shadowBlockId] + || (parameter.shadowBlockId === COLOR_PICKER_BLOCK_TYPE ? defaultColor : undefined); + if (color) { + colorPickerStyles.push({ + parentBlockType, + inputName: parameter.definitionName, + color + }); + } + }); + }); +} export function initColorPickerBlock() { Blockly.Blocks[COLOR_PICKER_BLOCK_TYPE] = { @@ -57,15 +99,30 @@ export function initColorPickerBlock() { 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.updateColorPickerStyle(); this.updateColorPreview(); } }); }, updateBeforeRender: function (this: ColorPickerBlock) { + this.updateColorPickerStyle(); this.updateColorPreview(); }, + updateColorPickerStyle: function (this: ColorPickerBlock) { + const targetConnection = this.outputConnection?.targetConnection; + const parentBlockType = targetConnection?.getSourceBlock().type; + const inputName = targetConnection?.getParentInput()?.name; + if (!parentBlockType || !inputName) return; + + const style = colorPickerStyles.find(candidate => + candidate.parentBlockType === parentBlockType && candidate.inputName === inputName); + if (style && this.getColour().toLowerCase() !== style.color.toLowerCase()) { + this.setColour(style.color); + } + }, + updateColorPreview: function (this: ColorPickerBlock) { const preview = this.getField("PREVIEW") as Blockly.FieldImage; const color = getColorPickerColor(this); @@ -97,13 +154,6 @@ export function initColorPickerBlock() { this.setFormat(this.getFieldValue("FORMAT")); } - - if (xmlElement.hasAttribute("color")) { - const color = xmlElement.getAttribute("color"); - if (color) { - this.setColour(color); - } - } }, mutationToDom: function () { @@ -113,7 +163,6 @@ export function initColorPickerBlock() { container.setAttribute("saturation", this.colorHSV[1].toString()); container.setAttribute("value", this.colorHSV[2].toString()); } - container.setAttribute("color", this.getColour()); return container; }, diff --git a/pxtblocks/toolbox.ts b/pxtblocks/toolbox.ts index f1eb54f21dea..db01d5361a82 100644 --- a/pxtblocks/toolbox.ts +++ b/pxtblocks/toolbox.ts @@ -122,7 +122,6 @@ export function createShadowValue(info: pxtc.BlocksInfo, p: pxt.blocks.BlockPara value.replaceChild(createColorPickerBlock( defaultV !== undefined ? defaultV : defaults?.defaultValue, p.fieldOptions?.format || defaults?.fieldOptions?.format, - contributor?.attributes.color, true ), shadow); return value; @@ -608,14 +607,14 @@ function createBuiltinBlock(fn: pxtc.SymbolInfo, comp: pxt.blocks.BlockCompileIn const id = fn.attributes.builtinBlockId; if (id === COLOR_PICKER_BLOCK_TYPE) { const defaults = comp.parameters[0]; - return createColorPickerBlock(defaults?.defaultValue, defaults?.fieldOptions?.format, fn.attributes.color, isShadow); + return createColorPickerBlock(defaults?.defaultValue, defaults?.fieldOptions?.format, isShadow); } pxt.warn(`Unsupported builtin block id: ${id}`); return undefined; } -function createColorPickerBlock(defaultValue: string, requestedFormat: string, blockColor: string, isShadow: boolean): HTMLElement { +function createColorPickerBlock(defaultValue: string, requestedFormat: 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; @@ -624,7 +623,6 @@ function createColorPickerBlock(defaultValue: string, requestedFormat: string, b 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); diff --git a/pxtcompiler/emitter/decompiler.ts b/pxtcompiler/emitter/decompiler.ts index bef8aec96cfb..c308e1b013f9 100644 --- a/pxtcompiler/emitter/decompiler.ts +++ b/pxtcompiler/emitter/decompiler.ts @@ -686,13 +686,6 @@ ${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; @@ -2024,11 +2017,6 @@ ${output}`; r.fields = [getField("FORMAT", info.qName.substring(info.qName.lastIndexOf(".") + 1))]; - const blockDef = blocksInfo.blocks.find(b => b.attributes.builtinBlockId === "makecode_color_picker"); - if (blockDef && blockDef.attributes.color) { - r.mutation = { color: blockDef.attributes.color }; - } - return r; } else if (pxt.Util.startsWith(info.qName, "Math.")) { diff --git a/tests/blocklycompiler-test/test.spec.ts b/tests/blocklycompiler-test/test.spec.ts index a2aa1beb9585..1231c3d739d7 100644 --- a/tests/blocklycompiler-test/test.spec.ts +++ b/tests/blocklycompiler-test/test.spec.ts @@ -393,29 +393,58 @@ 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 contributor = { + name: "test_color_picker", namespace: "test", fileName: "test.ts", + kind: pxtc.SymbolKind.Function, retType: "number", + attributes: { + blockId: "test_color_picker", builtinBlockId: "makecode_color_picker", color: "#6554C0", + paramDefl: {}, callingConvention: pxtc.ir.CallingConvention.Plain + }, + parameters: [{ name: "value", type: "number", description: "" }] + } as pxtc.SymbolInfo; + const block = "Use %color=test_color_picker"; + const parent = { + name: "test_color_parent", qName: "test.test_color_parent", namespace: "test", fileName: "test.ts", + kind: pxtc.SymbolKind.Function, retType: "void", + attributes: { + block, blockId: "test_color_parent", _def: pxtc.parseBlockDefinition(block), + paramDefl: {}, callingConvention: pxtc.ir.CallingConvention.Plain + }, + parameters: [{ name: "color", type: "number", description: "" }] + } as pxtc.SymbolInfo; const coloredInfo = { ...info, - blocks: info.blocks.concat({ - attributes: { builtinBlockId: "makecode_color_picker", color: "#6554C0" } - } as pxtc.SymbolInfo) + blocks: info.blocks.concat(contributor, parent), + blocksById: { + ...info.blocksById, + test_color_picker: contributor, + test_color_parent: parent + } + }; + pxtblockly.setColorPickerBlockStyles(coloredInfo); + Blockly.Blocks[parent.attributes.blockId] = { + init: function () { this.appendValueInput("color"); } }; 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", + shadowBlockId: "test_color_picker", defaultValue: "0x7f3fbf", fieldOptions: { format } }); const picker = Blockly.Xml.domToBlock(value.firstElementChild, workspace) as pxtblockly.ColorPickerBlock; + const parentBlock = workspace.newBlock(parent.attributes.blockId); + parentBlock.getInput("color").connection.connect(picker.outputConnection); const preview = picker.getField("PREVIEW"); - picker.updateColorPreview(); + picker.updateBeforeRender(); 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.isFalse(mutation.hasAttribute("color")); mutation.removeAttribute("hue"); mutation.removeAttribute("saturation"); mutation.removeAttribute("value"); @@ -435,10 +464,13 @@ describe("blockly compiler", function () { picker.updateColorPreview(); chai.assert.isFalse(preview.isVisible()); picker.dispose(); + parentBlock.dispose(); } } finally { workspace.dispose(); + delete Blockly.Blocks[parent.attributes.blockId]; + pxtblockly.setColorPickerBlockStyles(info); } }); @@ -486,9 +518,10 @@ describe("blockly compiler", function () { 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.isFalse(shadow.querySelector("mutation").hasAttribute("color")); chai.assert.equal(shadow.querySelector('field[name="FORMAT"]').textContent, "hex"); chai.assert.equal(shadow.querySelector('field[name="TEXT"]').textContent, "#7F3FBF"); + pxtblockly.setColorPickerBlockStyles(namedInfo); setDraggableShadowBlocks(namedInfo); const marked = pxtblockly.createShadowValue(namedInfo, { definitionName: "color", actualName: "color", type: "number", shadowBlockId: "test_color_picker" @@ -501,26 +534,36 @@ describe("blockly compiler", function () { }; const workspace = new Blockly.Workspace(); try { - const picker = Blockly.Xml.domToBlock(marked, workspace); + const picker = Blockly.Xml.domToBlock(marked, workspace) as pxtblockly.ColorPickerBlock; const selectedParentBlock = workspace.newBlock(selectedParent.attributes.blockId); selectedParentBlock.getInput("color").connection.connect(picker.outputConnection); - const unmarked = Blockly.Xml.domToBlock(otherShadow, workspace); + const unmarked = Blockly.Xml.domToBlock(otherShadow, workspace) as pxtblockly.ColorPickerBlock; const otherParentBlock = workspace.newBlock(otherParent.attributes.blockId); otherParentBlock.getInput("color").connection.connect(unmarked.outputConnection); const standalonePicker = Blockly.Xml.domToBlock(pxtblockly.createToolboxBlock(namedInfo, selected, pxt.blocks.compileInfo(selected)), workspace); + picker.updateBeforeRender(); + unmarked.updateBeforeRender(); chai.assert.isFalse(marked.querySelector("mutation").hasAttribute("duplicateondrag")); + chai.assert.equal(picker.getColour().toLowerCase(), "#6554c0"); + chai.assert.equal(unmarked.getColour().toLowerCase(), "#008800"); + chai.assert.notEqual(standalonePicker.getColour().toLowerCase(), "#6554c0"); chai.assert.isTrue(shouldDuplicateOnDrag(picker)); chai.assert.isFalse(shouldDuplicateOnDrag(unmarked)); chai.assert.isFalse(shouldDuplicateOnDrag(standalonePicker)); - const restored = Blockly.Xml.domToBlock(Blockly.Xml.blockToDom(picker, true) as Element, workspace); + const serialized = Blockly.Xml.blockToDom(picker, true) as Element; + chai.assert.isFalse(serialized.querySelector("mutation").hasAttribute("color")); + const restored = Blockly.Xml.domToBlock(serialized, workspace); const restoredParent = workspace.newBlock(selectedParent.attributes.blockId); restoredParent.getInput("color").connection.connect(restored.outputConnection); + (restored as pxtblockly.ColorPickerBlock).updateBeforeRender(); + chai.assert.equal(restored.getColour().toLowerCase(), "#6554c0"); chai.assert.isTrue(shouldDuplicateOnDrag(restored)); } finally { workspace.dispose(); delete Blockly.Blocks[selectedParent.attributes.blockId]; delete Blockly.Blocks[otherParent.attributes.blockId]; + pxtblockly.setColorPickerBlockStyles(info); setDraggableShadowBlocks(info); } }); diff --git a/tests/decompile-test/baselines/contributed_builtin_shadows.blocks b/tests/decompile-test/baselines/contributed_builtin_shadows.blocks index fad148bb4dbc..af525b637182 100644 --- a/tests/decompile-test/baselines/contributed_builtin_shadows.blocks +++ b/tests/decompile-test/baselines/contributed_builtin_shadows.blocks @@ -6,7 +6,6 @@ 0 - hex @@ -27,7 +26,6 @@ - hex From fa79f36a86192c343b4ead8d0023750a7ef5feb6 Mon Sep 17 00:00:00 2001 From: Joey Wunderlich Date: Tue, 29 Sep 2026 13:55:04 -0700 Subject: [PATCH 10/11] fix draggability, missed how early gesture needed to be captured --- pxtblocks/monkeyPatches/gesture.ts | 6 +- .../duplicateOnDrag/connectionChecker.ts | 4 +- .../plugins/duplicateOnDrag/dragStrategy.ts | 6 +- tests/blocklycompiler-test/test.spec.ts | 58 ++++++++++++++++++- 4 files changed, 63 insertions(+), 11 deletions(-) diff --git a/pxtblocks/monkeyPatches/gesture.ts b/pxtblocks/monkeyPatches/gesture.ts index e7c138be3379..351d7281c56e 100644 --- a/pxtblocks/monkeyPatches/gesture.ts +++ b/pxtblocks/monkeyPatches/gesture.ts @@ -1,5 +1,5 @@ import * as Blockly from "blockly"; -import { isAllowlistedShadow } from "../plugins/duplicateOnDrag/duplicateOnDrag"; +import { shouldDuplicateOnDrag } from "../plugins/duplicateOnDrag/duplicateOnDrag"; import { assertMethod } from "./util"; interface PatchedGesture extends Blockly.Gesture { @@ -8,7 +8,7 @@ interface PatchedGesture extends Blockly.Gesture { } /** - * Make allowlisted shadow blocks (marked `duplicateShadowOnDrag`) the drag + * Make duplicate-on-drag shadow blocks the drag * target rather than their parent. Blockly's default walks up to the nearest * non-shadow ancestor; we want the shadow itself so the duplicate-on-drag * strategy can extract it and refill the parent slot via setShadowDom. @@ -18,7 +18,7 @@ export function monkeyPatchShadowDragTargetBlock() { assertMethod(proto, "setTargetBlock"); const origSetTargetBlock = proto.setTargetBlock; proto.setTargetBlock = function (block: Blockly.BlockSvg) { - if (block.isShadow() && isAllowlistedShadow(block)) { + if (block.isShadow() && shouldDuplicateOnDrag(block)) { this.targetBlock = block; block.bringToFront(); Blockly.getFocusManager().focusNode(block); diff --git a/pxtblocks/plugins/duplicateOnDrag/connectionChecker.ts b/pxtblocks/plugins/duplicateOnDrag/connectionChecker.ts index a11727050758..6f34d6bd22f7 100644 --- a/pxtblocks/plugins/duplicateOnDrag/connectionChecker.ts +++ b/pxtblocks/plugins/duplicateOnDrag/connectionChecker.ts @@ -1,5 +1,5 @@ import * as Blockly from "blockly"; -import { isAllowlistedShadow, shouldDuplicateOnDrag } from "./duplicateOnDrag"; +import { shouldDuplicateOnDrag } from "./duplicateOnDrag"; import { doArgumentReporterDragChecks } from "../functions/utils"; import { FUNCTION_DEFINITION_BLOCK_TYPE } from "../functions/constants"; @@ -21,7 +21,7 @@ export class DuplicateOnDragConnectionChecker extends Blockly.ConnectionChecker if ( replacedBlock && shouldDuplicateOnDrag(replacedBlock) && - !(replacedBlock.isShadow() && isAllowlistedShadow(replacedBlock)) + !replacedBlock.isShadow() ) { return false; } diff --git a/pxtblocks/plugins/duplicateOnDrag/dragStrategy.ts b/pxtblocks/plugins/duplicateOnDrag/dragStrategy.ts index f8645d42f26b..2d0c3917d0ca 100644 --- a/pxtblocks/plugins/duplicateOnDrag/dragStrategy.ts +++ b/pxtblocks/plugins/duplicateOnDrag/dragStrategy.ts @@ -5,7 +5,7 @@ */ import * as Blockly from "blockly"; -import { isAllowlistedShadow, shouldDuplicateOnDrag, updateDuplicateOnDragState } from "./duplicateOnDrag"; +import { shouldDuplicateOnDrag, updateDuplicateOnDragState } from "./duplicateOnDrag"; interface DragStrategyInternals { block: Blockly.BlockSvg; @@ -16,9 +16,9 @@ interface DragStrategyInternals { export class DuplicateOnDragStrategy extends Blockly.dragging.BlockDragStrategy { protected getTargetBlock(): Blockly.BlockSvg { const self = this as unknown as DragStrategyInternals; - // Keep the drag on an allowlisted shadow so disconnectBlock can extract + // Keep the drag on a duplicating shadow so disconnectBlock can extract // it; otherwise Blockly's default would delegate the drag to the parent. - if (self.block.isShadow() && isAllowlistedShadow(self.block)) { + if (self.block.isShadow() && shouldDuplicateOnDrag(self.block)) { return self.block; } return super.getTargetBlock(); diff --git a/tests/blocklycompiler-test/test.spec.ts b/tests/blocklycompiler-test/test.spec.ts index 1231c3d739d7..9e8b404f5adf 100644 --- a/tests/blocklycompiler-test/test.spec.ts +++ b/tests/blocklycompiler-test/test.spec.ts @@ -3,13 +3,24 @@ import * as Blockly from "blockly"; import * as pxtblockly from "../../pxtblocks"; -import { DuplicateOnDragConnectionChecker, setDraggableShadowBlocks, shouldDuplicateOnDrag } from "../../pxtblocks/plugins/duplicateOnDrag"; +import { monkeyPatchShadowDragTargetBlock } from "../../pxtblocks/monkeyPatches/gesture"; +import { DuplicateOnDragConnectionChecker, DuplicateOnDragStrategy, setDraggableShadowBlocks, shouldDuplicateOnDrag } from "../../pxtblocks/plugins/duplicateOnDrag"; import "./commentparsing.spec"; import "./fieldUserEnum.spec"; const WEB_PREFIX = "http://localhost:9876"; +class TestDuplicateOnDragStrategy extends DuplicateOnDragStrategy { + public getTargetBlockForTest(): Blockly.BlockSvg { + return this.getTargetBlock(); + } +} + +interface TestGestureInternals { + targetBlock: Blockly.BlockSvg; +} + // Blockly crashes if this isn't defined (Blockly as any).Msg.DELETE_VARIABLE = "Delete the '%1' variable"; @@ -534,10 +545,10 @@ describe("blockly compiler", function () { }; const workspace = new Blockly.Workspace(); try { - const picker = Blockly.Xml.domToBlock(marked, workspace) as pxtblockly.ColorPickerBlock; + const picker = Blockly.Xml.domToBlock(marked, workspace) as pxtblockly.ColorPickerBlock & Blockly.BlockSvg; const selectedParentBlock = workspace.newBlock(selectedParent.attributes.blockId); selectedParentBlock.getInput("color").connection.connect(picker.outputConnection); - const unmarked = Blockly.Xml.domToBlock(otherShadow, workspace) as pxtblockly.ColorPickerBlock; + const unmarked = Blockly.Xml.domToBlock(otherShadow, workspace) as pxtblockly.ColorPickerBlock & Blockly.BlockSvg; const otherParentBlock = workspace.newBlock(otherParent.attributes.blockId); otherParentBlock.getInput("color").connection.connect(unmarked.outputConnection); const standalonePicker = Blockly.Xml.domToBlock(pxtblockly.createToolboxBlock(namedInfo, selected, pxt.blocks.compileInfo(selected)), workspace); @@ -550,6 +561,47 @@ describe("blockly compiler", function () { chai.assert.isTrue(shouldDuplicateOnDrag(picker)); chai.assert.isFalse(shouldDuplicateOnDrag(unmarked)); chai.assert.isFalse(shouldDuplicateOnDrag(standalonePicker)); + const strategy = new TestDuplicateOnDragStrategy(picker); + chai.assert.equal(strategy.getTargetBlockForTest(), picker); + const unmarkedStrategy = new TestDuplicateOnDragStrategy(unmarked); + chai.assert.equal(unmarkedStrategy.getTargetBlockForTest(), otherParentBlock); + + const container = document.createElement("div"); + container.style.width = "800px"; + container.style.height = "600px"; + document.body.appendChild(container); + monkeyPatchShadowDragTargetBlock(); + const dragWorkspace = Blockly.inject(container, { renderer: "pxt" }); + try { + const withShadow = (parentType: string, shadowXml: Element) => { + const parentXml = document.createElement("block"); + parentXml.setAttribute("type", parentType); + const valueXml = document.createElement("value"); + valueXml.setAttribute("name", "color"); + valueXml.appendChild(shadowXml.cloneNode(true)); + parentXml.appendChild(valueXml); + return Blockly.Xml.domToBlock(parentXml, dragWorkspace) as Blockly.BlockSvg; + }; + const dragParent = withShadow(selectedParent.attributes.blockId, marked); + const dragPicker = dragParent.getInputTargetBlock("color") as Blockly.BlockSvg; + const event = new PointerEvent("pointerdown", { pointerId: 1 }); + const gesture = new Blockly.Gesture(event, dragWorkspace); + gesture.setStartBlock(dragPicker); + chai.assert.equal((gesture as unknown as TestGestureInternals).targetBlock, dragPicker); + gesture.dispose(); + + const otherDragParent = withShadow(otherParent.attributes.blockId, otherShadow); + const otherDragPicker = otherDragParent.getInputTargetBlock("color") as Blockly.BlockSvg; + const otherGesture = new Blockly.Gesture(event, dragWorkspace); + otherGesture.setStartBlock(otherDragPicker); + chai.assert.equal((otherGesture as unknown as TestGestureInternals).targetBlock, otherDragParent); + otherGesture.dispose(); + } + finally { + dragWorkspace.dispose(); + container.remove(); + } + const serialized = Blockly.Xml.blockToDom(picker, true) as Element; chai.assert.isFalse(serialized.querySelector("mutation").hasAttribute("color")); const restored = Blockly.Xml.domToBlock(serialized, workspace); From 40b09ae39dbb84af7ee7e1ff66b9ed2f36a612ae Mon Sep 17 00:00:00 2001 From: Joey Wunderlich Date: Tue, 29 Sep 2026 15:54:17 -0700 Subject: [PATCH 11/11] roll back mutation changes, and most of draggable changes --- pxtblocks/loader.ts | 8 +- pxtblocks/monkeyPatches/gesture.ts | 6 +- .../plugins/colorpicker/colorPickerBlock.ts | 65 +------ .../duplicateOnDrag/connectionChecker.ts | 4 +- .../plugins/duplicateOnDrag/dragStrategy.ts | 6 +- .../duplicateOnDrag/duplicateOnDrag.ts | 30 +--- pxtblocks/toolbox.ts | 6 +- pxtcompiler/emitter/decompiler.ts | 12 ++ tests/blocklycompiler-test/test.spec.ts | 160 ++---------------- .../contributed_builtin_shadows.blocks | 2 + .../decompile-test/cases/testBlocks/basic.ts | 2 +- 11 files changed, 58 insertions(+), 243 deletions(-) diff --git a/pxtblocks/loader.ts b/pxtblocks/loader.ts index 9d2978f04be8..216496aee8bc 100644 --- a/pxtblocks/loader.ts +++ b/pxtblocks/loader.ts @@ -23,7 +23,7 @@ import { initOnStart } from "./builtins/misc"; import { initContextMenu } from "./contextMenu"; import { renderCodeCard } from "./codecardRenderer"; import { FieldDropdown } from "./fields/field_dropdown"; -import { setColorPickerBlockStyles } from "./plugins/colorpicker"; +import { COLOR_PICKER_BLOCK_TYPE } from "./plugins/colorpicker"; import { setDraggableShadowBlocks, setDuplicateOnDrag, setDuplicateOnDragStrategy } from "./plugins/duplicateOnDrag"; import { initCopyPaste } from "./copyPaste"; export { initCopyPaste } from "./copyPaste"; @@ -111,8 +111,10 @@ export function blockSymbol(type: string): pxtc.SymbolInfo { export function injectBlocks(blockInfo: pxtc.BlocksInfo): pxtc.SymbolInfo[] { cachedBlockInfo = blockInfo; - setColorPickerBlockStyles(blockInfo); - setDraggableShadowBlocks(blockInfo); + 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/monkeyPatches/gesture.ts b/pxtblocks/monkeyPatches/gesture.ts index 351d7281c56e..e7c138be3379 100644 --- a/pxtblocks/monkeyPatches/gesture.ts +++ b/pxtblocks/monkeyPatches/gesture.ts @@ -1,5 +1,5 @@ import * as Blockly from "blockly"; -import { shouldDuplicateOnDrag } from "../plugins/duplicateOnDrag/duplicateOnDrag"; +import { isAllowlistedShadow } from "../plugins/duplicateOnDrag/duplicateOnDrag"; import { assertMethod } from "./util"; interface PatchedGesture extends Blockly.Gesture { @@ -8,7 +8,7 @@ interface PatchedGesture extends Blockly.Gesture { } /** - * Make duplicate-on-drag shadow blocks the drag + * Make allowlisted shadow blocks (marked `duplicateShadowOnDrag`) the drag * target rather than their parent. Blockly's default walks up to the nearest * non-shadow ancestor; we want the shadow itself so the duplicate-on-drag * strategy can extract it and refill the parent slot via setShadowDom. @@ -18,7 +18,7 @@ export function monkeyPatchShadowDragTargetBlock() { assertMethod(proto, "setTargetBlock"); const origSetTargetBlock = proto.setTargetBlock; proto.setTargetBlock = function (block: Blockly.BlockSvg) { - if (block.isShadow() && shouldDuplicateOnDrag(block)) { + if (block.isShadow() && isAllowlistedShadow(block)) { this.targetBlock = block; block.bringToFront(); Blockly.getFocusManager().focusNode(block); diff --git a/pxtblocks/plugins/colorpicker/colorPickerBlock.ts b/pxtblocks/plugins/colorpicker/colorPickerBlock.ts index e73a11fee902..1f246cdc6ba1 100644 --- a/pxtblocks/plugins/colorpicker/colorPickerBlock.ts +++ b/pxtblocks/plugins/colorpicker/colorPickerBlock.ts @@ -13,7 +13,6 @@ export interface ColorPickerBlock extends Blockly.Block { setFormat: (format: string, prevFormat?: string) => void; readColorFromInputs: () => void; updateBeforeRender: () => void; - updateColorPickerStyle: () => void; updateColorPreview: () => void; } @@ -21,47 +20,6 @@ const HEX_INPUT_NAME = "HEX_INPUT"; export const COLOR_PICKER_BLOCK_TYPE = "makecode_color_picker"; -interface ColorPickerStyle { - parentBlockType: string; - inputName: string; - color: string; -} - -let colorPickerStyles: ColorPickerStyle[] = []; - -/** Registers contributed color-picker styles for the parent inputs that reference them. */ -export function setColorPickerBlockStyles(info: pxtc.BlocksInfo): void { - const contributorColors: pxt.Map = {}; - let defaultColor: string; - info.blocks.forEach(fn => { - if (fn.attributes.builtinBlockId === COLOR_PICKER_BLOCK_TYPE && fn.attributes.color) { - defaultColor = defaultColor || fn.attributes.color; - if (fn.attributes.blockId) { - contributorColors[fn.attributes.blockId] = fn.attributes.color; - } - } - }); - - colorPickerStyles = []; - info.blocks.forEach(fn => { - if (fn.attributes.builtinBlockId === COLOR_PICKER_BLOCK_TYPE) return; - const parentBlockType = fn.attributes.builtinBlockId || fn.attributes.blockId; - if (!parentBlockType) return; - const comp = pxt.blocks.compileInfo(fn); - const parameters = comp.thisParameter ? [comp.thisParameter, ...comp.parameters] : comp.parameters; - parameters.forEach(parameter => { - const color = contributorColors[parameter.shadowBlockId] - || (parameter.shadowBlockId === COLOR_PICKER_BLOCK_TYPE ? defaultColor : undefined); - if (color) { - colorPickerStyles.push({ - parentBlockType, - inputName: parameter.definitionName, - color - }); - } - }); - }); -} export function initColorPickerBlock() { Blockly.Blocks[COLOR_PICKER_BLOCK_TYPE] = { @@ -99,30 +57,15 @@ export function initColorPickerBlock() { 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.updateColorPickerStyle(); this.updateColorPreview(); } }); }, updateBeforeRender: function (this: ColorPickerBlock) { - this.updateColorPickerStyle(); this.updateColorPreview(); }, - updateColorPickerStyle: function (this: ColorPickerBlock) { - const targetConnection = this.outputConnection?.targetConnection; - const parentBlockType = targetConnection?.getSourceBlock().type; - const inputName = targetConnection?.getParentInput()?.name; - if (!parentBlockType || !inputName) return; - - const style = colorPickerStyles.find(candidate => - candidate.parentBlockType === parentBlockType && candidate.inputName === inputName); - if (style && this.getColour().toLowerCase() !== style.color.toLowerCase()) { - this.setColour(style.color); - } - }, - updateColorPreview: function (this: ColorPickerBlock) { const preview = this.getField("PREVIEW") as Blockly.FieldImage; const color = getColorPickerColor(this); @@ -154,6 +97,13 @@ export function initColorPickerBlock() { this.setFormat(this.getFieldValue("FORMAT")); } + + if (xmlElement.hasAttribute("color")) { + const color = xmlElement.getAttribute("color"); + if (color) { + this.setColour(color); + } + } }, mutationToDom: function () { @@ -163,6 +113,7 @@ export function initColorPickerBlock() { container.setAttribute("saturation", this.colorHSV[1].toString()); container.setAttribute("value", this.colorHSV[2].toString()); } + container.setAttribute("color", this.getColour()); return container; }, diff --git a/pxtblocks/plugins/duplicateOnDrag/connectionChecker.ts b/pxtblocks/plugins/duplicateOnDrag/connectionChecker.ts index 6f34d6bd22f7..a11727050758 100644 --- a/pxtblocks/plugins/duplicateOnDrag/connectionChecker.ts +++ b/pxtblocks/plugins/duplicateOnDrag/connectionChecker.ts @@ -1,5 +1,5 @@ import * as Blockly from "blockly"; -import { shouldDuplicateOnDrag } from "./duplicateOnDrag"; +import { isAllowlistedShadow, shouldDuplicateOnDrag } from "./duplicateOnDrag"; import { doArgumentReporterDragChecks } from "../functions/utils"; import { FUNCTION_DEFINITION_BLOCK_TYPE } from "../functions/constants"; @@ -21,7 +21,7 @@ export class DuplicateOnDragConnectionChecker extends Blockly.ConnectionChecker if ( replacedBlock && shouldDuplicateOnDrag(replacedBlock) && - !replacedBlock.isShadow() + !(replacedBlock.isShadow() && isAllowlistedShadow(replacedBlock)) ) { return false; } diff --git a/pxtblocks/plugins/duplicateOnDrag/dragStrategy.ts b/pxtblocks/plugins/duplicateOnDrag/dragStrategy.ts index 2d0c3917d0ca..f8645d42f26b 100644 --- a/pxtblocks/plugins/duplicateOnDrag/dragStrategy.ts +++ b/pxtblocks/plugins/duplicateOnDrag/dragStrategy.ts @@ -5,7 +5,7 @@ */ import * as Blockly from "blockly"; -import { shouldDuplicateOnDrag, updateDuplicateOnDragState } from "./duplicateOnDrag"; +import { isAllowlistedShadow, shouldDuplicateOnDrag, updateDuplicateOnDragState } from "./duplicateOnDrag"; interface DragStrategyInternals { block: Blockly.BlockSvg; @@ -16,9 +16,9 @@ interface DragStrategyInternals { export class DuplicateOnDragStrategy extends Blockly.dragging.BlockDragStrategy { protected getTargetBlock(): Blockly.BlockSvg { const self = this as unknown as DragStrategyInternals; - // Keep the drag on a duplicating shadow so disconnectBlock can extract + // Keep the drag on an allowlisted shadow so disconnectBlock can extract // it; otherwise Blockly's default would delegate the drag to the parent. - if (self.block.isShadow() && shouldDuplicateOnDrag(self.block)) { + if (self.block.isShadow() && isAllowlistedShadow(self.block)) { return self.block; } return super.getTargetBlock(); diff --git a/pxtblocks/plugins/duplicateOnDrag/duplicateOnDrag.ts b/pxtblocks/plugins/duplicateOnDrag/duplicateOnDrag.ts index a39d5c73ab09..4e92607a8635 100644 --- a/pxtblocks/plugins/duplicateOnDrag/duplicateOnDrag.ts +++ b/pxtblocks/plugins/duplicateOnDrag/duplicateOnDrag.ts @@ -10,34 +10,8 @@ interface DuplicateOnDragRef { childBlockType?: string; } -/** Registers draggable shadows, scoping contributed built-ins to inputs that reference their contributor ID. */ -export function setDraggableShadowBlocks(info: pxtc.BlocksInfo) { - const contributedBuiltins: pxt.Map = {}; - draggableShadowAllowlist = info.blocks - .filter(fn => fn.attributes.duplicateShadowOnDrag) - .map(fn => { - if (fn.attributes.builtinBlockId) { - contributedBuiltins[fn.attributes.blockId] = fn.attributes.builtinBlockId; - return undefined; - } - return fn.attributes.blockId; - }) - .filter(id => !!id); - - if (!Object.keys(contributedBuiltins).length) return; - - info.blocks.forEach(fn => { - const parentBlockType = fn.attributes.builtinBlockId || fn.attributes.blockId; - if (!parentBlockType) return; - const comp = pxt.blocks.compileInfo(fn); - const parameters = comp.thisParameter ? [comp.thisParameter, ...comp.parameters] : comp.parameters; - parameters.forEach(parameter => { - const childBlockType = contributedBuiltins[parameter.shadowBlockId]; - if (childBlockType) { - setDuplicateOnDrag(parentBlockType, parameter.definitionName, childBlockType); - } - }); - }); +export function setDraggableShadowBlocks(ids: string[]) { + draggableShadowAllowlist = ids; } /** diff --git a/pxtblocks/toolbox.ts b/pxtblocks/toolbox.ts index db01d5361a82..f1eb54f21dea 100644 --- a/pxtblocks/toolbox.ts +++ b/pxtblocks/toolbox.ts @@ -122,6 +122,7 @@ export function createShadowValue(info: pxtc.BlocksInfo, p: pxt.blocks.BlockPara value.replaceChild(createColorPickerBlock( defaultV !== undefined ? defaultV : defaults?.defaultValue, p.fieldOptions?.format || defaults?.fieldOptions?.format, + contributor?.attributes.color, true ), shadow); return value; @@ -607,14 +608,14 @@ function createBuiltinBlock(fn: pxtc.SymbolInfo, comp: pxt.blocks.BlockCompileIn const id = fn.attributes.builtinBlockId; if (id === COLOR_PICKER_BLOCK_TYPE) { const defaults = comp.parameters[0]; - return createColorPickerBlock(defaults?.defaultValue, defaults?.fieldOptions?.format, isShadow); + 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, isShadow: boolean): HTMLElement { +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; @@ -623,6 +624,7 @@ function createColorPickerBlock(defaultValue: string, requestedFormat: string, i 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); diff --git a/pxtcompiler/emitter/decompiler.ts b/pxtcompiler/emitter/decompiler.ts index c308e1b013f9..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; @@ -2017,6 +2024,11 @@ ${output}`; r.fields = [getField("FORMAT", info.qName.substring(info.qName.lastIndexOf(".") + 1))]; + const blockDef = blocksInfo.blocks.find(b => b.attributes.builtinBlockId === "makecode_color_picker"); + if (blockDef && blockDef.attributes.color) { + r.mutation = { color: blockDef.attributes.color }; + } + return r; } else if (pxt.Util.startsWith(info.qName, "Math.")) { diff --git a/tests/blocklycompiler-test/test.spec.ts b/tests/blocklycompiler-test/test.spec.ts index 9e8b404f5adf..2c7ed6fbaa75 100644 --- a/tests/blocklycompiler-test/test.spec.ts +++ b/tests/blocklycompiler-test/test.spec.ts @@ -3,24 +3,13 @@ import * as Blockly from "blockly"; import * as pxtblockly from "../../pxtblocks"; -import { monkeyPatchShadowDragTargetBlock } from "../../pxtblocks/monkeyPatches/gesture"; -import { DuplicateOnDragConnectionChecker, DuplicateOnDragStrategy, setDraggableShadowBlocks, shouldDuplicateOnDrag } from "../../pxtblocks/plugins/duplicateOnDrag"; +import { DuplicateOnDragConnectionChecker, shouldDuplicateOnDrag } from "../../pxtblocks/plugins/duplicateOnDrag"; import "./commentparsing.spec"; import "./fieldUserEnum.spec"; const WEB_PREFIX = "http://localhost:9876"; -class TestDuplicateOnDragStrategy extends DuplicateOnDragStrategy { - public getTargetBlockForTest(): Blockly.BlockSvg { - return this.getTargetBlock(); - } -} - -interface TestGestureInternals { - targetBlock: Blockly.BlockSvg; -} - // Blockly crashes if this isn't defined (Blockly as any).Msg.DELETE_VARIABLE = "Delete the '%1' variable"; @@ -404,58 +393,31 @@ 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 contributor = { - name: "test_color_picker", namespace: "test", fileName: "test.ts", - kind: pxtc.SymbolKind.Function, retType: "number", - attributes: { - blockId: "test_color_picker", builtinBlockId: "makecode_color_picker", color: "#6554C0", - paramDefl: {}, callingConvention: pxtc.ir.CallingConvention.Plain - }, - parameters: [{ name: "value", type: "number", description: "" }] - } as pxtc.SymbolInfo; - const block = "Use %color=test_color_picker"; - const parent = { - name: "test_color_parent", qName: "test.test_color_parent", namespace: "test", fileName: "test.ts", - kind: pxtc.SymbolKind.Function, retType: "void", - attributes: { - block, blockId: "test_color_parent", _def: pxtc.parseBlockDefinition(block), - paramDefl: {}, callingConvention: pxtc.ir.CallingConvention.Plain - }, - parameters: [{ name: "color", type: "number", description: "" }] - } as pxtc.SymbolInfo; const coloredInfo = { ...info, - blocks: info.blocks.concat(contributor, parent), - blocksById: { - ...info.blocksById, - test_color_picker: contributor, - test_color_parent: parent - } - }; - pxtblockly.setColorPickerBlockStyles(coloredInfo); - Blockly.Blocks[parent.attributes.blockId] = { - init: function () { this.appendValueInput("color"); } + 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: "test_color_picker", defaultValue: "0x7f3fbf", + shadowBlockId: "makecode_color_picker", defaultValue: "0x7f3fbf", fieldOptions: { format } }); const picker = Blockly.Xml.domToBlock(value.firstElementChild, workspace) as pxtblockly.ColorPickerBlock; - const parentBlock = workspace.newBlock(parent.attributes.blockId); - parentBlock.getInput("color").connection.connect(picker.outputConnection); const preview = picker.getField("PREVIEW"); - picker.updateBeforeRender(); + 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.isFalse(mutation.hasAttribute("color")); + chai.assert.equal(mutation.getAttribute("color"), "#6554C0"); + chai.assert.isTrue(shouldDuplicateOnDrag(picker)); mutation.removeAttribute("hue"); mutation.removeAttribute("saturation"); mutation.removeAttribute("value"); @@ -475,13 +437,10 @@ describe("blockly compiler", function () { picker.updateColorPreview(); chai.assert.isFalse(preview.isVisible()); picker.dispose(); - parentBlock.dispose(); } } finally { workspace.dispose(); - delete Blockly.Blocks[parent.attributes.blockId]; - pxtblockly.setColorPickerBlockStyles(info); } }); @@ -498,26 +457,10 @@ describe("blockly compiler", function () { }); const first = contributor("other_picker", "#008800", "rgb", "0xff0000"); const selected = contributor("test_color_picker", "#6554C0", "hex", "0x7f3fbf"); - selected.attributes.duplicateShadowOnDrag = true; - const parent = (id: string, shadowId: string): pxtc.SymbolInfo => { - const block = `Use %color=${shadowId}`; - return { - name: id, qName: `test.${id}`, namespace: "test", fileName: "test.ts", - kind: pxtc.SymbolKind.Function, retType: "void", - attributes: { - block, blockId: id, _def: pxtc.parseBlockDefinition(block), - paramDefl: {}, callingConvention: pxtc.ir.CallingConvention.Plain - }, - parameters: [{ name: "color", type: "number", description: "" }] - }; - }; - const selectedParent = parent("test_selected_color_parent", "test_color_picker"); - const otherParent = parent("test_other_color_parent", "other_picker"); const namedInfo = { - ...info, blocks: info.blocks.concat(first, selected, selectedParent, otherParent), + ...info, blocks: info.blocks.concat(first, selected), blocksById: { - ...info.blocksById, other_picker: first, test_color_picker: selected, - test_selected_color_parent: selectedParent, test_other_color_parent: otherParent + ...info.blocksById, other_picker: first, test_color_picker: selected } }; const shadow = pxtblockly.createShadowValue(namedInfo, { @@ -529,94 +472,23 @@ describe("blockly compiler", function () { 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.isFalse(shadow.querySelector("mutation").hasAttribute("color")); + 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"); - pxtblockly.setColorPickerBlockStyles(namedInfo); - setDraggableShadowBlocks(namedInfo); - const marked = pxtblockly.createShadowValue(namedInfo, { - definitionName: "color", actualName: "color", type: "number", shadowBlockId: "test_color_picker" - }).firstElementChild; - Blockly.Blocks[selectedParent.attributes.blockId] = { - init: function () { this.appendValueInput("color"); } - }; - Blockly.Blocks[otherParent.attributes.blockId] = { - init: function () { this.appendValueInput("color"); } - }; const workspace = new Blockly.Workspace(); try { - const picker = Blockly.Xml.domToBlock(marked, workspace) as pxtblockly.ColorPickerBlock & Blockly.BlockSvg; - const selectedParentBlock = workspace.newBlock(selectedParent.attributes.blockId); - selectedParentBlock.getInput("color").connection.connect(picker.outputConnection); - const unmarked = Blockly.Xml.domToBlock(otherShadow, workspace) as pxtblockly.ColorPickerBlock & Blockly.BlockSvg; - const otherParentBlock = workspace.newBlock(otherParent.attributes.blockId); - otherParentBlock.getInput("color").connection.connect(unmarked.outputConnection); + 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); - picker.updateBeforeRender(); - unmarked.updateBeforeRender(); - chai.assert.isFalse(marked.querySelector("mutation").hasAttribute("duplicateondrag")); - chai.assert.equal(picker.getColour().toLowerCase(), "#6554c0"); - chai.assert.equal(unmarked.getColour().toLowerCase(), "#008800"); - chai.assert.notEqual(standalonePicker.getColour().toLowerCase(), "#6554c0"); chai.assert.isTrue(shouldDuplicateOnDrag(picker)); - chai.assert.isFalse(shouldDuplicateOnDrag(unmarked)); + chai.assert.isTrue(shouldDuplicateOnDrag(unmarked)); chai.assert.isFalse(shouldDuplicateOnDrag(standalonePicker)); - const strategy = new TestDuplicateOnDragStrategy(picker); - chai.assert.equal(strategy.getTargetBlockForTest(), picker); - const unmarkedStrategy = new TestDuplicateOnDragStrategy(unmarked); - chai.assert.equal(unmarkedStrategy.getTargetBlockForTest(), otherParentBlock); - - const container = document.createElement("div"); - container.style.width = "800px"; - container.style.height = "600px"; - document.body.appendChild(container); - monkeyPatchShadowDragTargetBlock(); - const dragWorkspace = Blockly.inject(container, { renderer: "pxt" }); - try { - const withShadow = (parentType: string, shadowXml: Element) => { - const parentXml = document.createElement("block"); - parentXml.setAttribute("type", parentType); - const valueXml = document.createElement("value"); - valueXml.setAttribute("name", "color"); - valueXml.appendChild(shadowXml.cloneNode(true)); - parentXml.appendChild(valueXml); - return Blockly.Xml.domToBlock(parentXml, dragWorkspace) as Blockly.BlockSvg; - }; - const dragParent = withShadow(selectedParent.attributes.blockId, marked); - const dragPicker = dragParent.getInputTargetBlock("color") as Blockly.BlockSvg; - const event = new PointerEvent("pointerdown", { pointerId: 1 }); - const gesture = new Blockly.Gesture(event, dragWorkspace); - gesture.setStartBlock(dragPicker); - chai.assert.equal((gesture as unknown as TestGestureInternals).targetBlock, dragPicker); - gesture.dispose(); - - const otherDragParent = withShadow(otherParent.attributes.blockId, otherShadow); - const otherDragPicker = otherDragParent.getInputTargetBlock("color") as Blockly.BlockSvg; - const otherGesture = new Blockly.Gesture(event, dragWorkspace); - otherGesture.setStartBlock(otherDragPicker); - chai.assert.equal((otherGesture as unknown as TestGestureInternals).targetBlock, otherDragParent); - otherGesture.dispose(); - } - finally { - dragWorkspace.dispose(); - container.remove(); - } - - const serialized = Blockly.Xml.blockToDom(picker, true) as Element; - chai.assert.isFalse(serialized.querySelector("mutation").hasAttribute("color")); - const restored = Blockly.Xml.domToBlock(serialized, workspace); - const restoredParent = workspace.newBlock(selectedParent.attributes.blockId); - restoredParent.getInput("color").connection.connect(restored.outputConnection); - (restored as pxtblockly.ColorPickerBlock).updateBeforeRender(); - chai.assert.equal(restored.getColour().toLowerCase(), "#6554c0"); + const restored = Blockly.Xml.domToBlock(Blockly.Xml.blockToDom(picker, true) as Element, workspace); chai.assert.isTrue(shouldDuplicateOnDrag(restored)); } finally { workspace.dispose(); - delete Blockly.Blocks[selectedParent.attributes.blockId]; - delete Blockly.Blocks[otherParent.attributes.blockId]; - pxtblockly.setColorPickerBlockStyles(info); - setDraggableShadowBlocks(info); } }); diff --git a/tests/decompile-test/baselines/contributed_builtin_shadows.blocks b/tests/decompile-test/baselines/contributed_builtin_shadows.blocks index af525b637182..fad148bb4dbc 100644 --- a/tests/decompile-test/baselines/contributed_builtin_shadows.blocks +++ b/tests/decompile-test/baselines/contributed_builtin_shadows.blocks @@ -6,6 +6,7 @@ 0 + hex @@ -26,6 +27,7 @@ + hex diff --git a/tests/decompile-test/cases/testBlocks/basic.ts b/tests/decompile-test/cases/testBlocks/basic.ts index f13c467715a0..d01dffbdab5d 100644 --- a/tests/decompile-test/cases/testBlocks/basic.ts +++ b/tests/decompile-test/cases/testBlocks/basic.ts @@ -21,7 +21,7 @@ namespace testNamespace { export function colorArgument(color: number): void {} //% block blockId=test_color_picker builtinBlockId=makecode_color_picker - //% blockHidden=true duplicateShadowOnDrag=true + //% blockHidden=true //% value.fieldOptions.format=hex value.defl=0x7f3fbf //% color="#6554C0" export function __colorPicker(value: number): number {