improve keyboard handling #2515

simplify
make all keyboard actions preventDefault
remove scroll preventer
remove action `closeActiveScreen` - use escape button instead
try to implement proper alt key handling #2180
pull/2575/head
Mikael Finstad 9 months ago
parent 25110782b2
commit 251107bcdd
No known key found for this signature in database
GPG Key ID: 25AB36E3E81CBC26

@ -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' },

@ -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<KeyboardAction, 'closeActiveScreen' | 'toggleKeyboardShortcuts' | 'goToTimecodeDirect'>;
type MainKeyboardAction = Exclude<KeyboardAction, 'goToTimecodeDirect'>;
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<AppContextType>(() => ({
working,
setWorking,

@ -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,

@ -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<string, () => void>,
getKeyboardAction: (action: KeyboardAction) => (() => boolean) | (() => void),
exportConfirmOpen: boolean,
closeExportConfirm: () => void,
concatSheetOpen: boolean,
setConcatSheetOpen: (open: boolean) => void,
}) => {
const [keyboardLayoutMap, setKeyboardLayoutMap] = useState<KeyboardLayoutMap | undefined>();
const altActionRef = useRef<boolean>(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<string, KeyBinding[]>);
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,

@ -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,

Loading…
Cancel
Save