Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions src/actions/clipboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import type {BlockSvg, WorkspaceSvg} from 'blockly';
import {Navigation} from '../navigation';
import {getShortActionShortcut} from '../shortcut_formatting';
import * as Blockly from 'blockly';
import {clearPasteHints, showCopiedHint, showCutHint} from '../hints';

const KeyCodes = blocklyUtils.KeyCodes;
const createSerializedKey = ShortcutRegistry.registry.createSerializedKey.bind(
Expand Down Expand Up @@ -168,6 +169,10 @@ export class Clipboard {
if (cursor instanceof LineCursor) cursor.preDelete(sourceBlock);
sourceBlock.checkAndDelete();
if (cursor instanceof LineCursor) cursor.postDelete();
const cut = !!this.copyData;
if (cut) {
showCutHint(workspace);
}
return true;
}

Expand Down Expand Up @@ -274,8 +279,11 @@ export class Clipboard {
this.copyData = sourceBlock.toCopyData();
this.copyWorkspace = sourceBlock.workspace;
const copied = !!this.copyData;
if (copied && navigationState === Constants.STATE.FLYOUT) {
this.navigation.focusWorkspace(workspace);
if (copied) {
if (navigationState === Constants.STATE.FLYOUT) {
this.navigation.focusWorkspace(workspace);
}
showCopiedHint(workspace);
}
return copied;
}
Expand Down Expand Up @@ -361,6 +369,8 @@ export class Clipboard {
*/
private pasteCallback(workspace: WorkspaceSvg) {
if (!this.copyData || !this.copyWorkspace) return false;
clearPasteHints(workspace);

const pasteWorkspace = this.copyWorkspace.isFlyout
? workspace
: this.copyWorkspace;
Expand Down
22 changes: 17 additions & 5 deletions src/actions/enter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
Events,
ShortcutRegistry,
utils as BlocklyUtils,
dialog,
} from 'blockly/core';

import type {
Expand All @@ -22,8 +21,12 @@ import type {

import * as Constants from '../constants';
import type {Navigation} from '../navigation';
import {getShortActionShortcut} from '../shortcut_formatting';
import {Mover} from './mover';
import {
showConstrainedMovementHint,
showHelpHint,
showUnconstrainedMoveHint,
} from '../hints';

const KeyCodes = BlocklyUtils.KeyCodes;

Expand Down Expand Up @@ -104,9 +107,7 @@ export class EnterAction {
} else if (nodeType === ASTNode.types.BLOCK) {
const block = curNode.getLocation() as Block;
if (!this.tryShowFullBlockFieldEditor(block)) {
const shortcut = getShortActionShortcut('list_shortcuts');
const message = `Press ${shortcut} for help on keyboard controls`;
dialog.alert(message);
showHelpHint(workspace);
}
} else if (curNode.isConnection() || nodeType === ASTNode.types.WORKSPACE) {
this.navigation.openToolboxOrFlyout(workspace);
Expand All @@ -120,6 +121,7 @@ export class EnterAction {
* Tries to find a connection on the block to connect to the marked
* location. If no connection has been marked, or there is not a compatible
* connection then the block is placed on the workspace.
* Trigger a toast per session if possible.
*
* @param workspace The main workspace. The workspace
* the block will be placed on.
Expand Down Expand Up @@ -150,6 +152,16 @@ export class EnterAction {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
workspace.getCursor()?.setCurNode(ASTNode.createBlockNode(newBlock)!);
this.mover.startMove(workspace);

const isStartBlock =
!newBlock.outputConnection &&
!newBlock.nextConnection &&
!newBlock.previousConnection;
if (isStartBlock) {
showUnconstrainedMoveHint(workspace, false);
} else {
showConstrainedMovementHint(workspace);
}
}

/**
Expand Down
5 changes: 5 additions & 0 deletions src/actions/mover.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import * as Constants from '../constants';
import {Direction, getXYFromDirection} from '../drag_direction';
import {KeyboardDragStrategy} from '../keyboard_drag_strategy';
import {Navigation} from '../navigation';
import {clearMoveHints} from '../hints';

/**
* The distance to move an item, in workspace coordinates, when
Expand Down Expand Up @@ -134,6 +135,8 @@ export class Mover {
* @returns True iff move successfully finished.
*/
finishMove(workspace: WorkspaceSvg) {
clearMoveHints(workspace);

const info = this.moves.get(workspace);
if (!info) throw new Error('no move info for workspace');

Expand All @@ -157,6 +160,8 @@ export class Mover {
* @returns True iff move successfully aborted.
*/
abortMove(workspace: WorkspaceSvg) {
clearMoveHints(workspace);

const info = this.moves.get(workspace);
if (!info) throw new Error('no move info for workspace');

Expand Down
121 changes: 121 additions & 0 deletions src/hints.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/**
* Centralises hints that we show.
*
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import {WorkspaceSvg, Toast} from 'blockly';
import {SHORTCUT_NAMES} from './constants';
import {getShortActionShortcut} from './shortcut_formatting';

const unconstrainedMoveHintId = 'unconstrainedMoveHint';
const constrainedMoveHintId = 'constrainedMoveHint';
const copiedHintId = 'copiedHint';
const cutHintId = 'cutHint';
const helpHintId = 'helpHint';

/**
* Nudge the user to use unconstrained movement.
*
* @param workspace Workspace.
* @param force Set to show it even if previously shown.
*/
export function showUnconstrainedMoveHint(
workspace: WorkspaceSvg,
force = false,
) {
const enter = getShortActionShortcut(SHORTCUT_NAMES.EDIT_OR_CONFIRM);
const modifier = navigator.platform.startsWith('Mac') ? '⌥' : 'Ctrl';
const message = `Hold ${modifier} and use arrow keys to move freely, then ${enter} to accept the position`;
Toast.show(workspace, {
message,
id: unconstrainedMoveHintId,
oncePerSession: !force,
});
}

/**
* Nudge the user to move a block that's in move mode.
*
* @param workspace Workspace.
*/
export function showConstrainedMovementHint(workspace: WorkspaceSvg) {
const enter = getShortActionShortcut(SHORTCUT_NAMES.EDIT_OR_CONFIRM);
const message = `Use the arrow keys to move, then ${enter} to accept the position`;
Toast.show(workspace, {
message,
id: constrainedMoveHintId,
oncePerSession: true,
});
}

/**
* Clear active move-related hints, if any.
*
* @param workspace The workspace.
*/
export function clearMoveHints(workspace: WorkspaceSvg) {
Toast.hide(workspace, constrainedMoveHintId);
Toast.hide(workspace, unconstrainedMoveHintId);
}

/**
* Nudge the user to paste after a copy.
*
* @param workspace Workspace.
*/
export function showCopiedHint(workspace: WorkspaceSvg) {
Toast.show(workspace, {
message: `Copied. Press ${getShortActionShortcut('paste')} to paste.`,
duration: 7000,
id: copiedHintId,
});
}

/**
* Nudge the user to paste after a cut.
*
* @param workspace Workspace.
*/
export function showCutHint(workspace: WorkspaceSvg) {
Toast.show(workspace, {
message: `Cut. Press ${getShortActionShortcut('paste')} to paste.`,
duration: 7000,
id: cutHintId,
});
}

/**
* Clear active paste-related hints, if any.
*
* @param workspace The workspace.
*/
export function clearPasteHints(workspace: WorkspaceSvg) {
Toast.hide(workspace, cutHintId);
Toast.hide(workspace, copiedHintId);
}

/**
* Nudge the user to open the help.
*
* @param workspace The workspace.
*/
export function showHelpHint(workspace: WorkspaceSvg) {
const shortcut = getShortActionShortcut('list_shortcuts');
const message = `Press ${shortcut} for help on keyboard controls`;
const id = helpHintId;
Toast.show(workspace, {message, id});
}

/**
* Clear the help hint.
*
* @param workspace The workspace.
*/
export function clearHelpHint(workspace: WorkspaceSvg) {
// TODO: We'd like to do this in MakeCode too as we override.
// Could have an option for showing help in the plugin?
Toast.hide(workspace, helpHintId);
}
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ export class KeyboardNavigation {
* Toggle visibility of a help dialog for the keyboard shortcuts.
*/
toggleShortcutDialog(): void {
this.navigationController.shortcutDialog.toggle();
this.navigationController.shortcutDialog.toggle(this.workspace);
}

/**
Expand Down
7 changes: 7 additions & 0 deletions src/keyboard_drag_strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
utils,
} from 'blockly';
import {Direction, getDirectionFromXY} from './drag_direction';
import {showUnconstrainedMoveHint} from './hints';

// Copied in from core because it is not exported.
interface ConnectionCandidate {
Expand Down Expand Up @@ -70,6 +71,12 @@ export class KeyboardDragStrategy extends dragging.BlockDragStrategy {
} else {
// Handle the case when unconstrained drag was far from any candidate.
this.searchNode = null;

if (this.isConstrainedMovement()) {
// @ts-expect-error private field
const workspace = this.workspace;
showUnconstrainedMoveHint(workspace, true);
}
}
}

Expand Down
14 changes: 10 additions & 4 deletions src/shortcut_dialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
getLongActionShortcutsAsKeys,
upperCaseFirst,
} from './shortcut_formatting';
import {clearHelpHint} from './hints';

/**
* Class for handling the shortcuts dialog.
Expand Down Expand Up @@ -64,7 +65,12 @@ export class ShortcutDialog {
}
}

toggle() {
toggle(workspace: Blockly.WorkspaceSvg) {
clearHelpHint(workspace);
this.toggleInternal();
}

toggleInternal() {
if (this.modalContainer && this.shortcutDialog) {
// Use built in dialog methods.
if (this.shortcutDialog.hasAttribute('open')) {
Expand Down Expand Up @@ -132,7 +138,7 @@ export class ShortcutDialog {
// Can we also intercept the Esc key to dismiss.
if (this.closeButton) {
this.closeButton.addEventListener('click', (e) => {
this.toggle();
this.toggleInternal();
});
}
}
Expand Down Expand Up @@ -161,8 +167,8 @@ export class ShortcutDialog {
/** List all of the currently registered shortcuts. */
const announceShortcut: ShortcutRegistry.KeyboardShortcut = {
name: Constants.SHORTCUT_NAMES.LIST_SHORTCUTS,
callback: () => {
this.toggle();
callback: (workspace) => {
this.toggle(workspace);
return true;
},
keyCodes: [Blockly.utils.KeyCodes.SLASH],
Expand Down
Loading