1
0
mirror of https://github.com/mifi/lossless-cut.git synced 2024-11-21 18:02:35 +01:00

implent customisable wheel modifiers #1884

- <kbd>alt</kbd> (customisable) + wheel: seek 1 frame
- <kbd>shift</kbd> (customisable) + wheel: seek keyframe
This commit is contained in:
Mikael Finstad 2024-09-29 09:54:15 +02:00
parent 2452de6de5
commit b855e9e7d1
No known key found for this signature in database
GPG Key ID: 25AB36E3E81CBC26
7 changed files with 76 additions and 34 deletions

View File

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

View File

@ -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 }) => {

View File

@ -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 (
<div style={{ ...rowStyle, alignItems: 'center' }}>
<span>{text}</span>
<div style={{ flexGrow: 1 }} />
{getModifier(modifier).map((v) => <kbd key={v} style={{ marginRight: '.7em' }}>{v}</kbd>)}
<FaMouse style={{ marginRight: '.3em' }} />
<span>{wheelText}</span>
</div>
);
}
// 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<Category, ReactNode> = {
[zoomOperationsCategory]: [
<div key="1" style={{ ...rowStyle, alignItems: 'center' }}>
<span>{t('Zoom in/out timeline')}</span>
<span>{t('Pan timeline')}</span>
<div style={{ flexGrow: 1 }} />
<FaMouse style={{ marginRight: '.3em' }} />
<span>{t('Mouse scroll/wheel up/down')}</span>
</div>,
<div key="2" style={{ ...rowStyle, alignItems: 'center' }}>
<span>{t('Pan timeline')}</span>
<div style={{ flexGrow: 1 }} />
{getModifier(mouseWheelZoomModifierKey).map((v) => <kbd key={v} style={{ marginRight: '.7em' }}>{v}</kbd>)}
<FaMouse style={{ marginRight: '.3em' }} />
<span>{t('Mouse scroll/wheel up/down')}</span>
</div>,
<WheelModifier key="2" text={t('Seek one frame')} wheelText={t('Mouse scroll/wheel up/down')} modifier={mouseWheelFrameSeekModifierKey} />,
<WheelModifier key="3" text={t('Seek one key frame')} wheelText={t('Mouse scroll/wheel up/down')} modifier={mouseWheelKeyframeSeekModifierKey} />,
<WheelModifier key="4" text={t('Zoom in/out timeline')} wheelText={t('Mouse scroll/wheel up/down')} modifier={mouseWheelZoomModifierKey} />,
],
};
@ -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

View File

@ -39,6 +39,20 @@ const Header = ({ title }: { title: string }) => (
</Row>
);
function ModifierKeySetting({ text, value, setValue }: { text: string, value: ModifierKey, setValue: (v: ModifierKey) => void }) {
return (
<Row>
<KeyCell>{text}</KeyCell>
<td>
<Select value={value} onChange={(e) => setValue(e.target.value as ModifierKey)}>
{Object.entries(getModifierKeyNames()).map(([key, values]) => (
<option key={key} value={key}>{values.join(' / ')}</option>
))}
</Select>
</td>
</Row>
);
}
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<ChangeEventHandler<HTMLSelectElement>>((e) => {
const { value } = e.target;
@ -362,16 +376,9 @@ function Settings({
</td>
</Row>
<Row>
<KeyCell>{t('Mouse wheel zoom modifier key')}</KeyCell>
<td>
<Select value={mouseWheelZoomModifierKey} onChange={(e) => setMouseWheelZoomModifierKey(e.target.value as ModifierKey)}>
{Object.entries(getModifierKeyNames()).map(([key, values]) => (
<option key={key} value={key}>{values.join(' / ')}</option>
))}
</Select>
</td>
</Row>
<ModifierKeySetting text={t('Mouse wheel zoom modifier key')} value={mouseWheelZoomModifierKey} setValue={setMouseWheelZoomModifierKey} />
<ModifierKeySetting text={t('Mouse wheel frame seek modifier key')} value={mouseWheelFrameSeekModifierKey} setValue={setMouseWheelFrameSeekModifierKey} />
<ModifierKeySetting text={t('Mouse wheel keyframe seek modifier key')} value={mouseWheelKeyframeSeekModifierKey} setValue={setMouseWheelKeyframeSeekModifierKey} />
<Row>
<KeyCell>{t('Timeline trackpad/wheel sensitivity')}</KeyCell>

View File

@ -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<WheelEventHandler<Element>>((e) => {
const { pixelX, pixelY } = normalizeWheel(e);
const onWheel = useCallback<WheelEventHandler<Element>>((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;
}

View File

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

View File

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