From 65812d9eb98688f3367d7b2284423d3b727655a0 Mon Sep 17 00:00:00 2001 From: Robin Fernandes Date: Mon, 4 Sep 2023 12:58:36 +1000 Subject: [PATCH 1/2] Add buttons and keyboard shortcuts for zoom and scroll. Add hotkey list dialog. --- src/ParseqUI.js | 5 +- src/components/AudioWaveform.tsx | 33 +++++++--- src/components/Header.tsx | 44 ++++++++++++-- src/components/ParseqGrid.tsx | 14 ++++- src/components/Viewport.tsx | 100 ++++++++++++++++++++++++------- src/index.css | 17 ++++++ src/utils/utils.ts | 9 +++ 7 files changed, 186 insertions(+), 36 deletions(-) diff --git a/src/ParseqUI.js b/src/ParseqUI.js index b223a9d..d2b4785 100644 --- a/src/ParseqUI.js +++ b/src/ParseqUI.js @@ -1468,12 +1468,13 @@ const ParseqUI = (props) => { useHotkeys('mod+z', () => { undoManager.undo((recovered => setPersistableState(recovered))); setDebugUndoStack(undoManager.confessUndoStack()); - }, {preventDefault:true, scopes:['main']}, [loadVersion, setPersistableState, undoManager]) + }, {preventDefault:true, scopes:['main']}, [loadVersion, setPersistableState, undoManager]); useHotkeys('shift+mod+z', () => { undoManager.redo((recovered => setPersistableState(recovered))); setDebugUndoStack(undoManager.confessUndoStack()); - }, {preventDefault:true, scopes:['main']}, [loadVersion, setPersistableState, undoManager]) + }, {preventDefault:true, scopes:['main']}, [loadVersion, setPersistableState, undoManager]); + ////////////////////////////////////////// // Main layout diff --git a/src/components/AudioWaveform.tsx b/src/components/AudioWaveform.tsx index deff2de..40c0573 100644 --- a/src/components/AudioWaveform.tsx +++ b/src/components/AudioWaveform.tsx @@ -259,7 +259,17 @@ export function AudioWaveform(props: AudioWaveformProps) { useEffect(() => { //wavesurferRef.current?.on("dblclick", handleDoubleClick); - wavesurferRef.current?.drawer?.on("lick", handleClick); + wavesurferRef.current?.drawer?.on("click", handleClick); + + //HACK - find a better place for this. + if (wavesurferRef.current?.drawer?.wrapper) { + wavesurferRef.current.drawer.wrapper.onmousemove = (e: MouseEvent) => { + if (e.buttons === 1 && wavesurferRef.current) { + wavesurferRef.current.drawer.wrapper.scrollLeft -= e.movementX; + } + } + } + return () => { //wavesurferRef.current?.un("dblclick", handleDoubleClick); wavesurferRef.current?.drawer?.un("click", handleClick); @@ -326,6 +336,10 @@ export function AudioWaveform(props: AudioWaveformProps) { wavesurferRef.current.on("finish", (data) => { setIsPlaying(false); }); + wavesurferRef.current.on('drag', (relativeX) => { + console.log('Drag', relativeX) + }) + if (window) { //@ts-ignore @@ -647,17 +661,17 @@ export function AudioWaveform(props: AudioWaveformProps) { useHotkeys('space', () => playPause(), - {preventDefault:true, scopes: ['main']}, + {preventDefault:true, scopes: ['main', 'grid']}, [playPause]); useHotkeys('shift+space', () => playPause(0, false), - {preventDefault:true, scopes: ['main']}, + {preventDefault:true, scopes: ['main', 'grid']}, [playPause]); useHotkeys('ctrl+space', () => playPause(capturedPos, false), - {preventDefault:true, scopes: ['main']}, + {preventDefault:true, scopes: ['main', 'grid']}, [playPause, capturedPos]); useHotkeys('shift+a', @@ -668,7 +682,7 @@ export function AudioWaveform(props: AudioWaveformProps) { //@ts-ignore setManualEvents(newMarkers); }, - {preventDefault:true, scopes: ['main']}, + {preventDefault:true, scopes: ['main', 'grid']}, [manualEvents]) return <> @@ -715,6 +729,7 @@ export function AudioWaveform(props: AudioWaveformProps) { minPxPerSec={10} autoCenter={false} interact={true} + dragSelection={false} cursorColor={palette.success.light} // @ts-ignore - type definition is wrong? waveColor={[palette.waveformStart.main, palette.waveformEnd.main]} @@ -728,9 +743,11 @@ export function AudioWaveform(props: AudioWaveformProps) { - + + + {playbackPos} {/* */} diff --git a/src/components/Header.tsx b/src/components/Header.tsx index d9ff889..8a66343 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -1,17 +1,18 @@ import { faDiscord, faGithub } from '@fortawesome/free-brands-svg-icons'; -import { faBook, faBug, faFilm, faWaveSquare, faMoon, faLightbulb } from '@fortawesome/free-solid-svg-icons'; +import { faBook, faBug, faFilm, faWaveSquare, faMoon, faLightbulb, faKeyboard } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Box, Chip, Link, Stack, SupportedColorScheme, Typography, useColorScheme, useMediaQuery } from '@mui/material'; +import { Box, Button, Chip, Dialog, DialogActions, DialogContent, DialogTitle, Link, Stack, SupportedColorScheme, Typography, useColorScheme, useMediaQuery } from '@mui/material'; import Grid from '@mui/material/Unstable_Grid2'; import { getAnalytics, isSupported } from "firebase/analytics"; import GitInfo from 'react-git-info/macro'; import Login from "../Login"; import { UserAuthContextProvider } from "../UserAuthContext"; import { app, auth } from '../firebase-config'; -import { getVersionNumber } from '../utils/utils'; +import { getVersionNumber, getModifierKey } from '../utils/utils'; import { useLocation } from 'react-router-dom'; import { UserSettings } from '../UserSettings'; -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; +import { KeyBadge } from './KeyBadge'; var analytics: any; isSupported().then((isSupported) => { @@ -37,6 +38,8 @@ export default function Header() { const changeLogLink = all changes const environment = (process.env.NODE_ENV === 'development' ? 'dev' : getEnvFromHostname()) ; + const [openHotkeysDialog, setOpenHotkeysDialog] = useState(false); + function getEnvFromHostname() { const hostname = window.location.hostname; if (hostname.includes('--dev')) { @@ -74,7 +77,7 @@ export default function Header() { // Don't render a header in raw view. if (location.pathname === '/raw') { return <> - } + } return ( @@ -104,6 +107,10 @@ export default function Header() { UserSettings.setColorScheme(newColorScheme); }} /> + } label="Hotkeys" + onClick={() => setOpenHotkeysDialog(true)} + /> + } label="Tutorial" /> } label="Docs" /> } label="Reference" /> @@ -114,6 +121,33 @@ export default function Header() { + setOpenHotkeysDialog(false)}> + Keyboard shortcuts + +

