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:
parent
2452de6de5
commit
b855e9e7d1
@ -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',
|
||||
|
@ -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 }) => {
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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,
|
||||
|
2
types.ts
2
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',
|
||||
|
Loading…
Reference in New Issue
Block a user