UNPKG

@blockly/dev-tools

Version:

A library of common utilities for Blockly extension development.

1,084 lines (1,008 loc) 32.1 kB
/** * @license * Copyright 2020 Google LLC * SPDX-License-Identifier: Apache-2.0 */ /** * @fileoverview A helper to use the dat.GUI interface for configuring Blockly * workspace options. * @author samelh@google.com (Sam El-Husseini) */ import * as Blockly from 'blockly/core'; import * as dat from 'dat.gui'; import {DebugDrawer} from '../debugDrawer'; import {registerDebugRendererFromName, debugRendererName} from '../debug'; import {disableLogger, enableLogger} from '../logger'; import {HashState} from './hash_state'; import {populateRandom} from '../populateRandom'; import {spaghetti} from '../spaghetti'; import {id} from './id'; import toolboxCategories from '../toolboxCategories'; import toolboxSimple from '../toolboxSimple'; import darkTheme from '@blockly/theme-dark'; import deuteranopiaTheme from '@blockly/theme-deuteranopia'; import themeTritanopia from '@blockly/theme-tritanopia'; import highContrastTheme from '@blockly/theme-highcontrast'; const assign = require('lodash.assign'); const merge = require('lodash.merge'); /** * @typedef {Blockly.utils.toolbox.ToolboxDefinition} BlocklyToolbox */ /** * @typedef {Object} GUIConfig * @property {boolean} [disableResize] Whether or not to disable automatically * resizing the GUI control. * @property {Object<string,BlocklyToolbox>} [toolboxes] The toolbox registry. */ /** * Use dat.GUI to add controls to adjust configuration of a Blockly workspace. * @param {function(!Blockly.BlocklyOptions):Blockly.WorkspaceSvg} genWorkspace * A workspace creation method called every time the toolbox is * re-configured. * @param {Blockly.BlocklyOptions} defaultOptions The default workspace options * to use. * @param {GUIConfig=} config Optional GUI config. * @returns {dat.GUI} The dat.GUI instance. */ export function addGUIControls(genWorkspace, defaultOptions, config = {}) { // Initialize state. const guiState = loadGUIState(); // Initialize toolboxes. const toolboxes = /** @type {Object<string,Blockly.utils.toolbox.ToolboxDefinition>} */ ( config.toolboxes || { categories: toolboxCategories, simple: toolboxSimple, } ); const defaultToolboxName = initDefaultToolbox(defaultOptions, toolboxes); guiState.toolboxName = guiState.toolboxName || defaultToolboxName; guiState.options.toolbox = toolboxes[guiState.toolboxName]; // Initialize themes. const themes = getThemes(defaultOptions); const defaultThemeName = defaultOptions.theme ? /** @type {!Blockly.Theme} */ (defaultOptions.theme).name : 'classic'; guiState.themeName = guiState.themeName || defaultThemeName; guiState.options.theme = themes[guiState.themeName]; const defaultRendererName = defaultOptions.renderer ? defaultOptions.renderer : 'geras'; guiState.renderer = guiState.renderer || defaultRendererName; guiState.debugEnabled = guiState.debugEnabled || false; // Merge default and saved state. const saveOptions = /** @type {!Blockly.BlocklyOptions} */ ({ ...defaultOptions, ...guiState.options, }); initDebugRenderer(guiState); let workspace = genWorkspace(saveOptions); const resizeEnabled = !config.disableResize; const gui = new dat.GUI({ autoPlace: false, closeOnTop: true, width: 250, load: guiState.guiObject || {}, }); const guiElement = gui.domElement; guiElement.style.position = 'absolute'; guiElement.style.zIndex = '1000'; guiElement.onclick = () => { // Save the GUI state locally. guiState.guiObject = gui.getSaveObject(); saveGUIState(guiState, defaultToolboxName, defaultThemeName); }; const onResize = () => { const metrics = workspace.getMetrics(); if (workspace.RTL) { guiElement.style.left = metrics.absoluteLeft + 'px'; guiElement.style.right = 'auto'; } else { guiElement.style.left = 'auto'; if (metrics.toolboxPosition === Blockly.TOOLBOX_AT_RIGHT) { guiElement.style.right = metrics.toolboxWidth + 'px'; } else { guiElement.style.right = '0'; } } guiElement.style.top = metrics.absoluteTop + 'px'; }; if (resizeEnabled) { onResize(); } const container = /** @type {!HTMLElement} */ ( workspace.getInjectionDiv().parentNode ); container.style.position = 'relative'; container.appendChild(guiElement); const options = Object.assign({}, workspace.options); const onChangeInternal = () => { // Serialize current workspace state. const state = Blockly.serialization.workspaces.save(workspace); // Dispose of the current workspace workspace.dispose(); // Create a new workspace with options. workspace = genWorkspace(saveOptions); // Deserialize state into workspace. Blockly.serialization.workspaces.load(state, workspace); // Resize the gui. if (resizeEnabled) { onResize(); } // Save the GUI state locally. guiState.guiObject = gui.getSaveObject(); saveGUIState(guiState, defaultToolboxName, defaultThemeName); // Update options. merge(saveOptions, workspace.options); gui.updateDisplay(); }; const onChange = (key, value) => { saveOptions[key] = value; guiState.options[key] = value; onChangeInternal(); }; const reset = () => { // Reset saved options. Object.keys(saveOptions).forEach((k) => delete saveOptions[k]); assign(saveOptions, defaultOptions); Object.keys(guiState.options).forEach((k) => delete guiState.options[k]); // Reset debug options. guiState.renderer = defaultRendererName; initDebugRenderer(guiState, true); updateDebugFolder(debugOptionsFolder, false); // Reset toolbox selection. guiState.toolboxName = defaultToolboxName; guiState.themeName = defaultThemeName; // Close all folders. Object.values(gui.__folders).forEach((f) => f.close()); onChangeInternal(); }; gui.add( { Reset: reset, }, 'Reset', ); // Options folder. const optionsFolder = gui.addFolder('Options'); setTooltip( optionsFolder, 'Options that affect the appearance of the workspace.', ); openFolderIfOptionSelected(optionsFolder, guiState, guiState.options, [ 'rtl', 'renderer', 'toolboxPosition', 'horizontalLayout', ]); setTooltip( optionsFolder .add(options, 'RTL') .name('rtl') .onChange((value) => onChange('rtl', value)), 'If true, mirror the editor (for Arabic or Hebrew locales).', ); // Renderer. populateRendererOption(optionsFolder, guiState, onChange); // Theme. populateThemeOption( optionsFolder, guiState, themes, defaultThemeName, onChange, ); // Toolbox. populateToolboxOption( optionsFolder, guiState, toolboxes, defaultToolboxName, onChange, ); populateToolboxSidesOption( optionsFolder, options, saveOptions, guiState, onChangeInternal, ); // Basic options. const basicFolder = optionsFolder.addFolder('Basic'); setTooltip(basicFolder, 'Basic options like sound, comment etc...'); populateBasicOptions(basicFolder, options, guiState, onChange); // Move options. const moveFolder = optionsFolder.addFolder('Move'); setTooltip(moveFolder, 'Move options like scrollbars, drag etc...'); populateMoveOptions(moveFolder, options, saveOptions, onChange); openFolderIfOptionSelected(moveFolder, guiState, guiState.options, ['move']); // Zoom options. const zoomFolder = optionsFolder.addFolder('Zoom'); setTooltip(zoomFolder, 'Zoom options like controls, startScale etc...'); populateZoomOptions(zoomFolder, options, saveOptions, onChange); openFolderIfOptionSelected(moveFolder, guiState, guiState.options, ['zoom']); // Grid options. const gridFolder = optionsFolder.addFolder('Grid'); setTooltip(gridFolder, 'Grid options like spacing, length etc...'); populateGridOptions(gridFolder, options, saveOptions, onChange); openFolderIfOptionSelected(moveFolder, guiState, guiState.options, ['grid']); // Debug renderer. const debugFolder = gui.addFolder('Debug'); // Adds the checkbox to toggle using the debug renderer. const debugController = debugFolder.add(guiState, 'debugEnabled'); // Folder with all the debug options. Hidden if the debugger is not enabled. const debugOptionsFolder = debugFolder.addFolder('Debug Options'); setTooltip(debugFolder, 'Rendering debug configuration.'); populateDebugFolder(debugController, debugOptionsFolder, guiState, onChange); populateDebugOptionsFolder(debugOptionsFolder, guiState, onChangeInternal); // GUI actions. const actionsFolder = gui.addFolder('Actions'); const actionSubFolders = {}; const actions = {}; /** * Get the current workspace. * @returns {!Blockly.WorkspaceSvg} The Blockly workspace. */ const getWorkspace = () => { return workspace; }; /** * Add a custom action to the list of playground actions. * @param {string} name The action label. * @param {function(!Blockly.Workspace):void} callback The callback to call * when the action is clicked. * @param {string=} folderName Optional folder to place the action under. * @param {string=} tooltip Optional tooltip to set. * @returns {dat.GUIController} The GUI controller. */ const addAction = (name, callback, folderName, tooltip) => { actions[name] = () => { callback(workspace); }; let folder = actionsFolder; if (folderName) { if (actionSubFolders[folderName]) { folder = actionSubFolders[folderName]; } else { folder = actionsFolder.addFolder(folderName); folder.open(); actionSubFolders[folderName] = folder; } } const controller = folder.add(actions, name); tooltip && setTooltip(controller, tooltip); name && controller.name(name); return controller; }; /** * Add a custom checkbox action to the list of playground actions. * @param {string} name The action label. * @param {function(!Blockly.Workspace,boolean):void} callback The callback to * call when the action is clicked. * @param {string=} folderName Optional folder to place the action under. * @param {boolean=} defaultValue Default value. * @param {string=} tooltip Optional tooltip to set. * @returns {dat.GUIController} The GUI controller. */ const addCheckboxAction = ( name, callback, folderName, defaultValue, tooltip, ) => { actions[name] = !!defaultValue; let folder = actionsFolder; if (folderName) { if (actionSubFolders[folderName]) { folder = actionSubFolders[folderName]; } else { folder = actionsFolder.addFolder(folderName); folder.open(); actionSubFolders[folderName] = folder; } } const controller = folder.add(actions, name); tooltip && setTooltip(controller, tooltip); name && controller.name(name); controller.listen().onFinishChange((value) => { callback(workspace, value); }); return controller; }; const devGui = /** @type {?} */ (gui); devGui.addCheckboxAction = addCheckboxAction; devGui.addAction = addAction; devGui.getWorkspace = getWorkspace; addActions(devGui, workspace); return devGui; } /** * Save the GUI state to local storage and the window hash. * @param {Object} guiState The GUI State. * @param {string} defaultToolboxName The default toolbox name. * @param {string} defaultThemeName The default theme name. */ function saveGUIState(guiState, defaultToolboxName, defaultThemeName) { // Don't save toolbox and theme, as we'll save their names instead. delete guiState.options['toolbox']; delete guiState.options['theme']; if (guiState.debugEnabled) { // In this case guiState.options.renderer is 'debugRenderer'. Storing this // is not helpful, so instead store the actual name of the renderer. guiState.options.renderer = guiState.renderer; } // Save GUI control options to local storage. const guiStateKey = `guiState_${id}`; localStorage.setItem(guiStateKey, JSON.stringify(guiState)); // Save GUI state into the URL: const hashGuiState = Object.assign({}, guiState.options); if (guiState.toolboxName !== defaultToolboxName) { hashGuiState.toolbox = guiState.toolboxName; } if (guiState.themeName !== defaultThemeName) { hashGuiState.theme = guiState.themeName; } // Save whether the debug is enabled. hashGuiState.debugEnabled = guiState.debugEnabled; window.location.hash = HashState.save(hashGuiState); } /** * Load the GUI state from local storage and the window hash. * @returns {Object} The GUI state. */ function loadGUIState() { const defaultState = {options: {}, debug: {}, debugEnabled: false}; const guiStateKey = `guiState_${id}`; const guiState = JSON.parse(localStorage.getItem(guiStateKey)) || defaultState; if (window.location.hash) { HashState.parse(window.location.hash, guiState.options); } // Move GUI toolbox state out of options, as it refers to the toolbox name // and not the toolbox value. if (guiState.options.toolbox) { guiState.toolboxName = guiState.options.toolbox; delete guiState.options.toolbox; } // Move GUI theme state out of options, as it refers to the theme name // and not the theme object. if (guiState.options.theme) { guiState.themeName = guiState.options.theme; delete guiState.options.theme; } // If we end up using the 'debugRenderer' we still need to store the name of // the original renderer. guiState.renderer = guiState.options.renderer; // Use the debug renderer. if (guiState.debugEnabled) { guiState.options.renderer = debugRendererName; } return guiState; } /** * Open a dat.GUI folder and all of its parents if one of the options is present * in the GUI state. * @param {dat.GUI} folder The GUI folder. * @param {Object} guiState GUI state. * @param {Object} mainObj The main object to check options against. * @param {Array<string>} options The options to check. */ function openFolderIfOptionSelected(folder, guiState, mainObj, options) { if (guiState.guiObject) { // The GUI state is controlling the folder state. return; } options.forEach((option) => { if (mainObj[option] != undefined) { folder.open(); while (folder.parent) { folder = folder.parent; folder.open(); } } }); } /** * Initialize the default toolbox. If the default toolbox is not in the list of * toolboxes, add a "default" option to the toolbox list. * @param {Blockly.BlocklyOptions} defaultOptions Default Blockly options. * @param {Object<string,Blockly.utils.toolbox.ToolboxDefinition>} toolboxes The * registered toolboxes. * @returns {string} The default toolbox name. */ function initDefaultToolbox(defaultOptions, toolboxes) { const defaultToolbox = defaultOptions.toolbox; const isDefaultInToolboxes = Object.keys(toolboxes).filter( (k) => toolboxes[k] == defaultToolbox, ); if (defaultToolbox && !isDefaultInToolboxes.length) { // Default toolbox not in the toolbox list. Add a "default" option. toolboxes['default'] = defaultToolbox; return 'default'; } else if (isDefaultInToolboxes.length) { // Get the return isDefaultInToolboxes[0]; } else { // No default toolbox set, choose the first one. return Object.keys(toolboxes)[0]; } } /** * Populate basic options. * @param {dat.GUI} basicFolder The dat.GUI basic folder. * @param {Blockly.Options} options Blockly options. * @param {Object} guiState The GUI state. * @param {function(string, *):void} onChange On Change method. */ function populateBasicOptions(basicFolder, options, guiState, onChange) { setTooltip( basicFolder .add(options, 'readOnly') .onChange((value) => onChange('readOnly', value)), 'If true, prevent the user from editing. Suppresses the toolbox and' + ' trashcan.', ); setTooltip( basicFolder .add(options, 'hasTrashcan') .name('trashCan') .onChange((value) => onChange('trashcan', value)), 'Displays or hides the trashcan.', ); setTooltip( basicFolder .add(options, 'hasSounds') .name('sounds') .onChange((value) => onChange('sounds', value)), `If false, don't play sounds (e.g. click and delete).`, ); setTooltip( basicFolder .add(options, 'disable') .onChange((value) => onChange('disable', value)), 'Allows blocks to be disabled. ', ); setTooltip( basicFolder .add(options, 'collapse') .onChange((value) => onChange('collapse', value)), 'Allows blocks to be collapsed or expanded.', ); setTooltip( basicFolder .add(options, 'comments') .onChange((value) => onChange('comments', value)), 'Allows blocks to have comments.', ); openFolderIfOptionSelected(basicFolder, guiState, guiState.options, [ 'readOnly', 'trashcan', 'sounds', 'disable', 'collapse', 'comments', ]); } /** * Populate the renderer option. * @param {dat.GUI} folder The dat.GUI folder. * @param {Object} guiState The GUI state. * @param {function(string, *):void} onChange On Change method. */ function populateRendererOption(folder, guiState, onChange) { // Get the list of renderers. Previous versions of Blockly used the // rendererMap_, whereas newer versions that use the global registry get their // list of renderers from somewhere else. const renderers = Blockly.blockRendering.rendererMap_ || (Blockly.registry && Blockly.registry.getAllItems('renderer')); const publicRenderers = Object.keys(renderers).filter( (name) => name !== debugRendererName.toLowerCase(), ); setTooltip( folder.add(guiState, 'renderer', publicRenderers).onChange((value) => { guiState.renderer = value; registerDebugRendererFromName(value); onChange('renderer', guiState.debugEnabled ? debugRendererName : value); }), 'The renderer used by Blockly.', ); } /** * Populate the toolbox option. * @param {dat.GUI} folder The dat.GUI folder. * @param {Object} guiState The GUI state. * @param {Object<string,Blockly.utils.toolbox.ToolboxDefinition>} toolboxes The * registered toolboxes. * @param {string} defaultToolboxName The default toolbox name. * @param {function(string, *):void} onChange On Change method. */ function populateToolboxOption( folder, guiState, toolboxes, defaultToolboxName, onChange, ) { setTooltip( folder .add(guiState, 'toolboxName') .options(Object.keys(toolboxes)) .name('toolbox') .onChange((value) => { guiState.toolboxName = value; onChange('toolbox', toolboxes[value]); }), 'The toolbox used by Blockly.', ); if (guiState.toolboxName !== defaultToolboxName) { openFolderIfOptionSelected(folder, guiState, guiState.options, ['toolbox']); } } /** * Populate the toolbox sides option. * @param {dat.GUI} folder The dat.GUI folder. * @param {Blockly.Options} options Blockly options. * @param {Blockly.BlocklyOptions} saveOptions Saved Blockly options. * @param {Object} guiState GUI state. * @param {function():void} onChangeInternal Internal on change method. */ function populateToolboxSidesOption( folder, options, saveOptions, guiState, onChangeInternal, ) { const toolboxSides = {top: 0, bottom: 1, left: 2, right: 3}; setTooltip( folder .add(options, 'toolboxPosition', toolboxSides) .name('toolboxPosition') .onChange((value) => { const side = Object.keys(toolboxSides).find( (key) => toolboxSides[key] == value, ); saveOptions['horizontalLayout'] = side == 'top' || side == 'bottom'; saveOptions['toolboxPosition'] = side == 'top' || side == 'left' ? 'start' : 'end'; guiState.options['toolboxPosition'] = saveOptions['toolboxPosition']; guiState.options['horizontalLayout'] = saveOptions['horizontalLayout']; onChangeInternal(); }), 'The toolbox position.', ); } /** * Get the list of Blockly themes. * @param {Blockly.BlocklyOptions} defaultOptions Default Blockly options. * @returns {Object<string,Blockly.Theme>} The list of registered themes. */ function getThemes(defaultOptions) { let themes; if (Blockly.registry && Blockly.registry.getAllItems('theme')) { // Using a version of Blockly that registers themes. themes = Blockly.registry.getAllItems('theme'); } else { // Fall back to a pre-set list of themes. themes = { classic: Blockly.Themes.Classic, dark: darkTheme, deuteranopia: deuteranopiaTheme, tritanopia: themeTritanopia, highcontrast: highContrastTheme, }; if (defaultOptions.theme) { themes[/** @type {!Blockly.Theme} */ (defaultOptions.theme).name] = defaultOptions.theme; } } return themes; } /** * Populate the theme option. * @param {dat.GUI} folder The dat.GUI folder. * @param {Object} guiState The GUI state. * @param {Object<string,Blockly.Theme>} themes The list of * themes. * @param {string} defaultThemeName Default Theme name. * @param {function(string, *):void} onChange On Change method. */ function populateThemeOption( folder, guiState, themes, defaultThemeName, onChange, ) { setTooltip( folder .add(guiState, 'themeName') .options(Object.keys(themes)) .name('theme') .onChange((value) => { guiState.themeName = value; onChange('theme', themes[value]); }), 'The theme used by Blockly.', ); if (guiState.themeName !== defaultThemeName) { openFolderIfOptionSelected(folder, guiState, guiState.options, ['theme']); } } /** * Populate move options. * @param {dat.GUI} moveFolder The dat.GUI move options folder. * @param {Blockly.Options} options Blockly options. * @param {Blockly.BlocklyOptions} saveOptions Saved Blockly options. * @param {function(string, *):void} onChange On Change method. */ function populateMoveOptions(moveFolder, options, saveOptions, onChange) { setTooltip( moveFolder.add(options.moveOptions, 'scrollbars').onChange((value) => onChange('move', { ...saveOptions.move, scrollbars: value, }), ), 'True if the workspace has scrollbars.', ); setTooltip( moveFolder.add(options.moveOptions, 'wheel').onChange((value) => onChange('move', { ...saveOptions.move, wheel: value, }), ), 'True if the workspace can be scrolled with the mouse wheel.', ); setTooltip( moveFolder.add(options.moveOptions, 'drag').onChange((value) => onChange('move', { ...saveOptions.move, drag: value, }), ), 'True if the workspace can be dragged with the mouse.', ); } /** * Populate zoom options. * @param {dat.GUI} zoomFolder The dat.GUI zoom options folder. * @param {Blockly.Options} options Blockly options. * @param {Blockly.BlocklyOptions} saveOptions Saved Blockly options. * @param {function(string, *):void} onChange On Change method. */ function populateZoomOptions(zoomFolder, options, saveOptions, onChange) { setTooltip( zoomFolder.add(options.zoomOptions, 'controls').onChange((value) => onChange('zoom', { ...saveOptions.zoom, controls: value, }), ), 'Set to true to show zoom-centre, zoom-in, and zoom-out buttons.', ); setTooltip( zoomFolder.add(options.zoomOptions, 'wheel').onChange((value) => onChange('zoom', { ...saveOptions.zoom, wheel: value, }), ), 'Set to true to allow the mouse wheel to zoom.', ); setTooltip( zoomFolder .add(options.zoomOptions, 'startScale', 0.1, 4) .onChange((value) => onChange('zoom', { ...saveOptions.zoom, startScale: value, }), ), 'Initial magnification factor. For applications with multiple levels,' + ' startScale is often set to a higher value on the first level, then' + ' incrementally decreased as subsequent levels become more complex.', ); setTooltip( zoomFolder .add(options.zoomOptions, 'maxScale', 1, 20) .onChange((value) => onChange('zoom', { ...saveOptions.zoom, maxScale: value, }), ) .step(1), 'Maximum multiplication factor for how far one can zoom in.', ); setTooltip( zoomFolder .add(options.zoomOptions, 'minScale', 0.1, 1) .onChange((value) => onChange('zoom', { ...saveOptions.zoom, minScale: value, }), ) .step(0.05), 'Minimum multiplication factor for how far one can zoom out.', ); } /** * Populate grid options. * @param {dat.GUI} gridFolder The dat.GUI grid options folder. * @param {Blockly.Options} options Blockly options. * @param {Blockly.BlocklyOptions} saveOptions Saved Blockly options. * @param {function(string, *):void} onChange On Change method. */ function populateGridOptions(gridFolder, options, saveOptions, onChange) { setTooltip( gridFolder.add(options.gridOptions, 'spacing', 0, 50).onChange((value) => onChange('grid', { ...saveOptions.grid, spacing: value, }), ), `The distance between the grid's points.`, ); setTooltip( gridFolder.add(options.gridOptions, 'length', 0, 30).onChange((value) => onChange('grid', { ...saveOptions.grid, length: value, }), ), 'The shape of the grid points. A length of 0 results in an invisible' + ' grid (but still one that may be snapped to), a length of 1 (the' + ' default value) results in dots, a longer length results in crosses, ' + 'and a length equal or greater than the spacing results in graph paper.', ); setTooltip( gridFolder.addColor(options.gridOptions, 'colour').onChange((value) => onChange('grid', { ...saveOptions.grid, colour: value, }), ), 'The colour of the grid points.', ); setTooltip( gridFolder.add(options.gridOptions, 'snap').onChange((value) => onChange('grid', { ...saveOptions.grid, snap: value, }), ), 'Whether blocks should snap to the nearest grid point when placed on' + ' the workspace.', ); } /** * Set a tooltip on a GUI folder or controller. * @param {dat.GUI|dat.GUIController} controller GUI folder or controller. * @param {string} tooltip Tooltip string. */ function setTooltip(controller, tooltip) { const parentElement = controller.domElement.parentElement; parentElement.setAttribute('title', tooltip); } /** * Initialize debug renderer. * @param {!Object} guiState The GUI State. * @param {boolean=} reset Whether or not to reset the renderer config. */ function initDebugRenderer(guiState, reset) { const guiDebugState = guiState.debug; registerDebugRendererFromName(guiState.renderer); Object.keys(DebugDrawer.config).map((key) => { if (guiDebugState[key] == undefined || reset) { guiDebugState[key] = false; } DebugDrawer.config[key] = guiDebugState[key]; }); if (reset) { guiState.debugEnabled = false; } } /** * Populate the parent folder that holds the debug enabled button and the * folder with all the debug options in it. When debug is enabled it should * open the folder holding all the debug options. * @param {dat.GUIController} debugController The GUI controller. * @param {dat.GUI} debugOptionsFolder The folder that holds all the debug * options. * @param {Object} guiState The GUI State. * @param {function(string, *):void} onChange On Change method. */ function populateDebugFolder( debugController, debugOptionsFolder, guiState, onChange, ) { updateDebugFolder(debugOptionsFolder, guiState.debugEnabled); debugController.onChange((value) => { guiState.debugEnabled = value; onChange('renderer', value ? debugRendererName : guiState.renderer); updateDebugFolder(debugOptionsFolder, guiState.debugEnabled); }); } /** * Updates the debug folder to be hidden/displayed based on whether the * rendering debugger is enabled. * @param {dat.GUI} folder The folder that holds all the debug * options. * @param {boolean} isEnabled True if the debugger is enabled, false otherwise. */ function updateDebugFolder(folder, isEnabled) { if (isEnabled) { folder.show(); folder.open(); } else { folder.hide(); } } /** * Populates the folder holding all the debug renderer options. * @param {dat.GUI} debugOptionsFolder The folder that holds all the debug * options. * @param {Object} guiState The GUI State. * @param {function():void} onChangeInternal Internal on change method. */ function populateDebugOptionsFolder( debugOptionsFolder, guiState, onChangeInternal, ) { const debugState = guiState.debug; Object.keys(DebugDrawer.config).map((key) => { debugOptionsFolder.add(debugState, key, 0, 50).onChange((value) => { debugState[key] = value; DebugDrawer.config[key] = debugState[key]; onChangeInternal(); }); }); } /** * Add default actions to the GUI instance. * @param {?} gui The GUI instance. * @param {!Blockly.WorkspaceSvg} workspace The Blockly workspace. */ function addActions(gui, workspace) { // Visibility actions. gui.addAction( 'Show', (workspace) => { workspace.setVisible(true); }, 'Visibility', 'Show the workspace.', ); gui.addAction( 'Hide', (workspace) => { workspace.setVisible(false); }, 'Visibility', 'Hide the workspace.', ); // Block actions. gui.addAction( 'Clear', (workspace) => { workspace.clear(); }, 'Blocks', 'Clear all the blocks from the workspace.', ); gui.addAction( 'Format', (workspace) => { workspace.cleanUp(); }, 'Blocks', 'Format the blocks on the workspace.', ); // Undo/Redo actions. gui.addAction( 'Undo', (workspace) => { workspace.undo(); }, 'Undo/Redo', 'Undo last action.', ); gui.addAction( 'Redo', (workspace) => { workspace.undo(true); }, 'Undo/Redo', 'Redo last action.', ); gui.addAction( 'Clear Undo Stack', (workspace) => { workspace.clearUndo(); }, 'Undo/Redo', 'Clear the undo stack.', ); // Scale actions. gui.addAction( 'Zoom reset', (workspace) => { workspace.setScale(workspace.options.zoomOptions.startScale); workspace.scrollCenter(); }, 'Scale', 'Reset zoom.', ); gui.addAction( 'Zoom center', (workspace) => { workspace.scrollCenter(); }, 'Scale', 'Center the workspace.', ); gui.addAction( 'Zoom to Fit', (workspace) => { workspace.zoomToFit(); }, 'Scale', 'Zoom the blocks to fit in the workspace if possible.', ); // Stress Test. gui.addAction( 'Random Blocks', (workspace) => { populateRandom(workspace, 100); }, 'Stress Test', 'Populate the workspace with a random set of blocks, for testing.', ); gui.addAction( 'Spaghetti!', (workspace) => { spaghetti(workspace, 8); }, 'Stress Test', 'Populate the workspace with nested if-statement blocks, for testing.', ); // Logging. gui.addCheckboxAction( 'Log Events', function (workspace, value) { if (value) { enableLogger(workspace); } else { disableLogger(workspace); } }, 'Logging', false, 'Toggle console logging of workspace events.', ); gui.addCheckboxAction( 'Log Flyout Events', function (workspace, value) { if (value) { if (workspace.getFlyout()) { enableLogger(workspace.getFlyout().getWorkspace()); } } else { if (workspace.getFlyout()) { disableLogger(workspace.getFlyout().getWorkspace()); } } }, 'Logging', false, 'Toggle console logging of flyout events.', ); }