8000 feat: add keyboard shortcut to open context menu by maribethb · Pull Request #8921 · google/blockly · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

feat: add keyboard shortcut to open context menu #8921

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 3 commits into from
Closed
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
2 changes: 2 additions & 0 deletions core/contextmenu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,8 @@ function createWidget_(menu: Menu) {
);
// Focus only after the initial render to avoid issue #1329.
menu.focus();
// Highlight the first thing in the menu
menu.highlightNext();
}
/**
* Halts the propagation of the event without doing anything else.
Expand Down
5 changes: 5 additions & 0 deletions core/interfaces/i_contextmenu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,8 @@ export interface IContextMenu {
*/
showContextMenu(e: Event): void;
}

/** Type guard for objects that implement IContextMenu. */
export function hasContextMenu(obj: object): obj is IContextMenu {
return (obj as any).showContextMenu !== undefined;
}
133 changes: 81 additions & 52 deletions core/shortcut_items.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@

import {BlockSvg} from './block_svg.js';
import * as clipboard from './clipboard.js';
import * as common from './common.js';
import * as eventUtils from './events/utils.js';
import {Gesture} from './gesture.js';
import {hasContextMenu} from './interfaces/i_contextmenu.js';
import {ICopyData, isCopyable} from './interfaces/i_copyable.js';
import {isDeletable} from './interfaces/i_deletable.js';
import {isDraggable} from './interfaces/i_draggable.js';
import {isSelectable} from './interfaces/i_selectable.js';
import {KeyboardShortcut, ShortcutRegistry} from './shortcut_registry.js';
import {Coordinate} from './utils/coordinate.js';
import {KeyCodes} from './utils/keycodes.js';
Expand All @@ -31,6 +32,7 @@ export enum names {
PASTE = 'paste',
UNDO = 'undo',
REDO = 'redo',
MENU = 'menu',
}

/**
Expand All @@ -43,9 +45,7 @@ export function registerEscape() {
return !workspace.isReadOnly();
},
callback(workspace) {
// AnyDuringMigration because: Property 'hideChaff' does not exist on
// type 'Workspace'.
(workspace as AnyDuringMigration).hideChaff();
workspace.hideChaff();
return true;
},
keyCodes: [KeyCodes.ESC],
Expand All @@ -59,28 +59,28 @@ export function registerEscape() {
export function registerDelete() {
const deleteShortcut: KeyboardShortcut = {
name: names.DELETE,
preconditionFn(workspace) {
const selected = common.getSelected();
preconditionFn(workspace, scope) {
const focused = scope.focusedNode;
return (
!workspace.isReadOnly() &&
selected != null &&
isDeletable(selected) &&
selected.isDeletable() &&
focused != null &&
isDeletable(focused) &&
focused.isDeletable() &&
!Gesture.inProgress()
);
},
callback(workspace, e) {
callback(workspace, e, shortcut, scope) {
// Delete or backspace.
// Stop the browser from going back to the previous page.
// Do this first to prevent an error in the delete code from resulting in
// data loss.
e.preventDefault();
const selected = common.getSelected();
if (selected instanceof BlockSvg) {
selected.checkAndDelete();
} else if (isDeletable(selected) && selected.isDeletable()) {
const focused = scope.focusedNode;
if (focused instanceof BlockSvg) {
focused.checkAndDelete();
} else if (isDeletable(focused) && focused.isDeletable()) {
eventUtils.setGroup(true);
selected.dispose();
focused.dispose();
eventUtils.setGroup(false);
}
return true;
Expand Down Expand Up @@ -110,33 +110,33 @@ export function registerCopy() {

const copyShortcut: KeyboardShortcut = {
name: names.COPY,
preconditionFn(workspace) {
const selected = common.getSelected();
preconditionFn(workspace, scope) {
const focused = scope.focusedNode;
return (
!workspace.isReadOnly() &&
!Gesture.inProgress() &&
selected != null &&
isDeletable(selected) &&
selected.isDeletable() &&
isDraggable(selected) &&
selected.isMovable() &&
isCopyable(selected)
focused != null &&
isDeletable(focused) &&
focused.isDeletable() &&
isDraggable(focused) &&
focused.isMovable() &&
isCopyable(focused)
);
},
callback(workspace, e) {
callback(workspace, e, shortcut, scope) {
// Prevent the default copy behavior, which may beep or otherwise indicate
// an error due to the lack of a selection.
e.preventDefault();
workspace.hideChaff();
const selected = common.getSelected();
if (!selected || !isCopyable(selected)) return false;
copyData = selected.toCopyData();
const focused = scope.focusedNode;
if (!focused || !isCopyable(focused)) return false;
copyData = focused.toCopyData();
copyWorkspace =
selected.workspace instanceof WorkspaceSvg
? selected.workspace
focused.workspace instanceof WorkspaceSvg
? focused.workspace
: workspace;
copyCoords = isDraggable(selected)
? selected.getRelativeToSurfaceXY()
copyCoords = isDraggable(focused)
? focused.getRelativeToSurfaceXY()
: null;
return !!copyData;
},
Expand All @@ -161,39 +161,40 @@ export function registerCut() {

const cutShortcut: KeyboardShortcut = {
name: names.CUT,
preconditionFn(workspace) {
const selected = common.getSelected();
preconditionFn(workspace, scope) {
const focused = scope.focusedNode;
return (
!workspace.isReadOnly() &&
!Gesture.inProgress() &&
selected != null &&
isDeletable(selected) &&
selected.isDeletable() &&
isDraggable(selected) &&
selected.isMovable() &&
!selected.workspace!.isFlyout
focused != null &&
isDeletable(focused) &&
focused.isDeletable() &&
isDraggable(focused) &&
focused.isMovable() &&
isSelectable(focused) &&
!focused.workspace.isFlyout
);
},
callback(workspace) {
const selected = common.getSelected();
callback(workspace, e, shortcut, scope) {
const focused = scope.focusedNode;

if (selected instanceof BlockSvg) {
copyData = selected.toCopyData();
if (focused instanceof BlockSvg) {
copyData = focused.toCopyData();
copyWorkspace = workspace;
copyCoords = selected.getRelativeToSurfaceXY();
selected.checkAndDelete();
copyCoords = focused.getRelativeToSurfaceXY();
focused.checkAndDelete();
return true;
} else if (
isDeletable(selected) &&
selected.isDeletable() &&
isCopyable(selected)
isDeletable(focused) &&
focused.isDeletable() &&
isCopyable(focused)
) {
copyData = selected.toCopyData();
copyData = focused.toCopyData();
copyWorkspace = workspace;
copyCoords = isDraggable(selected)
? selected.getRelativeToSurfaceXY()
copyCoords = isDraggable(focused)
? focused.getRelativeToSurfaceXY()
: null;
selected.dispose();
focused.dispose();
return true;
}
return false;
Expand Down Expand Up @@ -322,6 +323,33 @@ export function registerRedo() {
ShortcutRegistry.registry.register(redoShortcut);
}

/**
* Keyboard shortcut to open the context menu on ctrl/cmd+Enter.
*/
export function registerMenu() {
const ctrlEnter = ShortcutRegistry.registry.createSerializedKey(
KeyCodes.ENTER,
[KeyCodes.CTRL],
);
const cmdEnter = ShortcutRegistry.registry.createSerializedKey(
KeyCodes.ENTER,
[KeyCodes.META],
);
const menuShortcut: KeyboardShortcut = {
name: names.MENU,
preconditionFn: (workspace, scope) => {
return hasContextMenu(scope.focusedNode);
},
callback: (workspace, e, shortcut, scope) => {
if (!hasContextMenu(scope.focusedNode)) return false;
scope.focusedNode.showContextMenu(e);
return true;
},
keyCodes: [ctrlEnter, cmdEnter],
};
ShortcutRegistry.registry.register(menuShortcut);
}

