diff --git a/src/App.tsx b/src/App.tsx index fcc94ea0..5019038b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -46,6 +46,7 @@ import * as CustomBlocks from './blocks/setup_custom_blocks'; import { initialize as initializePythonBlocks } from './blocks/utils/python'; import * as ChangeFramework from './blocks/utils/change_framework' +import { registerToolboxButton } from './blocks/mrc_event_handler' import { mutatorOpenListener } from './blocks/mrc_param_container' import { TOOLBOX_UPDATE_EVENT } from './blocks/mrc_mechanism_component_holder'; import { antdThemeFromString } from './reactComponents/ThemeModal'; @@ -432,6 +433,9 @@ const AppContent: React.FC = ({ project, setProject }): React.J ChangeFramework.setup(newWorkspace); newWorkspace.addChangeListener(mutatorOpenListener); newWorkspace.addChangeListener(handleBlocksChanged); + + registerToolboxButton(newWorkspace, messageApi); + generatorContext.current = createGeneratorContext(); if (currentModule) { diff --git a/src/blocks/mrc_event_handler.ts b/src/blocks/mrc_event_handler.ts index 648b89dd..65df31f4 100644 --- a/src/blocks/mrc_event_handler.ts +++ b/src/blocks/mrc_event_handler.ts @@ -23,6 +23,7 @@ import * as Blockly from 'blockly'; import {Order} from 'blockly/python'; +import type { MessageInstance } from 'antd/es/message/interface'; import { Editor } from '../editor/editor'; import { ExtendedPythonGenerator } from '../editor/extended_python_generator'; import { createFieldFlydown } from '../fields/field_flydown'; @@ -34,6 +35,9 @@ import * as storageModuleContent from '../storage/module_content'; export const BLOCK_NAME = 'mrc_event_handler'; +const BUTTON_CALLBACK_KEY = 'EVENT_HANDLER_ALREADY_ON_WORKSPACE'; +const BUTTON_STYLE_PREFIX = 'eventHandlerButtonStyle_'; + const FIELD_SENDER = 'SENDER'; const FIELD_EVENT_NAME = 'EVENT_NAME'; @@ -48,6 +52,7 @@ export interface Parameter { type?: string; } +const SENDER_VALUE_ROBOT = 'robot'; const WARNING_ID_EVENT_CHANGED = 'event changed'; export type EventHandlerBlock = Blockly.Block & EventHandlerMixin & Blockly.BlockSvg; @@ -404,10 +409,22 @@ function generateRegisterEventHandler( // Functions used for creating blocks for the toolbox. export function addRobotEventHandlerBlocks( + workspace: Blockly.WorkspaceSvg, events: storageModuleContent.Event[], + eventHandlerBlocks: EventHandlerBlock[], contents: toolboxItems.ContentsType[]) { + // Collect the ids of events for which there is already an event handler. + const eventIds: string[] = []; + eventHandlerBlocks.forEach(eventHandlerBlock => { + eventIds.push(eventHandlerBlock.getEventId()); + }); events.forEach(event => { - contents.push(createRobotEventHandlerBlock(event)); + if (eventIds.includes(event.eventId)) { + // If there is already an event handler for this event, put a button in the toolbox. + contents.push(createButton(workspace, SENDER_VALUE_ROBOT, event.name)); + } else { + contents.push(createRobotEventHandlerBlock(event)); + } }); } @@ -425,21 +442,44 @@ function createRobotEventHandlerBlock( }); }); const fields: {[key: string]: any} = {}; - fields[FIELD_SENDER] = 'robot'; + fields[FIELD_SENDER] = SENDER_VALUE_ROBOT; fields[FIELD_EVENT_NAME] = event.name; const inputs: {[key: string]: any} = {}; return new toolboxItems.Block(BLOCK_NAME, extraState, fields, Object.keys(inputs).length ? inputs : null); } export function addMechanismEventHandlerBlocks( + workspace: Blockly.WorkspaceSvg, mechanismInRobot: storageModuleContent.MechanismInRobot, events: storageModuleContent.Event[], + eventHandlerBlocks: EventHandlerBlock[], contents: toolboxItems.ContentsType[]) { + // Collect the ids of events for which there is already an event handler. + const eventIds: string[] = []; + eventHandlerBlocks.forEach(eventHandlerBlock => { + eventIds.push(eventHandlerBlock.getEventId()); + }); events.forEach(event => { - contents.push(createMechanismEventHandlerBlock(mechanismInRobot, event)); + if (eventIds.includes(event.eventId)) { + // If there is already an event handler for this event, put a button in the toolbox. + contents.push(createButton(workspace, mechanismInRobot.name, event.name)); + } else { + contents.push(createMechanismEventHandlerBlock(mechanismInRobot, event)); + } }); } +function createButton( + workspace: Blockly.WorkspaceSvg, senderName: string, eventName: string): toolboxItems.Button { + // Use non-breakable spaces so it looks more like an event handler block. + const spaces = '\u00A0\u00A0'; + const text = workspace.RTL + ? (spaces + eventName + spaces + senderName + spaces + Blockly.Msg.WHEN + spaces) + : (spaces + Blockly.Msg.WHEN + spaces + senderName + spaces + eventName + spaces); + const style = BUTTON_STYLE_PREFIX + workspace.getTheme().name; + return new toolboxItems.Button(text, BUTTON_CALLBACK_KEY, style); +} + function createMechanismEventHandlerBlock( mechanismInRobot: storageModuleContent.MechanismInRobot, event: storageModuleContent.Event): toolboxItems.Block { @@ -502,3 +542,9 @@ export function renameMechanismName(workspace: Blockly.Workspace, mechanismId: s (block as EventHandlerBlock).renameMechanismName(mechanismId, newName); }); } + +export function registerToolboxButton(workspace: Blockly.WorkspaceSvg, messageApi: MessageInstance) { + workspace.registerButtonCallback(BUTTON_CALLBACK_KEY, function (_button) { + messageApi.info(Blockly.Msg.EVENT_HANDLER_ALREADY_ON_WORKSPACE); + }); +} diff --git a/src/blocks/tokens.ts b/src/blocks/tokens.ts index 45946aac..74496a39 100644 --- a/src/blocks/tokens.ts +++ b/src/blocks/tokens.ts @@ -35,6 +35,8 @@ export function customTokens(t: (key: string) => string): typeof Blockly.Msg { PARAMETER: t('BLOCKLY.PARAMETER'), PARAMETERS_CAN_ONLY_GO_IN_THEIR_METHODS_BLOCK: t('BLOCKLY.PARAMETERS_CAN_ONLY_GO_IN_THEIR_METHODS_BLOCK'), + EVENT_HANDLER_ALREADY_ON_WORKSPACE: + t('BLOCKLY.EVENT_HANDLER_ALREADY_ON_WORKSPACE'), MECHANISMS: t('MECHANISMS'), OPMODES: t('OPMODES'), COMPONENTS: t('BLOCKLY.COMPONENTS'), diff --git a/src/editor/editor.ts b/src/editor/editor.ts index a8b6fc0a..b431913f 100644 --- a/src/editor/editor.ts +++ b/src/editor/editor.ts @@ -174,6 +174,7 @@ export class Editor { private clearBlocklyWorkspace() { if (this.bindedOnChange) { this.blocklyWorkspace.removeChangeListener(this.bindedOnChange); + this.bindedOnChange = null; } this.blocklyWorkspace.hideChaff(); this.blocklyWorkspace.clear(); diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index feaffcd1..4ec9b695 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -45,6 +45,7 @@ "WHEN": "when", "PARAMETER": "parameter", "PARAMETERS_CAN_ONLY_GO_IN_THEIR_METHODS_BLOCK": "Parameters can only go in their method's block", + "EVENT_HANDLER_ALREADY_ON_WORKSPACE": "This event handler is already on the workspace.", "COMPONENTS": "Components", "EVENTS": "Events", "EVALUATE_BUT_IGNORE_RESULT": "evaluate but ignore result", diff --git a/src/i18n/locales/es/translation.json b/src/i18n/locales/es/translation.json index c2dbfd82..fad57cc3 100644 --- a/src/i18n/locales/es/translation.json +++ b/src/i18n/locales/es/translation.json @@ -46,6 +46,7 @@ "WHEN": "cuando", "PARAMETER": "parámetro", "PARAMETERS_CAN_ONLY_GO_IN_THEIR_METHODS_BLOCK": "Los parámetros solo pueden ir en el bloque de su método", + "EVENT_HANDLER_ALREADY_ON_WORKSPACE": "Este controlador de eventos ya está en el área de trabajo.", "COMPONENTS": "Componentes", "EVENTS": "Eventos", "EVALUATE_BUT_IGNORE_RESULT": "evaluar pero ignorar resultado", diff --git a/src/i18n/locales/he/translation.json b/src/i18n/locales/he/translation.json index 6b5f87b9..1ef0b4a8 100644 --- a/src/i18n/locales/he/translation.json +++ b/src/i18n/locales/he/translation.json @@ -45,6 +45,7 @@ "WHEN": "כאשר", "PARAMETER": "פרמטר", "PARAMETERS_CAN_ONLY_GO_IN_THEIR_METHODS_BLOCK": "פרמטרים יכולים להיכנס רק בבלוק השיטה שלהם", + "EVENT_HANDLER_ALREADY_ON_WORKSPACE": "מטפל אירועים זה כבר נמצא בסביבת העבודה.", "COMPONENTS": "רכיבים", "EVENTS": "אירועים", "EVALUATE_BUT_IGNORE_RESULT": "הערך אך התעלם מהתוצאה", diff --git a/src/themes/mrc_themes.ts b/src/themes/mrc_themes.ts index ea360e37..2cbe5152 100644 --- a/src/themes/mrc_themes.ts +++ b/src/themes/mrc_themes.ts @@ -4,7 +4,7 @@ import DeuteranopiaTheme from '@blockly/theme-deuteranopia'; import TritanopiaTheme from '@blockly/theme-tritanopia'; import HighContrastTheme from '@blockly/theme-highcontrast'; -import { add_mrc_styles } from './styles'; +import { add_mrc_styles, MRC_STYLE_EVENT_HANDLER } from './styles'; export const DARK_THEME_NAME = 'mrc_theme_dark'; export const LIGHT_THEME_NAME = 'mrc_theme_light'; @@ -39,7 +39,7 @@ const create_theme = function (name: string, base: Blockly.Theme, dark: boolean }; const create_themes = function (): Blockly.Theme[] { - return [ + const themes: Blockly.Theme[] = [ create_theme(DARK_THEME_NAME, Blockly.Themes.Classic, true), create_theme(LIGHT_THEME_NAME, Blockly.Themes.Classic), create_theme(DEUTERANOPIA_THEME_NAME, DeuteranopiaTheme), @@ -49,6 +49,30 @@ const create_themes = function (): Blockly.Theme[] { create_theme(TRITANOPIA_DARK_THEME_NAME, TritanopiaTheme, true), create_theme(HIGHCONTRAST_DARK_THEME_NAME, HighContrastTheme, true), ]; + + // Create CSS classes for event handler buttons, which are placed in the toolbox when an event + // handler already exists on the workspace. + let cssClasses = ''; + themes.forEach(theme => { + let fill = theme.blockStyles[MRC_STYLE_EVENT_HANDLER].colourPrimary; + if (!fill.startsWith('#')) { + try { + fill = Blockly.utils.colour.hueToHex(Number(fill)); + } catch (e) { + console.error( + 'Unable to determine event handler block color for theme ' + theme.name); + } + } + cssClasses += + '.eventHandlerButtonStyle_' + theme.name + ' {\n' + + ' fill: ' + fill + ';\n' + + '}\n'; + }); + const styleElement = document.createElement('style'); + styleElement.innerHTML = cssClasses; + document.head.appendChild(styleElement); + + return themes; }; -export const themes = create_themes(); \ No newline at end of file +export const themes = create_themes(); diff --git a/src/toolbox/event_handlers_category.ts b/src/toolbox/event_handlers_category.ts index c3edea40..16de9564 100644 --- a/src/toolbox/event_handlers_category.ts +++ b/src/toolbox/event_handlers_category.ts @@ -96,16 +96,8 @@ class EventHandlersCategory { // Get the list of events from the robot. const eventsFromRobot = editor.getEventsFromRobot(); - // Remove events if there is already a corresponding handler in the workspace. const eventHandlerBlocks = editor.getRobotEventHandlersAlreadyInWorkspace(); - const eventIds: string[] = []; - eventHandlerBlocks.forEach(eventHandlerBlock => { - eventIds.push(eventHandlerBlock.getEventId()); - }); - const eventsToShow = eventsFromRobot.filter(event => { - return !eventIds.includes(event.eventId); - }); - addRobotEventHandlerBlocks(eventsToShow, contents); + addRobotEventHandlerBlocks(workspace, eventsFromRobot, eventHandlerBlocks, contents); const toolboxInfo = { contents: contents, @@ -129,17 +121,10 @@ class EventHandlersCategory { const mechanism = editor.getMechanism(this.mechanismInRobot); if (mechanism) { const eventsFromMechanism = editor.getEventsFromMechanism(mechanism); - // Remove events if there is already a corresponding handler in the workspace. const eventHandlerBlocks = editor.getMechanismEventHandlersAlreadyInWorkspace( this.mechanismInRobot); - const eventIds: string[] = []; - eventHandlerBlocks.forEach(eventHandlerBlock => { - eventIds.push(eventHandlerBlock.getEventId()); - }); - const eventsToShow = eventsFromMechanism.filter(event => { - return !eventIds.includes(event.eventId); - }); - addMechanismEventHandlerBlocks(this.mechanismInRobot, eventsToShow, contents); + addMechanismEventHandlerBlocks( + workspace, this.mechanismInRobot, eventsFromMechanism, eventHandlerBlocks, contents); if (contents.length === 0) { const label : toolboxItems.Label = new toolboxItems.Label(Blockly.Msg['NO_MECHANISM_CONTENTS']); contents.push(label); diff --git a/src/toolbox/hardware_category.ts b/src/toolbox/hardware_category.ts index 3617e8cf..9df39e29 100644 --- a/src/toolbox/hardware_category.ts +++ b/src/toolbox/hardware_category.ts @@ -31,33 +31,29 @@ import { addInstanceMechanismBlocks } from '../blocks/mrc_call_python_function'; import { Editor } from '../editor/editor'; -export function getHardwareCategory( - editor: Editor): toolboxItems.Category { - +export function getHardwareCategory(editor: Editor): toolboxItems.Category { const moduleType = editor.getCurrentModuleType(); switch (moduleType) { case storageModule.ModuleType.ROBOT: - return { - kind: 'category', - name: Blockly.Msg['MRC_CATEGORY_HARDWARE'], - contents: [ - getRobotMechanismsCategory(editor), - getComponentsCategory(editor, moduleType), - ], - }; + return new toolboxItems.Category( + Blockly.Msg['MRC_CATEGORY_HARDWARE'], + [ + getRobotMechanismsCategory(editor), + getComponentsCategory(editor, moduleType), + ], + toolboxItems.ExpandedState.EXPANDED); case storageModule.ModuleType.MECHANISM: return getComponentsCategory(editor, moduleType); case storageModule.ModuleType.OPMODE: - return { - kind: 'category', - name: Blockly.Msg['MRC_CATEGORY_ROBOT'], - contents: [ - getRobotMechanismsCategory(editor), - getRobotComponentsCategory(editor), - getRobotMethodsCategory(editor), - getRobotEventHandlersCategory(editor), - ], - }; + return new toolboxItems.Category( + Blockly.Msg['MRC_CATEGORY_ROBOT'], + [ + getRobotMechanismsCategory(editor), + getRobotComponentsCategory(editor), + getRobotMethodsCategory(editor), + getRobotEventHandlersCategory(editor), + ], + toolboxItems.ExpandedState.EXPANDED); } throw new Error('moduleType has unexpected value: ' + moduleType); } diff --git a/src/toolbox/items.ts b/src/toolbox/items.ts index de135b71..a60a8d91 100644 --- a/src/toolbox/items.ts +++ b/src/toolbox/items.ts @@ -25,18 +25,39 @@ export class Item { } } +// TODO(lizlooney): Use new toolboxItems.Sep() to create separators in other files. + export class Sep extends Item { constructor() { super('sep'); } } +// TODO(lizlooney): Use new toolboxItems.Label() to create labels in other files. + export class Label extends Item { text: string; - constructor(text: string) { + constructor(text: string, webClass?: string) { super('label'); this.text = text; + if (webClass) { + (this as any)['web-class'] = webClass; + } + } +} + +export class Button extends Item { + text: string; + callbackkey: string; + + constructor(text: string, callbackkey: string, webClass?: string) { + super('button'); + this.text = text; + this.callbackkey = callbackkey; + if (webClass) { + (this as any)['web-class'] = webClass; + } } } @@ -68,23 +89,34 @@ export class Block extends Item { } } -export type ContentsType = Sep | Label | Block | Category; +export type ContentsType = Sep | Label | Button | Block | Category; + +export enum ExpandedState { + COLLAPSED, + EXPANDED, +} + +// TODO(lizlooney): Use new toolboxItems.Category() to create categories in other files. export class Category extends Item { /** The category name. */ name: string; + expanded?: boolean; categorystyle?: string; custom?: string; /** The blocks for this category. */ contents?: ContentsType[] = []; - constructor(name: string, contents: ContentsType[], categorystyle?: string, custom?: string) { + constructor(name: string, contents: ContentsType[], expandedState?: ExpandedState, categorystyle?: string, custom?: string) { super('category'); this.name = name; if (contents) { this.contents = contents; } + if (expandedState === ExpandedState.EXPANDED) { + this.expanded = true; + } if (categorystyle) { this.categorystyle = categorystyle; } @@ -97,14 +129,14 @@ export class Category extends Item { export class PythonModuleCategory extends Category { moduleName: string; - constructor(moduleName: string, name: string, contents: ContentsType[], categorystyle?: string, custom?: string) { - super(name, contents); - if (categorystyle) { - this.categorystyle = categorystyle; - } - if (custom) { - this.custom = custom; - } + constructor( + moduleName: string, + name: string, + contents: ContentsType[], + expandedState?: ExpandedState, + categorystyle?: string, + custom?: string) { + super(name, contents, expandedState, categorystyle, custom); this.moduleName = moduleName; } } @@ -112,15 +144,14 @@ export class PythonModuleCategory extends Category { export class PythonClassCategory extends Category { className: string; - constructor(className: string, name: string, contents: ContentsType[]); - constructor(className: string, name: string, contents: ContentsType[], categorystyle?: string, custom?: string) { - super(name, contents); - if (categorystyle) { - this.categorystyle = categorystyle; - } - if (custom) { - this.custom = custom; - } + constructor( + className: string, + name: string, + contents: ContentsType[], + expandedState?: ExpandedState, + categorystyle?: string, + custom?: string) { + super(name, contents, expandedState, categorystyle, custom); this.className = className; } } diff --git a/src/toolbox/toolbox.ts b/src/toolbox/toolbox.ts index 8f991ed9..fbedc431 100644 --- a/src/toolbox/toolbox.ts +++ b/src/toolbox/toolbox.ts @@ -1,5 +1,6 @@ import * as Blockly from 'blockly/core'; import * as storageModule from '../storage/module'; +import * as toolboxItems from './items'; import * as common from './toolbox_common' import { Editor } from '../editor/editor'; import { getHardwareCategory } from './hardware_category'; @@ -9,31 +10,27 @@ export function getToolboxJSON( shownPythonToolboxCategories: Set | null, editor: Editor): Blockly.utils.toolbox.ToolboxDefinition { + const toolbox: Blockly.utils.toolbox.ToolboxDefinition = { + kind: 'categoryToolbox', + contents: [] + }; + switch (editor.getCurrentModuleType()) { case storageModule.ModuleType.ROBOT: case storageModule.ModuleType.MECHANISM: - return { - kind: 'categoryToolbox', - contents: [ - getHardwareCategory(editor), - { kind: 'sep' }, - ...common.getToolboxItems(shownPythonToolboxCategories, editor), - getEventCategory(editor), - ] - }; + toolbox.contents.push(getHardwareCategory(editor)); + toolbox.contents.push(new toolboxItems.Sep()); + toolbox.contents.push(...common.getToolboxItems(shownPythonToolboxCategories, editor)); + toolbox.contents.push(getEventCategory(editor)); + break; case storageModule.ModuleType.OPMODE: - return { - kind: 'categoryToolbox', - contents: [ - getHardwareCategory(editor), - { kind: 'sep' }, - ...common.getToolboxItems(shownPythonToolboxCategories, editor) - ] - }; - default: - return { - kind: 'categoryToolbox', - contents: [] - }; + toolbox.contents.push(getHardwareCategory(editor)); + toolbox.contents.push(new toolboxItems.Sep()); + toolbox.contents.push(...common.getToolboxItems(shownPythonToolboxCategories, editor)); + break; } + + // Blockly has trouble with categories are created with new toolboxItem.Category(...). + // This trouble is prevented by stringifying and parsing. + return JSON.parse(JSON.stringify(toolbox)); }