diff --git a/accessibility/keyboardui.js b/accessibility/keyboardui.js
index d5d3dec1..e372c39b 100644
--- a/accessibility/keyboardui.js
+++ b/accessibility/keyboardui.js
@@ -577,6 +577,11 @@ function getShortcuts() {
keys: `Shift + K`,
category: translate('shortcut_category_editor'),
},
+ {
+ label: translate('shortcut_enable_disable_block'),
+ keys: `L`,
+ category: translate('shortcut_category_editor'),
+ },
{
label: translate('shortcut_start_move_block'),
keys: `M`,
diff --git a/locale/de.js b/locale/de.js
index 6e4f4529..871b5106 100644
--- a/locale/de.js
+++ b/locale/de.js
@@ -1295,6 +1295,7 @@ export default {
shortcut_detach_block: 'Block trennen',
shortcut_comment_block: 'Kommentar ein-/ausblenden',
shortcut_delete_comment: 'Kommentar löschen',
+ shortcut_enable_disable_block: 'Block aktivieren/deaktivieren',
shortcut_start_move_block: 'Block verschieben',
shortcut_move_arrows: 'Verschieben: zur Verbindung',
shortcut_move_anywhere: 'Verschieben: überall',
diff --git a/locale/en.js b/locale/en.js
index dfdd4acf..df41e289 100644
--- a/locale/en.js
+++ b/locale/en.js
@@ -1418,6 +1418,7 @@ export default {
shortcut_detach_block: 'Detach block',
shortcut_comment_block: 'Show/hide comment',
shortcut_delete_comment: 'Delete comment',
+ shortcut_enable_disable_block: 'Enable/disable block',
shortcut_start_move_block: 'Move block',
shortcut_move_arrows: 'Move: to connection',
shortcut_move_anywhere: 'Move: anywhere',
diff --git a/locale/es.js b/locale/es.js
index 3b62d2a4..75d51d6c 100644
--- a/locale/es.js
+++ b/locale/es.js
@@ -1363,6 +1363,7 @@ export default {
shortcut_detach_block: 'Desconectar bloque',
shortcut_comment_block: 'Mostrar/ocultar comentario',
shortcut_delete_comment: 'Eliminar comentario',
+ shortcut_enable_disable_block: 'Habilitar/deshabilitar bloque',
shortcut_start_move_block: 'Mover bloque',
shortcut_move_arrows: 'Mover: a conexión',
shortcut_move_anywhere: 'Mover: a cualquier lugar',
diff --git a/locale/fr.js b/locale/fr.js
index bc8295ca..37c2b096 100644
--- a/locale/fr.js
+++ b/locale/fr.js
@@ -1303,6 +1303,7 @@ export default {
shortcut_detach_block: 'Détacher le bloc',
shortcut_comment_block: 'Afficher/masquer le commentaire',
shortcut_delete_comment: 'Supprimer le commentaire',
+ shortcut_enable_disable_block: 'Activer/désactiver le bloc',
shortcut_start_move_block: 'Déplacer le bloc',
shortcut_move_arrows: 'Déplacer : vers une connexion',
shortcut_move_anywhere: "Déplacer : n'importe où",
diff --git a/locale/it.js b/locale/it.js
index 809771b1..876b7b68 100644
--- a/locale/it.js
+++ b/locale/it.js
@@ -1305,6 +1305,7 @@ export default {
shortcut_detach_block: 'Stacca blocco',
shortcut_comment_block: 'Mostra/nascondi commento',
shortcut_delete_comment: 'Elimina commento',
+ shortcut_enable_disable_block: 'Abilita/disabilita blocco',
shortcut_start_move_block: 'Sposta blocco',
shortcut_move_arrows: 'Sposta: alla connessione',
shortcut_move_anywhere: 'Sposta: ovunque',
diff --git a/locale/pl.js b/locale/pl.js
index e595b573..1730bede 100644
--- a/locale/pl.js
+++ b/locale/pl.js
@@ -1297,6 +1297,7 @@ export default {
shortcut_detach_block: 'Odłącz blok',
shortcut_comment_block: 'Pokaż/ukryj komentarz',
shortcut_delete_comment: 'Usuń komentarz',
+ shortcut_enable_disable_block: 'Włącz/wyłącz blok',
shortcut_start_move_block: 'Przesuń blok',
shortcut_move_arrows: 'Przesuń: do połączenia',
shortcut_move_anywhere: 'Przesuń: gdziekolwiek',
diff --git a/locale/pt.js b/locale/pt.js
index 4b8050a6..9a53a3be 100644
--- a/locale/pt.js
+++ b/locale/pt.js
@@ -1306,6 +1306,7 @@ export default {
shortcut_detach_block: 'Desconectar bloco',
shortcut_comment_block: 'Mostrar/ocultar comentário',
shortcut_delete_comment: 'Excluir comentário',
+ shortcut_enable_disable_block: 'Ativar/desativar bloco',
shortcut_start_move_block: 'Mover bloco',
shortcut_move_arrows: 'Mover: para ligação',
shortcut_move_anywhere: 'Mover: para qualquer lugar',
diff --git a/locale/sv.js b/locale/sv.js
index fdeb2564..20d06777 100644
--- a/locale/sv.js
+++ b/locale/sv.js
@@ -1281,6 +1281,7 @@ export default {
shortcut_detach_block: 'Koppla loss block',
shortcut_comment_block: 'Visa/dölj kommentar',
shortcut_delete_comment: 'Ta bort kommentar',
+ shortcut_enable_disable_block: 'Aktivera/inaktivera block',
shortcut_start_move_block: 'Flytta block',
shortcut_move_arrows: 'Flytta: till anslutning',
shortcut_move_anywhere: 'Flytta: var som helst',
diff --git a/main/blocklyinit.js b/main/blocklyinit.js
index 5f61cb59..d0c728bb 100644
--- a/main/blocklyinit.js
+++ b/main/blocklyinit.js
@@ -1586,6 +1586,42 @@ function installShadowNavigationPatch(ws) {
},
});
}
+
+ // No built-in shortcut exists for this, so it resolves its own target like K above.
+ {
+ const enableTargetBlock = (scope) => {
+ const node = scope?.focusedNode;
+ if (node) {
+ if (typeof node.hasDisabledReason === 'function') return node;
+ if (typeof node.getSourceBlock === 'function') {
+ const block = node.getSourceBlock();
+ if (block) return block;
+ }
+ }
+ return skippableFieldBlock();
+ };
+ const enableEditable = (ws, block) =>
+ !!block &&
+ !ws.isDragging?.() &&
+ !ws.isReadOnly?.() &&
+ !block.isShadow?.() &&
+ !isBlockLocked(block);
+
+ shortcutRegistry.register({
+ name: 'toggle_block_enabled',
+ keyCodes: [shortcutRegistry.createSerializedKey(Blockly.utils.KeyCodes.L)],
+ preconditionFn: (ws, scope) => enableEditable(ws, enableTargetBlock(scope)),
+ callback: (ws, event, _shortcut, scope) => {
+ const block = enableTargetBlock(scope);
+ if (!enableEditable(ws, block)) return false;
+ event?.preventDefault?.();
+ Blockly.Events.setGroup('toolbar_disable');
+ block.setDisabledReason(!block.hasDisabledReason('MANUALLY_DISABLED'), 'MANUALLY_DISABLED');
+ Blockly.Events.setGroup(false);
+ return true;
+ },
+ });
+ }
}
export function createBlocklyWorkspace() {
diff --git a/ui/contextmenu.js b/ui/contextmenu.js
index c232a3b3..66b37205 100644
--- a/ui/contextmenu.js
+++ b/ui/contextmenu.js
@@ -668,8 +668,9 @@ export function initContextMenus(workspace) {
badgeOverlay.className = 'fc-toolbar-badges';
document.body.appendChild(badgeOverlay);
- // Icon paths: Font Awesome Free 6.7.2 by @fontawesome — https://fontawesome.com
- // License: https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.
+ // Icon paths below are Font Awesome Free 6.7.2 solid-style icons (each call
+ // site names the icon it draws) by @fontawesome — https://fontawesome.com
+ // License: https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.
const mkFaSvg = (path, vw = '0 0 448 512') =>
``;
@@ -683,6 +684,7 @@ export function initContextMenus(workspace) {
duplicateBtn.type = 'button';
duplicateBtn.className = 'fc-block-toolbar-btn';
duplicateBtn.setAttribute('aria-label', getToolbarLabel('duplicate_button_ui', 'Duplicate'));
+ // fa-copy
duplicateBtn.innerHTML = mkFaSvg(
''
);
@@ -691,6 +693,7 @@ export function initContextMenus(workspace) {
deleteBtn.type = 'button';
deleteBtn.className = 'fc-block-toolbar-btn fc-block-toolbar-btn--delete';
deleteBtn.setAttribute('aria-label', getToolbarLabel('delete_button_ui', 'Delete'));
+ // fa-trash
deleteBtn.innerHTML = mkFaSvg(
''
);
@@ -699,6 +702,7 @@ export function initContextMenus(workspace) {
detachBtn.type = 'button';
detachBtn.className = 'fc-block-toolbar-btn';
detachBtn.setAttribute('aria-label', getToolbarLabel('shortcut_detach_block', 'Detach'));
+ // fa-link-slash
detachBtn.innerHTML = mkFaSvg(
'',
'0 0 640 512'
@@ -710,6 +714,7 @@ export function initContextMenus(workspace) {
const moveHint = document.createElement('span');
moveHint.className = 'fc-block-toolbar-btn fc-block-toolbar-hint';
moveHint.setAttribute('aria-hidden', 'true');
+ // fa-up-down-left-right
moveHint.innerHTML = mkFaSvg(
'',
'0 0 512 512'
@@ -719,20 +724,38 @@ export function initContextMenus(workspace) {
commentBtn.type = 'button';
commentBtn.className = 'fc-block-toolbar-btn';
commentBtn.setAttribute('aria-label', getToolbarLabel('add_comment', 'Add comment'));
+ // fa-comment (pre-6.7 path revision; same rendered glyph as current fa-comment)
const commentAddSvg = mkFaSvg(
'',
'0 0 512 512'
);
+ // fa-comment-slash
const commentDeleteSvg = mkFaSvg(
'',
'0 0 640 512'
);
commentBtn.innerHTML = commentAddSvg;
+ const enableBtn = document.createElement('button');
+ enableBtn.type = 'button';
+ enableBtn.className = 'fc-block-toolbar-btn';
+ // fa-ban — shown when the block is currently disabled.
+ const blockDisableSvg = mkFaSvg(
+ '',
+ '0 0 512 512'
+ );
+ // fa-circle-check — shown when the block is currently enabled.
+ const blockEnableSvg = mkFaSvg(
+ '',
+ '0 0 512 512'
+ );
+
+ // fa-eye
const viewEnterSvg = mkFaSvg(
'',
'0 0 576 512'
);
+ // fa-eye-slash
const viewExitSvg = mkFaSvg(
'',
'0 0 640 512'
@@ -744,7 +767,7 @@ export function initContextMenus(workspace) {
viewBtn.setAttribute('aria-label', getToolbarLabel('view_in_canvas', 'View in canvas'));
viewBtn.innerHTML = viewEnterSvg;
- blockToolbar.append(duplicateBtn, detachBtn, moveHint, commentBtn, viewBtn, deleteBtn);
+ blockToolbar.append(duplicateBtn, detachBtn, moveHint, commentBtn, enableBtn, viewBtn, deleteBtn);
// The keyboard shortcut that each toolbar button mirrors. The overlay shows
// these as a passive legend — the keys themselves are bound elsewhere
@@ -758,6 +781,7 @@ export function initContextMenus(workspace) {
// Match the comment button's icon: '⇧K' (Shift+K, delete) when the block
// already has a comment, 'K' (show/hide) when it doesn't.
[commentBtn, () => (toolbarBlock?.getCommentText?.() != null ? '⇧K' : 'K')],
+ [enableBtn, 'L'],
[viewBtn, 'V'],
[deleteBtn, 'Del'],
];
@@ -1017,17 +1041,28 @@ export function initContextMenus(workspace) {
commentBtn.innerHTML = hasComment ? commentDeleteSvg : commentAddSvg;
}
+ function updateEnableButton(block) {
+ const disabled = block.hasDisabledReason('MANUALLY_DISABLED');
+ enableBtn.setAttribute(
+ 'aria-label',
+ disabled ? getToolbarLabel('context_enable_option', 'Enable') : getToolbarLabel('context_disable_option', 'Disable')
+ );
+ enableBtn.innerHTML = disabled ? blockDisableSvg : blockEnableSvg;
+ }
+
function showBlockToolbar(block, { keyboard = false } = {}) {
toolbarBlock = block;
toolbarKeyboardMode = keyboard;
// Locked blocks can't be edited: hide the mutating buttons (detach,
- // comment, delete), leaving duplicate and view-in-canvas available.
+ // comment, enable/disable, delete), leaving duplicate and view-in-canvas available.
const locked = isBlockLocked(block);
detachBtn.style.display = !locked && isDetachable(block) ? '' : 'none';
deleteBtn.style.display = locked ? 'none' : '';
+ enableBtn.style.display = locked ? 'none' : '';
updateSimplifiedToolbar();
updateCommentButton(block);
+ updateEnableButton(block);
let mesh = null;
try {
mesh = getMeshFromBlock(block);
@@ -1164,6 +1199,13 @@ export function initContextMenus(workspace) {
// refresh the button icon and, in keyboard mode, its badge (K ⇄ ⇧K).
updateCommentButton(toolbarBlock);
if (toolbarKeyboardMode) renderBadges();
+ } else if (
+ e.type === Blockly.Events.BLOCK_CHANGE &&
+ e.element === 'disabled' &&
+ toolbarBlock &&
+ e.blockId === toolbarBlock.id
+ ) {
+ updateEnableButton(toolbarBlock);
} else if (e.type === Blockly.Events.BLOCK_DRAG && e.isStart) {
// A keyboard-initiated move (M) fires this the same as a pointer
// drag; flag the block so the SELECTED handler above ignores the
@@ -1262,6 +1304,18 @@ export function initContextMenus(workspace) {
hideBlockToolbar();
});
+ enableBtn.addEventListener('pointerdown', (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ if (!toolbarBlock) return;
+ const block = toolbarBlock;
+ Blockly.Events.setGroup('toolbar_disable');
+ block.setDisabledReason(!block.hasDisabledReason('MANUALLY_DISABLED'), 'MANUALLY_DISABLED');
+ Blockly.Events.setGroup(false);
+ updateEnableButton(block);
+ if (toolbarKeyboardMode) renderBadges();
+ });
+
viewBtn.addEventListener('pointerdown', async (e) => {
e.preventDefault();
e.stopPropagation();