From eeaad336ae70b0b95e9a194d6b4b87727b179db2 Mon Sep 17 00:00:00 2001 From: Matt Hillsdon Date: Mon, 7 Apr 2025 13:39:44 +0100 Subject: [PATCH] refactor: move editable/navigable checks to navigation Less indirection and more freedom for actions to pick finer grained preconditions. --- src/actions/action_menu.ts | 16 ++---- src/actions/arrow_navigation.ts | 17 ++++--- src/actions/clipboard.ts | 19 ++------ src/actions/delete.ts | 20 ++------ src/actions/disconnect.ts | 16 ++---- src/actions/edit.ts | 6 +-- src/actions/enter.ts | 8 ++- src/actions/exit.ts | 10 ++-- src/actions/insert.ts | 15 +----- src/actions/mover.ts | 11 ++--- src/actions/ws_movement.ts | 26 +++++----- src/navigation.ts | 31 ++++++++++++ src/navigation_controller.ts | 86 ++++++--------------------------- 13 files changed, 99 insertions(+), 182 deletions(-) diff --git a/src/actions/action_menu.ts b/src/actions/action_menu.ts index 349da56f..2f7ed8c2 100644 --- a/src/actions/action_menu.ts +++ b/src/actions/action_menu.ts @@ -30,23 +30,12 @@ export interface ScopeWithConnection extends ContextMenuRegistry.Scope { * Keyboard shortcut to show the action menu on Cmr/Ctrl/Alt+Enter key. */ export class ActionMenu { - /** - * Function provided by the navigation controller to say whether navigation - * is allowed. - */ - private canCurrentlyNavigate: (ws: WorkspaceSvg) => boolean; - /** * Registration name for the keyboard shortcut. */ private shortcutName = Constants.SHORTCUT_NAMES.MENU; - constructor( - private navigation: Navigation, - canNavigate: (ws: WorkspaceSvg) => boolean, - ) { - this.canCurrentlyNavigate = canNavigate; - } + constructor(private navigation: Navigation) {} /** * Install this action. @@ -68,7 +57,8 @@ export class ActionMenu { private registerShortcut() { const menuShortcut: ShortcutRegistry.KeyboardShortcut = { name: Constants.SHORTCUT_NAMES.MENU, - preconditionFn: (workspace) => this.canCurrentlyNavigate(workspace), + preconditionFn: (workspace) => + this.navigation.canCurrentlyNavigate(workspace), callback: (workspace) => { switch (this.navigation.getState(workspace)) { case Constants.STATE.WORKSPACE: diff --git a/src/actions/arrow_navigation.ts b/src/actions/arrow_navigation.ts index 16cc3828..0bae1c83 100644 --- a/src/actions/arrow_navigation.ts +++ b/src/actions/arrow_navigation.ts @@ -17,10 +17,7 @@ const KeyCodes = BlocklyUtils.KeyCodes; * Class for registering shortcuts for navigating the workspace with arrow keys. */ export class ArrowNavigation { - constructor( - private navigation: Navigation, - private canCurrentlyNavigate: (ws: WorkspaceSvg) => boolean, - ) {} + constructor(private navigation: Navigation) {} /** * Gives the cursor to the field to handle if the cursor is on a field. @@ -56,7 +53,8 @@ export class ArrowNavigation { /** Go to the next location to the right. */ right: { name: Constants.SHORTCUT_NAMES.RIGHT, - preconditionFn: (workspace) => this.canCurrentlyNavigate(workspace), + preconditionFn: (workspace) => + this.navigation.canCurrentlyNavigate(workspace), callback: (workspace, e, shortcut) => { const toolbox = workspace.getToolbox() as Toolbox; let isHandled = false; @@ -93,7 +91,8 @@ export class ArrowNavigation { /** Go to the next location to the left. */ left: { name: Constants.SHORTCUT_NAMES.LEFT, - preconditionFn: (workspace) => this.canCurrentlyNavigate(workspace), + preconditionFn: (workspace) => + this.navigation.canCurrentlyNavigate(workspace), callback: (workspace, e, shortcut) => { const toolbox = workspace.getToolbox() as Toolbox; let isHandled = false; @@ -128,7 +127,8 @@ export class ArrowNavigation { /** Go down to the next location. */ down: { name: Constants.SHORTCUT_NAMES.DOWN, - preconditionFn: (workspace) => this.canCurrentlyNavigate(workspace), + preconditionFn: (workspace) => + this.navigation.canCurrentlyNavigate(workspace), callback: (workspace, e, shortcut) => { const toolbox = workspace.getToolbox() as Toolbox; const flyout = workspace.getFlyout(); @@ -169,7 +169,8 @@ export class ArrowNavigation { /** Go up to the previous location. */ up: { name: Constants.SHORTCUT_NAMES.UP, - preconditionFn: (workspace) => this.canCurrentlyNavigate(workspace), + preconditionFn: (workspace) => + this.navigation.canCurrentlyNavigate(workspace), callback: (workspace, e, shortcut) => { const flyout = workspace.getFlyout(); const toolbox = workspace.getToolbox() as Toolbox; diff --git a/src/actions/clipboard.ts b/src/actions/clipboard.ts index d293e3bb..197e8d3d 100644 --- a/src/actions/clipboard.ts +++ b/src/actions/clipboard.ts @@ -44,18 +44,7 @@ export class Clipboard { /** The workspace a copy or cut keyboard shortcut happened in. */ private copyWorkspace: WorkspaceSvg | null = null; - /** - * Function provided by the navigation controller to say whether editing - * is allowed. - */ - private canCurrentlyEdit: (ws: WorkspaceSvg) => boolean; - - constructor( - private navigation: Navigation, - canEdit: (ws: WorkspaceSvg) => boolean, - ) { - this.canCurrentlyEdit = canEdit; - } + constructor(private navigation: Navigation) {} /** * Install these actions as both keyboard shortcuts and context menu items. @@ -141,7 +130,7 @@ export class Clipboard { * @returns True iff `cutCallback` function should be called. */ private cutPrecondition(workspace: WorkspaceSvg) { - if (this.canCurrentlyEdit(workspace)) { + if (this.navigation.canCurrentlyEdit(workspace)) { const curNode = workspace.getCursor()?.getCurNode(); if (curNode && curNode.getSourceBlock()) { const sourceBlock = curNode.getSourceBlock(); @@ -236,7 +225,7 @@ export class Clipboard { * @returns True iff `copyCallback` function should be called. */ private copyPrecondition(workspace: WorkspaceSvg) { - if (!this.canCurrentlyEdit(workspace)) return false; + if (!this.navigation.canCurrentlyEdit(workspace)) return false; switch (this.navigation.getState(workspace)) { case Constants.STATE.WORKSPACE: { const curNode = workspace?.getCursor()?.getCurNode(); @@ -348,7 +337,7 @@ export class Clipboard { private pastePrecondition(workspace: WorkspaceSvg) { if (!this.copyData || !this.copyWorkspace) return false; - return this.canCurrentlyEdit(workspace) && !Gesture.inProgress(); + return this.navigation.canCurrentlyEdit(workspace) && !Gesture.inProgress(); } /** diff --git a/src/actions/delete.ts b/src/actions/delete.ts index ba341e2a..ce6207c3 100644 --- a/src/actions/delete.ts +++ b/src/actions/delete.ts @@ -28,23 +28,12 @@ export class DeleteAction { */ private oldContextMenuItem: ContextMenuRegistry.RegistryItem | null = null; - /** - * Function provided by the navigation controller to say whether editing - * is allowed. - */ - private canCurrentlyEdit: (ws: WorkspaceSvg) => boolean; - /** * Registration name for the keyboard shortcut. */ private deleteShortcutName = Constants.SHORTCUT_NAMES.DELETE; - constructor( - private navigation: Navigation, - canEdit: (ws: WorkspaceSvg) => boolean, - ) { - this.canCurrentlyEdit = canEdit; - } + constructor(private navigation: Navigation) {} /** * Install this action as both a keyboard shortcut and a context menu item. @@ -148,10 +137,11 @@ export class DeleteAction { * @returns True iff `deleteCallback` function should be called. */ private deletePrecondition(workspace: WorkspaceSvg) { - if (!this.canCurrentlyEdit(workspace)) return false; - const sourceBlock = workspace.getCursor()?.getCurNode()?.getSourceBlock(); - return !!sourceBlock?.isDeletable(); + return ( + this.navigation.canCurrentlyEdit(workspace) && + !!sourceBlock?.isDeletable() + ); } /** diff --git a/src/actions/disconnect.ts b/src/actions/disconnect.ts index bbf91a7d..0f6b8c94 100644 --- a/src/actions/disconnect.ts +++ b/src/actions/disconnect.ts @@ -24,23 +24,12 @@ const KeyCodes = BlocklyUtils.KeyCodes; * item. */ export class DisconnectAction { - /** - * Function provided by the navigation controller to say whether editing - * is allowed. - */ - private canCurrentlyEdit: (ws: WorkspaceSvg) => boolean; - /** * Registration name for the keyboard shortcut. */ private shortcutName = Constants.SHORTCUT_NAMES.DISCONNECT; - constructor( - private navigation: Navigation, - canEdit: (ws: WorkspaceSvg) => boolean, - ) { - this.canCurrentlyEdit = canEdit; - } + constructor(private navigation: Navigation) {} /** * Install this action as both a keyboard shortcut and a context menu item. @@ -63,7 +52,8 @@ export class DisconnectAction { private registerShortcut() { const disconnectShortcut: ShortcutRegistry.KeyboardShortcut = { name: this.shortcutName, - preconditionFn: (workspace) => this.canCurrentlyEdit(workspace), + preconditionFn: (workspace) => + this.navigation.canCurrentlyEdit(workspace), callback: (workspace) => { switch (this.navigation.getState(workspace)) { case Constants.STATE.WORKSPACE: diff --git a/src/actions/edit.ts b/src/actions/edit.ts index be997fdf..328655ec 100644 --- a/src/actions/edit.ts +++ b/src/actions/edit.ts @@ -5,8 +5,8 @@ */ import {ContextMenuRegistry} from 'blockly'; -import type {WorkspaceSvg} from 'blockly'; import {LineCursor} from '../line_cursor'; +import {Navigation} from 'src/navigation'; /** * Action to edit a block. This just moves the cursor to the first @@ -29,7 +29,7 @@ import {LineCursor} from '../line_cursor'; * is already a corresponding "right" shortcut item. */ export class EditAction { - constructor(private canCurrentlyNavigate: (ws: WorkspaceSvg) => boolean) {} + constructor(private navigation: Navigation) {} /** * Install this action as a context menu item. @@ -54,7 +54,7 @@ export class EditAction { displayText: 'Edit Block contents (→︎)', preconditionFn: (scope: ContextMenuRegistry.Scope) => { const workspace = scope.block?.workspace; - if (!workspace || !this.canCurrentlyNavigate(workspace)) { + if (!workspace || !this.navigation.canCurrentlyNavigate(workspace)) { return 'disabled'; } const cursor = workspace.getCursor() as LineCursor | null; diff --git a/src/actions/enter.ts b/src/actions/enter.ts index 4cec64b0..a61afb77 100644 --- a/src/actions/enter.ts +++ b/src/actions/enter.ts @@ -29,10 +29,7 @@ const KeyCodes = BlocklyUtils.KeyCodes; * Class for registering a shortcut for the enter action. */ export class EnterAction { - constructor( - private navigation: Navigation, - private canCurrentlyEdit: (ws: WorkspaceSvg) => boolean, - ) {} + constructor(private navigation: Navigation) {} /** * Adds the enter action shortcut to the registry. @@ -47,7 +44,8 @@ export class EnterAction { */ ShortcutRegistry.registry.register({ name: Constants.SHORTCUT_NAMES.EDIT_OR_CONFIRM, - preconditionFn: (workspace) => this.canCurrentlyEdit(workspace), + preconditionFn: (workspace) => + this.navigation.canCurrentlyEdit(workspace), callback: (workspace, event) => { event.preventDefault(); diff --git a/src/actions/exit.ts b/src/actions/exit.ts index 780c4e0f..3b9666be 100644 --- a/src/actions/exit.ts +++ b/src/actions/exit.ts @@ -6,8 +6,6 @@ import {ShortcutRegistry, utils as BlocklyUtils} from 'blockly/core'; -import type {WorkspaceSvg} from 'blockly/core'; - import * as Constants from '../constants'; import type {Navigation} from '../navigation'; @@ -17,10 +15,7 @@ const KeyCodes = BlocklyUtils.KeyCodes; * Class for registering a shortcut for the exit action. */ export class ExitAction { - constructor( - private navigation: Navigation, - private canCurrentlyNavigate: (ws: WorkspaceSvg) => boolean, - ) {} + constructor(private navigation: Navigation) {} /** * Adds the exit action shortcut to the registry. @@ -28,7 +23,8 @@ export class ExitAction { install() { ShortcutRegistry.registry.register({ name: Constants.SHORTCUT_NAMES.EXIT, - preconditionFn: (workspace) => this.canCurrentlyNavigate(workspace), + preconditionFn: (workspace) => + this.navigation.canCurrentlyNavigate(workspace), callback: (workspace) => { switch (this.navigation.getState(workspace)) { case Constants.STATE.FLYOUT: diff --git a/src/actions/insert.ts b/src/actions/insert.ts index 37eecc2f..6984216a 100644 --- a/src/actions/insert.ts +++ b/src/actions/insert.ts @@ -23,23 +23,12 @@ const KeyCodes = BlocklyUtils.KeyCodes; * item. */ export class InsertAction { - /** - * Function provided by the navigation controller to say whether editing - * is allowed. - */ - private canCurrentlyEdit: (ws: WorkspaceSvg) => boolean; - /** * Registration name for the keyboard shortcut. */ private insertShortcutName = Constants.SHORTCUT_NAMES.INSERT; - constructor( - private navigation: Navigation, - canEdit: (ws: WorkspaceSvg) => boolean, - ) { - this.canCurrentlyEdit = canEdit; - } + constructor(private navigation: Navigation) {} /** * Install this action as both a keyboard shortcut and a context menu item. @@ -112,7 +101,7 @@ export class InsertAction { * @returns True iff `insertCallback` function should be called. */ private insertPrecondition(workspace: WorkspaceSvg): boolean { - return this.canCurrentlyEdit(workspace); + return this.navigation.canCurrentlyEdit(workspace); } /** diff --git a/src/actions/mover.ts b/src/actions/mover.ts index faeef164..6767f6e7 100644 --- a/src/actions/mover.ts +++ b/src/actions/mover.ts @@ -56,10 +56,7 @@ export class Mover { */ private oldDragStrategy: IDragStrategy | null = null; - constructor( - protected navigation: Navigation, - protected canEdit: (ws: WorkspaceSvg) => boolean, - ) {} + constructor(protected navigation: Navigation) {} private shortcuts: ShortcutRegistry.KeyboardShortcut[] = [ // Begin and end move. @@ -212,7 +209,7 @@ export class Mover { return !!( this.navigation.getState(workspace) === Constants.STATE.WORKSPACE && - this.canEdit(workspace) && + this.navigation.canCurrentlyEdit(workspace) && !this.moves.has(workspace) && // No move in progress. block?.isMovable() ); @@ -226,7 +223,9 @@ export class Mover { * @returns True iff we are moving. */ isMoving(workspace: WorkspaceSvg) { - return this.canEdit(workspace) && this.moves.has(workspace); + return ( + this.navigation.canCurrentlyEdit(workspace) && this.moves.has(workspace) + ); } /** diff --git a/src/actions/ws_movement.ts b/src/actions/ws_movement.ts index 048a0b90..719a4cfa 100644 --- a/src/actions/ws_movement.ts +++ b/src/actions/ws_movement.ts @@ -7,6 +7,7 @@ import {ASTNode, ShortcutRegistry, utils as BlocklyUtils} from 'blockly'; import * as Constants from '../constants'; import type {WorkspaceSvg} from 'blockly'; +import {Navigation} from 'src/navigation'; const KeyCodes = BlocklyUtils.KeyCodes; const createSerializedKey = ShortcutRegistry.registry.createSerializedKey.bind( @@ -23,35 +24,30 @@ const WS_MOVE_DISTANCE = 40; * shortcuts. */ export class WorkspaceMovement { - /** - * Function provided by the navigation controller to say whether editing - * is allowed. - */ - private canCurrentlyEdit: (ws: WorkspaceSvg) => boolean; - - constructor(canEdit: (ws: WorkspaceSvg) => boolean) { - this.canCurrentlyEdit = canEdit; - } + constructor(private navigation: Navigation) {} private shortcuts: ShortcutRegistry.KeyboardShortcut[] = [ /** Move the cursor on the workspace to the left. */ { name: Constants.SHORTCUT_NAMES.MOVE_WS_CURSOR_LEFT, - preconditionFn: (workspace) => this.canCurrentlyEdit(workspace), + preconditionFn: (workspace) => + this.navigation.canCurrentlyEdit(workspace), callback: (workspace) => this.moveWSCursor(workspace, -1, 0), keyCodes: [createSerializedKey(KeyCodes.A, [KeyCodes.SHIFT])], }, /** Move the cursor on the workspace to the right. */ { name: Constants.SHORTCUT_NAMES.MOVE_WS_CURSOR_RIGHT, - preconditionFn: (workspace) => this.canCurrentlyEdit(workspace), + preconditionFn: (workspace) => + this.navigation.canCurrentlyEdit(workspace), callback: (workspace) => this.moveWSCursor(workspace, 1, 0), keyCodes: [createSerializedKey(KeyCodes.D, [KeyCodes.SHIFT])], }, /** Move the cursor on the workspace up. */ { name: Constants.SHORTCUT_NAMES.MOVE_WS_CURSOR_UP, - preconditionFn: (workspace) => this.canCurrentlyEdit(workspace), + preconditionFn: (workspace) => + this.navigation.canCurrentlyEdit(workspace), callback: (workspace) => this.moveWSCursor(workspace, 0, -1), keyCodes: [createSerializedKey(KeyCodes.W, [KeyCodes.SHIFT])], }, @@ -59,7 +55,8 @@ export class WorkspaceMovement { /** Move the cursor on the workspace down. */ { name: Constants.SHORTCUT_NAMES.MOVE_WS_CURSOR_DOWN, - preconditionFn: (workspace) => this.canCurrentlyEdit(workspace), + preconditionFn: (workspace) => + this.navigation.canCurrentlyEdit(workspace), callback: (workspace) => this.moveWSCursor(workspace, 0, 1), keyCodes: [createSerializedKey(KeyCodes.S, [KeyCodes.SHIFT])], }, @@ -67,7 +64,8 @@ export class WorkspaceMovement { /** Move the cursor to the workspace. */ { name: Constants.SHORTCUT_NAMES.CREATE_WS_CURSOR, - preconditionFn: (workspace) => this.canCurrentlyEdit(workspace), + preconditionFn: (workspace) => + this.navigation.canCurrentlyEdit(workspace), callback: (workspace) => this.createWSCursor(workspace), keyCodes: [KeyCodes.W], }, diff --git a/src/navigation.ts b/src/navigation.ts index 04ee2ab6..d32b9ce3 100644 --- a/src/navigation.ts +++ b/src/navigation.ts @@ -1090,6 +1090,37 @@ export class Navigation { return false; } + /** + * Determines whether keyboard navigation should be allowed based on the + * current state of the workspace. + * + * A return value of 'true' generally indicates that either the workspace, + * toolbox or flyout has enabled keyboard navigation and is currently in a + * state (e.g. focus) that can support keyboard navigation. + * + * @param workspace the workspace in which keyboard navigation may be allowed. + * @returns whether keyboard navigation is currently allowed. + */ + canCurrentlyNavigate(workspace: Blockly.WorkspaceSvg) { + return ( + workspace.keyboardAccessibilityMode && + this.getState(workspace) !== Constants.STATE.NOWHERE + ); + } + + /** + * Determines whether the provided workspace is currently keyboard navigable + * and editable. + * + * For the navigability criteria, see canCurrentlyKeyboardNavigate. + * + * @param workspace the workspace in which keyboard editing may be allowed. + * @returns whether keyboard navigation and editing is currently allowed. + */ + canCurrentlyEdit(workspace: Blockly.WorkspaceSvg) { + return this.canCurrentlyNavigate(workspace) && !workspace.options.readOnly; + } + /** * Removes the change listeners on all registered workspaces. */ diff --git a/src/navigation_controller.ts b/src/navigation_controller.ts index bf6a1e8f..d39fb8c1 100644 --- a/src/navigation_controller.ts +++ b/src/navigation_controller.ts @@ -47,57 +47,31 @@ export class NavigationController { shortcutDialog: ShortcutDialog = new ShortcutDialog(); /** Context menu and keyboard action for deletion. */ - deleteAction: DeleteAction = new DeleteAction( - this.navigation, - this.canCurrentlyEdit.bind(this), - ); + deleteAction: DeleteAction = new DeleteAction(this.navigation); /** Context menu and keyboard action for deletion. */ - editAction: EditAction = new EditAction(this.canCurrentlyEdit.bind(this)); + editAction: EditAction = new EditAction(this.navigation); /** Context menu and keyboard action for insertion. */ - insertAction: InsertAction = new InsertAction( - this.navigation, - this.canCurrentlyEdit.bind(this), - ); + insertAction: InsertAction = new InsertAction(this.navigation); /** Keyboard shortcut for disconnection. */ - disconnectAction: DisconnectAction = new DisconnectAction( - this.navigation, - this.canCurrentlyEdit.bind(this), - ); + disconnectAction: DisconnectAction = new DisconnectAction(this.navigation); - clipboard: Clipboard = new Clipboard( - this.navigation, - this.canCurrentlyEdit.bind(this), - ); + clipboard: Clipboard = new Clipboard(this.navigation); - workspaceMovement: WorkspaceMovement = new WorkspaceMovement( - this.canCurrentlyEdit.bind(this), - ); + workspaceMovement: WorkspaceMovement = new WorkspaceMovement(this.navigation); /** Keyboard navigation actions for the arrow keys. */ - arrowNavigation: ArrowNavigation = new ArrowNavigation( - this.navigation, - this.canCurrentlyNavigate.bind(this), - ); + arrowNavigation: ArrowNavigation = new ArrowNavigation(this.navigation); - exitAction: ExitAction = new ExitAction( - this.navigation, - this.canCurrentlyNavigate.bind(this), - ); + exitAction: ExitAction = new ExitAction(this.navigation); - enterAction: EnterAction = new EnterAction( - this.navigation, - this.canCurrentlyEdit.bind(this), - ); + enterAction: EnterAction = new EnterAction(this.navigation); - actionMenu: ActionMenu = new ActionMenu( - this.navigation, - this.canCurrentlyNavigate.bind(this), - ); + actionMenu: ActionMenu = new ActionMenu(this.navigation); - mover = new Mover(this.navigation, this.canCurrentlyEdit.bind(this)); + mover = new Mover(this.navigation); /** * Original Toolbox.prototype.onShortcut method, saved by @@ -234,37 +208,6 @@ export class NavigationController { this.navigation.handleBlurFlyout(workspace, closeFlyout); } - /** - * Determines whether keyboard navigation should be allowed based on the - * current state of the workspace. - * - * A return value of 'true' generally indicates that either the workspace or - * toolbox both has enabled keyboard navigation and is currently in a state - * (e.g. focus) that can support keyboard navigation. - * - * @param workspace the workspace in which keyboard navigation may be allowed. - * @returns whether keyboard navigation is currently allowed. - */ - private canCurrentlyNavigate(workspace: WorkspaceSvg) { - return ( - workspace.keyboardAccessibilityMode && - this.navigation.getState(workspace) !== Constants.STATE.NOWHERE - ); - } - - /** - * Determines whether the provided workspace is currently keyboard navigable - * and editable. - * - * For the navigability criteria, see canCurrentlyKeyboardNavigate. - * - * @param workspace the workspace in which keyboard editing may be allowed. - * @returns whether keyboard navigation and editing is currently allowed. - */ - private canCurrentlyEdit(workspace: WorkspaceSvg) { - return this.canCurrentlyNavigate(workspace) && !workspace.options.readOnly; - } - /** * Turns on keyboard navigation. * @@ -294,7 +237,8 @@ export class NavigationController { /** Move focus to or from the toolbox. */ focusToolbox: { name: Constants.SHORTCUT_NAMES.TOOLBOX, - preconditionFn: (workspace) => this.canCurrentlyEdit(workspace), + preconditionFn: (workspace) => + this.navigation.canCurrentlyEdit(workspace), callback: (workspace) => { switch (this.navigation.getState(workspace)) { case Constants.STATE.WORKSPACE: @@ -314,7 +258,9 @@ export class NavigationController { /** Clean up the workspace. */ cleanup: { name: Constants.SHORTCUT_NAMES.CLEAN_UP, - preconditionFn: (workspace) => workspace.getTopBlocks(false).length > 0, + preconditionFn: (workspace) => + this.navigation.canCurrentlyEdit(workspace) && + workspace.getTopBlocks(false).length > 0, callback: (workspace) => { workspace.cleanUp(); return true;