/**
* Registers all default keyboard shortcut item. This should be called once per
* instance of KeyboardShortcutRegistry.
Expand All @@ -336,6 +364,7 @@ export function registerDefaultShortcuts() {
registerPaste();
registerUndo();
registerRedo();
registerMenu();
}

registerDefaultShortcuts();
21 changes: 18 additions & 3 deletions core/shortcut_registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
*/
// Former goog.module ID: Blockly.ShortcutRegistry

import {Scope} from './contextmenu_registry.js';
import {getFocusManager} from './focus_manager.js';
import {KeyCodes} from './utils/keycodes.js';
import * as object from './utils/object.js';
import {WorkspaceSvg} from './workspace_svg.js';
Expand Down Expand Up @@ -249,12 +251,20 @@ export class ShortcutRegistry {
const shortcut = this.shortcuts.get(shortcutName);
if (
!shortcut ||
(shortcut.preconditionFn && !shortcut.preconditionFn(workspace))
(shortcut.preconditionFn &&
!shortcut.preconditionFn(workspace, {
focusedNode: getFocusManager().getFocusedNode(),
}))
) {
continue;
}
// If the key has been handled, stop processing shortcuts.
if (shortcut.callback?.(workspace, e, shortcut)) return true;
if (
shortcut.callback?.(workspace, e, shortcut, {
focusedNode: getFocusManager().getFocusedNode(),
})
)
return true;
}
return false;
}
Expand Down Expand Up @@ -372,6 +382,8 @@ export namespace ShortcutRegistry {
* @param e The event that caused the shortcut to be activated.
* @param shortcut The `KeyboardShortcut` that was activated
* (i.e., the one this callback is attached to).
* @param scope Information about the focused item when the
* shortcut was invoked.
* @returns Returning true ends processing of the invoked keycode.
* Returning false causes processing to continue with the
* next-most-recently registered shortcut for the invoked
Expand All @@ -381,6 +393,7 @@ export namespace ShortcutRegistry {
workspace: WorkspaceSvg,
e: Event,
shortcut: KeyboardShortcut,
scope: Scope,
) => boolean;

/** The name of the shortcut. Should be unique. */
Expand All @@ -393,9 +406,11 @@ export namespace ShortcutRegistry {
*
* @param workspace The `WorkspaceSvg` where the shortcut was
* invoked.
* @param scope Information about the focused item when the
* shortcut would be invoked.
* @returns True iff `callback` function should be called.
*/
preconditionFn?: (workspace: WorkspaceSvg) => boolean;
preconditionFn?: (workspace: WorkspaceSvg, scope: Scope) => boolean;

/** Optional arbitray extra data attached to the shortcut. */
metadata?: object;
Expand Down
1 change: 1 addition & 0 deletions tests/mocha/keydown_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ suite('Key Down', function () {
defineStackBlock();
const block = workspace.newBlock('stack_block');
Blockly.common.setSelected(block);
sinon.stub(Blockly.getFocusManager(), 'getFocusedNode').returns(block);
return block;
}

Expand Down
Loading
Loading
0