diff --git a/src/main/configStore.ts b/src/main/configStore.ts index b675cc24..ddf85a44 100644 --- a/src/main/configStore.ts +++ b/src/main/configStore.ts @@ -88,7 +88,6 @@ const defaultKeyBindings: KeyBinding[] = [ { keys: 'KeyE', action: 'export' }, { keys: 'ShiftLeft+Slash', action: 'toggleKeyboardShortcuts' }, - { keys: 'Escape', action: 'closeActiveScreen' }, { keys: 'AltLeft+ArrowUp', action: 'increaseVolume' }, { keys: 'AltLeft+ArrowDown', action: 'decreaseVolume' }, diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index f249f0a0..6e799f17 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -86,7 +86,6 @@ import { generateCutFileNames as generateCutFileNamesRaw, generateCutMergedFileN import { rightBarWidth, leftBarWidth, ffmpegExtractWindow, zoomMax } from './util/constants'; import BigWaveform from './components/BigWaveform'; -import isDev from './isDev'; import { BatchFile, Chapter, CustomTagsByFile, EdlExportType, EdlFileType, EdlImportType, FfmpegCommandLog, FilesMeta, goToTimecodeDirectArgsSchema, openFilesActionArgsSchema, ParamsByStreamId, PlaybackMode, SegmentBase, SegmentColorIndex, SegmentTags, StateSegment, TunerType } from './types'; import { CaptureFormat, KeyboardAction, ApiActionRequest } from '../../../types'; import { FFprobeChapter, FFprobeFormat, FFprobeStream } from '../../../ffprobe'; @@ -119,7 +118,6 @@ const hevcPlaybackSupportedPromise = doesPlayerSupportHevcPlayback(); // eslint-disable-next-line unicorn/prefer-top-level-await hevcPlaybackSupportedPromise.catch((err) => console.error(err)); - function App() { const { t } = useTranslation(); @@ -1913,7 +1911,7 @@ function App() { } }, [customOutDir, ensureWritableOutDir, loadMedia, setWorking, t, withErrorHandling]); - type MainKeyboardAction = Exclude; + type MainKeyboardAction = Exclude; const mainActions = useMemo(() => { async function exportYouTube() { @@ -1952,15 +1950,15 @@ function App() { focusSegmentAtCursor, selectSegmentsAtCursor, increaseRotation, - goToTimecode: () => { goToTimecode(); return false; }, + goToTimecode: () => goToTimecode(), seekBackwards: () => seekRelAccelerated(-1 * keyboardNormalSeekSpeed), seekBackwards2: () => seekRelAccelerated(-1 * keyboardSeekSpeed2), seekBackwards3: () => seekRelAccelerated(-1 * keyboardSeekSpeed3), seekForwards: () => seekRelAccelerated(keyboardNormalSeekSpeed), seekForwards2: () => seekRelAccelerated(keyboardSeekSpeed2), seekForwards3: () => seekRelAccelerated(keyboardSeekSpeed3), - seekBackwardsPercent: () => { seekRelPercent(-0.01); return false; }, - seekForwardsPercent: () => { seekRelPercent(0.01); return false; }, + seekBackwardsPercent: () => seekRelPercent(-0.01), + seekForwardsPercent: () => seekRelPercent(0.01), seekBackwardsKeyframe: () => seekClosestKeyframe(-1), seekForwardsKeyframe: () => seekClosestKeyframe(1), seekPreviousFrame: () => shortStep(-1), @@ -1977,8 +1975,8 @@ function App() { jumpCutEnd, jumpTimelineStart, jumpTimelineEnd, - timelineZoomIn: () => { zoomRel(1); return false; }, - timelineZoomOut: () => { zoomRel(-1); return false; }, + timelineZoomIn: () => zoomRel(1), + timelineZoomOut: () => zoomRel(-1), batchPreviousFile: () => batchFileJump(-1, false), batchNextFile: () => batchFileJump(1, false), batchOpenPreviousFile: () => batchFileJump(-1, true), @@ -1989,11 +1987,11 @@ function App() { removeCurrentCutpoint: () => removeSegment(currentSegIndexSafe), undo: () => cutSegmentsHistory.back(), redo: () => cutSegmentsHistory.forward(), - labelCurrentSegment: () => { labelSegment(currentSegIndexSafe); return false; }, + labelCurrentSegment: () => labelSegment(currentSegIndexSafe), addSegment, duplicateCurrentSegment, - toggleLastCommands: () => { toggleLastCommands(); return false; }, - export: onExportPress, + toggleLastCommands, + export: () => onExportPress(), extractCurrentSegmentFramesAsImages, extractSelectedSegmentsFramesAsImages, reorderSegsByStartTime, @@ -2048,9 +2046,9 @@ function App() { openDirDialog, toggleSettings, openSendReportDialog: () => { openSendReportDialogWithState(); }, - detectBlackScenes: () => { detectBlackScenes(); return false; }, - detectSilentScenes: () => { detectSilentScenes(); return false; }, - detectSceneChanges: () => { detectSceneChanges(); return false; }, + detectBlackScenes, + detectSilentScenes, + detectSceneChanges, readAllKeyframes, createSegmentsFromKeyframes, toggleWaveformMode, @@ -2059,19 +2057,20 @@ function App() { showIncludeExternalStreamsDialog, toggleFullscreenVideo, selectAllMarkers, + // also called from menu (note: no longer a toggle. Esc must be used to close it because it's a dialog now): + toggleKeyboardShortcuts, }; return ret; - }, [togglePlaySelectedSegments, toggleLoopSelectedSegments, pause, timelineToggleComfortZoom, captureSnapshot, captureSnapshotAsCoverArt, setCutStart, setCutEnd, cleanupFilesDialog, splitCurrentSegment, focusSegmentAtCursor, selectSegmentsAtCursor, increaseRotation, jumpCutStart, jumpCutEnd, jumpTimelineStart, jumpTimelineEnd, batchOpenSelectedFile, closeBatch, addSegment, duplicateCurrentSegment, onExportPress, extractCurrentSegmentFramesAsImages, extractSelectedSegmentsFramesAsImages, reorderSegsByStartTime, invertAllSegments, fillSegmentsGaps, combineOverlappingSegments, combineSelectedSegments, createFixedDurationSegments, createNumSegments, createFixedByteSizedSegments, createRandomSegments, alignSegmentTimesToKeyframes, shuffleSegments, clearSegments, toggleSegmentsList, toggleStreamsSelector, extractAllStreams, convertFormatBatch, concatBatch, toggleCaptureFormat, toggleStripAudio, toggleStripVideo, toggleStripSubtitle, toggleStripThumbnail, toggleStripAll, toggleDarkMode, askStartTimeOffset, deselectAllSegments, selectAllSegments, selectOnlyCurrentSegment, editCurrentSegmentTags, toggleCurrentSegmentSelected, invertSelectedSegments, removeSelectedSegments, tryFixInvalidDuration, shiftAllSegmentTimes, toggleMuted, copySegmentsToClipboard, handleShowStreamsSelectorClick, openFilesDialog, openDirDialog, toggleSettings, readAllKeyframes, createSegmentsFromKeyframes, toggleWaveformMode, toggleShowThumbnails, toggleShowKeyframes, showIncludeExternalStreamsDialog, toggleFullscreenVideo, selectAllMarkers, checkFileOpened, cutSegments, seekRel, keyboardSeekAccFactor, togglePlay, play, userChangePlaybackRate, goToTimecode, keyboardNormalSeekSpeed, keyboardSeekSpeed2, keyboardSeekSpeed3, seekRelPercent, seekClosestKeyframe, shortStep, jumpSeg, zoomRel, batchFileJump, removeSegment, currentSegIndexSafe, cutSegmentsHistory, labelSegment, toggleLastCommands, userHtml5ifyCurrentFile, toggleKeyframeCut, applyEnabledStreamsFilter, setPlaybackVolume, commandedTimeRef, closeFileWithConfirm, openSendReportDialogWithState, detectBlackScenes, detectSilentScenes, detectSceneChanges]); + }, [togglePlaySelectedSegments, toggleLoopSelectedSegments, pause, timelineToggleComfortZoom, captureSnapshot, captureSnapshotAsCoverArt, setCutStart, setCutEnd, cleanupFilesDialog, splitCurrentSegment, focusSegmentAtCursor, selectSegmentsAtCursor, increaseRotation, jumpCutStart, jumpCutEnd, jumpTimelineStart, jumpTimelineEnd, batchOpenSelectedFile, closeBatch, addSegment, duplicateCurrentSegment, onExportPress, extractCurrentSegmentFramesAsImages, extractSelectedSegmentsFramesAsImages, reorderSegsByStartTime, invertAllSegments, fillSegmentsGaps, combineOverlappingSegments, combineSelectedSegments, createFixedDurationSegments, createNumSegments, createFixedByteSizedSegments, createRandomSegments, alignSegmentTimesToKeyframes, shuffleSegments, clearSegments, toggleSegmentsList, toggleStreamsSelector, extractAllStreams, convertFormatBatch, concatBatch, toggleCaptureFormat, toggleStripAudio, toggleStripVideo, toggleStripSubtitle, toggleStripThumbnail, toggleStripAll, toggleDarkMode, askStartTimeOffset, deselectAllSegments, selectAllSegments, selectOnlyCurrentSegment, editCurrentSegmentTags, toggleCurrentSegmentSelected, invertSelectedSegments, removeSelectedSegments, tryFixInvalidDuration, shiftAllSegmentTimes, toggleMuted, copySegmentsToClipboard, handleShowStreamsSelectorClick, openFilesDialog, openDirDialog, toggleSettings, readAllKeyframes, createSegmentsFromKeyframes, toggleWaveformMode, toggleShowThumbnails, toggleShowKeyframes, showIncludeExternalStreamsDialog, toggleFullscreenVideo, selectAllMarkers, checkFileOpened, cutSegments, seekRel, keyboardSeekAccFactor, togglePlay, play, userChangePlaybackRate, goToTimecode, keyboardNormalSeekSpeed, keyboardSeekSpeed2, keyboardSeekSpeed3, seekRelPercent, seekClosestKeyframe, shortStep, jumpSeg, zoomRel, batchFileJump, removeSegment, currentSegIndexSafe, cutSegmentsHistory, labelSegment, toggleLastCommands, userHtml5ifyCurrentFile, toggleKeyframeCut, applyEnabledStreamsFilter, setPlaybackVolume, commandedTimeRef, closeFileWithConfirm, openSendReportDialogWithState, detectBlackScenes, detectSilentScenes, detectSceneChanges, toggleKeyboardShortcuts]); const getKeyboardAction = useCallback((action: MainKeyboardAction) => mainActions[action], [mainActions]); - const onKeyUp = useCallback(({ action }: { action: KeyboardAction }) => { + const keyUpActions = useMemo(() => { function seekReset() { seekAccelerationRef.current = 1; } - - const keyUpActions = { + return { seekBackwards: () => seekReset(), seekBackwards2: () => seekReset(), seekBackwards3: () => seekReset(), @@ -2079,56 +2078,9 @@ function App() { seekForwards2: () => seekReset(), seekForwards3: () => seekReset(), }; - const fn = keyUpActions[action as keyof typeof keyUpActions]; - if (fn == null) return true; - fn(); - return true; - }, [seekAccelerationRef]); - - const onKeyDown = useCallback(({ action }: { action: KeyboardAction }) => { - function tryMainActions(mainAction: MainKeyboardAction) { - const fn = getKeyboardAction(mainAction); - if (!fn) return { match: false }; - const bubble = fn(); - if (bubble === undefined) return { match: true }; - return { match: true, bubble }; - } - - if (isDev) console.log('key event', action); - - // always allow - if (action === 'closeActiveScreen') { - closeExportConfirm(); - setConcatSheetOpen(false); - return false; - } - - if (action === 'toggleKeyboardShortcuts') { - toggleKeyboardShortcuts(); - return false; - } - - if (concatSheetOpen || keyboardShortcutsVisible || genericDialog != null) { - return true; // don't allow any further hotkeys, but bubble - } - - if (exportConfirmOpen) { - // only allow export hotkey on export confirm screen - if (action === 'export') { - onExportConfirm(); - return false; // don't bubble - } - return true; // don't allow any other hotkeys, but bubble - } - - // allow main actions - const { match, bubble } = tryMainActions(action); - if (match) return bubble; - - return true; // bubble the event - }, [closeExportConfirm, concatSheetOpen, exportConfirmOpen, genericDialog, getKeyboardAction, keyboardShortcutsVisible, onExportConfirm, toggleKeyboardShortcuts]); + }, []); - const { keyboardLayoutMap, updateKeyboardLayout } = useKeyboard({ keyBindings, onKeyDown, onKeyUp }); + const { keyboardLayoutMap, updateKeyboardLayout } = useKeyboard({ keyBindings, keyUpActions, getKeyboardAction, closeExportConfirm, exportConfirmOpen, concatSheetOpen, setConcatSheetOpen }); useEffect(() => { // eslint-disable-next-line unicorn/prefer-add-event-listener @@ -2292,8 +2244,6 @@ function App() { fn(); }, ] as const), - // also called from menu: - ['toggleKeyboardShortcuts', async () => toggleKeyboardShortcuts()] as const, ]; const allActionsMap = Object.fromEntries(allActions); @@ -2395,26 +2345,6 @@ function App() { runStartupCheck({ onError: ({ title, message }) => setGenericError({ title, err: message }) }); }, [customFfPath, setGenericError]); - const haveBoundAlt = useMemo(() => keyBindings.some(({ keys }) => keys.split('+').includes('alt')), [keyBindings]); - - useEffect(() => { - const handleKeydown = (e: KeyboardEvent) => { - // Keyboard scroll prevention: - // https://stackoverflow.com/questions/8916620/disable-arrow-key-scrolling-in-users-browser - if (e.target === document.body && [32, 37, 38, 39, 40].includes(e.keyCode)) { - e.preventDefault(); - } - - // if the user has bound alt in any of their keybindings, prevent alt from triggering the menu https://github.com/mifi/lossless-cut/issues/2180 - if (haveBoundAlt && e.code === 'AltLeft') { - e.preventDefault(); - } - }; - - window.addEventListener('keydown', handleKeydown); - return () => window.removeEventListener('keydown', handleKeydown); - }, [haveBoundAlt]); - const appContext = useMemo(() => ({ working, setWorking, diff --git a/src/renderer/src/components/KeyboardShortcuts.tsx b/src/renderer/src/components/KeyboardShortcuts.tsx index c075dace..27624456 100644 --- a/src/renderer/src/components/KeyboardShortcuts.tsx +++ b/src/renderer/src/components/KeyboardShortcuts.tsx @@ -713,10 +713,6 @@ const KeyboardShortcuts = memo(({ name: t('Open folder'), category: otherCategory, }, - closeActiveScreen: { - name: t('Close current screen'), - category: otherCategory, - }, closeCurrentFile: { name: t('Close current file'), category: otherCategory, diff --git a/src/renderer/src/hooks/useKeyboard.ts b/src/renderer/src/hooks/useKeyboard.ts index c3f5311d..b2309551 100644 --- a/src/renderer/src/hooks/useKeyboard.ts +++ b/src/renderer/src/hooks/useKeyboard.ts @@ -3,6 +3,7 @@ import { useCallback, useEffect, useRef, useState } from 'react'; import { KeyBinding, KeyboardAction } from '../../../../types'; import { allModifiers, altModifiers, controlModifiers, metaModifiers, shiftModifiers } from '../util'; import { KeyboardLayoutMap } from '../types'; +import isDev from '../isDev'; /* Keyboard testing points (when making large changes): @@ -18,25 +19,94 @@ import { KeyboardLayoutMap } from '../types'; See also https://github.com/mifi/lossless-cut/issues/2515 */ -interface StoredAction { action: KeyboardAction, keyup?: boolean } +interface KeyEventParams { e: KeyboardEvent, action: KeyboardAction | undefined } -export default ({ keyBindings, onKeyDown: onKeyDownProp, onKeyUp: onKeyUpProp }: { +export default ({ keyBindings, keyUpActions, getKeyboardAction, closeExportConfirm, exportConfirmOpen, concatSheetOpen, setConcatSheetOpen }: { keyBindings: KeyBinding[], - onKeyDown: ((a: { action: KeyboardAction }) => boolean) | ((a: { action: KeyboardAction }) => void), - onKeyUp: ((a: { action: KeyboardAction }) => boolean) | ((a: { action: KeyboardAction }) => void), + keyUpActions: Record void>, + getKeyboardAction: (action: KeyboardAction) => (() => boolean) | (() => void), + exportConfirmOpen: boolean, + closeExportConfirm: () => void, + concatSheetOpen: boolean, + setConcatSheetOpen: (open: boolean) => void, }) => { const [keyboardLayoutMap, setKeyboardLayoutMap] = useState(); + const altActionRef = useRef(false); + + const onKeyUp2 = useCallback(({ action }: KeyEventParams) => { + const fn = action && keyUpActions[action]; + fn?.(); + }, [keyUpActions]); + + const onKeyDown2 = useCallback(({ e, action }: KeyEventParams) => { + // Allow escape to close dialogs, no matter what's focused + // todo remove once we use Dialog component (already supports escape button) + const isEscape = e.code === 'Escape'; + if (exportConfirmOpen) { + if (isEscape) { + closeExportConfirm(); + e.preventDefault(); + e.stopPropagation(); + return; + } + if (action !== 'export') { + return; // stop here, don't allow other key actions than export while dialog is open + } + } + if (concatSheetOpen) { + if (isEscape) { + setConcatSheetOpen(false); + e.preventDefault(); + e.stopPropagation(); + } + return; // don't allow any other key actions while dialog is open + } + + // From now on, only handle key events when focus is on document body + // because we don't allow focus to anything else + // except for inputs, buttons, dialogs etc, which we want allowed to handle keys normally. + if (e.target !== document.body) { + return; + } + // Alternatively, this is how mousetrap does it: + // ignore when focus is inside inputs / textareas / selects / contentEditable, + // including elements inside shadow DOM. use composedPath() when available. + /* const path = (typeof e.composedPath === 'function' ? e.composedPath() : [e.target]) as EventTarget[]; + const isEditableInPath = path.some((node) => { + if (!(node instanceof Element)) return false; + const tag = node.tagName; + return tag === 'INPUT' || tag === 'SELECT' || tag === 'TEXTAREA' || (node as HTMLElement).isContentEditable; + }); + if (isEditableInPath) return; + */ + + // run main actions + const matchingFn = action && getKeyboardAction(action); + if (matchingFn != null) { + matchingFn(); + e.preventDefault(); + e.stopPropagation(); + + if (e.altKey) { + altActionRef.current = true; + } + } + + if (isDev) console.log('key event', e.code, action, { defaultPrevented: e.defaultPrevented }); + }, [closeExportConfirm, concatSheetOpen, exportConfirmOpen, getKeyboardAction, setConcatSheetOpen]); + + // optimization to prevent re-binding all the time: - const onKeyDownRef = useRef<(a: StoredAction) => void>(); + const onKeyDownRef = useRef<(a: KeyEventParams) => void>(); useEffect(() => { - onKeyDownRef.current = onKeyDownProp; - }, [onKeyDownProp]); + onKeyDownRef.current = onKeyDown2; + }, [onKeyDown2]); - const onKeyUpRef = useRef<(a: StoredAction) => void>(); + const onKeyUpRef = useRef<(a: KeyEventParams) => void>(); useEffect(() => { - onKeyUpRef.current = onKeyUpProp; - }, [onKeyUpProp]); + onKeyUpRef.current = onKeyUp2; + }, [onKeyUp2]); useEffect(() => { const keyBindingsByKeyCode = keyBindings.reduce((acc, kb) => { @@ -47,38 +117,28 @@ export default ({ keyBindings, onKeyDown: onKeyDownProp, onKeyUp: onKeyUpProp }: return acc; }, {} as Record); - function onKeyDown(params: StoredAction) { + function onKeyDown(params: KeyEventParams) { if (onKeyDownRef.current == null) return true; return onKeyDownRef.current(params); } - function onKeyUp(params: StoredAction) { + function onKeyUp(params: KeyEventParams) { if (onKeyUpRef.current == null) return true; return onKeyUpRef.current(params); } - const handleKeyEvent = (e: KeyboardEvent, isKeyUp: boolean) => { + const handleKeyEvent = (e: KeyboardEvent, ev: 'keydown' | 'keyup') => { // console.log(e) - // Only capture key events when focus is on document body - // because we don't allow focus to anything else - // except for inputs, buttons, dialogs etc, which we want allowed to handle keys normally. - if (e.target !== document.body) { - return; + // If alt has been pressed and the `keydown` resulted in a keyboard action being triggered, prevent alt from triggering the menu popup. + // Note: This has not been tested, and I assume that it can be prevented on `keyup`. + // https://github.com/mifi/lossless-cut/issues/2180 + if (ev === 'keyup' && altActionRef.current && (e.code === 'AltLeft' || e.code === 'AltRight')) { + console.log('Preventing alt menu popup'); + e.preventDefault(); + altActionRef.current = false; } - // Alternatively, this is how mousetrap does it: - // ignore when focus is inside inputs / textareas / selects / contentEditable, - // including elements inside shadow DOM. use composedPath() when available. - /* const path = (typeof e.composedPath === 'function' ? e.composedPath() : [e.target]) as EventTarget[]; - const isEditableInPath = path.some((node) => { - if (!(node instanceof Element)) return false; - const tag = node.tagName; - return tag === 'INPUT' || tag === 'SELECT' || tag === 'TEXTAREA' || (node as HTMLElement).isContentEditable; - }); - if (isEditableInPath) return; - */ - if (allModifiers.has(e.code)) { return; // ignore pure modifier key events } @@ -88,7 +148,8 @@ export default ({ keyBindings, onKeyDown: onKeyDownProp, onKeyUp: onKeyUpProp }: const alt = e.altKey; const meta = e.metaKey; - const matchingKeyBindings = (keyBindingsByKeyCode[e.code] ?? []).filter((kb) => { + // only use first one, if multiple matches (shouldn't happen anyway) + const matchingKeyBinding = (keyBindingsByKeyCode[e.code] ?? []).find((kb) => { const kbKeys = new Set(kb.keys.split('+')); if ((controlModifiers.intersection(kbKeys).size > 0) !== ctrl) return false; if ((shiftModifiers.intersection(kbKeys).size > 0) !== shift) return false; @@ -97,22 +158,12 @@ export default ({ keyBindings, onKeyDown: onKeyDownProp, onKeyUp: onKeyUpProp }: return true; }); - if (matchingKeyBindings.length === 0) { - return; - } - const bubble = matchingKeyBindings.every((kb) => ( - isKeyUp ? onKeyUp({ action: kb.action }) : onKeyDown({ action: kb.action }) - )); - - console.log('handled', isKeyUp ? 'keyup' : 'keydown', e.code, { bubble }); - - if (!bubble) { - e.preventDefault(); - } + if (ev === 'keyup') onKeyUp({ e, action: matchingKeyBinding?.action }); + else onKeyDown({ e, action: matchingKeyBinding?.action }); }; - const handleKeyDown = (e: KeyboardEvent) => handleKeyEvent(e, false); - const handleKeyUp = (e: KeyboardEvent) => handleKeyEvent(e, true); + const handleKeyDown = (e: KeyboardEvent) => handleKeyEvent(e, 'keydown'); + const handleKeyUp = (e: KeyboardEvent) => handleKeyEvent(e, 'keyup'); document.addEventListener('keydown', handleKeyDown); document.addEventListener('keyup', handleKeyUp); @@ -127,6 +178,13 @@ export default ({ keyBindings, onKeyDown: onKeyDownProp, onKeyUp: onKeyUpProp }: setKeyboardLayoutMap(await navigator.keyboard.getLayoutMap()); }, [setKeyboardLayoutMap]); + useEffect(() => { + updateKeyboardLayout(); + + window.addEventListener('focus', updateKeyboardLayout); + return () => window.removeEventListener('focus', updateKeyboardLayout); + }, [updateKeyboardLayout]); + return { keyboardLayoutMap, updateKeyboardLayout, diff --git a/types.ts b/types.ts index 321c14ed..f432a85d 100644 --- a/types.ts +++ b/types.ts @@ -1,4 +1,4 @@ -export type KeyboardAction = 'addSegment' | 'togglePlayResetSpeed' | 'togglePlayNoResetSpeed' | 'reducePlaybackRate' | 'reducePlaybackRateMore' | 'increasePlaybackRate' | 'increasePlaybackRateMore' | 'timelineToggleComfortZoom' | 'seekPreviousFrame' | 'seekNextFrame' | 'captureSnapshot' | 'setCutStart' | 'setCutEnd' | 'removeCurrentSegment' | 'removeCurrentCutpoint' | 'cleanupFilesDialog' | 'splitCurrentSegment' | 'focusSegmentAtCursor' | 'selectSegmentsAtCursor' | 'increaseRotation' | 'goToTimecode' | 'seekBackwards' | 'seekBackwards2' | 'seekBackwards3' | 'seekBackwardsPercent' | 'seekBackwardsPercent' | 'seekBackwardsKeyframe' | 'jumpCutStart' | 'seekForwards' | 'seekForwards2' | 'seekForwards3' | 'seekForwardsPercent' | 'seekForwardsPercent' | 'seekForwardsKeyframe' | 'jumpCutEnd' | 'jumpTimelineStart' | 'jumpTimelineEnd' | 'jumpFirstSegment' | 'jumpPrevSegment' | 'jumpSeekFirstSegment' | 'jumpSeekPrevSegment' | 'timelineZoomIn' | 'timelineZoomIn' | 'batchPreviousFile' | 'jumpLastSegment' | 'jumpNextSegment' | 'jumpSeekLastSegment' | 'jumpSeekNextSegment' | 'timelineZoomOut' | 'timelineZoomOut' | 'batchNextFile' | 'batchOpenSelectedFile' | 'batchOpenPreviousFile' | 'batchOpenNextFile' | 'undo' | 'undo' | 'redo' | 'redo' | 'copySegmentsToClipboard' | 'copySegmentsToClipboard' | 'toggleFullscreenVideo' | 'labelCurrentSegment' | 'export' | 'toggleKeyboardShortcuts' | 'closeActiveScreen' | 'increaseVolume' | 'decreaseVolume' | 'toggleMuted' | 'detectBlackScenes' | 'detectSilentScenes' | 'detectSceneChanges' | 'toggleLastCommands' | 'play' | 'pause' | 'reloadFile' | 'html5ify' | 'makeCursorTimeZero' | 'togglePlayOnlyCurrentSegment' | 'toggleLoopOnlyCurrentSegment' | 'toggleLoopStartEndOnlyCurrentSegment' | 'togglePlaySelectedSegments' | 'toggleLoopSelectedSegments' | 'editCurrentSegmentTags' | 'duplicateCurrentSegment' | 'reorderSegsByStartTime' | 'invertAllSegments' | 'fillSegmentsGaps' | 'shiftAllSegmentTimes' | 'alignSegmentTimesToKeyframes' | 'readAllKeyframes' | 'createSegmentsFromKeyframes' | 'createFixedDurationSegments' | 'createNumSegments' | 'createFixedByteSizedSegments' | 'createRandomSegments' | 'shuffleSegments' | 'combineOverlappingSegments' | 'combineSelectedSegments' | 'clearSegments' | 'toggleSegmentsList' | 'selectOnlyCurrentSegment' | 'deselectAllSegments' | 'selectAllSegments' | 'toggleCurrentSegmentSelected' | 'invertSelectedSegments' | 'removeSelectedSegments' | 'toggleStreamsSelector' | 'extractAllStreams' | 'showStreamsSelector' | 'showIncludeExternalStreamsDialog' | 'captureSnapshotAsCoverArt' | 'extractCurrentSegmentFramesAsImages' | 'extractSelectedSegmentsFramesAsImages' | 'convertFormatBatch' | 'convertFormatCurrentFile' | 'fixInvalidDuration' | 'closeBatch' | 'concatBatch' | 'toggleKeyframeCutMode' | 'toggleCaptureFormat' | 'toggleStripAudio' | 'toggleStripVideo' | 'toggleStripSubtitle' | 'toggleStripThumbnail' | 'toggleStripCurrentFilter' | 'toggleStripAll' | 'toggleDarkMode' | 'setStartTimeOffset' | 'toggleWaveformMode' | 'toggleShowThumbnails' | 'toggleShowKeyframes' | 'toggleSettings' | 'openSendReportDialog' | 'openFilesDialog' | 'openDirDialog' | 'exportYouTube' | 'closeCurrentFile' | 'quit' | 'selectAllMarkers'; +export type KeyboardAction = 'addSegment' | 'togglePlayResetSpeed' | 'togglePlayNoResetSpeed' | 'reducePlaybackRate' | 'reducePlaybackRateMore' | 'increasePlaybackRate' | 'increasePlaybackRateMore' | 'timelineToggleComfortZoom' | 'seekPreviousFrame' | 'seekNextFrame' | 'captureSnapshot' | 'setCutStart' | 'setCutEnd' | 'removeCurrentSegment' | 'removeCurrentCutpoint' | 'cleanupFilesDialog' | 'splitCurrentSegment' | 'focusSegmentAtCursor' | 'selectSegmentsAtCursor' | 'increaseRotation' | 'goToTimecode' | 'seekBackwards' | 'seekBackwards2' | 'seekBackwards3' | 'seekBackwardsPercent' | 'seekBackwardsPercent' | 'seekBackwardsKeyframe' | 'jumpCutStart' | 'seekForwards' | 'seekForwards2' | 'seekForwards3' | 'seekForwardsPercent' | 'seekForwardsPercent' | 'seekForwardsKeyframe' | 'jumpCutEnd' | 'jumpTimelineStart' | 'jumpTimelineEnd' | 'jumpFirstSegment' | 'jumpPrevSegment' | 'jumpSeekFirstSegment' | 'jumpSeekPrevSegment' | 'timelineZoomIn' | 'timelineZoomIn' | 'batchPreviousFile' | 'jumpLastSegment' | 'jumpNextSegment' | 'jumpSeekLastSegment' | 'jumpSeekNextSegment' | 'timelineZoomOut' | 'timelineZoomOut' | 'batchNextFile' | 'batchOpenSelectedFile' | 'batchOpenPreviousFile' | 'batchOpenNextFile' | 'undo' | 'undo' | 'redo' | 'redo' | 'copySegmentsToClipboard' | 'copySegmentsToClipboard' | 'toggleFullscreenVideo' | 'labelCurrentSegment' | 'export' | 'toggleKeyboardShortcuts' | 'increaseVolume' | 'decreaseVolume' | 'toggleMuted' | 'detectBlackScenes' | 'detectSilentScenes' | 'detectSceneChanges' | 'toggleLastCommands' | 'play' | 'pause' | 'reloadFile' | 'html5ify' | 'makeCursorTimeZero' | 'togglePlayOnlyCurrentSegment' | 'toggleLoopOnlyCurrentSegment' | 'toggleLoopStartEndOnlyCurrentSegment' | 'togglePlaySelectedSegments' | 'toggleLoopSelectedSegments' | 'editCurrentSegmentTags' | 'duplicateCurrentSegment' | 'reorderSegsByStartTime' | 'invertAllSegments' | 'fillSegmentsGaps' | 'shiftAllSegmentTimes' | 'alignSegmentTimesToKeyframes' | 'readAllKeyframes' | 'createSegmentsFromKeyframes' | 'createFixedDurationSegments' | 'createNumSegments' | 'createFixedByteSizedSegments' | 'createRandomSegments' | 'shuffleSegments' | 'combineOverlappingSegments' | 'combineSelectedSegments' | 'clearSegments' | 'toggleSegmentsList' | 'selectOnlyCurrentSegment' | 'deselectAllSegments' | 'selectAllSegments' | 'toggleCurrentSegmentSelected' | 'invertSelectedSegments' | 'removeSelectedSegments' | 'toggleStreamsSelector' | 'extractAllStreams' | 'showStreamsSelector' | 'showIncludeExternalStreamsDialog' | 'captureSnapshotAsCoverArt' | 'extractCurrentSegmentFramesAsImages' | 'extractSelectedSegmentsFramesAsImages' | 'convertFormatBatch' | 'convertFormatCurrentFile' | 'fixInvalidDuration' | 'closeBatch' | 'concatBatch' | 'toggleKeyframeCutMode' | 'toggleCaptureFormat' | 'toggleStripAudio' | 'toggleStripVideo' | 'toggleStripSubtitle' | 'toggleStripThumbnail' | 'toggleStripCurrentFilter' | 'toggleStripAll' | 'toggleDarkMode' | 'setStartTimeOffset' | 'toggleWaveformMode' | 'toggleShowThumbnails' | 'toggleShowKeyframes' | 'toggleSettings' | 'openSendReportDialog' | 'openFilesDialog' | 'openDirDialog' | 'exportYouTube' | 'closeCurrentFile' | 'quit' | 'selectAllMarkers'; export interface KeyBinding { keys: string,