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