Skip to content
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs/defining-blocks.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 3 additions & 1 deletion pxtblocks/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -110,7 +111,8 @@ 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));
setColorPickerBlockStyles(blockInfo);
setDraggableShadowBlocks(blockInfo);

setArgumentReporterLocalizeFunction((arg, block) => {
return localizeArgumentReporter(blockInfo, arg, block);
Expand Down
6 changes: 3 additions & 3 deletions pxtblocks/monkeyPatches/gesture.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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.
Expand All @@ -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);
Expand Down
144 changes: 135 additions & 9 deletions pxtblocks/plugins/colorpicker/colorPickerBlock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ColorDropdownField } from "./colorPickerDropdown";
import { ColorPickerNumberBlock, COLOR_NUMBER_BLOCK_TYPE, generateColorPickerNumberShadowDom } from "./colorPickerNumberBlock";
import { COLOR_STRING_BLOCK_TYPE, generateColorPickerStringShadowDom } from "./colorPickerStringBlock";
import { FieldColorPickerNumberType, fromFormatToHex, fromFormatToHSV, fromHexToFormat, fromHSVToFormat, getFieldTypesForFormat } from "./util";
import { setDuplicateOnDragStrategy, updateDuplicateOnDragState } from "../duplicateOnDrag";

export interface ColorPickerBlock extends Blockly.Block {
colorHSVLoaded: boolean;
Expand All @@ -11,12 +12,56 @@ export interface ColorPickerBlock extends Blockly.Block {
setColorHSV: (hsv: number[]) => void;
setFormat: (format: string, prevFormat?: string) => void;
readColorFromInputs: () => void;
updateBeforeRender: () => void;
updateColorPickerStyle: () => void;
updateColorPreview: () => void;
}

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<string> = {};
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] = {
Expand All @@ -31,10 +76,71 @@ export function initColorPickerBlock() {
this.setInputsInline(true);

this.appendDummyInput()
.appendField(new Blockly.FieldImage(previewImage("#000000"), 24, 24, lf("Choose color"), () => {
const inputs = [HEX_INPUT_NAME, "INPUT0", "INPUT1", "INPUT2", "INPUT3"];
for (const input of inputs) {
const child = this.getInputTargetBlock(input);
if (child?.type === COLOR_STRING_BLOCK_TYPE || child?.type === COLOR_NUMBER_BLOCK_TYPE) {
this.readColorFromInputs();
child.getField(input === HEX_INPUT_NAME ? "TEXT" : "NUM").showEditor();
return;
}
}
}), "PREVIEW")
.appendField(new ColorDropdownField("rgb"), "FORMAT");

this.updateShape(this.getFieldValue("FORMAT"));
this.setColorHSV(this.colorHSV);
setDuplicateOnDragStrategy(this);
if (this instanceof Blockly.BlockSvg) updateDuplicateOnDragState(this);
this.setOnChange((event: Blockly.Events.BlockBase) => {
if (event.type !== Blockly.Events.BLOCK_CHANGE && event.type !== Blockly.Events.BLOCK_MOVE
&& event.type !== Blockly.Events.BLOCK_CREATE) return;
const moved = event as Blockly.Events.BlockMove;
if (event.blockId === this.id || moved.oldParentId === this.id || moved.newParentId === this.id
|| this.getChildren(false).some(child => child.id === event.blockId)) {
this.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);
// This is derived UI, not an edit: don't add history or invalidate redo.
Blockly.Events.disable();
try {
if (color) {
const image = previewImage(color);
if (preview.getValue() !== image) {
preview.setValue(image);
}
preview.setAlt(lf("Color {0}. Choose color", color));
}
preview.setVisible(!!color);
}
finally {
Blockly.Events.enable();
}
},

domToMutation: function (this: ColorPickerBlock, xmlElement: Element) {
Expand All @@ -48,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 () {
Expand All @@ -64,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;
},
Expand Down Expand Up @@ -134,6 +232,7 @@ export function initColorPickerBlock() {
},

setColorHSV: function (this: ColorPickerBlock, hsv: number[]) {
this.colorHSV = hsv;
if (this.getFieldValue("FORMAT") === "hex") {
const color = fromFormatToHex("hsv", hsv);

Expand All @@ -144,6 +243,7 @@ export function initColorPickerBlock() {
const field = target.getField("TEXT");
field.setValue(color);
}
this.updateColorPreview();
return;
}

Expand All @@ -159,7 +259,7 @@ export function initColorPickerBlock() {
}
}

this.colorHSV = hsv;
this.updateColorPreview();
},

setFormat: function (this: ColorPickerBlock, format: string, prevFormat = format) {
Expand Down Expand Up @@ -210,4 +310,30 @@ export function initColorPickerBlock() {
this.colorHSV = fromFormatToHSV(this.getFieldValue("FORMAT"), newValues);
}
}
}

/** Evaluate literal inputs only; never guess a runtime value for variables or expressions. */
export function getColorPickerColor(block: Blockly.Block): string | undefined {
const format = block.getFieldValue("FORMAT");
if (format === "hex") {
const child = block.getInputTargetBlock(HEX_INPUT_NAME);
if (child?.type !== COLOR_STRING_BLOCK_TYPE && child?.type !== "text") return undefined;
const hex = child.getFieldValue("TEXT");
return /^#?(?:[0-9a-f]{3}|[0-9a-f]{6})$/i.test(hex)
? fromFormatToHex("hsv", fromHexToFormat("hsv", hex)) : undefined;
}
const values: number[] = [];
for (let i = 0; i < getFieldTypesForFormat(format).length; i++) {
const child = block.getInputTargetBlock("INPUT" + i);
if (child?.type !== COLOR_NUMBER_BLOCK_TYPE && child?.type !== "math_number") return undefined;
const value = Number(child.getFieldValue("NUM"));
if (!Number.isFinite(value)) return undefined;
values.push(value);
}
return fromFormatToHex(format, values);
}

function previewImage(color: string): string {
const content = `<rect x="1" y="1" width="22" height="22" fill="${color}" stroke="white"/><rect x="2" y="2" width="20" height="20" fill="none" stroke="black"/>`;
return "data:image/svg+xml," + encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24">${content}</svg>`);
}
5 changes: 3 additions & 2 deletions pxtblocks/plugins/colorpicker/colorPickerDropdown.ts
Original file line number Diff line number Diff line change
@@ -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"],
Expand All @@ -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();
}
}
Expand Down
4 changes: 2 additions & 2 deletions pxtblocks/plugins/duplicateOnDrag/connectionChecker.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -21,7 +21,7 @@ export class DuplicateOnDragConnectionChecker extends Blockly.ConnectionChecker
if (
replacedBlock &&
shouldDuplicateOnDrag(replacedBlock) &&
!(replacedBlock.isShadow() && isAllowlistedShadow(replacedBlock))
!replacedBlock.isShadow()
) {
return false;
}
Expand Down
9 changes: 5 additions & 4 deletions pxtblocks/plugins/duplicateOnDrag/dragStrategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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();
Expand All @@ -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;
Expand Down
30 changes: 28 additions & 2 deletions pxtblocks/plugins/duplicateOnDrag/duplicateOnDrag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,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<string> = {};
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);
}
});
});
}

/**
Expand Down
Loading
Loading