diff --git a/src/main/configStore.ts b/src/main/configStore.ts index 5412b186..390bb956 100644 --- a/src/main/configStore.ts +++ b/src/main/configStore.ts @@ -132,6 +132,8 @@ const defaults: Config = { storeProjectInWorkingDir: true, enableOverwriteOutput: true, mouseWheelZoomModifierKey: 'ctrl', + mouseWheelFrameSeekModifierKey: 'alt', + mouseWheelKeyframeSeekModifierKey: 'shift', captureFrameMethod: 'videotag', // we don't default to ffmpeg because ffmpeg might choose a frame slightly off captureFrameQuality: 0.95, captureFrameFileNameFormat: 'timestamp', diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 1618502d..e4740a04 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -171,7 +171,7 @@ function App() { const allUserSettings = useUserSettingsRoot(); const { - captureFormat, setCaptureFormat, customOutDir, setCustomOutDir, keyframeCut, setKeyframeCut, preserveMovData, setPreserveMovData, movFastStart, setMovFastStart, avoidNegativeTs, autoMerge, timecodeFormat, invertCutSegments, setInvertCutSegments, autoExportExtraStreams, askBeforeClose, enableAskForImportChapters, enableAskForFileOpenAction, playbackVolume, setPlaybackVolume, autoSaveProjectFile, wheelSensitivity, invertTimelineScroll, language, ffmpegExperimental, hideNotifications, hideOsNotifications, autoLoadTimecode, autoDeleteMergedSegments, exportConfirmEnabled, setExportConfirmEnabled, segmentsToChapters, setSegmentsToChapters, preserveMetadataOnMerge, setPreserveMetadataOnMerge, simpleMode, setSimpleMode, outSegTemplate, setOutSegTemplate, mergedFileTemplate, setMergedFileTemplate, keyboardSeekAccFactor, keyboardNormalSeekSpeed, keyboardSeekSpeed2, keyboardSeekSpeed3, treatInputFileModifiedTimeAsStart, treatOutputFileModifiedTimeAsStart, outFormatLocked, setOutFormatLocked, safeOutputFileName, setSafeOutputFileName, enableAutoHtml5ify, segmentsToChaptersOnly, keyBindings, setKeyBindings, resetKeyBindings, enableSmartCut, customFfPath, storeProjectInWorkingDir, setStoreProjectInWorkingDir, enableOverwriteOutput, mouseWheelZoomModifierKey, captureFrameMethod, captureFrameQuality, captureFrameFileNameFormat, enableNativeHevc, cleanupChoices, setCleanupChoices, darkMode, setDarkMode, preferStrongColors, outputFileNameMinZeroPadding, cutFromAdjustmentFrames, + captureFormat, setCaptureFormat, customOutDir, setCustomOutDir, keyframeCut, setKeyframeCut, preserveMovData, setPreserveMovData, movFastStart, setMovFastStart, avoidNegativeTs, autoMerge, timecodeFormat, invertCutSegments, setInvertCutSegments, autoExportExtraStreams, askBeforeClose, enableAskForImportChapters, enableAskForFileOpenAction, playbackVolume, setPlaybackVolume, autoSaveProjectFile, wheelSensitivity, invertTimelineScroll, language, ffmpegExperimental, hideNotifications, hideOsNotifications, autoLoadTimecode, autoDeleteMergedSegments, exportConfirmEnabled, setExportConfirmEnabled, segmentsToChapters, setSegmentsToChapters, preserveMetadataOnMerge, setPreserveMetadataOnMerge, simpleMode, setSimpleMode, outSegTemplate, setOutSegTemplate, mergedFileTemplate, setMergedFileTemplate, keyboardSeekAccFactor, keyboardNormalSeekSpeed, keyboardSeekSpeed2, keyboardSeekSpeed3, treatInputFileModifiedTimeAsStart, treatOutputFileModifiedTimeAsStart, outFormatLocked, setOutFormatLocked, safeOutputFileName, setSafeOutputFileName, enableAutoHtml5ify, segmentsToChaptersOnly, keyBindings, setKeyBindings, resetKeyBindings, enableSmartCut, customFfPath, storeProjectInWorkingDir, setStoreProjectInWorkingDir, enableOverwriteOutput, mouseWheelZoomModifierKey, mouseWheelFrameSeekModifierKey, mouseWheelKeyframeSeekModifierKey, captureFrameMethod, captureFrameQuality, captureFrameFileNameFormat, enableNativeHevc, cleanupChoices, setCleanupChoices, darkMode, setDarkMode, preferStrongColors, outputFileNameMinZeroPadding, cutFromAdjustmentFrames, } = allUserSettings; const { working, setWorking, workingRef, abortWorking } = useLoading(); @@ -354,8 +354,6 @@ function App() { seekRel(val * zoomedDuration); }, [seekRel, zoomedDuration]); - const onTimelineWheel = useTimelineScroll({ wheelSensitivity, mouseWheelZoomModifierKey, invertTimelineScroll, zoomRel, seekRel }); - const shortStep = useCallback((direction: number) => { // If we don't know fps, just assume 30 (for example if unknown audio file) const fps = detectedFps || 30; @@ -1406,6 +1404,8 @@ function App() { seekAbs(time); }, [findNearestKeyFrameTime, getRelevantTime, seekAbs]); + const onTimelineWheel = useTimelineScroll({ wheelSensitivity, mouseWheelZoomModifierKey, mouseWheelFrameSeekModifierKey, mouseWheelKeyframeSeekModifierKey, invertTimelineScroll, zoomRel, seekRel, shortStep, seekClosestKeyframe }); + const seekAccelerationRef = useRef(1); const userOpenSingleFile = useCallback(async ({ path: pathIn, isLlcProject }: { path: string, isLlcProject?: boolean }) => { diff --git a/src/renderer/src/components/KeyboardShortcuts.tsx b/src/renderer/src/components/KeyboardShortcuts.tsx index 8d83e944..a3955d28 100644 --- a/src/renderer/src/components/KeyboardShortcuts.tsx +++ b/src/renderer/src/components/KeyboardShortcuts.tsx @@ -12,7 +12,7 @@ import Swal from '../swal'; import SetCutpointButton from './SetCutpointButton'; import SegmentCutpointButton from './SegmentCutpointButton'; import { getModifier } from '../hooks/useTimelineScroll'; -import { KeyBinding, KeyboardAction } from '../../../../types'; +import { KeyBinding, KeyboardAction, ModifierKey } from '../../../../types'; import { StateSegment } from '../types'; import Sheet from './Sheet'; @@ -119,6 +119,18 @@ const CreateBinding = memo(({ const rowStyle = { display: 'flex', alignItems: 'center', borderBottom: '1px solid rgba(0,0,0,0.1)', paddingBottom: '.2em' }; +function WheelModifier({ text, wheelText, modifier }: { text: string, wheelText: string, modifier: ModifierKey }) { + return ( +
+ {text} +
+ {getModifier(modifier).map((v) => {v})} + + {wheelText} +
+ ); +} + // eslint-disable-next-line react/display-name const KeyboardShortcuts = memo(({ keyBindings, setKeyBindings, resetKeyBindings, currentCutSeg, @@ -127,7 +139,7 @@ const KeyboardShortcuts = memo(({ }) => { const { t } = useTranslation(); - const { mouseWheelZoomModifierKey } = useUserSettings(); + const { mouseWheelZoomModifierKey, mouseWheelFrameSeekModifierKey, mouseWheelKeyframeSeekModifierKey } = useUserSettings(); const { actionsMap, extraLinesPerCategory } = useMemo(() => { const playbackCategory = t('Playback'); @@ -631,19 +643,17 @@ const KeyboardShortcuts = memo(({ const extraLinesPerCategory: Record = { [zoomOperationsCategory]: [
- {t('Zoom in/out timeline')} + {t('Pan timeline')}
{t('Mouse scroll/wheel up/down')}
, -
- {t('Pan timeline')} -
- {getModifier(mouseWheelZoomModifierKey).map((v) => {v})} - - {t('Mouse scroll/wheel up/down')} -
, + , + + , + + , ], }; @@ -651,7 +661,7 @@ const KeyboardShortcuts = memo(({ extraLinesPerCategory, actionsMap, }; - }, [currentCutSeg, mouseWheelZoomModifierKey, t]); + }, [currentCutSeg, mouseWheelFrameSeekModifierKey, mouseWheelKeyframeSeekModifierKey, mouseWheelZoomModifierKey, t]); useEffect(() => { // cleanup invalid bindings, to prevent renamed actions from blocking user to rebind diff --git a/src/renderer/src/components/Settings.tsx b/src/renderer/src/components/Settings.tsx index 7252b8a9..d784812e 100644 --- a/src/renderer/src/components/Settings.tsx +++ b/src/renderer/src/components/Settings.tsx @@ -39,6 +39,20 @@ const Header = ({ title }: { title: string }) => ( ); +function ModifierKeySetting({ text, value, setValue }: { text: string, value: ModifierKey, setValue: (v: ModifierKey) => void }) { + return ( + + {text} + + + + + ); +} const detailsStyle: CSSProperties = { opacity: 0.75, fontSize: '.9em', marginTop: '.3em' }; function Settings({ @@ -59,7 +73,7 @@ function Settings({ const { t } = useTranslation(); const [showAdvanced, setShowAdvanced] = useState(!simpleMode); - const { customOutDir, changeOutDir, keyframeCut, toggleKeyframeCut, timecodeFormat, setTimecodeFormat, invertCutSegments, setInvertCutSegments, askBeforeClose, setAskBeforeClose, enableAskForImportChapters, setEnableAskForImportChapters, enableAskForFileOpenAction, setEnableAskForFileOpenAction, autoSaveProjectFile, setAutoSaveProjectFile, invertTimelineScroll, setInvertTimelineScroll, language, setLanguage, hideNotifications, setHideNotifications, hideOsNotifications, setHideOsNotifications, autoLoadTimecode, setAutoLoadTimecode, enableAutoHtml5ify, setEnableAutoHtml5ify, customFfPath, setCustomFfPath, storeProjectInWorkingDir, mouseWheelZoomModifierKey, setMouseWheelZoomModifierKey, captureFrameMethod, setCaptureFrameMethod, captureFrameQuality, setCaptureFrameQuality, captureFrameFileNameFormat, setCaptureFrameFileNameFormat, enableNativeHevc, setEnableNativeHevc, enableUpdateCheck, setEnableUpdateCheck, allowMultipleInstances, setAllowMultipleInstances, preferStrongColors, setPreferStrongColors, treatInputFileModifiedTimeAsStart, setTreatInputFileModifiedTimeAsStart, treatOutputFileModifiedTimeAsStart, setTreatOutputFileModifiedTimeAsStart, exportConfirmEnabled, toggleExportConfirmEnabled } = useUserSettings(); + const { customOutDir, changeOutDir, keyframeCut, toggleKeyframeCut, timecodeFormat, setTimecodeFormat, invertCutSegments, setInvertCutSegments, askBeforeClose, setAskBeforeClose, enableAskForImportChapters, setEnableAskForImportChapters, enableAskForFileOpenAction, setEnableAskForFileOpenAction, autoSaveProjectFile, setAutoSaveProjectFile, invertTimelineScroll, setInvertTimelineScroll, language, setLanguage, hideNotifications, setHideNotifications, hideOsNotifications, setHideOsNotifications, autoLoadTimecode, setAutoLoadTimecode, enableAutoHtml5ify, setEnableAutoHtml5ify, customFfPath, setCustomFfPath, storeProjectInWorkingDir, mouseWheelZoomModifierKey, setMouseWheelZoomModifierKey, mouseWheelFrameSeekModifierKey, setMouseWheelFrameSeekModifierKey, mouseWheelKeyframeSeekModifierKey, setMouseWheelKeyframeSeekModifierKey, captureFrameMethod, setCaptureFrameMethod, captureFrameQuality, setCaptureFrameQuality, captureFrameFileNameFormat, setCaptureFrameFileNameFormat, enableNativeHevc, setEnableNativeHevc, enableUpdateCheck, setEnableUpdateCheck, allowMultipleInstances, setAllowMultipleInstances, preferStrongColors, setPreferStrongColors, treatInputFileModifiedTimeAsStart, setTreatInputFileModifiedTimeAsStart, treatOutputFileModifiedTimeAsStart, setTreatOutputFileModifiedTimeAsStart, exportConfirmEnabled, toggleExportConfirmEnabled } = useUserSettings(); const onLangChange = useCallback>((e) => { const { value } = e.target; @@ -362,16 +376,9 @@ function Settings({ - - {t('Mouse wheel zoom modifier key')} - - - - + + + {t('Timeline trackpad/wheel sensitivity')} diff --git a/src/renderer/src/hooks/useTimelineScroll.ts b/src/renderer/src/hooks/useTimelineScroll.ts index b44d98d3..ae7f3880 100644 --- a/src/renderer/src/hooks/useTimelineScroll.ts +++ b/src/renderer/src/hooks/useTimelineScroll.ts @@ -9,7 +9,7 @@ export const keyMap = { shift: 'shiftKey', alt: 'altKey', meta: 'metaKey', -}; +} as const; export const getModifierKeyNames = () => ({ ctrl: [t('Ctrl')], @@ -20,22 +20,35 @@ export const getModifierKeyNames = () => ({ export const getModifier = (key: ModifierKey) => getModifierKeyNames()[key]; -function useTimelineScroll({ wheelSensitivity, mouseWheelZoomModifierKey, invertTimelineScroll, zoomRel, seekRel }: { - wheelSensitivity: number, mouseWheelZoomModifierKey: ModifierKey, invertTimelineScroll?: boolean | undefined, zoomRel: (a: number) => void, seekRel: (a: number) => void, +function useTimelineScroll({ wheelSensitivity, mouseWheelZoomModifierKey, mouseWheelFrameSeekModifierKey, mouseWheelKeyframeSeekModifierKey, invertTimelineScroll, zoomRel, seekRel, shortStep, seekClosestKeyframe }: { + wheelSensitivity: number, + mouseWheelZoomModifierKey: ModifierKey, + mouseWheelFrameSeekModifierKey: ModifierKey, + mouseWheelKeyframeSeekModifierKey: ModifierKey, + invertTimelineScroll?: boolean | undefined, + zoomRel: (a: number) => void, + seekRel: (a: number) => void, + shortStep: (a: number) => void, + seekClosestKeyframe: (a: number) => void, }) { - const onWheel = useCallback>((e) => { - const { pixelX, pixelY } = normalizeWheel(e); + const onWheel = useCallback>((wheelEvent) => { + const { pixelX, pixelY } = normalizeWheel(wheelEvent); // console.log({ spinX, spinY, pixelX, pixelY }); const direction = invertTimelineScroll ? 1 : -1; - const modifierKey = keyMap[mouseWheelZoomModifierKey]; - if (e[modifierKey]) { - zoomRel(direction * (pixelY) * wheelSensitivity * 0.4); + const makeUnit = (v: number) => ((direction * v) > 0 ? 1 : -1); + + if (wheelEvent[keyMap[mouseWheelZoomModifierKey]]) { + zoomRel(direction * pixelY * wheelSensitivity * 0.4); + } else if (wheelEvent[keyMap[mouseWheelFrameSeekModifierKey]]) { + shortStep(makeUnit(pixelX + pixelY)); + } else if (wheelEvent[keyMap[mouseWheelKeyframeSeekModifierKey]]) { + seekClosestKeyframe(makeUnit(pixelX + pixelY)); } else { seekRel(direction * (pixelX + pixelY) * wheelSensitivity * 0.2); } - }, [invertTimelineScroll, mouseWheelZoomModifierKey, zoomRel, wheelSensitivity, seekRel]); + }, [invertTimelineScroll, mouseWheelZoomModifierKey, mouseWheelFrameSeekModifierKey, mouseWheelKeyframeSeekModifierKey, zoomRel, wheelSensitivity, shortStep, seekClosestKeyframe, seekRel]); return onWheel; } diff --git a/src/renderer/src/hooks/useUserSettingsRoot.ts b/src/renderer/src/hooks/useUserSettingsRoot.ts index b204e428..b8667f37 100644 --- a/src/renderer/src/hooks/useUserSettingsRoot.ts +++ b/src/renderer/src/hooks/useUserSettingsRoot.ts @@ -133,6 +133,10 @@ export default () => { useEffect(() => safeSetConfig({ enableOverwriteOutput }), [enableOverwriteOutput]); const [mouseWheelZoomModifierKey, setMouseWheelZoomModifierKey] = useState(safeGetConfigInitial('mouseWheelZoomModifierKey')); useEffect(() => safeSetConfig({ mouseWheelZoomModifierKey }), [mouseWheelZoomModifierKey]); + const [mouseWheelFrameSeekModifierKey, setMouseWheelFrameSeekModifierKey] = useState(safeGetConfigInitial('mouseWheelFrameSeekModifierKey')); + useEffect(() => safeSetConfig({ mouseWheelFrameSeekModifierKey }), [mouseWheelFrameSeekModifierKey]); + const [mouseWheelKeyframeSeekModifierKey, setMouseWheelKeyframeSeekModifierKey] = useState(safeGetConfigInitial('mouseWheelKeyframeSeekModifierKey')); + useEffect(() => safeSetConfig({ mouseWheelKeyframeSeekModifierKey }), [mouseWheelKeyframeSeekModifierKey]); const [captureFrameMethod, setCaptureFrameMethod] = useState(safeGetConfigInitial('captureFrameMethod')); useEffect(() => safeSetConfig({ captureFrameMethod }), [captureFrameMethod]); const [captureFrameQuality, setCaptureFrameQuality] = useState(safeGetConfigInitial('captureFrameQuality')); @@ -261,6 +265,10 @@ export default () => { setEnableOverwriteOutput, mouseWheelZoomModifierKey, setMouseWheelZoomModifierKey, + mouseWheelFrameSeekModifierKey, + setMouseWheelFrameSeekModifierKey, + mouseWheelKeyframeSeekModifierKey, + setMouseWheelKeyframeSeekModifierKey, captureFrameMethod, setCaptureFrameMethod, captureFrameQuality, diff --git a/types.ts b/types.ts index 96bae79f..82183ab3 100644 --- a/types.ts +++ b/types.ts @@ -91,6 +91,8 @@ export interface Config { storeProjectInWorkingDir: boolean, enableOverwriteOutput: boolean, mouseWheelZoomModifierKey: ModifierKey, + mouseWheelFrameSeekModifierKey: ModifierKey, + mouseWheelKeyframeSeekModifierKey: ModifierKey, captureFrameMethod: 'videotag' | 'ffmpeg', captureFrameQuality: number, captureFrameFileNameFormat: 'timestamp' | 'index',