diff --git a/editor/deploy.ts b/editor/deploy.ts index 8695e340..c2ebf646 100644 --- a/editor/deploy.ts +++ b/editor/deploy.ts @@ -1,4 +1,4 @@ -/// +/// /// import HF2 = pxt.HF2; @@ -14,6 +14,7 @@ export function debug() { } // Web Serial API https://wicg.github.io/serial/ +// https://www.npmjs.com/package/@types/web-bluetooth // chromium bug https://bugs.chromium.org/p/chromium/issues/detail?id=884928 // Under experimental features in Chrome Desktop 77+ enum ParityType { @@ -23,6 +24,7 @@ enum ParityType { "mark", "space" } + declare interface SerialOptions { baudRate?: number; databits?: number; @@ -34,15 +36,18 @@ declare interface SerialOptions { xoff?: boolean; xany?: boolean; } + type SerialPortInfo = pxt.Map; type SerialPortRequestOptions = any; + declare class SerialPort { open(options?: SerialOptions): Promise; close(): void; readonly readable: any; readonly writable: any; - //getInfo(): SerialPortInfo; + // getInfo(): SerialPortInfo; } + declare interface Serial extends EventTarget { onconnect: any; ondisconnect: any; @@ -60,7 +65,11 @@ class WebSerialPackageIO implements pxt.packetio.PacketIO { private _writer: any; constructor(private port: SerialPort, private options: SerialOptions) { - console.log(`serial: new io`) + console.log(`serial: New io`) + } + + bufferSize(buffer: Uint8Array) { + return HF2.read16(buffer, 0) + 2; } async readSerialAsync() { @@ -68,21 +77,28 @@ class WebSerialPackageIO implements pxt.packetio.PacketIO { let buffer: Uint8Array; const reader = this._reader; while (reader === this._reader) { // will change if we recycle the connection - const { done, value } = await this._reader.read() + const { done, value } = await this._reader.read(); if (!buffer) buffer = value; else { // concat - let tmp = new Uint8Array(buffer.length + value.byteLength) - tmp.set(buffer, 0) - tmp.set(value, buffer.length) + let tmp = new Uint8Array(buffer.length + value.byteLength); + tmp.set(buffer, 0); + tmp.set(value, buffer.length); buffer = tmp; } if (buffer) { - let size = HF2.read16(buffer, 0); - if (buffer.length == size + 2) { + let size = this.bufferSize(buffer); + if (buffer.length == size) { this.onData(new Uint8Array(buffer)); buffer = undefined; + } else if (buffer.length > size) { + console.warn(`Received larger bufer then command command: ${buffer.length} recieved but waiting for ${size}`); + let tmp = buffer.slice(0, size - 1); + this.onData(new Uint8Array(tmp)); + tmp = buffer.slice(size, buffer.length - 1); + buffer = tmp; + console.debug(`Next buffer size: ${this.bufferSize(buffer)}`); } else { - console.warn("Incomplete command " + size); + console.warn(`Incomplete command: ${buffer.length} recieved but waiting for ${size}. Keep waiting...`); } } } @@ -93,11 +109,14 @@ class WebSerialPackageIO implements pxt.packetio.PacketIO { } static portIos: WebSerialPackageIO[] = []; + static async mkPacketIOAsync(): Promise { const serial = (navigator).serial; if (serial) { try { - const requestOptions: SerialPortRequestOptions = {}; + const requestOptions: SerialPortRequestOptions = { + // filters: [{ usbVendorId: 0x0694, usbProductId: 0x0005 }], + }; const port = await serial.requestPort(requestOptions); let io = WebSerialPackageIO.portIos.filter(i => i.port == port)[0]; @@ -111,7 +130,7 @@ class WebSerialPackageIO implements pxt.packetio.PacketIO { } return io; } catch (e) { - console.log(`connection error`, e) + console.log(`Connection error`, e) } } throw new Error("could not open serial port"); @@ -123,7 +142,8 @@ class WebSerialPackageIO implements pxt.packetio.PacketIO { } private openAsync() { - console.log(`serial: opening port`) + console.log(`serial: Opening port`); + // this.io.onConnectionChanged(); if (!!this._reader) return Promise.resolve(); this._reader = undefined; this._writer = undefined; @@ -189,6 +209,7 @@ function hf2Async() { let useHID = false; let useWebSerial = false; + export function initAsync(): Promise { if (pxt.U.isNodeJS) { // doesn't seem to work ATM @@ -234,6 +255,7 @@ async function cleanupAsync() { } let initPromise: Promise + function initHidAsync() { // needs to run within a click handler if (initPromise) return initPromise @@ -261,6 +283,7 @@ const rbfTemplate = ` 4c45474f580000006d000100000000001c000000000000000e000000821b038405018130813e8053 74617274696e672e2e2e0084006080XX00448581644886488405018130813e80427965210084000a ` + export function deployCoreAsync(resp: pxtc.CompileResult) { let filename = resp.downloadFileBaseName || "pxt" filename = filename.replace(/^lego-/, "") diff --git a/editor/extension.ts b/editor/extension.ts index 866a173e..2fcfc1a2 100644 --- a/editor/extension.ts +++ b/editor/extension.ts @@ -1,8 +1,7 @@ /// -/// /// /// -/// +/// /// import { deployCoreAsync, initAsync } from "./deploy"; diff --git a/editor/wrap.ts b/editor/wrap.ts index 630ddaa9..1702d589 100644 --- a/editor/wrap.ts +++ b/editor/wrap.ts @@ -110,14 +110,112 @@ export class Ev3Wrapper { }) } + dumpInputCmd(buf : Uint8Array) { + log(`Reply size: ${HF2.read16(buf, 0)}, Message counter: ${HF2.read16(buf, 2)}, Reply type: ${buf[4]} (${buf[4] === 0x03 ? "System command reply OK" : buf[4] === 0x05 ? "System command reply ERROR" : "Unknown"})`); + switch (buf[6]) { + case 0x00: + log("Reply Status SUCCESS"); + break; + case 0x01: + log("Reply Status UNKNOWN_HANDLE"); + break; + case 0x02: + log("Reply Status HANDLE_NOT_READY"); + break; + case 0x03: + log("Reply Status CORRUPT_FILE"); + break; + case 0x04: + log("Reply Status NO_HANDLES_AVAILABLE"); + break; + case 0x05: + log("Reply Status NO_PERMISSION"); + break; + case 0x06: + log("Reply Status ILLEGAL_PATH"); + break; + case 0x07: + log("Reply Status FILE_EXITS"); + break; + case 0x08: + log("Reply Status END_OF_FILE"); + break; + case 0x09: + log("Reply Status SIZE_ERROR"); + break; + case 0x0A: + log("Reply Status UNKNOWN_ERROR"); + break; + case 0x0B: + log("Reply Status ILLEGAL_FILENAME"); + break; + case 0x0C: + log("Reply Status ILLEGAL_CONNECTION"); + break; + } + } + + dumpOutputCmd(buf: Uint8Array) { + log(`Command size: ${HF2.read16(buf, 0)}, Message counter: ${HF2.read16(buf, 2)}, Command type: ${buf[4]} (${buf[4] === 0x01 ? "System command, reply required" : buf[4] === 0x81 ? "System command, reply not required" : "Unknown"})`); + switch (buf[5]) { + case 0x92: + log("System command: Begin file download"); + break; + case 0x93: + log("System command: Continue file download"); + break; + case 0x94: + log("System command: Begin file upload"); + break; + case 0x95: + log("System command: Continue file upload"); + break; + case 0x96: + log("System command: Begin get bytes from a file (while writing to the file)"); + break; + case 0x97: + log("System command: Continue get byte from a file (while writing to the file)"); + break; + case 0x98: + log("System command: Close file handle"); + break; + case 0x99: + log("System command: List files"); + break; + case 0x9A: + log("System command: Continue list files"); + break; + case 0x9B: + log("System command: Create directory"); + break; + case 0x9C: + log("System command: Delete"); + break; + case 0x9D: + log("System command: List handles"); + break; + case 0x9E: + log("System command: Write to mailbox"); + break; + case 0x9F: + log("System command: Transfer trusted pin code to brick"); + break; + case 0xA0: + log("System command: Restart the brick in Firmware update mode"); + break; + } + } + talkAsync(buf: Uint8Array, altResponse = 0) { return this.lock.enqueue("talk", () => { this.msgs.drain() if (this.dataDump) log("TALK: " + U.toHex(buf)) + this.dumpOutputCmd(buf) return this.io.sendPacketAsync(buf) .then(() => this.msgs.shiftAsync(5000)) .then(resp => { + this.dumpInputCmd(resp) if (resp[2] != buf[2] || resp[3] != buf[3]) U.userError("msg count de-sync") if (buf[4] == 1) { @@ -132,7 +230,7 @@ export class Ev3Wrapper { } flashAsync(path: string, file: Uint8Array) { - log(`write ${file.length} bytes to ${path}`) + log(`Write ${file.length} bytes to ${path}`) let handle = -1 @@ -274,13 +372,13 @@ export class Ev3Wrapper { reconnectAsync(first = false): Promise { this.resetState() if (first) return this.initAsync() - log(`reconnect`); + log(`Reconnect`); return this.io.reconnectAsync() .then(() => this.initAsync()) } disconnectAsync() { - log(`disconnect`); + log(`Disconnect`); return this.io.disconnectAsync() } } diff --git a/fieldeditors/extension.ts b/fieldeditors/extension.ts index 10944a73..11c4dda5 100644 --- a/fieldeditors/extension.ts +++ b/fieldeditors/extension.ts @@ -1,20 +1,18 @@ -/// +/// /// -import { FieldPorts } from "./field_ports"; +const Blockly = pxt.blocks.requireBlockly(); + import { FieldMotors } from "./field_motors"; import { FieldBrickButtons } from "./field_brickbuttons"; import { FieldColorEnum } from "./field_color"; import { FieldMusic } from "./field_music"; pxt.editor.initFieldExtensionsAsync = function (opts: pxt.editor.FieldExtensionOptions): Promise { - pxt.debug('loading pxt-ev3 target extensions...') + pxt.debug('loading pxt-ev3 target extensions...'); updateBlocklyShape(); const res: pxt.editor.FieldExtensionResult = { fieldEditors: [{ - selector: "ports", - editor: FieldPorts - }, { selector: "motors", editor: FieldMotors }, { @@ -132,10 +130,4 @@ function updateBlocklyShape() { */ (Blockly as any).Flyout.prototype.MARGIN = 8; -} - -// When require()d from node, bind the global pxt namespace -// namespace pxt { -// export const dummyExport = 1; -// } -// eval("if (typeof process === 'object' && process + '' === '[object process]') pxt = global.pxt") +} \ No newline at end of file diff --git a/fieldeditors/field_brickbuttons.ts b/fieldeditors/field_brickbuttons.ts index 647beaff..2d8c8108 100644 --- a/fieldeditors/field_brickbuttons.ts +++ b/fieldeditors/field_brickbuttons.ts @@ -1,26 +1,26 @@ -/// -/// +/// -export interface FieldPortsOptions extends Blockly.FieldCustomDropdownOptions { +const Blockly = pxt.blocks.requireBlockly(); + +export interface FieldBrickButtonsOptions { + data: string; columns?: string; width?: string; } -export class FieldBrickButtons extends Blockly.FieldDropdown implements Blockly.FieldCustom { - public isFieldCustom_ = true; +export class FieldBrickButtons extends Blockly.FieldDropdown { - // Width in pixels - private width_: number; + public isFieldCustom_ = true; - // Columns in grid - private columns_: number; + private width_: number; // Width in pixels + private columns_: number; // Columns in grid private savedPrimary_: string; - constructor(text: string, options: FieldPortsOptions, validator?: Function) { - super(options.data); - - this.columns_ = parseInt(options.columns) || 4; + constructor(text: string, options: FieldBrickButtonsOptions, validator?: Function) { + super(options.data as any); + + this.columns_ = parseInt(options.columns) || 1; this.width_ = parseInt(options.width) || 150; } @@ -84,7 +84,7 @@ export class FieldBrickButtons extends Blockly.FieldDropdown implements Blockly. d: 'M 157.78865,189.01028 108.18908,189.38233 70.654464,150.794 86.323259,135.4895 v -28.08625 h 14.101921 v 16.11144 a 12.006218,12.006218 0 0 0 11.85346,11.9788 c 11.59882,0.1841 43.13227,0 43.13227,0 a 10.18472,10.18472 0 0 0 10.38059,-10.38058 v -17.70966 h 14.39179 v 28.08632 l 15.3045,15.3045 z' }) - const buttons = [buttonEnter, buttonLeft, buttonRight, buttonTop, buttonBottom]; + const buttons = [buttonEnter, buttonLeft, buttonTop, buttonBottom, buttonRight]; const options = this.getOptions(); for (let i = 0, option: any; option = options[i]; i++) { let content = (options[i] as any)[0]; // Human-readable text or image. @@ -96,20 +96,24 @@ export class FieldBrickButtons extends Blockly.FieldDropdown implements Blockly. const title = pxsim.svg.child(button, 'title'); title.textContent = content; - Blockly.bindEvent_(button, 'click', this, this.buttonClick_); - Blockly.bindEvent_(button, 'mouseup', this, this.buttonClick_); + button.addEventListener("click", (event) => { + this.buttonClick_(event); + }); + button.addEventListener("mouseup", (event) => { + this.buttonClick_(event); + }); // These are applied manually instead of using the :hover pseudoclass // because Android has a bad long press "helper" menu and green highlight // that we must prevent with ontouchstart preventDefault - Blockly.bindEvent_(button, 'mousedown', button, function (e) { - this.setAttribute('stroke', '#ffffff'); - e.preventDefault(); + button.addEventListener("mousedown", (event) => { + button.setAttribute('stroke', '#ffffff'); + event.preventDefault(); }); - Blockly.bindEvent_(button, 'mouseover', button, function () { - this.setAttribute('stroke', '#ffffff'); + button.addEventListener("mouseover", (event) => { + button.setAttribute('stroke', '#ffffff'); }); - Blockly.bindEvent_(button, 'mouseout', button, function () { - this.setAttribute('stroke', 'transparent'); + button.addEventListener("mouseout", (event) => { + button.setAttribute('stroke', 'transparent'); }); button.setAttribute('data-value', value); @@ -124,12 +128,12 @@ export class FieldBrickButtons extends Blockly.FieldDropdown implements Blockly. Blockly.DropDownDiv.showPositionedByField(this, this.onHide_.bind(this)); // Update colour to look selected. - let source = this.sourceBlock_ as Blockly.BlockSvg; + let source = this.sourceBlock_ as any; this.savedPrimary_ = source?.getColour(); if (source?.isShadow()) { source.setColour(source.getColourTertiary()); } else if (this.borderRect_) { - this.borderRect_.setAttribute('fill', (this.sourceBlock_ as Blockly.BlockSvg).getColourTertiary()); + this.borderRect_.setAttribute('fill', (this.sourceBlock_ as any).getColourTertiary()); } } @@ -155,7 +159,7 @@ export class FieldBrickButtons extends Blockly.FieldDropdown implements Blockly. content.removeAttribute('aria-activedescendant'); (content as HTMLElement).style.width = ''; // Update color (deselect) on dropdown hide - let source = this.sourceBlock_ as Blockly.BlockSvg; + let source = this.sourceBlock_ as any; if (source?.isShadow()) { source.setColour(this.savedPrimary_); } else if (this.borderRect_) { diff --git a/fieldeditors/field_color.ts b/fieldeditors/field_color.ts index a7330a19..e29e105e 100644 --- a/fieldeditors/field_color.ts +++ b/fieldeditors/field_color.ts @@ -1,18 +1,24 @@ -/// -/// +/// -export interface FieldColorEnumOptions extends pxtblockly.FieldColourNumberOptions { +const pxtblockly = pxt.blocks.requirePxtBlockly() +const Blockly = pxt.blocks.requireBlockly(); + +export interface FieldColorEnumOptions { + className?: string; } -export class FieldColorEnum extends pxtblockly.FieldColorNumber implements Blockly.FieldCustom { +export class FieldColorEnum extends pxtblockly.FieldColorNumber { public isFieldCustom_ = true; + private paramsData: any[]; + private className_: string; - constructor(text: string, params: FieldColorEnumOptions, opt_validator?: Function) { - super(text, params, opt_validator); + constructor(text: string, params: FieldColorEnumOptions) { + super(text, params as any); this.paramsData = params["data"]; + this.className_ = params.className; } mapColour(enumString: string) { @@ -44,6 +50,10 @@ export class FieldColorEnum extends pxtblockly.FieldColorNumber implements Block showEditor_() { super.showEditor_(); + const picker = Blockly.DropDownDiv.getContentDiv().childNodes[0] as HTMLElement; + if (this.className_ && picker) { + pxt.BrowserUtils.addClass(picker as HTMLElement, this.className_); + } const colorCells = document.querySelectorAll('.legoColorPicker td'); colorCells.forEach((cell) => { const titleName = this.mapColour(cell.getAttribute("title")); @@ -52,6 +62,20 @@ export class FieldColorEnum extends pxtblockly.FieldColorNumber implements Block }); } + doValueUpdate_(colour: string) { + super.doValueUpdate_(colour); + this.applyColour(); + } + + applyColour() { + if (this.borderRect_) { + this.borderRect_.style.fill = this.value_; + } else if (this.sourceBlock_) { + (this.sourceBlock_ as any)?.pathObject?.svgPath?.setAttribute('fill', this.value_); + (this.sourceBlock_ as any)?.pathObject?.svgPath?.setAttribute('stroke', '#fff'); + } + }; + /** * Return the current colour. * @param {boolean} opt_asHex optional field if the returned value should be a hex @@ -81,4 +105,5 @@ export class FieldColorEnum extends pxtblockly.FieldColorNumber implements Block this.sourceBlock_.setColour(colour); } } + } \ No newline at end of file diff --git a/fieldeditors/field_motors.ts b/fieldeditors/field_motors.ts index 069d7c30..f5170a19 100644 --- a/fieldeditors/field_motors.ts +++ b/fieldeditors/field_motors.ts @@ -1,14 +1,16 @@ -/// -/// -/// +/// -export interface FieldMotorsOptions extends pxtblockly.FieldImagesOptions { +const pxtblockly = pxt.blocks.requirePxtBlockly() +const Blockly = pxt.blocks.requireBlockly(); + +export interface FieldMotorsOptions { + blocksInfo: any; columns?: string; width?: string; //sort?: boolean; } -export class FieldMotors extends pxtblockly.FieldImages implements Blockly.FieldCustom { +export class FieldMotors extends pxtblockly.FieldImages { public isFieldCustom_ = true; //public shouldSort_: boolean; @@ -19,21 +21,105 @@ export class FieldMotors extends pxtblockly.FieldImages implements Blockly.Field this.width_ = parseInt(options.width) || 400; //this.shouldSort_ = options.sort; this.addLabel_ = true; + } - this.renderSelectedImage_ = Blockly.FieldDropdown.prototype.renderSelectedText_; - this.updateSize_ = (Blockly.Field as any).prototype.updateSize_; + protected render_(): void { + if (this.addLabel_) { + this.renderSelectedText_() + this.positionBorderRect_(); + } + else { + super.render_(); + } + } + + /** Renders the selected option, which must be text. */ + protected renderSelectedText_() { + // Retrieves the selected option to display through getText_. + this.getTextContent().nodeValue = this.getDisplayText_(); + const textElement = this.getTextElement(); + Blockly.utils.dom.addClass(textElement, 'blocklyDropdownText'); + textElement.setAttribute('text-anchor', 'start'); + + // Height and width include the border rect. + const hasBorder = !!this.borderRect_; + const height = Math.max( + hasBorder ? this.getConstants()!.FIELD_DROPDOWN_BORDER_RECT_HEIGHT : 0, + this.getConstants()!.FIELD_TEXT_HEIGHT, + ); + const textWidth = Blockly.utils.dom.getFastTextWidth( + this.getTextElement(), + this.getConstants()!.FIELD_TEXT_FONTSIZE, + this.getConstants()!.FIELD_TEXT_FONTWEIGHT, + this.getConstants()!.FIELD_TEXT_FONTFAMILY, + ); + const xPadding = hasBorder + ? this.getConstants()!.FIELD_BORDER_RECT_X_PADDING + : 0; + let arrowWidth = 0; + if (this.getSvgArrow()) { + arrowWidth = this.positionSVGArrow_( + textWidth + xPadding, + height / 2 - this.getConstants()!.FIELD_DROPDOWN_SVG_ARROW_SIZE / 2, + ); + } + this.size_.width = textWidth + arrowWidth + xPadding * 2; + this.size_.height = height; + + this.positionTextElement_(xPadding, textWidth); + } + + positionSVGArrow_(x: number, y: number): number { + const svgArrow = this.getSvgArrow(); + if (!svgArrow) { + return 0; + } + const block = this.getSourceBlock(); + const hasBorder = !!this.borderRect_; + const xPadding = hasBorder + ? this.getConstants()!.FIELD_BORDER_RECT_X_PADDING + : 0; + const textPadding = this.getConstants()!.FIELD_DROPDOWN_SVG_ARROW_PADDING; + const svgArrowSize = this.getConstants()!.FIELD_DROPDOWN_SVG_ARROW_SIZE; + const arrowX = block.RTL ? xPadding : x + textPadding; + svgArrow.setAttribute( + 'transform', + 'translate(' + arrowX + ',' + y + ')', + ); + return svgArrowSize + textPadding; + } + + // This hack exists because svgArrow is private in Blockly's field dropdown. + // It should always be the last image element in the field group + protected getSvgArrow() { + if (this.fieldGroup_) { + const children = this.fieldGroup_.children; + + let lastImage: SVGImageElement; + + for (let i = 0; i < children.length; i++) { + if (children.item(i).tagName.toLowerCase() === "image") { + lastImage = children.item(i) as SVGImageElement; + } + } + + return lastImage; + } + + return undefined; } /** * Create a dropdown menu under the text. * @private */ - public showEditor_() { + public showEditor_(e?: Event) { + this.setOpeningPointerCoords(e); // If there is an existing drop-down we own, this is a request to hide the drop-down. if (Blockly.DropDownDiv.hideIfOwner(this)) { return; } - let sourceBlock = this.sourceBlock_ as Blockly.BlockSvg; + let sourceBlock = this.sourceBlock_ as any; // If there is an existing drop-down someone else owns, hide it immediately and clear it. Blockly.DropDownDiv.hideWithoutAnimation(); Blockly.DropDownDiv.clearContent(); @@ -41,10 +127,15 @@ export class FieldMotors extends pxtblockly.FieldImages implements Blockly.Field let dropdownDiv = Blockly.DropDownDiv.getContentDiv(); let contentDiv = document.createElement('div'); // Accessibility properties + dropdownDiv.setAttribute('class', 'blocklyDropDownMotorsContent'); contentDiv.setAttribute('role', 'menu'); contentDiv.setAttribute('aria-haspopup', 'true'); + contentDiv.setAttribute('class', 'blocklyMotorsFieldOptions'); + this.addPointerListener(dropdownDiv); + this.addKeyDownHandler(contentDiv); const options = this.getOptions(); - //if (this.shouldSort_) options.sort(); + // if (this.shouldSort_) options.sort(); + let row = this.createRow(); for (let i = 0; i < options.length; i++) { const content = (options[i] as any)[0]; // Human-readable text or image. const value = (options[i] as any)[1]; // Language-neutral value. @@ -58,9 +149,12 @@ export class FieldMotors extends pxtblockly.FieldImages implements Blockly.Field contentDiv.appendChild(placeholder); continue; } - let button = document.createElement('button'); + const buttonContainer = document.createElement('div'); + buttonContainer.setAttribute('class', 'blocklyDropDownButtonContainer'); + let button = document.createElement('div'); button.setAttribute('id', ':' + i); // For aria-activedescendant button.setAttribute('role', 'menuitem'); + button.setAttribute('aria-selected', 'false'); button.setAttribute('class', 'blocklyDropDownButton'); button.title = content.alt; if ((this as any).columns_) { @@ -75,17 +169,27 @@ export class FieldMotors extends pxtblockly.FieldImages implements Blockly.Field // This icon is selected, show it in a different colour backgroundColor = sourceBlock.getColourTertiary(); button.setAttribute('aria-selected', 'true'); + this.activeDescendantIndex = i; + contentDiv.setAttribute('aria-activedescendant', button.id); + button.setAttribute('class', `blocklyDropDownButton ${this.openingPointerCoords ? "blocklyDropDownButtonHover" : "blocklyDropDownButtonFocus"}`); } button.style.backgroundColor = backgroundColor; button.style.borderColor = sourceBlock.getColourTertiary(); - Blockly.bindEvent_(button, 'click', this, this.buttonClick_); - Blockly.bindEvent_(button, 'mouseover', button, function () { - this.setAttribute('class', 'blocklyDropDownButton blocklyDropDownButtonHover'); - contentDiv.setAttribute('aria-activedescendant', this.id); + Blockly.browserEvents.bind(button, 'click', this, () => this.buttonClickAndClose_(value)); + Blockly.browserEvents.bind(button, 'pointermove', this, () => { + if (this.pointerMoveTriggeredByUser()) { + this.gridItems.forEach(button => button.setAttribute('class', 'blocklyDropDownButton')); + this.activeDescendantIndex = i; + button.setAttribute('class', 'blocklyDropDownButton blocklyDropDownButtonHover'); + contentDiv.setAttribute('aria-activedescendant', button.id); + } }); - Blockly.bindEvent_(button, 'mouseout', button, function () { - this.setAttribute('class', 'blocklyDropDownButton'); - contentDiv.removeAttribute('aria-activedescendant'); + Blockly.browserEvents.bind(button, 'pointerout', this, () => { + if (this.pointerOutTriggeredByUser()) { + button.setAttribute('class', 'blocklyDropDownButton'); + contentDiv.removeAttribute('aria-activedescendant'); + this.activeDescendantIndex = undefined; + } }); let buttonImg = document.createElement('img'); buttonImg.src = content.src; @@ -94,21 +198,24 @@ export class FieldMotors extends pxtblockly.FieldImages implements Blockly.Field // Store a data attribute on all possible click targets so we can match it to the icon. button.setAttribute('data-value', value); buttonImg.setAttribute('data-value', value); - buttonImg.style.height = 'auto'; button.appendChild(buttonImg); if (this.addLabel_) { const buttonText = this.createTextNode_(content.alt); buttonText.setAttribute('data-value', value); - buttonText.style.whiteSpace = 'inherit'; - buttonText.style.width = 'auto'; - buttonText.style.padding = '0 10px'; button.appendChild(buttonText); } - contentDiv.appendChild(button); + this.gridItems.push(button); + buttonContainer.appendChild(button); + row.append(buttonContainer); + if (row.childElementCount === this.columns_) { + contentDiv.appendChild(row); + row = this.createRow(); + } + } + if (row.childElementCount) { + contentDiv.appendChild(row); } contentDiv.style.width = (this as any).width_ + 'px'; - contentDiv.style.display = 'flex'; - contentDiv.style.alignItems = 'stretch'; dropdownDiv.appendChild(contentDiv); Blockly.DropDownDiv.setColour(sourceBlock.getColour(), sourceBlock.getColourTertiary()); @@ -116,6 +223,8 @@ export class FieldMotors extends pxtblockly.FieldImages implements Blockly.Field // Position based on the field position. Blockly.DropDownDiv.showPositionedByField(this, this.onHideCallback.bind(this)); + contentDiv.focus(); + // Update colour to look selected. this.savedPrimary_ = sourceBlock?.getColour(); if (sourceBlock?.isShadow()) { @@ -125,12 +234,4 @@ export class FieldMotors extends pxtblockly.FieldImages implements Blockly.Field } } - trimOptions_() { - } - - protected buttonClick_ = function (e: any) { - let value = e.target.getAttribute('data-value'); - this.setValue(value); - Blockly.DropDownDiv.hide(); - }; } \ No newline at end of file diff --git a/fieldeditors/field_music.ts b/fieldeditors/field_music.ts index 2d98c61e..7eb62122 100644 --- a/fieldeditors/field_music.ts +++ b/fieldeditors/field_music.ts @@ -1,10 +1,12 @@ -/// -/// -/// +/// -export interface FieldMusicOptions extends pxtblockly.FieldImagesOptions { +const pxtblockly = pxt.blocks.requirePxtBlockly() +const Blockly = pxt.blocks.requireBlockly(); + +export interface FieldMusicOptions { columns?: string; width?: string; + addLabel?: string; } declare const pxtTargetBundle: any; @@ -13,20 +15,21 @@ let soundCache: any; let soundIconCache: any; let soundIconCacheArray: any; -export class FieldMusic extends pxtblockly.FieldImages implements Blockly.FieldCustom { +export class FieldMusic extends pxtblockly.FieldImages { + public isFieldCustom_ = true; private selectedCategory_: string; - private categoriesCache_: string[]; constructor(text: string, options: FieldMusicOptions, validator?: Function) { - super(text, { blocksInfo: options.blocksInfo, sort: true, data: options.data }, validator); + super(text, options as any, validator); this.columns_ = parseInt(options.columns) || 4; this.width_ = parseInt(options.width) || 450; - //this.setText = Blockly.FieldDropdown.prototype.setText; + this.addLabel_ = true; + this.updateSize_ = (Blockly.Field as any).prototype.updateSize_; if (!pxt.BrowserUtils.isIE() && !soundCache) { @@ -42,11 +45,13 @@ export class FieldMusic extends pxtblockly.FieldImages implements Blockly.FieldC * Create a dropdown menu under the text. * @private */ - public showEditor_() { + public showEditor_(e?: Event) { + this.setOpeningPointerCoords(e); // If there is an existing drop-down we own, this is a request to hide the drop-down. if (Blockly.DropDownDiv.hideIfOwner(this)) { return; } + let sourceBlock = this.sourceBlock_ as any; // If there is an existing drop-down someone else owns, hide it immediately and clear it. Blockly.DropDownDiv.hideWithoutAnimation(); Blockly.DropDownDiv.clearContent(); @@ -54,15 +59,15 @@ export class FieldMusic extends pxtblockly.FieldImages implements Blockly.FieldC let dropdownDiv = Blockly.DropDownDiv.getContentDiv() as HTMLElement; let contentDiv = document.createElement('div'); // Accessibility properties + dropdownDiv.setAttribute('class', 'blocklyDropDownMusicContent'); contentDiv.setAttribute('role', 'menu'); contentDiv.setAttribute('aria-haspopup', 'true'); - contentDiv.className = 'blocklyMusicFieldOptions'; - contentDiv.style.display = "flex"; - contentDiv.style.flexWrap = "wrap"; - contentDiv.style.float = "none"; + contentDiv.setAttribute('class', 'blocklyMusicFieldOptions'); + this.addPointerListener(dropdownDiv); + this.addKeyDownHandler(contentDiv); const options = this.getOptions(); - //options.sort(); // Do not need to use to not apply sorting in different languages - + //if (this.shouldSort_) options.sort(); // Do not need to use to not apply sorting in different languages + // Create categoies const categories = this.getCategories(options); const selectedCategory = this.parseCategory(this.getText()); @@ -72,32 +77,29 @@ export class FieldMusic extends pxtblockly.FieldImages implements Blockly.FieldC // Accessibility properties categoriesDiv.setAttribute('role', 'menu'); categoriesDiv.setAttribute('aria-haspopup', 'true'); - categoriesDiv.style.backgroundColor = (this.sourceBlock_ as Blockly.BlockSvg).getColourTertiary(); categoriesDiv.className = 'blocklyMusicFieldCategories'; + categoriesDiv.style.backgroundColor = sourceBlock.getColourTertiary(); + categoriesDiv.style.width = (this as any).width_ + 'px'; this.refreshCategories(categoriesDiv, categories); - this.refreshOptions(contentDiv, options); - contentDiv.style.width = (this as any).width_ + 'px'; - contentDiv.style.cssFloat = 'left'; - - (dropdownDiv as HTMLElement).style.maxHeight = `410px`; dropdownDiv.appendChild(categoriesDiv); dropdownDiv.appendChild(contentDiv); - Blockly.DropDownDiv.setColour(this.sourceBlock_.getColour(), (this.sourceBlock_ as Blockly.BlockSvg).getColourTertiary()); + Blockly.DropDownDiv.setColour(this.sourceBlock_.getColour(), sourceBlock.getColourTertiary()); // Position based on the field position. - Blockly.DropDownDiv.showPositionedByField(this, this.onHide_.bind(this)); + Blockly.DropDownDiv.showPositionedByField(this, this.onHideCallback.bind(this)); + + contentDiv.focus(); // Update colour to look selected. - let source = this.sourceBlock_ as Blockly.BlockSvg; - this.savedPrimary_ = source?.getColour(); - if (source?.isShadow()) { - source.setColour(source.getColourTertiary()); + this.savedPrimary_ = sourceBlock?.getColour(); + if (sourceBlock?.isShadow()) { + sourceBlock.setColour(sourceBlock.style.colourTertiary); } else if (this.borderRect_) { - this.borderRect_.setAttribute('fill', (this.sourceBlock_ as Blockly.BlockSvg).getColourTertiary()); + this.borderRect_.setAttribute('fill', sourceBlock.style.colourTertiary); } } @@ -133,8 +135,8 @@ export class FieldMusic extends pxtblockly.FieldImages implements Blockly.FieldC button.style.padding = "2px 6px"; button.style.backgroundColor = backgroundColor; button.style.borderColor = backgroundColor; - Blockly.bindEvent_(button, 'click', this, this.categoryClick_); - Blockly.bindEvent_(button, 'mouseup', this, this.categoryClick_); + Blockly.browserEvents.bind(button, 'click', this, this.categoryClick_); + Blockly.browserEvents.bind(button, 'mouseup', this, this.categoryClick_); const textNode = this.createTextNode_(category); textNode.setAttribute('data-value', category); @@ -144,10 +146,12 @@ export class FieldMusic extends pxtblockly.FieldImages implements Blockly.FieldC } refreshOptions(contentDiv: Element, options: any) { + let sourceBlock = this.sourceBlock_ as any; const categories = this.getCategories(options); + let row = this.createRow(); // Show options for (let i = 0, option: any; option = options[i]; i++) { - let content = (options[i] as any)[0]; // Human-readable text or image. + const content = (options[i] as any)[0]; // Human-readable text or image. const value = (options[i] as any)[1]; // Language-neutral value. // Filter for options in selected category @@ -164,9 +168,12 @@ export class FieldMusic extends pxtblockly.FieldImages implements Blockly.FieldC contentDiv.appendChild(placeholder); continue; } - let button = document.createElement('button'); + const buttonContainer = document.createElement('div'); + buttonContainer.setAttribute('class', 'blocklyDropDownButtonContainer') + let button = document.createElement('div'); button.setAttribute('id', ':' + i); // For aria-activedescendant - button.setAttribute('role', 'menuitem'); + button.setAttribute('role', 'gridcell'); + button.setAttribute('aria-selected', 'false'); button.setAttribute('class', 'blocklyDropDownButton'); button.title = content; if ((this as any).columns_) { @@ -176,37 +183,34 @@ export class FieldMusic extends pxtblockly.FieldImages implements Blockly.FieldC button.style.width = content.width + 'px'; button.style.height = content.height + 'px'; } - let backgroundColor = this.savedPrimary_ || this.sourceBlock_.getColour(); + let backgroundColor = this.savedPrimary_ || sourceBlock.getColour(); if (value == this.getValue()) { // This icon is selected, show it in a different colour - backgroundColor = (this.sourceBlock_ as Blockly.BlockSvg).getColourTertiary(); + backgroundColor = sourceBlock.getColourTertiary(); button.setAttribute('aria-selected', 'true'); + this.activeDescendantIndex = i; + contentDiv.setAttribute('aria-activedescendant', button.id); + button.setAttribute('class', `blocklyDropDownButton ${this.openingPointerCoords ? "blocklyDropDownButtonHover" : "blocklyDropDownButtonFocus"}`); } button.style.backgroundColor = backgroundColor; - button.style.borderColor = (this.sourceBlock_ as Blockly.BlockSvg).getColourTertiary(); - Blockly.bindEvent_(button, 'click', this, this.buttonClick_); - Blockly.bindEvent_(button, 'mouseup', this, this.buttonClick_); - // These are applied manually instead of using the :hover pseudoclass - // because Android has a bad long press "helper" menu and green highlight - // that we must prevent with ontouchstart preventDefault - let that = this; - Blockly.bindEvent_(button, 'mousedown', button, function (e) { - this.setAttribute('class', 'blocklyDropDownButton blocklyDropDownButtonHover'); - e.preventDefault(); - }); - Blockly.bindEvent_(button, 'mouseenter', button, function () { - that.buttonEnter_(value); - }); - Blockly.bindEvent_(button, 'mouseleave', button, function () { - that.buttonLeave_(); - }); - Blockly.bindEvent_(button, 'mouseover', button, function () { - this.setAttribute('class', 'blocklyDropDownButton blocklyDropDownButtonHover'); - contentDiv.setAttribute('aria-activedescendant', this.id); + button.style.borderColor = sourceBlock.getColourTertiary(); + Blockly.browserEvents.bind(button, 'click', this, () => this.buttonClickAndClose_(value)); + Blockly.browserEvents.bind(button, 'mouseenter', button, () => this.buttonEnter_(value)); + Blockly.browserEvents.bind(button, 'mouseleave', button, () => this.buttonLeave_()); + Blockly.browserEvents.bind(button, 'pointermove', this, () => { + if (this.pointerMoveTriggeredByUser()) { + this.gridItems.forEach(button => button.setAttribute('class', 'blocklyDropDownButton')); + this.activeDescendantIndex = i; + button.setAttribute('class', 'blocklyDropDownButton blocklyDropDownButtonHover'); + contentDiv.setAttribute('aria-activedescendant', button.id); + } }); - Blockly.bindEvent_(button, 'mouseout', button, function () { - this.setAttribute('class', 'blocklyDropDownButton'); - contentDiv.removeAttribute('aria-activedescendant'); + Blockly.browserEvents.bind(button, 'pointerout', this, () => { + if (this.pointerOutTriggeredByUser()) { + button.setAttribute('class', 'blocklyDropDownButton'); + contentDiv.removeAttribute('aria-activedescendant'); + this.activeDescendantIndex = undefined; + } }); // Find index in array by category name @@ -214,29 +218,33 @@ export class FieldMusic extends pxtblockly.FieldImages implements Blockly.FieldC let buttonImg = document.createElement('img'); buttonImg.src = this.getSoundIcon(categoryIndex); - + //buttonImg.alt = icon.alt; // Upon click/touch, we will be able to get the clicked element as e.target // Store a data attribute on all possible click targets so we can match it to the icon. - const textNode = this.createTextNode_(content); button.setAttribute('data-value', value); buttonImg.setAttribute('data-value', value); - buttonImg.style.height = "auto"; - textNode.setAttribute('data-value', value); - if (pxt.Util.userLanguage() !== "en") textNode.setAttribute('lang', pxt.Util.userLanguage()); // for hyphens, here you need to set the correct abbreviation of the selected language - textNode.style.display = "block"; - textNode.style.lineHeight = "1rem"; - textNode.style.marginBottom = "5%"; - textNode.style.padding = "0px 8px"; - textNode.style.wordBreak = "break-word"; - textNode.style.hyphens = "auto"; - button.appendChild(buttonImg); - button.appendChild(textNode); - contentDiv.appendChild(button); - } - } - trimOptions_() { + if (this.addLabel_) { + const buttonText = this.createTextNode_(content); + buttonText.setAttribute('data-value', value); + if (pxt.Util.userLanguage() !== "en") { + buttonText.setAttribute('lang', pxt.Util.userLanguage()); // for hyphens, here you need to set the correct abbreviation of the selected language + } + button.appendChild(buttonText); + } + + this.gridItems.push(button); + buttonContainer.appendChild(button); + row.append(buttonContainer); + if (row.childElementCount === this.columns_) { + contentDiv.appendChild(row); + row = this.createRow(); + } + } + if (row.childElementCount) { + contentDiv.appendChild(row); + } } protected onHide_() { @@ -244,7 +252,7 @@ export class FieldMusic extends pxtblockly.FieldImages implements Blockly.FieldC (Blockly.DropDownDiv.getContentDiv() as HTMLElement).style.maxHeight = ''; this.stopSounds(); // Update color (deselect) on dropdown hide - let source = this.sourceBlock_ as Blockly.BlockSvg; + let source = this.sourceBlock_ as any; if (source?.isShadow()) { source.setColour(this.savedPrimary_); } else if (this.borderRect_) { @@ -254,7 +262,7 @@ export class FieldMusic extends pxtblockly.FieldImages implements Blockly.FieldC protected createTextNode_(content: string) { const category = this.parseCategory(content); - let text = content.substr(content.indexOf(' ') + 1); + let text = content.slice(content.indexOf(' ') + 1); const textSpan = document.createElement('span'); textSpan.setAttribute('class', 'blocklyDropdownText'); @@ -263,20 +271,14 @@ export class FieldMusic extends pxtblockly.FieldImages implements Blockly.FieldC } private parseCategory(content: string) { - return content.substr(0, content.indexOf(' ')); + return content.slice(0, content.indexOf(' ')); } - protected buttonClick_ = function (e: any) { - let value = e.target.getAttribute('data-value'); - this.setValue(value); - Blockly.DropDownDiv.hide(); - }; - private setSelectedCategory(value: string) { this.selectedCategory_ = value; } - protected categoryClick_ = function (e: any) { + protected categoryClick_(e: any) { let value = e.target.getAttribute('data-value'); this.setSelectedCategory(value); @@ -312,7 +314,7 @@ export class FieldMusic extends pxtblockly.FieldImages implements Blockly.FieldC } pxsim.AudioContextManager.playBufferAsync(refBuf as any); } - } + } }; protected buttonLeave_ = function () { @@ -329,4 +331,4 @@ export class FieldMusic extends pxtblockly.FieldImages implements Blockly.FieldC } return undefined; } -} +} \ No newline at end of file diff --git a/fieldeditors/field_ports.ts b/fieldeditors/field_ports.ts deleted file mode 100644 index f3035dc0..00000000 --- a/fieldeditors/field_ports.ts +++ /dev/null @@ -1,31 +0,0 @@ -/// -/// -/// - -export interface FieldPortsOptions extends pxtblockly.FieldImagesOptions { - columns?: string; - width?: string; -} - -export class FieldPorts extends pxtblockly.FieldImages implements Blockly.FieldCustom { - public isFieldCustom_ = true; - - constructor(text: string, options: FieldPortsOptions, validator?: Function) { - super(text, { blocksInfo: options.blocksInfo, sort: true, data: options.data }, validator); - - this.columns_ = parseInt(options.columns) || 4; - this.width_ = parseInt(options.width) || 300; - - //this.setText = Blockly.FieldDropdown.prototype.setText; - this.updateSize_ = (Blockly.Field as any).prototype.updateSize_; - } - - trimOptions_() { - } - - protected buttonClick_ = function (e: any) { - let value = e.target.getAttribute('data-value'); - this.setValue(value); - Blockly.DropDownDiv.hide(); - }; -} \ No newline at end of file diff --git a/libs/color-sensor/color.ts b/libs/color-sensor/color.ts index 6894e349..ff25629f 100644 --- a/libs/color-sensor/color.ts +++ b/libs/color-sensor/color.ts @@ -146,7 +146,9 @@ namespace sensors { //% blockId=colorOnColorDetected //% parts="colorsensor" //% blockNamespace=sensors - //% this.fieldEditor="ports" + //% this.fieldEditor="images" + //% this.fieldOptions.columns="4" + //% this.fieldOptions.width="300" //% weight=100 blockGap=8 //% group="Color Sensor" onColorDetected(color: number, handler: () => void) { @@ -166,7 +168,9 @@ namespace sensors { //% blockId=colorpauseUntilColorDetectedDetected //% parts="colorsensor" //% blockNamespace=sensors - //% this.fieldEditor="ports" + //% this.fieldEditor="images" + //% this.fieldOptions.columns="4" + //% this.fieldOptions.width="300" //% weight=98 blockGap=8 //% group="Color Sensor" pauseUntilColorDetected(color: number) { @@ -186,7 +190,9 @@ namespace sensors { //% blockId=colorGetColor //% parts="colorsensor" //% blockNamespace=sensors - //% this.fieldEditor="ports" + //% this.fieldEditor="images" + //% this.fieldOptions.columns="4" + //% this.fieldOptions.width="300" //% weight=97 blockGap=8 //% group="Color Sensor" color(): ColorSensorColor { @@ -204,7 +210,9 @@ namespace sensors { //% blockId=colorisColorDetectedDetected //% parts="colorsensor" //% blockNamespace=sensors - //% this.fieldEditor="ports" + //% this.fieldEditor="images" + //% this.fieldOptions.columns="4" + //% this.fieldOptions.width="300" //% weight=99 blockGap=8 //% group="Color Sensor" isColorDetected(color: number) { @@ -219,7 +227,9 @@ namespace sensors { //% blockId=colorRgbRaw block="**color sensor** %this| RGB raw" //% parts="colorsensor" //% blockNamespace=sensors - //% this.fieldEditor="ports" + //% this.fieldEditor="images" + //% this.fieldOptions.columns="4" + //% this.fieldOptions.width="300" //% weight=89 //% group="Color Sensor" rgbRaw(): number[] { @@ -238,7 +248,9 @@ namespace sensors { //% blockId=colorOnLightDetected //% parts="colorsensor" //% blockNamespace=sensors - //% this.fieldEditor="ports" + //% this.fieldEditor="images" + //% this.fieldOptions.columns="4" + //% this.fieldOptions.width="300" //% weight=79 blockGap=8 //% group="Color Sensor" onLightDetected(mode: LightIntensityMode, condition: Light, handler: () => void) { @@ -255,7 +267,9 @@ namespace sensors { //% blockId=colorPauseUntilLightDetected //% parts="colorsensor" //% blockNamespace=sensors - //% this.fieldEditor="ports" + //% this.fieldEditor="images" + //% this.fieldOptions.columns="4" + //% this.fieldOptions.width="300" //% weight=78 blockGap=8 //% group="Color Sensor" pauseUntilLightDetected(mode: LightIntensityMode, condition: Light) { @@ -273,7 +287,9 @@ namespace sensors { //% blockId=colorLight //% parts="colorsensor" //% blockNamespace=sensors - //% this.fieldEditor="ports" + //% this.fieldEditor="images" + //% this.fieldOptions.columns="4" + //% this.fieldOptions.width="300" //% weight=77 //% group="Color Sensor" light(mode: LightIntensityMode) { @@ -319,7 +335,9 @@ namespace sensors { //% help=sensors/color-sensor/set-threshold //% blockId=colorSetThreshold block="set **color sensor** %this|%condition|to %value" //% value.min=0 value.max=100 - //% this.fieldEditor="ports" + //% this.fieldEditor="images" + //% this.fieldOptions.columns="4" + //% this.fieldOptions.width="300" //% weight=67 blockGap=8 //% group="Color Sensor" setThreshold(condition: Light, value: number) { @@ -340,7 +358,9 @@ namespace sensors { */ //% help=sensors/color-sensor/threshold //% blockId=colorGetThreshold block="**color sensor** %this|%condition" - //% this.fieldEditor="ports" + //% this.fieldEditor="images" + //% this.fieldOptions.columns="4" + //% this.fieldOptions.width="300" //% weight=68 blockGap=8 //% group="Color Sensor" threshold(condition: Light): number { @@ -357,7 +377,9 @@ namespace sensors { */ //% help=sensors/color-sensor/calibrate-light //% blockId=colorCalibrateLight block="calibrate **color sensor** %this|for %mode" - //% this.fieldEditor="ports" + //% this.fieldEditor="images" + //% this.fieldOptions.columns="4" + //% this.fieldOptions.width="300" //% weight=69 blockGap=8 //% group="Color Sensor" calibrateLight(mode: LightIntensityMode, deviation: number = 8) { diff --git a/libs/color-sensor/ns.ts b/libs/color-sensor/ns.ts index 7bf4f847..b9323843 100644 --- a/libs/color-sensor/ns.ts +++ b/libs/color-sensor/ns.ts @@ -5,11 +5,12 @@ namespace sensors { * @param color to use, eg: ColorSensorColor.Blue */ //% blockId=colorEnumPicker block="%color" shim=TD_ID - //% weight=0 blockHidden=1 turnRatio.fieldOptions.decompileLiterals=1 + //% weight=0 blockHidden=1 //% color.fieldEditor="colorenum" //% color.fieldOptions.colours='["#f12a21", "#ffd01b", "#006db3", "#00934b", "#000000", "#6c2d00", "#ffffff", "#dfe6e9"]' - //% color.fieldOptions.columns=2 color.fieldOptions.className='legoColorPicker' + //% color.fieldOptions.columns=2 + //% color.fieldOptions.className='legoColorPicker' export function __colorEnumPicker(color: ColorSensorColor): number { return color; } -} \ No newline at end of file +} \ No newline at end of file diff --git a/libs/core/input.ts b/libs/core/input.ts index b25abe34..ed02c7c3 100644 --- a/libs/core/input.ts +++ b/libs/core/input.ts @@ -359,11 +359,13 @@ namespace sensors.internal { protected mode: number; // the mode user asked for protected realmode: number; + protected undetectable: boolean; // not all NXT analog sensors can be detected constructor(port: number) { super(port); this.mode = 0; this.realmode = 0; + this.undetectable = false; } _activated() { @@ -376,14 +378,14 @@ namespace sensors.internal { this.mode = v; if (!this.isActive()) return; if (this.realmode != this.mode) { - control.dmesg(`_setMode p=${this._port} m: ${this.realmode} -> ${v}`); + // control.dmesg(`_setMode p=${this._port} m: ${this.realmode} -> ${v}`); this.realmode = v; setAnalogMode(this._port, this._deviceType(), this.mode); } } _readPin1() { - if (!this.isActive()) return 0; + if (!this.undetectable && !this.isActive()) return 0; return analogMM.getNumber(NumberFormat.Int16LE, AnalogOff.InPin1 + 2 * this._port); } diff --git a/libs/core/output.ts b/libs/core/output.ts index c313db4b..c7190452 100644 --- a/libs/core/output.ts +++ b/libs/core/output.ts @@ -194,10 +194,11 @@ namespace motors { * Sets the automatic brake on or off when the motor is off * @param brake a value indicating if the motor should break when off */ - //% blockId=outputMotorSetBrakeMode block="set %motor|brake %brake=toggleOnOff" + //% blockId=outputMotorSetBrakeMode block="set %motor|brake $brake" //% motor.fieldEditor="motors" //% motor.fieldOptions.decompileLiterals=1 //% weight=60 blockGap=8 + //% brake.shadow="toggleOnOff" //% group="Properties" //% help=motors/motor/set-brake setBrake(brake: boolean) { @@ -207,30 +208,32 @@ namespace motors { /** * Indicates to pause while a motor moves for a given distance or duration. - * @param value true to pause; false to continue the program execution + * @param brake true to pause; false to continue the program execution */ - //% blockId=outputMotorSetPauseMode block="set %motor|pause on run %brake=toggleOnOff" + //% blockId=outputMotorSetPauseMode block="set %motor|pause on run $brake" //% motor.fieldEditor="motors" //% motor.fieldOptions.decompileLiterals=1 + //% brake.shadow="toggleOnOff" //% weight=60 blockGap=8 //% group="Properties" - setPauseOnRun(value: boolean) { + setPauseOnRun(brake: boolean) { this.init(); - this._pauseOnRun = value; + this._pauseOnRun = brake; } /** * Inverts the motor polarity */ - //% blockId=motorSetInverted block="set %motor|inverted %reversed=toggleOnOff" + //% blockId=motorSetInverted block="set %motor|inverted $reversed" //% motor.fieldEditor="motors" //% motor.fieldOptions.decompileLiterals=1 //% weight=59 blockGap=8 + //% reversed.shadow="toggleOnOff" //% group="Properties" //% help=motors/motor/set-inverted - setInverted(inverted: boolean) { + setInverted(reversed: boolean) { this.init(); - this._inverted = inverted; + this._inverted = reversed; } protected invertedFactor(): number { @@ -240,7 +243,7 @@ namespace motors { /** * Set the settle time after braking in milliseconds (default is 10ms). */ - //% blockId=motorSetBrakeSettleTime block="set %motor|brake settle time %millis|ms" + //% blockId=motorSetBrakeSettleTime block="set %motor|brake settle time $millis|ms" //% motor.fieldEditor="motors" //% motor.fieldOptions.decompileLiterals=1 //% weight=1 blockGap=8 @@ -344,11 +347,12 @@ namespace motors { * @param value (optional) measured distance or rotation * @param unit (optional) unit of the value */ - //% blockId=motorRun block="run %motor at %speed=motorSpeedPicker|\\%||for %value %unit" + //% blockId=motorRun block="run %motor at $speed|\\%||for $value $unit" //% weight=100 blockGap=8 //% group="Move" //% motor.fieldEditor="motors" //% motor.fieldOptions.decompileLiterals=1 + //% speed.shadow="motorSpeedPicker" //% expandableArgumentMode=toggle //% help=motors/motor/run run(speed: number, value: number = 0, unit: MoveUnit = MoveUnit.MilliSeconds) { @@ -394,11 +398,12 @@ namespace motors { * @param acceleration acceleration phase measured distance or rotation, eg: 500 * @param deceleration deceleration phase measured distance or rotation, eg: 500 */ - //% blockId=motorSchedule block="ramp %motor at %speed=motorSpeedPicker|\\%|for %value|%unit||accelerate %acceleration|decelerate %deceleration" + //% blockId=motorSchedule block="ramp %motor at $speed|\\%|for $value|$unit||accelerate $acceleration|decelerate $deceleration" //% weight=99 blockGap=8 //% group="Move" //% motor.fieldEditor="motors" //% motor.fieldOptions.decompileLiterals=1 + //% speed.shadow="motorSpeedPicker" //% help=motors/motor/ramp //% inlineInputMode=inline //% expandableArgumentMode=toggle @@ -498,9 +503,10 @@ namespace motors { * Indicates if the motor(s) speed should be regulated. Default is true. * @param value true for regulated motor */ - //% blockId=outputMotorSetRegulated block="set %motor|regulated %value=toggleOnOff" + //% blockId=outputMotorSetRegulated block="set %motor|regulated $value" //% motor.fieldEditor="motors" //% motor.fieldOptions.decompileLiterals=1 + //% value.shadow="toggleOnOff" //% weight=58 blockGap=8 //% group="Properties" //% help=motors/motor/set-regulated @@ -524,11 +530,12 @@ namespace motors { * Pauses the execution until the previous command finished. * @param timeOut optional maximum pausing time in milliseconds */ - //% blockId=motorPauseUntilRead block="pause until %motor|ready" + //% blockId=motorPauseUntilRead block="pause until %motor|ready||timeout $timeOut" //% motor.fieldEditor="motors" //% motor.fieldOptions.decompileLiterals=1 //% weight=90 blockGap=8 //% group="Move" + //% expandableArgumentMode="toggle" pauseUntilReady(timeOut?: number) { pauseUntil(() => this.isReady(), timeOut); } @@ -642,12 +649,14 @@ namespace motors { /** * Pauses the program until the motor is stalled. + * @param timeOut optional maximum pausing time in milliseconds */ - //% blockId=motorPauseUntilStall block="pause until %motor|stalled" + //% blockId=motorPauseUntilStall block="pause until %motor|stalled||timeout $timeOut" //% motor.fieldEditor="motors" //% motor.fieldOptions.decompileLiterals=1 //% weight=89 //% group="Move" + //% expandableArgumentMode="toggle" //% help=motors/motor/pause-until-stalled pauseUntilStalled(timeOut?: number): void { // let it start @@ -720,10 +729,12 @@ namespace motors { * @param value (optional) move duration or rotation * @param unit (optional) unit of the value */ - //% blockId=motorPairTank block="tank **motors** %motors %speedLeft=motorSpeedPicker|\\% %speedRight=motorSpeedPicker|\\%||for %value %unit" + //% blockId=motorPairTank block="tank **motors** %motors $speedLeft|\\% $speedRight|\\%||for $value $unit" //% motors.fieldEditor="motors" //% weight=96 blockGap=8 //% inlineInputMode=inline + //% speedLeft.shadow="motorSpeedPicker" + //% speedRight.shadow="motorSpeedPicker" //% group="Move" //% expandableArgumentMode=toggle //% help=motors/synced/tank @@ -750,10 +761,12 @@ namespace motors { * @param value (optional) move duration or rotation * @param unit (optional) unit of the value */ - //% blockId=motorPairSteer block="steer **motors** %chassis turn ratio %turnRatio=motorTurnRatioPicker speed %speed=motorSpeedPicker|\\%||for %value %unit" + //% blockId=motorPairSteer block="steer **motors** %chassis turn ratio $turnRatio speed $speed|\\%||for $value $unit" //% chassis.fieldEditor="motors" //% weight=95 + //% turnRatio.shadow="motorTurnRatioPicker" //% turnRatio.min=-200 turnRatio=200 + //% speed.shadow="motorSpeedPicker" //% inlineInputMode=inline //% group="Move" //% expandableArgumentMode=toggle diff --git a/libs/gyro-sensor/gyro.ts b/libs/gyro-sensor/gyro.ts index 0ee4628b..bc3c9691 100644 --- a/libs/gyro-sensor/gyro.ts +++ b/libs/gyro-sensor/gyro.ts @@ -46,7 +46,9 @@ namespace sensors { //% blockId=gyroGetAngle //% parts="gyroscope" //% blockNamespace=sensors - //% this.fieldEditor="ports" + //% this.fieldEditor="images" + //% this.fieldOptions.columns="4" + //% this.fieldOptions.width="300" //% weight=100 blockGap=8 //% group="Gyro Sensor" angle(): number { @@ -66,7 +68,9 @@ namespace sensors { //% blockId=gyroGetRate //% parts="gyroscope" //% blockNamespace=sensors - //% this.fieldEditor="ports" + //% this.fieldEditor="images" + //% this.fieldOptions.columns="4" + //% this.fieldOptions.width="300" //% weight=99 blockGap=8 //% group="Gyro Sensor" rate(): number { @@ -85,7 +89,9 @@ namespace sensors { //% blockId=gyroCalibrate //% parts="gyroscope" //% blockNamespace=sensors - //% this.fieldEditor="ports" + //% this.fieldEditor="images" + //% this.fieldOptions.columns="4" + //% this.fieldOptions.width="300" //% weight=89 blockGap=8 //% group="Gyro Sensor" calibrate(): void { @@ -156,7 +162,9 @@ namespace sensors { //% blockId=gyroReset //% parts="gyroscope" //% blockNamespace=sensors - //% this.fieldEditor="ports" + //% this.fieldEditor="images" + //% this.fieldOptions.columns="4" + //% this.fieldOptions.width="300" //% weight=88 //% group="Gyro Sensor" reset(): void { @@ -201,7 +209,9 @@ namespace sensors { //% blockId=gyroDrift //% parts="gyroscope" //% blockNamespace=sensors - //% this.fieldEditor="ports" + //% this.fieldEditor="images" + //% this.fieldOptions.columns="4" + //% this.fieldOptions.width="300" //% weight=78 blockGap=8 //% group="Gyro Sensor" drift(): number { @@ -216,7 +226,9 @@ namespace sensors { //% blockId=gyroComputeDrift //% parts="gyroscope" //% blockNamespace=sensors - //% this.fieldEditor="ports" + //% this.fieldEditor="images" + //% this.fieldOptions.columns="4" + //% this.fieldOptions.width="300" //% weight=79 blockGap=8 //% group="Gyro Sensor" computeDrift() { @@ -236,7 +248,9 @@ namespace sensors { //% blockId=gyroPauseUntilRotated //% parts="gyroscope" //% blockNamespace=sensors - //% this.fieldEditor="ports" + //% this.fieldEditor="images" + //% this.fieldOptions.columns="4" + //% this.fieldOptions.width="300" //% degrees.defl=90 //% weight=98 //% group="Gyro Sensor" diff --git a/libs/infrared-sensor/ir.ts b/libs/infrared-sensor/ir.ts index d69ce106..ea2b0691 100644 --- a/libs/infrared-sensor/ir.ts +++ b/libs/infrared-sensor/ir.ts @@ -200,7 +200,9 @@ namespace sensors { //% blockNamespace=sensors //% weight=100 blockGap=8 //% group="Infrared Sensor" - //% this.fieldEditor="ports" + //% this.fieldEditor="images" + //% this.fieldOptions.columns="4" + //% this.fieldOptions.width="300" onEvent(event: InfraredSensorEvent, handler: () => void) { this._setMode(InfraredSensorMode.Proximity) control.onEvent(this._id, event, handler); @@ -216,7 +218,9 @@ namespace sensors { //% blockNamespace=sensors //% weight=99 blockGap=8 //% group="Infrared Sensor" - //% this.fieldEditor="ports" + //% this.fieldEditor="images" + //% this.fieldOptions.columns="4" + //% this.fieldOptions.width="300" pauseUntil(event: InfraredSensorEvent) { this._setMode(InfraredSensorMode.Proximity) control.waitForEvent(this._id, event); @@ -233,7 +237,9 @@ namespace sensors { //% blockNamespace=sensors //% weight=98 //% group="Infrared Sensor" - //% this.fieldEditor="ports" + //% this.fieldEditor="images" + //% this.fieldOptions.columns="4" + //% this.fieldOptions.width="300" proximity(): number { this.poke(); this._setMode(InfraredSensorMode.Proximity) @@ -248,7 +254,9 @@ namespace sensors { //% blockId=irSetRemoteChannel block="set **infrared** %this|remote channel to %channel" //% weight=99 //% group="Remote Infrared Beacon" - //% this.fieldEditor="ports" + //% this.fieldEditor="images" + //% this.fieldOptions.columns="4" + //% this.fieldOptions.width="300" //% help=sensors/beacon/set-remote-channel setRemoteChannel(channel: InfraredRemoteChannel) { this.setMode(InfraredSensorMode.RemoteControl) @@ -265,7 +273,9 @@ namespace sensors { //% blockGap=8 weight=49 //% group="Infrared Sensor" //% value.min=0 value.max=100 - //% this.fieldEditor="ports" + //% this.fieldEditor="images" + //% this.fieldOptions.columns="4" + //% this.fieldOptions.width="300" setPromixityThreshold(condition: InfraredSensorEvent, value: number) { if (condition == InfraredSensorEvent.ObjectNear) this._proximityThreshold.setLowThreshold(value) @@ -280,7 +290,9 @@ namespace sensors { //% blockId=irGetThreshold block="**infrared** %this|%condition" //% weight=48 //% group="Infrared Sensor" - //% this.fieldEditor="ports" + //% this.fieldEditor="images" + //% this.fieldOptions.columns="4" + //% this.fieldOptions.width="300" proximityThreshold(condition: InfraredSensorEvent): number { return this._proximityThreshold.threshold(condition); } diff --git a/libs/nxt-light-sensor/light.ts b/libs/nxt-light-sensor/light.ts index 6f177ac8..a9fd314f 100644 --- a/libs/nxt-light-sensor/light.ts +++ b/libs/nxt-light-sensor/light.ts @@ -94,7 +94,9 @@ namespace sensors { //% blockId=nxtLight //% parts="nxtlightsensor" //% blockNamespace=sensors - //% this.fieldEditor="ports" + //% this.fieldEditor="images" + //% this.fieldOptions.columns="4" + //% this.fieldOptions.width="300" //% weight=99 blockGap=8 //% subcategory="NXT" //% group="Light Sensor" @@ -126,7 +128,9 @@ namespace sensors { //% blockId=setReflectedLightRange //% parts="nxtlightsensor" //% blockNamespace=sensors - //% this.fieldEditor="ports" + //% this.fieldEditor="images" + //% this.fieldOptions.columns="4" + //% this.fieldOptions.width="300" //% weight=89 blockGap=8 //% subcategory="NXT" //% group="Light Sensor" @@ -147,7 +151,9 @@ namespace sensors { //% blockId=setAmbientLightRange //% parts="nxtlightsensor" //% blockNamespace=sensors - //% this.fieldEditor="ports" + //% this.fieldEditor="images" + //% this.fieldOptions.columns="4" + //% this.fieldOptions.width="300" //% weight=88 blockGap=8 //% subcategory="NXT" //% group="Light Sensor" @@ -189,6 +195,7 @@ namespace sensors { */ //% reflectetLight() { + if (!this.isActive()) return 0; let reflectedVal = Math.map(this.readValue(), this.darkReflectedLight, this.brightReflectedLight, 0, 100); reflectedVal = Math.round(Math.constrain(reflectedVal, 0, 100)); return reflectedVal; @@ -199,6 +206,7 @@ namespace sensors { */ //% ambientLight() { + if (!this.isActive()) return 0; let ambientVal = Math.map(this.readValue(), this.darkAmbientLight, this.brightAmbientLight, 0, 100); ambientVal = Math.round(Math.constrain(ambientVal, 0, 100)); return ambientVal; diff --git a/libs/storage/storage-core.ts b/libs/storage/storage-core.ts index 539e5c9f..8356b028 100644 --- a/libs/storage/storage-core.ts +++ b/libs/storage/storage-core.ts @@ -1,11 +1,11 @@ -namespace storage { +enum Separators { + //% block="comma" + Comma, + //% block="semicolon" + Semicolon +} - export enum Separators { - //% block="comma" - Comma, - //% block="semicolon" - Semicolon - } +namespace storage { //% shim=storage::__unlink function __unlink(filename: string): void { } diff --git a/libs/touch-sensor/touch.ts b/libs/touch-sensor/touch.ts index 53958e8f..1c63037d 100644 --- a/libs/touch-sensor/touch.ts +++ b/libs/touch-sensor/touch.ts @@ -37,7 +37,9 @@ namespace sensors { //% blockId=touchEvent block="on **touch** %this|%event" //% parts="touch" //% blockNamespace=sensors - //% this.fieldEditor="ports" + //% this.fieldEditor="images" + //% this.fieldOptions.columns="4" + //% this.fieldOptions.width="300" //% weight=99 blockGap=8 //% group="Touch Sensor" onEvent(ev: ButtonEvent, body: () => void) { @@ -53,7 +55,9 @@ namespace sensors { //% blockId=touchWaitUntil block="pause until **touch** %this|%event" //% parts="touch" //% blockNamespace=sensors - //% this.fieldEditor="ports" + //% this.fieldEditor="images" + //% this.fieldOptions.columns="4" + //% this.fieldOptions.width="300" //% weight=98 blockGap=8 //% group="Touch Sensor" pauseUntil(ev: ButtonEvent) { @@ -68,7 +72,9 @@ namespace sensors { //% blockId=touchIsPressed block="**touch** %this|is pressed" //% parts="touch" //% blockNamespace=sensors - //% this.fieldEditor="ports" + //% this.fieldEditor="images" + //% this.fieldOptions.columns="4" + //% this.fieldOptions.width="300" //% weight=81 blockGap=8 //% group="Touch Sensor" isPressed() { @@ -85,7 +91,9 @@ namespace sensors { //% blockHidden=true //% parts="touch" //% blockNamespace=sensors - //% this.fieldEditor="ports" + //% this.fieldEditor="images" + //% this.fieldOptions.columns="4" + //% this.fieldOptions.width="300" //% weight=80 //% group="Touch Sensor" wasPressed() { diff --git a/libs/ultrasonic-sensor/ultrasonic.ts b/libs/ultrasonic-sensor/ultrasonic.ts index 3f683502..dd018bfc 100644 --- a/libs/ultrasonic-sensor/ultrasonic.ts +++ b/libs/ultrasonic-sensor/ultrasonic.ts @@ -49,7 +49,9 @@ namespace sensors { //% block="on **ultrasonic** %this|%event" //% parts="ultrasonicsensor" //% blockNamespace=sensors - //% this.fieldEditor="ports" + //% this.fieldEditor="images" + //% this.fieldOptions.columns="4" + //% this.fieldOptions.width="300" //% weight=100 blockGap=8 //% group="Ultrasonic Sensor" onEvent(event: UltrasonicSensorEvent, handler: () => void) { @@ -64,7 +66,9 @@ namespace sensors { //% blockId=ultrasonicWait //% parts="ultrasonicsensor" //% blockNamespace=sensors - //% this.fieldEditor="ports" + //% this.fieldEditor="images" + //% this.fieldOptions.columns="4" + //% this.fieldOptions.width="300" //% weight=99 blockGap=8 //% group="Ultrasonic Sensor" pauseUntil(event: UltrasonicSensorEvent) { @@ -80,7 +84,9 @@ namespace sensors { //% blockId=sonarGetDistance //% parts="ultrasonicsensor" //% blockNamespace=sensors - //% this.fieldEditor="ports" + //% this.fieldEditor="images" + //% this.fieldOptions.columns="4" + //% this.fieldOptions.width="300" //% weight=98 //% group="Ultrasonic Sensor" distance(): number { @@ -100,7 +106,9 @@ namespace sensors { //% weight=89 blockGap=8 //% group="Ultrasonic Sensor" //% value.min=0 value.max=255 - //% this.fieldEditor="ports" + //% this.fieldEditor="images" + //% this.fieldOptions.columns="4" + //% this.fieldOptions.width="300" setThreshold(condition: UltrasonicSensorEvent, value: number) { switch (condition) { case UltrasonicSensorEvent.ObjectNear: this.promixityThreshold.setLowThreshold(value); break; @@ -115,7 +123,9 @@ namespace sensors { //% blockId=ultrasonicGetThreshold block="**ultrasonic** %this|%condition" //% weight=88 //% group="Ultrasonic Sensor" - //% this.fieldEditor="ports" + //% this.fieldEditor="images" + //% this.fieldOptions.columns="4" + //% this.fieldOptions.width="300" threshold(condition: UltrasonicSensorEvent): number { switch (condition) { case UltrasonicSensorEvent.ObjectNear: return this.promixityThreshold.threshold(ThresholdState.Low); diff --git a/package.json b/package.json index 16afb0cd..f2bca04c 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "@types/node": "^9.3.0", "@types/react": "16.8.25", "@types/react-dom": "16.0.3", - "@types/web-bluetooth": "0.0.4", + "@types/web-bluetooth": "0.0.21", "@vusion/webfonts-generator": "^0.7.1", "less": "3.13.1", "react": "16.8.3", @@ -45,8 +45,8 @@ "typescript": "4.8.3" }, "dependencies": { - "pxt-common-packages": "11.1.6", - "pxt-core": "9.3.13" + "pxt-common-packages": "13.1.4", + "pxt-core": "12.2.8" }, "scripts": { "test": "node node_modules/pxt-core/built/pxt.js travis" diff --git a/pxtarget.json b/pxtarget.json index e78b80a6..5f6a7a51 100644 --- a/pxtarget.json +++ b/pxtarget.json @@ -80,7 +80,7 @@ }, "breakBlock": true, "continueBlock": true, - "bannedCategories": [] + "bannedCategories": ["images"] }, "compileService": { "buildEngine": "dockermake", @@ -110,6 +110,8 @@ "copyrightText": "LEGO, the LEGO logo, MINDSTORMS and the MINDSTORMS EV3 logo are trademarks and/ or copyrights of the LEGO Group. ©2018 The LEGO Group. All rights reserved.", "crowdinProject": "makecode", "selectLanguage": true, + "defaultColorTheme": "ev3-light", + "highContrastColorTheme": "pxt-high-contrast", "blocksCollapsing": true, "highContrast": true, "greenScreen": true, @@ -150,7 +152,7 @@ "print": true, "showHomeScreen": true, "homeScreenHero": "./static/hero.png", - "invertedMenu": false, + "invertedMenu": true, "invertedMonaco": false, "monacoToolbox": true, "invertedToolbox": false, @@ -205,7 +207,7 @@ "monacoColors": { "editor.background": "#f9f9f9" }, - "fileNameExclusiveFilter": "[^a-zA-Z0-9]", + "fileNameExclusiveFilter": "[^a-zA-Z0-9_]", "qrCode": true, "shareFinishedTutorials": true, "nameProjectFirst": true, @@ -213,7 +215,10 @@ "githubEditor": true, "chooseLanguageRestrictionOnNewProject": true, "openProjectNewTab": true, - "python": true + "python": true, + "timeMachine": true, + "timeMachineDiffInterval": 600000, + "timeMachineSnapshotInterval": 1800000 }, "ignoreDocsErrors": true, "uploadDocs": true diff --git a/sim/state/control.ts b/sim/state/control.ts index e3618b35..3ff2bee8 100644 --- a/sim/state/control.ts +++ b/sim/state/control.ts @@ -29,7 +29,9 @@ namespace pxsim.MMapMethods { destroy() { } buf(): Buffer { - return { data: this.impl.data } as any + const b = pxsim.BufferMethods.createBuffer(this.impl.data.length); + b.data.set(this.impl.data); + return b; } } @@ -77,11 +79,10 @@ namespace pxsim.MMapMethods { } namespace pxsim.control { - export function mmap(filename: string, size: number, offset: number): MMapMethods.MMap { - let impl = MMapMethods.mmapRegistry[filename] - if (!impl) impl = {} - return new MMapMethods.MMap(impl, size) + let impl = MMapMethods.mmapRegistry[filename] || {}; + const m = new MMapMethods.MMap(impl, size); + return m; } } diff --git a/theme/blockly.less b/theme/blockly.less index 658ce7ff..62e24ea3 100644 --- a/theme/blockly.less +++ b/theme/blockly.less @@ -28,22 +28,49 @@ ry: 0 !important; } -.blocklyDropDownButton { - border-radius: 0px !important; +/* Field editor */ +.blocklyMotorsFieldOptions .blocklyDropDownButton, .blocklyMusicFieldOptions .blocklyDropDownButton { + height: 100%; +} + +.blocklyDropDownMotorsContent .blocklyMotorsFieldOptions .blocklyDropdownTextLabel { + display: inline-block; + width: auto; + overflow: hidden; + text-overflow: ellipsis; + white-space: inherit; + padding: 0 10px; + line-height: 1rem; + color: #fff; + font-size: 13px; } -/* Music field editor */ +.blocklyDropDownMusicContent { + max-height: 450px; + overflow-y: auto; +} -.blocklyMusicFieldOptions .blocklyDropdownText, .blocklyMusicFieldCategories .blocklyDropdownText { +.blocklyMotorsFieldOptions div[role="row"], .blocklyMusicFieldOptions div[role="row"] { display: flex; - justify-content: center; - line-height: 1.5rem; +} + +.blocklyMusicFieldCategories .blocklyDropdownText, .blocklyMusicFieldOptions .blocklyDropdownText { + // display: flex; + // justify-content: center; + line-height: 1rem; color: #fff; font-size: 13px; } +.blocklyMusicFieldOptions .blocklyDropdownText { + display: block; + margin-bottom: 5%; + padding: 0px 8px; + word-break: break-word; + hyphens: auto; +} + .blocklyMusicFieldCategories { - position: absolute; text-align: center; left: 0; right: 0; @@ -51,9 +78,6 @@ width: 100%; font-size: 13px; } -.blocklyMusicFieldOptions { - margin-top: 80px; -} .blocklyDropdownTag { padding: 2px; @@ -64,4 +88,17 @@ border: 1px solid; transition: box-shadow .1s; cursor: pointer; +} + +.blocklyMotorsFieldOptions .blocklyDropDownButton > img, .blocklyMusicFieldOptions .blocklyDropDownButton > img { + position: relative; + width: 80%; + height: auto; + transform: unset; + top: unset; +} + +// Default DropDownContent +.blocklyDropDownContent { + overflow-y: auto; // Return scroll y } \ No newline at end of file diff --git a/theme/color-themes/ev3-light.json b/theme/color-themes/ev3-light.json new file mode 100644 index 00000000..de875a32 --- /dev/null +++ b/theme/color-themes/ev3-light.json @@ -0,0 +1,83 @@ +{ + "id": "ev3-light", + "name": "EV3 Light", + "weight": 20, + "overrideFiles": [ + "/overrides/ev3-light-overrides.css" + ], + "colors": { + "pxt-header-background": "#F2F2F2", + "pxt-header-foreground": "#FFFFFF", + "pxt-header-background-hover": "#1d3282", + "pxt-header-foreground-hover": "#FFFFFF", + "pxt-header-stencil": "#2742ab", + + "pxt-header-secondary-background": "#999", + "pxt-header-secondary-foreground": "#FFFFFF", + + "pxt-primary-background": "#1AA5C6", + "pxt-primary-foreground": "#FFFFFF", + "pxt-primary-background-hover": "#0F97b7", + "pxt-primary-foreground-hover": "#FFFFFF", + "pxt-primary-accent": "#0f97b7", + + "pxt-secondary-background": "#fa7f2a", + "pxt-secondary-foreground": "#FFFFFF", + "pxt-secondary-background-hover": "#2742ab", + "pxt-secondary-foreground-hover": "#FFFFFF", + "pxt-secondary-accent": "#eb6306", + + "pxt-tertiary-background": "#3454D1", + "pxt-tertiary-foreground": "#FFFFFF", + "pxt-tertiary-background-hover": "#2742ab", + "pxt-tertiary-foreground-hover": "#FFFFFF", + "pxt-tertiary-accent": "#1d3282", + + "pxt-target-background1": "#ECF0F1", + "pxt-target-foreground1": "#000000", + "pxt-target-background1-hover": "#cfd9db", + "pxt-target-foreground1-hover": "#000000", + "pxt-target-stencil1": "#e1e1e1", + + "pxt-target-background2": "#FDFDFF", + "pxt-target-foreground2": "#000000", + "pxt-target-background2-hover": "#cacaff", + "pxt-target-foreground2-hover": "#000000", + "pxt-target-stencil2": "#e1e1e1", + + "pxt-target-background3": "#F2F2F2", + "pxt-target-foreground3": "#000000", + "pxt-target-background3-hover": "#e6e6e6", + "pxt-target-foreground3-hover": "#000000", + "pxt-target-stencil3": "#d9d9d9", + + "pxt-neutral-background1": "#FFFFFF", + "pxt-neutral-foreground1": "#5d5d5d", + "pxt-neutral-background1-hover": "#e6e6e6", + "pxt-neutral-foreground1-hover": "rgba(0,0,0,.85)", + "pxt-neutral-stencil1": "rgba(34, 74, 114, 0.15)", + + "pxt-neutral-background2": "#F8F8F8", + "pxt-neutral-foreground2": "rgba(0,0,0,.85)", + "pxt-neutral-background2-hover": "#DFDFDF", + "pxt-neutral-foreground2-hover": "rgba(0,0,0,.85)", + "pxt-neutral-stencil2": "#e9eef2", + + "pxt-neutral-background3": "#3b3c3d", + "pxt-neutral-foreground3": "#FFFFFF", + "pxt-neutral-background3-hover": "#363c3d", + "pxt-neutral-foreground3-hover": "#FFFFFF", + "pxt-neutral-stencil3": "#FFFFFF", + "pxt-neutral-background3-alpha90": "#617374E5", + + "pxt-neutral-base": "rgba(0, 0, 0, 1)", + "pxt-neutral-alpha0": "rgba(0, 0, 0, 0)", + "pxt-neutral-alpha10": "rgba(0, 0, 0, 0.1)", + "pxt-neutral-alpha20": "rgba(0, 0, 0, 0.2)", + "pxt-neutral-alpha50": "rgba(0, 0, 0, 0.5)", + + "pxt-link": "#3977B4", + "pxt-link-hover": "#204467", + "pxt-focus-border": "#169CDF" + } +} \ No newline at end of file diff --git a/theme/color-themes/overrides/ev3-light-overrides.css b/theme/color-themes/overrides/ev3-light-overrides.css new file mode 100644 index 00000000..d87c4cba --- /dev/null +++ b/theme/color-themes/overrides/ev3-light-overrides.css @@ -0,0 +1,71 @@ +/* Lots of specificity to override another !important rule */ + +/* #simulator #editorSidebar .simtoolbar .ui.icon.tiny.buttons .ui.button.play-button.play .icon.play { + color: var(--pxt-colors-green-background) !important; +} + +.theme-preview-ev3-light .theme-preview-sim-button { + background-color: var(--pxt-neutral-background2) !important; +} */ + +#mainmenu { + /* Some parts of the app use the same color as a background. This keeps the menu from blending in */ + border-bottom: 5px solid #00a5c8; +} + +#computertogglesim, #mobiletogglesim, #sidedocstoggle { + background: #95a5a6 !important; +} + +.blocklyToolboxDiv, .monacoToolboxDiv { + background: var(--pxt-neutral-background1) !important; + color: var(--pxt-neutral-foreground1) !important; +} + +/* Фон панели панели выбранной категории с блоками */ +.blocklyFlyoutBackground { + fill: var(--pxt-neutral-background3) !important; + fill-opacity: .9 !important; +} + +/* Шапка в расширениях */ +.fullscreen.extensions-browser .common-modal .common-modal-header { + background-color: var(--pxt-header-secondary-background) !important; +} + +/* Лейбл бета в расширениях */ +.common-card-label { + color: var(--pxt-secondary-foreground); + background-color: var(--pxt-secondary-background); + border-color: var(--pxt-secondary-accent); +} + +/* Кнопка Понятно в окне загрузки */ +.ui.button.positive { + background-color: var(--pxt-primary-background) !important; + color: var(--pxt-primary-foreground) !important; + border: 1px solid var(--pxt-primary-background) !important; +} + +/* Не работает, кнопка троеточия */ +.ui.button.editortools-btn.hw-button:focus, .ui.button.editortools-btn.hw-button:hover { + filter: grayscale(.15) brightness(.85) contrast(1.3) !important; + background-color: var(--pxt-neutral-background1) !important; + color: var(--pxt-neutral-foreground1) !important; +} + +/* Кнопки переключения editortoggle */ +.menubar .ui.menu.fixed .ui.item.editor-menuitem .item.link:hover, .menubar .ui.inverted.menu .link.item:active, .ui.inverted.menu .dropdown.item:hover { + background: var(--pxt-neutral-alpha10); + color: #000; +} + +/* Кнопка переключения языков JS на Python editortoggle */ +#root:not(.hc) #editordropdown.active { + background-color: var(--pxt-neutral-alpha20) !important; + color: #000 !important; +} + +.ui.red.ribbon.label { + border-color: #760007 !important; +} \ No newline at end of file diff --git a/theme/style.less b/theme/style.less index 47092260..ae2a19a2 100644 --- a/theme/style.less +++ b/theme/style.less @@ -137,6 +137,10 @@ border-radius: 0; } +.collapsedEditorTools #downloadArea { + background: none; +} + /* Mobile */ @media only screen and (max-width: @largestMobileScreen) { #filelist {