diff --git a/packages/app-desktop/gui/MainScreen/commands/permanentlyDeleteNote.ts b/packages/app-desktop/gui/MainScreen/commands/permanentlyDeleteNote.ts index 67782e69e46..2048a4bd862 100644 --- a/packages/app-desktop/gui/MainScreen/commands/permanentlyDeleteNote.ts +++ b/packages/app-desktop/gui/MainScreen/commands/permanentlyDeleteNote.ts @@ -25,6 +25,6 @@ export const runtime = (): CommandRuntime => { await Note.batchDelete(noteIds, { toTrash: false, sourceDescription: 'permanentlyDeleteNote command' }); } }, - enabledCondition: '(!noteIsReadOnly || inTrash) && someNotesSelected', + enabledCondition: '(!noteIsReadOnly || inTrash) && (noteListFocused || inTrash) && someNotesSelected', }; }; diff --git a/packages/app-desktop/gui/NoteList/NoteList2.tsx b/packages/app-desktop/gui/NoteList/NoteList2.tsx index 5208eb70cea..26af75b4469 100644 --- a/packages/app-desktop/gui/NoteList/NoteList2.tsx +++ b/packages/app-desktop/gui/NoteList/NoteList2.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { useMemo, useRef, useEffect } from 'react'; +import { useMemo, useRef, useEffect, useCallback } from 'react'; import { AppState } from '../../app.reducer'; import BaseModel, { ModelType } from '@joplin/lib/BaseModel'; import { Props } from './utils/types'; @@ -262,6 +262,20 @@ const NoteList = (props: Props) => { return output; }, [listRenderer.flow]); + const onFocus = useCallback(() => { + props.dispatch({ + type: 'FOCUS_SET', + field: 'noteList', + }); + }, [props.dispatch]); + + const onBlur = useCallback(() => { + props.dispatch({ + type: 'FOCUS_CLEAR', + field: 'noteList', + }); + }, [props.dispatch]); + return (
{ onScroll={onScroll} onKeyDown={onKeyDown} onDrop={onDrop} + onFocus={onFocus} + onBlur={onBlur} > {renderEmptyList()} {renderFiller('top', topFillerStyle)} diff --git a/packages/app-desktop/gui/NoteList/utils/useOnKeyDown.ts b/packages/app-desktop/gui/NoteList/utils/useOnKeyDown.ts index 3cb5767a6f2..1d277fd8cd3 100644 --- a/packages/app-desktop/gui/NoteList/utils/useOnKeyDown.ts +++ b/packages/app-desktop/gui/NoteList/utils/useOnKeyDown.ts @@ -8,6 +8,7 @@ import { Dispatch } from 'redux'; import { FocusNote } from './useFocusNote'; import { ItemFlow } from '@joplin/lib/services/plugins/api/noteListType'; import { KeyboardEventKey } from '@joplin/lib/dom'; +import KeymapService from '@joplin/lib/services/KeymapService'; const useOnKeyDown = ( selectedNoteIds: string[], @@ -104,7 +105,14 @@ const useOnKeyDown = ( event.preventDefault(); } - if (noteIds.length && (key === 'Delete' || (key === 'Backspace' && event.metaKey))) { + if ( + noteIds.length && + CommandService.instance().isEnabled('permanentlyDeleteNote') + && KeymapService.instance().eventMatchesCommandAccelerator(event, 'permanentlyDeleteNote') + ) { + event.preventDefault(); + void CommandService.instance().execute('permanentlyDeleteNote', noteIds); + } else if (noteIds.length && (key === 'Delete' || (key === 'Backspace' && event.metaKey))) { event.preventDefault(); if (CommandService.instance().isEnabled('deleteNote')) { void CommandService.instance().execute('deleteNote', noteIds); diff --git a/packages/app-desktop/integration-tests/noteList.spec.ts b/packages/app-desktop/integration-tests/noteList.spec.ts index 01117ab7af2..8eb0eb78864 100644 --- a/packages/app-desktop/integration-tests/noteList.spec.ts +++ b/packages/app-desktop/integration-tests/noteList.spec.ts @@ -1,5 +1,7 @@ import { test, expect } from './util/test'; import MainScreen from './models/MainScreen'; +import activateMainMenuItem from './util/activateMainMenuItem'; +import setMessageBoxResponse from './util/setMessageBoxResponse'; test.describe('noteList', () => { test('should be possible to edit notes in a different notebook when searching', async ({ mainWindow }) => { @@ -35,4 +37,42 @@ test.describe('noteList', () => { // Updating the title should force the sidebar to update sooner await expect(editor.noteTitleInput).toHaveValue('note-1'); }); + + test('shift-delete should ask to permanently delete notes, but only when the note list is focused', async ({ electronApp, mainWindow }) => { + const mainScreen = new MainScreen(mainWindow); + const sidebar = mainScreen.sidebar; + + const folderBHeader = await sidebar.createNewFolder('Folder B'); + const folderAHeader = await sidebar.createNewFolder('Folder A'); + await expect(folderAHeader).toBeVisible(); + + await mainScreen.createNewNote('test note 1'); + await mainScreen.createNewNote('test note 2'); + + await activateMainMenuItem(electronApp, 'Note list', 'Focus'); + await expect(mainScreen.noteListContainer.getByText('test note 1')).toBeVisible(); + + await setMessageBoxResponse(electronApp, /^Delete/i); + + const pressShiftDelete = async () => { + await mainWindow.keyboard.press('Shift'); + await mainWindow.keyboard.press('Delete'); + await mainWindow.keyboard.up('Delete'); + await mainWindow.keyboard.up('Shift'); + }; + await pressShiftDelete(); + + await folderBHeader.click(); + await folderAHeader.click(); + await expect(mainScreen.noteListContainer.getByText('test note 2')).not.toBeVisible(); + + // Should not delete when the editor is focused + await mainScreen.noteEditor.focusCodeMirrorEditor(); + await mainWindow.keyboard.type('test'); + await pressShiftDelete(); + + await folderBHeader.click(); + await folderAHeader.click(); + await expect(mainScreen.noteListContainer.getByText('test note 1')).toBeVisible(); + }); }); diff --git a/packages/app-desktop/services/commands/stateToWhenClauseContext.ts b/packages/app-desktop/services/commands/stateToWhenClauseContext.ts index c35c6b34859..5d10d23f691 100644 --- a/packages/app-desktop/services/commands/stateToWhenClauseContext.ts +++ b/packages/app-desktop/services/commands/stateToWhenClauseContext.ts @@ -22,6 +22,8 @@ export default function stateToWhenClauseContext(state: AppState, options: WhenC sidebarVisible: !!state.mainLayout && layoutItemProp(state.mainLayout, 'sideBar', 'visible'), noteListHasNotes: !!state.notes.length, + noteListFocused: state.focusedField === 'noteList', + // Deprecated sideBarVisible: !!state.mainLayout && layoutItemProp(state.mainLayout, 'sideBar', 'visible'), }; diff --git a/packages/lib/services/KeymapService.test.js b/packages/lib/services/KeymapService.test.js index 5180a949902..e7a813d7fcb 100644 --- a/packages/lib/services/KeymapService.test.js +++ b/packages/lib/services/KeymapService.test.js @@ -337,4 +337,12 @@ describe('services_KeymapService', () => { } }); }); + + it('should check whether a DOM event matches an accelerator', () => { + keymapService.initialize(); + keymapService.overrideKeymap([{ command: 'print', accelerator: 'Ctrl+Alt+Shift+A' }]); + const mockEvent = { code: 'KeyA', keyCode: 65, key: 'a', shiftKey: true, ctrlKey: true, altKey: true }; + expect(keymapService.eventMatchesCommandAccelerator(mockEvent, 'print')).toBe(true); + expect(keymapService.eventMatchesCommandAccelerator(mockEvent, 'gotoAnything')).toBe(false); + }); }); diff --git a/packages/lib/services/KeymapService.ts b/packages/lib/services/KeymapService.ts index fc8f95fa7a9..aaef660fff6 100644 --- a/packages/lib/services/KeymapService.ts +++ b/packages/lib/services/KeymapService.ts @@ -3,6 +3,7 @@ import shim from '../shim'; import { _ } from '../locale'; import keysRegExp from './KeymapService_keysRegExp'; import keycodeToElectronMap from './KeymapService_keycodeToElectronMap'; +import type * as React from 'react'; import BaseService from './BaseService'; @@ -416,6 +417,11 @@ export default class KeymapService extends BaseService { return parts.join('+'); } + public eventMatchesCommandAccelerator(event: KeyboardEvent|React.KeyboardEvent, command: string) { + const accelerator = this.domToElectronAccelerator(event); + return this.getAccelerator(command) === accelerator; + } + public on(eventName: Name, callback: EventListenerCallback) { eventManager.on(eventName, callback); }