Skip to content

Commit

Permalink
Add buttons and keyboard shortcuts for zoom and scroll. Add hotkey li…
Browse files Browse the repository at this point in the history
…st dialog.
  • Loading branch information
rewbs committed Sep 4, 2023
1 parent 81124bb commit 65812d9
Show file tree
Hide file tree
Showing 7 changed files with 186 additions and 36 deletions.
5 changes: 3 additions & 2 deletions src/ParseqUI.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
33 changes: 25 additions & 8 deletions src/components/AudioWaveform.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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',
Expand All @@ -668,7 +682,7 @@ export function AudioWaveform(props: AudioWaveformProps) {
//@ts-ignore
setManualEvents(newMarkers);
},
{preventDefault:true, scopes: ['main']},
{preventDefault:true, scopes: ['main', 'grid']},
[manualEvents])

return <>
Expand Down Expand Up @@ -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]}
Expand All @@ -728,9 +743,11 @@ export function AudioWaveform(props: AudioWaveformProps) {
</Grid>
<Grid xs={12}>
<Stack direction="row" spacing={1} alignItems="center">
<Button size="small" disabled={!wavesurferRef.current?.isReady} variant='outlined' onClick={(e) => playPause()}>
{isPlaying ? "⏸️ Pause" : "▶️ Play"}
</Button>
<Tooltip title="Play/pause (space)">
<span><Button size="small" disabled={!wavesurferRef.current?.isReady} variant='outlined' onClick={(e) => playPause()}>
{isPlaying ? "⏸️ Pause" : "▶️ Play"}
</Button></span>
</Tooltip>
<Typography fontSize={"0.75em"}>{playbackPos}</Typography>
{/* <Button onClick={(e) => setShowSpectrogram(showSpectrogram => !showSpectrogram)} >{showSpectrogram? 'Hide' : 'Show'} spectrogram</Button> */}
</Stack>
Expand Down
44 changes: 39 additions & 5 deletions src/components/Header.tsx
Original file line number Diff line number Diff line change
@@ -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) => {
Expand All @@ -37,6 +38,8 @@ export default function Header() {
const changeLogLink = <Link href={"https://github.com/rewbs/sd-parseq/commits/" + (GIT_BRANCH ?? '')}>all changes</Link>
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')) {
Expand Down Expand Up @@ -74,7 +77,7 @@ export default function Header() {
// Don't render a header in raw view.
if (location.pathname === '/raw') {
return <></>
}
}

return (
<Grid container paddingLeft={5} paddingRight={5} paddingBottom={1}>
Expand Down Expand Up @@ -104,6 +107,10 @@ export default function Header() {
UserSettings.setColorScheme(newColorScheme);
}}
/>
<Chip style={{paddingLeft:'2px'}} size='small' variant="outlined" component="a" clickable icon={<FontAwesomeIcon size='2xs' icon={faKeyboard} />} label="Hotkeys"
onClick={() => setOpenHotkeysDialog(true)}
/>

<Chip style={{paddingLeft:'2px'}} size='small' variant="outlined" component="a" href="https://www.youtube.com/playlist?list=PLXbx1PHKHwIHsYFfb5lq2wS8g1FKz6aP8" clickable icon={<FontAwesomeIcon size='2xs' icon={faFilm} />} label="Tutorial" />
<Chip style={{paddingLeft:'2px'}} size='small' variant="outlined" component="a" href="https://github.com/rewbs/sd-parseq#readme" clickable icon={<FontAwesomeIcon size='2xs' icon={faBook} />} label="Docs" />
<Chip style={{paddingLeft:'2px'}} size='small' variant="outlined" component="a" href="/functionDocs" clickable icon={<FontAwesomeIcon size='2xs' icon={faWaveSquare} />} label="Reference" />
Expand All @@ -114,6 +121,33 @@ export default function Header() {
<Login />
</UserAuthContextProvider>
</Stack>
<Dialog maxWidth='md' fullWidth={true} open={openHotkeysDialog} onClose={() => setOpenHotkeysDialog(false)}>
<DialogTitle><FontAwesomeIcon size='2xs' icon={faKeyboard} /> Keyboard shortcuts</DialogTitle>
<DialogContent>
<h3>Editing</h3>
<ul>
<li><KeyBadge>{getModifierKey()}</KeyBadge>+<KeyBadge>z</KeyBadge>: Undo</li>
<li><KeyBadge>{getModifierKey()}</KeyBadge>+<KeyBadge>shift</KeyBadge>+<KeyBadge>z</KeyBadge>: Undo</li>
</ul>
<h3>Audio playback</h3>
<ul>
<li><KeyBadge>space</KeyBadge>: Play/pause</li>
<li><KeyBadge>shift</KeyBadge>+<KeyBadge>space</KeyBadge>: Play from start</li>
<li><KeyBadge>ctrl</KeyBadge>+<KeyBadge>space</KeyBadge>: Play from cursor</li>
<li><KeyBadge>ctrl</KeyBadge>+<KeyBadge>a</KeyBadge>: Add event marker at cursor position</li>
</ul>
<h3>Graph & audio views</h3>
<ul>
<li><KeyBadge>shift</KeyBadge>+<KeyBadge></KeyBadge>: Zoom in</li>
<li><KeyBadge>shift</KeyBadge>+<KeyBadge></KeyBadge>: Zoom out</li>
<li><KeyBadge>shift</KeyBadge>+<KeyBadge></KeyBadge>: Scroll left</li>
<li><KeyBadge>shift</KeyBadge>+<KeyBadge></KeyBadge>: Scroll right</li>
</ul>
<DialogActions>
<Button variant='contained' onClick={()=>setOpenHotkeysDialog(false)}>Close</Button>
</DialogActions>
</DialogContent>
</Dialog>
</Grid>
</Grid>
);
Expand Down
14 changes: 13 additions & 1 deletion src/components/ParseqGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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 = {};
Expand Down Expand Up @@ -247,7 +249,17 @@ export const ParseqGrid = forwardRef(({ rangeSelection, onSelectRange, onGridRea
}), [fps, bpm]);


return <div id='grid-container' className={colorScheme==='dark'?"ag-theme-alpine-dark":"ag-theme-alpine"} style={agGridStyle}>
return <div id='grid-container'
onFocus={() => {
disableHotkeyScope('main');
enableHotkeyScope('grid');
}}
onBlur={() => {
disableHotkeyScope('grid');
enableHotkeyScope('main');
}}
className={colorScheme==='dark'?"ag-theme-alpine-dark":"ag-theme-alpine"}
style={agGridStyle}>
{/* @ts-ignore */}
<AgGridReact
{...agGridProps}
Expand Down
100 changes: 80 additions & 20 deletions src/components/Viewport.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { Typography } from "@mui/material";
import { faArrowsLeftRightToLine, faLeftLong, faMagnifyingGlassMinus, faMagnifyingGlassPlus, faRightLong } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Button, Tooltip, Typography } from "@mui/material";
import { Stack } from "@mui/system";
import { Timeline, TimelineEffect, TimelineRow } from "@xzdarcy/react-timeline-editor";
import { useEffect, useRef, useState } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import { useHotkeys } from 'react-hotkeys-hook';
import { frameToXAxisType } from "../utils/maths";


Expand All @@ -24,14 +27,14 @@ export function Viewport(props: ViewportProps) {
const [timelineWidth, setTimelineWidth] = useState(600);
const scaleWidth = timelineWidth / ((props.lastFrame * 1.1) / scale);
const currentViewport = props.viewport;
const scaleSplitCount = Math.min(scale,20);
const scaleSplitCount = Math.min(scale, 20);
const data: TimelineRow[] = [{
id: 'viewport',
actions: [
{
id: 'viewport',
start: currentViewport?.startFrame??0,
end: currentViewport?.endFrame??props.lastFrame,
start: currentViewport?.startFrame ?? 0,
end: currentViewport?.endFrame ?? props.lastFrame,
effectId: "0",
},
]
Expand All @@ -54,23 +57,72 @@ export function Viewport(props: ViewportProps) {
return (_: any) => 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 (
<span ref={resizeRef}>
<Timeline
style={{ height: '75px', width: '100%' }}
style={{ height: '75px', width: '100%', marginBottom: '5px' }}
editorData={data}
effects={effects}
scale={scale}
scaleWidth={scaleWidth}
rowHeight={15}
gridSnap={true}
maxScaleCount={1000}
minScaleCount={Math.max(1,timelineWidth/scale)}
minScaleCount={Math.max(1, timelineWidth / scale)}
scaleSplitCount={scaleSplitCount}

onChange={(e: any) => {
Expand All @@ -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 <div style={{
borderRadius: '5px',
marginTop: '1px',
overflow: 'hidden',
maxHeight: '15px',
backgroundColor: getViewportColour(action) }}>

return <div style={{
borderRadius: '5px',
marginTop: '1px',
overflow: 'hidden',
maxHeight: '15px',
backgroundColor: getViewportColour(action)
}}>

<Stack direction="row" justifyContent="space-between" paddingLeft={'5px'} paddingRight={'5px'}>
<Typography paddingLeft={'5px'} color={'white'} fontSize='0.7em'>{start}</Typography>
<Typography paddingLeft={'5px'} color={'white'} fontSize='0.7em'>{end}</Typography>
Expand All @@ -112,6 +163,15 @@ export function Viewport(props: ViewportProps) {
return <Typography fontSize={'0.75em'} color={colour}>{value}</Typography>
}}
/>
<Stack direction={"row"} justifyContent={'space-between'} >
<Tooltip title="Scroll left (shift-left)"><span><Button size='small' variant='contained' onClick={scrollLeft} disabled={currentViewport.startFrame === 0} ><FontAwesomeIcon icon={faLeftLong} /></Button></span></Tooltip>
<Stack direction={"row"} spacing={2}>
<Tooltip title="Zoom out (shift-down)"><Button size='small' variant='contained' onClick={zoomOut}><FontAwesomeIcon icon={faMagnifyingGlassMinus} /></Button></Tooltip>
<Tooltip title="Reset viewport"><Button size='small' variant='contained' onClick={reset}><FontAwesomeIcon icon={faArrowsLeftRightToLine} /></Button></Tooltip>
<Tooltip title="Zoom in (shift-up)"><Button size='small' variant='contained' onClick={zoomIn}><FontAwesomeIcon icon={faMagnifyingGlassPlus} /></Button></Tooltip>
</Stack>
<Tooltip title="Scroll right (shift-right)"><Button size='small' variant='contained' onClick={scrollRight} ><FontAwesomeIcon icon={faRightLong} /></Button></Tooltip>
</Stack>
</span>
);

Expand Down
17 changes: 17 additions & 0 deletions src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Loading

0 comments on commit 65812d9

Please sign in to comment.