From a1b059171b4a790cec909dcbb288d836462b8da1 Mon Sep 17 00:00:00 2001 From: Sam El-Husseini Date: Sun, 24 Dec 2017 17:46:58 -0800 Subject: [PATCH] Add a output port field editor --- editor/extension.ts | 6 +- editor/field_ports.ts | 150 +++++++++++++++++++++++++++ libs/color-sensor/color.ts | 6 ++ libs/gyro-sensor/gyro.ts | 2 + libs/touch-sensor/touch.ts | 4 + libs/ultrasonic-sensor/ultrasonic.ts | 3 + 6 files changed, 170 insertions(+), 1 deletion(-) create mode 100644 editor/field_ports.ts diff --git a/editor/extension.ts b/editor/extension.ts index 163516b5..2b736d75 100644 --- a/editor/extension.ts +++ b/editor/extension.ts @@ -142,7 +142,11 @@ namespace pxt.editor { initExtensionsAsync = function (opts: pxt.editor.ExtensionOptions): Promise { pxt.debug('loading pxt-ev3 target extensions...') const res: pxt.editor.ExtensionResult = { - deployCoreAsync, + fieldEditors: [{ + selector: "ports", + editor: FieldPorts + }], + deployCoreAsync }; initAsync().catch(e => { // probably no HID - we'll try this again upon deployment diff --git a/editor/field_ports.ts b/editor/field_ports.ts new file mode 100644 index 00000000..12f3bb15 --- /dev/null +++ b/editor/field_ports.ts @@ -0,0 +1,150 @@ +/// +/// + +namespace pxt.editor { + + export interface FieldPortsOptions extends Blockly.FieldCustomDropdownOptions { + columns?: string; + width?: string; + } + + export class FieldPorts extends Blockly.FieldDropdown implements Blockly.FieldCustom { + public isFieldCustom_ = true; + + // Width in pixels + private width_: number; + + // Columns in grid + private columns_: number; + + private savedPrimary_: string; + + constructor(text: string, options: FieldPortsOptions, validator?: Function) { + super(options.data); + + this.columns_ = parseInt(options.columns) || 4; + this.width_ = parseInt(options.width) || 300; + } + + /** + * Create a dropdown menu under the text. + * @private + */ + public showEditor_() { + // If there is an existing drop-down we own, this is a request to hide the drop-down. + if (Blockly.DropDownDiv.hideIfOwner(this)) { + return; + } + // If there is an existing drop-down someone else owns, hide it immediately and clear it. + Blockly.DropDownDiv.hideWithoutAnimation(); + Blockly.DropDownDiv.clearContent(); + // Populate the drop-down with the icons for this field. + let dropdownDiv = Blockly.DropDownDiv.getContentDiv(); + let contentDiv = document.createElement('div'); + // Accessibility properties + contentDiv.setAttribute('role', 'menu'); + contentDiv.setAttribute('aria-haspopup', 'true'); + 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. + const value = (options[i] as any)[1]; // Language-neutral value. + // Icons with the type property placeholder take up space but don't have any functionality + // Use for special-case layouts + if (content.type == 'placeholder') { + let placeholder = document.createElement('span'); + placeholder.setAttribute('class', 'blocklyDropDownPlaceholder'); + placeholder.style.width = content.width + 'px'; + placeholder.style.height = content.height + 'px'; + contentDiv.appendChild(placeholder); + continue; + } + let button = document.createElement('button'); + button.setAttribute('id', ':' + i); // For aria-activedescendant + button.setAttribute('role', 'menuitem'); + button.setAttribute('class', 'blocklyDropDownButton'); + button.title = content.alt; + if (this.columns_) { + button.style.width = ((this.width_ / this.columns_) - 8) + 'px'; + button.style.height = ((this.width_ / this.columns_) - 8) + 'px'; + } else { + button.style.width = content.width + 'px'; + button.style.height = content.height + 'px'; + } + let backgroundColor = this.sourceBlock_.getColour(); + if (value == this.getValue()) { + // This icon is selected, show it in a different colour + backgroundColor = this.sourceBlock_.getColourTertiary(); + button.setAttribute('aria-selected', 'true'); + } + button.style.backgroundColor = backgroundColor; + button.style.borderColor = this.sourceBlock_.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 + Blockly.bindEvent_(button, 'mousedown', button, function (e) { + this.setAttribute('class', 'blocklyDropDownButton blocklyDropDownButtonHover'); + e.preventDefault(); + }); + Blockly.bindEvent_(button, 'mouseover', button, function () { + this.setAttribute('class', 'blocklyDropDownButton blocklyDropDownButtonHover'); + contentDiv.setAttribute('aria-activedescendant', this.id); + }); + Blockly.bindEvent_(button, 'mouseout', button, function () { + this.setAttribute('class', 'blocklyDropDownButton'); + contentDiv.removeAttribute('aria-activedescendant'); + }); + let buttonImg = document.createElement('img'); + buttonImg.src = content.src; + //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. + button.setAttribute('data-value', value); + buttonImg.setAttribute('data-value', value); + button.appendChild(buttonImg); + contentDiv.appendChild(button); + } + contentDiv.style.width = this.width_ + 'px'; + dropdownDiv.appendChild(contentDiv); + + Blockly.DropDownDiv.setColour(this.sourceBlock_.getColour(), this.sourceBlock_.getColourTertiary()); + + // Calculate positioning based on the field position. + var scale = this.sourceBlock_.workspace.scale; + var bBox = { width: this.size_.width, height: this.size_.height }; + bBox.width *= scale; + bBox.height *= scale; + var position = this.fieldGroup_.getBoundingClientRect(); + var primaryX = position.left + bBox.width / 2; + var primaryY = position.top + bBox.height; + var secondaryX = primaryX; + var secondaryY = position.top; + // Set bounds to workspace; show the drop-down. + (Blockly.DropDownDiv as any).setBoundsElement(this.sourceBlock_.workspace.getParentSvg().parentNode); + (Blockly.DropDownDiv as any).show(this, primaryX, primaryY, secondaryX, secondaryY, + this.onHide_.bind(this)); + } + + /** + * Callback for when a button is clicked inside the drop-down. + * Should be bound to the FieldIconMenu. + * @param {Event} e DOM event for the click/touch + * @private + */ + private buttonClick_ = function (e: any) { + let value = e.target.getAttribute('data-value'); + this.setValue(value); + Blockly.DropDownDiv.hide(); + }; + + /** + * Callback for when the drop-down is hidden. + */ + private onHide_ = function () { + Blockly.DropDownDiv.content_.removeAttribute('role'); + Blockly.DropDownDiv.content_.removeAttribute('aria-haspopup'); + Blockly.DropDownDiv.content_.removeAttribute('aria-activedescendant'); + }; + } +} \ No newline at end of file diff --git a/libs/color-sensor/color.ts b/libs/color-sensor/color.ts index 9b88e32d..46661529 100644 --- a/libs/color-sensor/color.ts +++ b/libs/color-sensor/color.ts @@ -110,6 +110,7 @@ namespace sensors { //% blockId=colorOnColorDetected //% parts="colorsensor" //% blockNamespace=sensors + //% sensor.fieldEditor="ports" //% weight=100 blockGap=8 //% group="Color Sensor" onColorDetected(color: ColorSensorColor, handler: () => void) { @@ -129,6 +130,7 @@ namespace sensors { //% blockId=colorPauseForColorDetected //% parts="colorsensor" //% blockNamespace=sensors + //% sensor.fieldEditor="ports" //% weight=99 blockGap=8 //% group="Color Sensor" pauseForColor(color: ColorSensorColor) { @@ -148,6 +150,7 @@ namespace sensors { //% blockId=colorGetColor //% parts="colorsensor" //% blockNamespace=sensors + //% sensor.fieldEditor="ports" //% weight=99 //% group="Color Sensor" color(): ColorSensorColor { @@ -165,6 +168,7 @@ namespace sensors { //% blockId=colorOnLightChanged //% parts="colorsensor" //% blockNamespace=sensors + //% sensor.fieldEditor="ports" //% weight=89 blockGap=8 //% group="Color Sensor" onLightChanged(mode: LightIntensityMode, condition: LightCondition, handler: () => void) { @@ -181,6 +185,7 @@ namespace sensors { //% blockId=colorPauseForLight //% parts="colorsensor" //% blockNamespace=sensors + //% sensor.fieldEditor="ports" //% weight=88 blockGap=8 //% group="Color Sensor" pauseForLight(mode: LightIntensityMode, condition: LightCondition) { @@ -198,6 +203,7 @@ namespace sensors { //% blockId=colorLight //% parts="colorsensor" //% blockNamespace=sensors + //% sensor.fieldEditor="ports" //% weight=87 //% group="Color Sensor" light(mode: LightIntensityMode) { diff --git a/libs/gyro-sensor/gyro.ts b/libs/gyro-sensor/gyro.ts index de0f54a3..d3adfa2a 100644 --- a/libs/gyro-sensor/gyro.ts +++ b/libs/gyro-sensor/gyro.ts @@ -28,6 +28,7 @@ namespace sensors { //% blockId=gyroGetAngle //% parts="gyroscope" //% blockNamespace=sensors + //% sensor.fieldEditor="ports" //% weight=65 blockGap=8 //% group="Gyro Sensor" angle(): number { @@ -44,6 +45,7 @@ namespace sensors { //% blockId=gyroGetRate //% parts="gyroscope" //% blockNamespace=sensors + //% sensor.fieldEditor="ports" //% weight=65 blockGap=8 //% group="Gyro Sensor" rate(): number { diff --git a/libs/touch-sensor/touch.ts b/libs/touch-sensor/touch.ts index 7d8b2540..5d592ed8 100644 --- a/libs/touch-sensor/touch.ts +++ b/libs/touch-sensor/touch.ts @@ -45,6 +45,7 @@ namespace sensors { //% blockId=touchEvent block="on %sensor|%event" //% parts="touch" //% blockNamespace=sensors + //% sensor.fieldEditor="ports" //% weight=99 blockGap=8 //% group="Touch Sensor" onEvent(ev: TouchSensorEvent, body: () => void) { @@ -60,6 +61,7 @@ namespace sensors { //% blockId=touchWaitUntil block="pause until %sensor|%event" //% parts="touch" //% blockNamespace=sensors + //% sensor.fieldEditor="ports" //% weight=98 blockGap=8 //% group="Touch Sensor" pauseUntil(ev: TouchSensorEvent) { @@ -75,6 +77,7 @@ namespace sensors { //% blockId=touchIsPressed //% parts="touch" //% blockNamespace=sensors + //% sensor.fieldEditor="ports" //% weight=81 blockGap=8 //% group="Touch Sensor" isPressed() { @@ -90,6 +93,7 @@ namespace sensors { //% blockId=touchWasPressed //% parts="touch" //% blockNamespace=sensors + //% sensor.fieldEditor="ports" //% weight=81 blockGap=8 //% group="Touch Sensor" wasPressed() { diff --git a/libs/ultrasonic-sensor/ultrasonic.ts b/libs/ultrasonic-sensor/ultrasonic.ts index 097749ca..3d192f6a 100644 --- a/libs/ultrasonic-sensor/ultrasonic.ts +++ b/libs/ultrasonic-sensor/ultrasonic.ts @@ -46,6 +46,7 @@ namespace sensors { //% block="on %sensor|%event" //% parts="ultrasonicsensor" //% blockNamespace=sensors + //% sensor.fieldEditor="ports" //% weight=100 blockGap=8 //% group="Ultrasonic Sensor" onEvent(event: UltrasonicSensorEvent, handler: () => void) { @@ -60,6 +61,7 @@ namespace sensors { //% blockId=ultrasonicWait //% parts="ultrasonicsensor" //% blockNamespace=sensors + //% sensor.fieldEditor="ports" //% weight=99 blockGap=8 //% group="Ultrasonic Sensor" pauseUntil(event: UltrasonicSensorEvent) { @@ -75,6 +77,7 @@ namespace sensors { //% blockId=sonarGetDistance //% parts="ultrasonicsensor" //% blockNamespace=sensors + //% sensor.fieldEditor="ports" //% weight=65 blockGap=8 //% group="Ultrasonic Sensor" distance(): number {