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') => `${path}`; @@ -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();