Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
6 changes: 5 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 { COLOR_PICKER_BLOCK_TYPE } 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,10 @@ export function blockSymbol(type: string): pxtc.SymbolInfo {
export function injectBlocks(blockInfo: pxtc.BlocksInfo): pxtc.SymbolInfo[] {
cachedBlockInfo = blockInfo;

setDraggableShadowBlocks(blockInfo.blocks.filter(fn => fn.attributes.duplicateShadowOnDrag).map(fn => fn.attributes.blockId));
setDraggableShadowBlocks([
COLOR_PICKER_BLOCK_TYPE,
...blockInfo.blocks.filter(fn => fn.attributes.duplicateShadowOnDrag).map(fn => fn.attributes.blockId)
]);

setArgumentReporterLocalizeFunction((arg, block) => {
return localizeArgumentReporter(blockInfo, arg, block);
Expand Down
79 changes: 78 additions & 1 deletion 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,6 +12,8 @@ export interface ColorPickerBlock extends Blockly.Block {
setColorHSV: (hsv: number[]) => void;
setFormat: (format: string, prevFormat?: string) => void;
readColorFromInputs: () => void;
updateBeforeRender: () => void;
updateColorPreview: () => void;
}

const HEX_INPUT_NAME = "HEX_INPUT";
Expand All @@ -31,10 +34,56 @@ export function initColorPickerBlock() {
this.setInputsInline(true);

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

this.updateShape(this.getFieldValue("FORMAT"));
this.setColorHSV(this.colorHSV);
setDuplicateOnDragStrategy(this);
if (this instanceof Blockly.BlockSvg) updateDuplicateOnDragState(this);
this.setOnChange((event: Blockly.Events.BlockBase) => {
if (event.type !== Blockly.Events.BLOCK_CHANGE && event.type !== Blockly.Events.BLOCK_MOVE
&& event.type !== Blockly.Events.BLOCK_CREATE) return;
const moved = event as Blockly.Events.BlockMove;
if (event.blockId === this.id || moved.oldParentId === this.id || moved.newParentId === this.id
|| this.getChildren(false).some(child => child.id === event.blockId)) {
this.updateColorPreview();
}
});
},

updateBeforeRender: function (this: ColorPickerBlock) {
this.updateColorPreview();
},

updateColorPreview: function (this: ColorPickerBlock) {
const preview = this.getField("PREVIEW") as Blockly.FieldImage;
const color = getColorPickerColor(this);
// This is derived UI, not an edit: don't add history or invalidate redo.
Blockly.Events.disable();
try {
if (color) {
const image = previewImage(color);
if (preview.getValue() !== image) {
preview.setValue(image);
}
preview.setAlt(lf("Color {0}. Choose color", color));
}
preview.setVisible(!!color);
}
finally {
Blockly.Events.enable();
}
},

domToMutation: function (this: ColorPickerBlock, xmlElement: Element) {
Expand Down Expand Up @@ -134,6 +183,7 @@ export function initColorPickerBlock() {
},

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

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

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

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

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

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

function previewImage(color: string): string {
const content = `<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
3 changes: 2 additions & 1 deletion pxtblocks/plugins/duplicateOnDrag/dragStrategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
111 changes: 59 additions & 52 deletions pxtblocks/toolbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)$/;

Expand Down Expand Up @@ -111,6 +115,19 @@ export function createShadowValue(info: pxtc.BlocksInfo, p: pxt.blocks.BlockPara
shadow.setAttribute("type", shadowId || (isArray ? 'lists_create_with' : typeInfo && typeInfo.block || p.type));
shadow.setAttribute("colour", "#fff");

const shadowSymbol = info.blocksById[shadowId];
if (shadowId === COLOR_PICKER_BLOCK_TYPE || shadowSymbol?.attributes.builtinBlockId === COLOR_PICKER_BLOCK_TYPE) {
const contributor = shadowSymbol || info.blocks.find(block => block.attributes.builtinBlockId === COLOR_PICKER_BLOCK_TYPE);
const defaults = contributor && pxt.blocks.compileInfo(contributor).parameters[0];
value.replaceChild(createColorPickerBlock(
defaultV !== undefined ? defaultV : defaults?.defaultValue,
p.fieldOptions?.format || defaults?.fieldOptions?.format,
contributor?.attributes.color,
true
), shadow);
return value;
}

if (isArray) {
// if an array of booleans, numbers, or strings
if (typeInfo && !shadowId) {
Expand Down Expand Up @@ -314,7 +331,7 @@ export function createToolboxBlock(info: pxtc.BlocksInfo, fn: pxtc.SymbolInfo, c
let parentInput: HTMLElement;

if (fn.attributes.builtinBlockId) {
return createBuiltinBlock(fn);
return createBuiltinBlock(fn, comp, isShadow);
}

if (fn.attributes.toolboxParent) {
Expand Down Expand Up @@ -587,60 +604,50 @@ export function createFunctionsFlyoutCategory(workspace: Blockly.WorkspaceSvg) {
return res;
};

function createBuiltinBlock(fn: pxtc.SymbolInfo) {
function createBuiltinBlock(fn: pxtc.SymbolInfo, comp: pxt.blocks.BlockCompileInfo, isShadow: boolean) {
const id = fn.attributes.builtinBlockId;

const blockColor = fn.attributes.color;

if (id === "makecode_color_picker") {
// <block type="makecode_color_picker">
// <field name="FORMAT">rgb</field>
// <value name="INPUT0">
// <shadow type="makecode_color_picker_number">
// <field name="NUM">255</field>
// </shadow>
// </value>
// <value name="INPUT1">
// <shadow type="makecode_color_picker_number">
// <field name="NUM">255</field>
// </shadow>
// </value>
// <value name="INPUT2">
// <shadow type="makecode_color_picker_number">
// <field name="NUM">0</field>
// </shadow>
// </value>
// </block>
const block = document.createElement("block");
block.setAttribute("type", "makecode_color_picker");

const field = document.createElement("field");
field.setAttribute("name", "FORMAT");
field.textContent = "rgb";
block.appendChild(field);

for (let i = 0; i < 3; i++) {
const value = document.createElement("value");
value.setAttribute("name", `INPUT${i}`);
const shadow = document.createElement("shadow");
shadow.setAttribute("type", "makecode_color_picker_number");
const numField = document.createElement("field");
numField.setAttribute("name", "NUM");
numField.textContent = "0";
shadow.appendChild(numField);
value.appendChild(shadow);
block.appendChild(value);
}

if (blockColor) {
const mutation = document.createElement("mutation");
mutation.setAttribute("color", blockColor);
block.appendChild(mutation);
}

return block;
if (id === COLOR_PICKER_BLOCK_TYPE) {
const defaults = comp.parameters[0];
return createColorPickerBlock(defaults?.defaultValue, defaults?.fieldOptions?.format, fn.attributes.color, isShadow);
}

pxt.warn(`Unsupported builtin block id: ${id}`);
return undefined;
}

function createColorPickerBlock(defaultValue: string, requestedFormat: string, blockColor: string, isShadow: boolean): HTMLElement {
if (defaultValue?.charAt(0) === '"') defaultValue = JSON.parse(defaultValue);
const formatName = ["rgb", "hsv", "hsl", "cmyk", "hex"].indexOf(requestedFormat) >= 0 ? requestedFormat : "rgb";
const rgb = Number(defaultValue) || 0;
const hex = typeof defaultValue === "string" && /^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i.test(defaultValue)
? defaultValue : fromFormatToHex("rgb", [rgb >> 16 & 255, rgb >> 8 & 255, rgb & 255]);
const block = document.createElement(isShadow ? "shadow" : "block");
block.setAttribute("type", COLOR_PICKER_BLOCK_TYPE);
const mutation = document.createElement("mutation");
if (blockColor) mutation.setAttribute("color", blockColor);
const hsv = fromHexToFormat("hsv", hex);
["hue", "saturation", "value"].forEach((name, index) => mutation.setAttribute(name, hsv[index].toString()));
block.appendChild(mutation);

const format = document.createElement("field");
format.setAttribute("name", "FORMAT");
format.textContent = formatName;
block.appendChild(format);

if (formatName === "hex") {
const input = document.createElement("value");
input.setAttribute("name", "HEX_INPUT");
input.appendChild(generateColorPickerStringShadowDom(hex));
block.appendChild(input);
}
else {
const types = getFieldTypesForFormat(formatName);
fromHSVToFormat(formatName, hsv).forEach((channel, index) => {
const input = document.createElement("value");
input.setAttribute("name", "INPUT" + index);
input.appendChild(generateColorPickerNumberShadowDom(types[index], channel));
block.appendChild(input);
});
}
return block;
}
15 changes: 12 additions & 3 deletions pxtcompiler/emitter/decompiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -686,6 +686,13 @@ ${output}</xml>`;
}

function mkValue(name: string, value: ExpressionNode | TextNode, shadowType?: string, shadowMutation?: pxt.Map<string>): 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;
Expand Down Expand Up @@ -4004,8 +4011,10 @@ ${output}</xml>`;
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;

Expand Down Expand Up @@ -4043,7 +4052,7 @@ ${output}</xml>`;
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
Expand Down
Loading
Loading