Editing

+
    +
  • {getModifierKey()}+z: Undo
  • +
  • {getModifierKey()}+shift+z: Undo
  • +
+

Audio playback

+
    +
  • space: Play/pause
  • +
  • shift+space: Play from start
  • +
  • ctrl+space: Play from cursor
  • +
  • ctrl+a: Add event marker at cursor position
  • +
+

Graph & audio views

+
    +
  • shift+: Zoom in
  • +
  • shift+: Zoom out
  • +
  • shift+: Scroll left
  • +
  • shift+: Scroll right
  • +
+ + + +
+
); diff --git a/src/components/ParseqGrid.tsx b/src/components/ParseqGrid.tsx index 7b7587e..a6249e3 100644 --- a/src/components/ParseqGrid.tsx +++ b/src/components/ParseqGrid.tsx @@ -8,6 +8,7 @@ import { GridTooltip } from './GridToolTip'; import { ValueParserParams, ValueSetterParams } from 'ag-grid-community'; import { experimental_extendTheme as extendTheme, useColorScheme } from "@mui/material/styles"; import { themeFactory } from "../theme"; +import { useHotkeysContext } from 'react-hotkeys-hook'; const config = {} const mathjs = create(all, config) @@ -42,6 +43,7 @@ export const ParseqGrid = forwardRef(({ rangeSelection, onSelectRange, onGridRea // eslint-disable-next-line @typescript-eslint/no-unused-vars const {colorScheme, setColorScheme } = useColorScheme(); + const { disableScope: disableHotkeyScope, enableScope: enableHotkeyScope } = useHotkeysContext(); if (!rangeSelection) { rangeSelection = {}; @@ -247,7 +249,17 @@ export const ParseqGrid = forwardRef(({ rangeSelection, onSelectRange, onGridRea }), [fps, bpm]); - return
+ return
{ + disableHotkeyScope('main'); + enableHotkeyScope('grid'); + }} + onBlur={() => { + disableHotkeyScope('grid'); + enableHotkeyScope('main'); + }} + className={colorScheme==='dark'?"ag-theme-alpine-dark":"ag-theme-alpine"} + style={agGridStyle}> {/* @ts-ignore */} window.removeEventListener('resize', handleResize); }, []); + const zoomIn = useCallback(() => { + const rangeSize = currentViewport.endFrame - currentViewport.startFrame; + const rangeCentre = currentViewport.startFrame + rangeSize / 2; + const newRangeSize = (currentViewport.endFrame - currentViewport.startFrame) * 0.9; + props.onChange({ startFrame: Math.max(0, rangeCentre - newRangeSize / 2), endFrame: rangeCentre + newRangeSize / 2 }); + }, [props, currentViewport]); + + const zoomOut = useCallback(() => { + const rangeSize = currentViewport.endFrame - currentViewport.startFrame; + const rangeCentre = currentViewport.startFrame + rangeSize / 2; + const newRangeSize = (currentViewport.endFrame - currentViewport.startFrame) * 1.1; + props.onChange({ startFrame: Math.max(0, rangeCentre - newRangeSize / 2), endFrame: rangeCentre + newRangeSize / 2 }); + }, [props, currentViewport]); + + const scrollLeft = useCallback(() => { + const rangeSize = currentViewport.endFrame - currentViewport.startFrame; + const stepSize = rangeSize * 0.25; + if (currentViewport.startFrame > 0) { + props.onChange({ startFrame: Math.max(0, currentViewport.startFrame - stepSize), endFrame: Math.max(rangeSize, currentViewport.endFrame - stepSize) }); + } + }, [props, currentViewport]); + + const scrollRight = useCallback(() => { + const rangeSize = currentViewport.endFrame - currentViewport.startFrame; + const stepSize = rangeSize * 0.25; + props.onChange({ startFrame: currentViewport.startFrame + stepSize, endFrame: currentViewport.endFrame + stepSize }); + }, [props, currentViewport]); + + const reset = useCallback(() => { + currentViewport.startFrame = 0; + currentViewport.endFrame = props.lastFrame; + props.onChange({ ...currentViewport }); + }, [props, currentViewport]); + + useHotkeys('shift+up', () => { + zoomIn(); + }, { preventDefault: true, scopes: ['main'] }, [zoomIn]); + + useHotkeys('shift+down', () => { + zoomOut(); + }, { preventDefault: true, scopes: ['main'] }, [zoomOut]); + + useHotkeys('shift+left', () => { + scrollLeft(); + }, { preventDefault: true, scopes: ['main'] }, [scrollLeft]); + + useHotkeys('shift+right', () => { + scrollRight(); + }, { preventDefault: true, scopes: ['main'] }, [scrollRight]); if (scale < 0) { return <>; - } + } return ( { @@ -80,22 +132,21 @@ export function Viewport(props: ViewportProps) { props.onChange({ ...currentViewport }); }} hideCursor={true} - onDoubleClickRow={(e, {row, time}) => { - currentViewport.startFrame = 0; - currentViewport.endFrame = props.lastFrame; - props.onChange({ ...currentViewport }); - }} + onDoubleClickRow={(e, { row, time }) => { + reset(); + }} getActionRender={(action: any, row: any) => { const start = frameToXAxisType(action.start, props.xaxisType, props.fps, props.bpm); const end = frameToXAxisType(action.end, props.xaxisType, props.fps, props.bpm); - return
- + return
+ {start} {end} @@ -112,6 +163,15 @@ export function Viewport(props: ViewportProps) { return {value} }} /> + + + + + + + + + ); diff --git a/src/index.css b/src/index.css index 15a84c5..f5e2112 100644 --- a/src/index.css +++ b/src/index.css @@ -38,4 +38,21 @@ p { #waveform .marker-label span:hover { font-size: 0.75em !important; +} + +.key-badge { + display: inline-block; + padding: 3px 3px; + margin-top: 3px; + margin-right: 5px; + margin-left: 5px; + border: 2px solid #ccc; + box-shadow: 1px 1px black; + letter-spacing: .05em; + border-radius: 3px; + background-color: #d4d4d4; + white-space: nowrap; + display: inline-block; + font-size: 1em; + line-height: .85em; } \ No newline at end of file diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 685aafc..a3967ec 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -167,3 +167,12 @@ export const compareParseqDocState = (a: ParseqPersistableState, b: ParseqPersis .filter((k) => !['meta', 'timestamp', 'versionId', 'changes', 'docId'].includes(k)) // exclude these fields they are expected to change. .flatMap((k) => _.isEqual(a[k as keyof ParseqPersistableState], b[k as keyof ParseqPersistableState]) ? [] : [k]); } + +export const getModifierKey = () : string => { + const platform = window.navigator.platform; + if (platform.indexOf("Mac") === 0 || platform === "iPhone") { + return "⌘"; + } else { + return 'ctrl'; + } + } \ No newline at end of file From 2af43ac0624cc4c9560c3ade73ab1865a07b8e95 Mon Sep 17 00:00:00 2001 From: Robin Fernandes Date: Mon, 4 Sep 2023 13:07:19 +1000 Subject: [PATCH 2/2] Add missing file & update supporters. --- src/components/KeyBadge.tsx | 5 +++++ src/data/supporterList.ts | 6 +++++- 2 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 src/components/KeyBadge.tsx diff --git a/src/components/KeyBadge.tsx b/src/components/KeyBadge.tsx new file mode 100644 index 0000000..8b56e9b --- /dev/null +++ b/src/components/KeyBadge.tsx @@ -0,0 +1,5 @@ +import { PropsWithChildren } from 'react'; + +type Props = {} + +export const KeyBadge = ({children} : PropsWithChildren) => {children}; diff --git a/src/data/supporterList.ts b/src/data/supporterList.ts index 5377974..6326293 100644 --- a/src/data/supporterList.ts +++ b/src/data/supporterList.ts @@ -13,5 +13,9 @@ export const supporterList = [ { name: 'Andreas Lewitzki', link: 'https://www.youtube.com/@ro0otz' }, { name: 'veryVANYA', link: 'https://twitter.com/veryVANYA'}, { name: 'Nenad Kuzmanovic', link: '' }, - { name: 'Stash', link: ''} + { name: 'Stash', link: ''}, + { name: 'Sani', link: ''}, + { name: 'Lottery Discountz', link: ''}, + { name: 'Sinneys', link: ''}, + { name: 'Ronny Khalil', link: 'https://ronnykhalil.com/'}, ] \ No newline at end of file