Skip to content

Commit

Permalink
Merge pull request #1033 from Arnei/zoom-timeline-revival
Browse files Browse the repository at this point in the history
Basic zoom functionality for the cutting view
  • Loading branch information
Arnei authored Jun 25, 2024
2 parents 7d8a616 + ed6fda8 commit b321cf9
Show file tree
Hide file tree
Showing 7 changed files with 286 additions and 45 deletions.
20 changes: 14 additions & 6 deletions src/globalKeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
* If you add a new keyMap, be sure to add it to the getAllHotkeys function
*/
import { match } from "@opencast/appkit";
import { isString } from "lodash";
import { ParseKeys } from "i18next";
import { isMacOs } from "react-device-detect";

Expand All @@ -20,13 +21,10 @@ const groupSubtitleList = "keyboardControls.groupSubtitleList";
/**
* Helper function that rewrites keys based on the OS
*/
export const rewriteKeys = (key: string) => {
let newKey = key;
if (isMacOs) {
newKey = newKey.replace("Alt", "Option");
}
export const rewriteKeys = (key: string | IKey) => {
const newKey = isString(key) ? key : key.key.replaceAll(key.combinationKey, "+");

return newKey;
return isMacOs ? newKey.replace("Alt", "Option") : newKey;
};

export const getGroupName = (groupName: string): ParseKeys => {
Expand All @@ -49,6 +47,7 @@ export interface IKeyGroup {
export interface IKey {
name: string;
key: string;
combinationKey?: string;
}

export const KEYMAP: IKeyMap = {
Expand Down Expand Up @@ -87,6 +86,15 @@ export const KEYMAP: IKeyMap = {
name: "cuttingActions.mergeRight-button",
key: "Shift+Alt+m",
},
zoomIn: {
name: "cuttingActions.zoomIn",
key: "Shift;Alt;z, +",
combinationKey: ";",
},
zoomOut: {
name: "cuttingActions.zoomOut",
key: "Shift+Alt+t, -",
},
},
timeline: {
left: {
Expand Down
7 changes: 6 additions & 1 deletion src/i18n/locales/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,12 @@
"mergeLeft-tooltip-aria": "Merge Left. Combine the currently active segment with the segment to its left. Hotkey: {{hotkeyName}}.",
"mergeRight-button": "Merge Right",
"mergeRight-tooltip": "Combine the currently active segment with the segment to its right. Hotkey: {{hotkeyName}}",
"mergeRight-tooltip-aria": "Merge Right. Combine the currently active segment with the segment to its right. Hotkey: {{hotkeyName}}."
"mergeRight-tooltip-aria": "Merge Right. Combine the currently active segment with the segment to its right. Hotkey: {{hotkeyName}}.",
"zoom": "Zoom",
"zoomSlider-aria": "Zoom. Zoom in or out of the timeline. Hotkey for Zoom in: {{hotkeyNameIn}}. Hotkey for Zoom out: {{hotkeyNameOut}}.",
"zoomSlider-tooltip": "Zoom in or out of the timeline. Hotkey for Zoom in: {{hotkeyNameIn}}. Hotkey for Zoom out: {{hotkeyNameOut}}.",
"zoomIn": "Zoom in",
"zoomOut": "Zoom out"
},

"video": {
Expand Down
165 changes: 155 additions & 10 deletions src/main/CuttingActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,25 @@ import { css } from "@emotion/react";

import { useAppDispatch, useAppSelector } from "../redux/store";
import {
cut, markAsDeletedOrAlive, selectIsCurrentSegmentAlive, mergeLeft, mergeRight, mergeAll,
cut,
markAsDeletedOrAlive,
mergeAll,
mergeLeft,
mergeRight,
selectIsCurrentSegmentAlive,
selectTimelineZoom,
selectTimelineZoomMax,
setTimelineZoom,
timelineZoomIn,
timelineZoomOut,
} from "../redux/videoSlice";
import { KEYMAP, rewriteKeys } from "../globalKeys";
import { ActionCreatorWithoutPayload } from "@reduxjs/toolkit";
import { ActionCreatorWithoutPayload, ActionCreatorWithPayload } from "@reduxjs/toolkit";

import { useTranslation } from "react-i18next";
import { useTheme } from "../themes";
import { ThemedTooltip } from "./Tooltip";
import { Slider } from "@mui/material";
import { useHotkeys } from "react-hotkeys-hook";

/**
Expand All @@ -36,8 +47,19 @@ const CuttingActions: React.FC = () => {
* @param action redux event to dispatch
* @param ref Pass a reference if the clicked element should lose focus
*/
const dispatchAction = (action: ActionCreatorWithoutPayload<string>, ref?: React.RefObject<HTMLDivElement>) => {
dispatch(action());
const dispatchAction = (
action: ActionCreatorWithoutPayload<string> | undefined,
actionWithPayload?: ActionCreatorWithPayload<number, string> | undefined,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
payload?: any,
ref?: React.RefObject<HTMLDivElement>
) => {
if (action) {
dispatch(action());
}
if (actionWithPayload) {
dispatch(actionWithPayload(payload));
}

// Lose focus if clicked by mouse
if (ref) {
Expand Down Expand Up @@ -70,6 +92,18 @@ const CuttingActions: React.FC = () => {
{ preventDefault: true },
[mergeRight]
);
useHotkeys(
KEYMAP.cutting.zoomIn.key,
() => dispatchAction(timelineZoomIn),
{ preventDefault: true, combinationKey: KEYMAP.cutting.zoomIn.combinationKey },
[timelineZoomIn]
);
useHotkeys(
KEYMAP.cutting.zoomOut.key,
() => dispatchAction(timelineZoomOut, undefined),
{ preventDefault: true },
[timelineZoomOut]
);

const cuttingStyle = css({
display: "flex",
Expand All @@ -89,6 +123,8 @@ const CuttingActions: React.FC = () => {
actionName={t("cuttingActions.cut-button")}
actionHandler={dispatchAction}
action={cut}
actionWithPayload={undefined}
payload={undefined}
tooltip={t("cuttingActions.cut-tooltip", { hotkeyName: rewriteKeys(KEYMAP.cutting.cut.key) })}
ariaLabelText={t("cuttingActions.cut-tooltip-aria", { hotkeyName: rewriteKeys(KEYMAP.cutting.cut.key) })}
/>
Expand All @@ -101,6 +137,8 @@ const CuttingActions: React.FC = () => {
actionName={t("cuttingActions.mergeLeft-button")}
actionHandler={dispatchAction}
action={mergeLeft}
actionWithPayload={undefined}
payload={undefined}
tooltip={t("cuttingActions.mergeLeft-tooltip", { hotkeyName: rewriteKeys(KEYMAP.cutting.mergeLeft.key) })}
ariaLabelText={
t("cuttingActions.mergeLeft-tooltip-aria", { hotkeyName: rewriteKeys(KEYMAP.cutting.mergeLeft.key) })
Expand All @@ -111,6 +149,8 @@ const CuttingActions: React.FC = () => {
actionName={t("cuttingActions.mergeRight-button")}
actionHandler={dispatchAction}
action={mergeRight}
actionWithPayload={undefined}
payload={undefined}
tooltip={t("cuttingActions.mergeRight-tooltip", { hotkeyName: rewriteKeys(KEYMAP.cutting.mergeRight.key) })}
ariaLabelText={
t("cuttingActions.mergeRight-tooltip-aria", { hotkeyName: rewriteKeys(KEYMAP.cutting.mergeRight.key) })
Expand All @@ -121,9 +161,22 @@ const CuttingActions: React.FC = () => {
actionName={t("cuttingActions.merge-all-button")}
actionHandler={dispatchAction}
action={mergeAll}
actionWithPayload={undefined}
payload={undefined}
tooltip={t("cuttingActions.merge-all-tooltip")}
ariaLabelText={t("cuttingActions.merge-all-tooltip-aria")}
/>
<div css={verticalLineStyle} />
<ZoomSlider actionHandler={dispatchAction}
tooltip={t("cuttingActions.zoomSlider-tooltip", {
hotkeyNameIn: rewriteKeys(KEYMAP.cutting.zoomIn),
hotkeyNameOut: rewriteKeys(KEYMAP.cutting.zoomOut),
})}
ariaLabelText={t("cuttingActions.zoomSlider-aria", {
hotkeyNameIn: rewriteKeys(KEYMAP.cutting.zoomIn),
hotkeyNameOut: rewriteKeys(KEYMAP.cutting.zoomOut),
})}
/>
{/* <CuttingActionsButton Icon={faQuestion} actionName="Reset changes" action={null}
tooltip="Not implemented"
ariaLabelText="Reset changes. Not implemented"
Expand All @@ -148,8 +201,17 @@ const cuttingActionButtonStyle = css({
interface cuttingActionsButtonInterface {
Icon: IconType,
actionName: string,
actionHandler: (action: ActionCreatorWithoutPayload<string>, ref?: React.RefObject<HTMLDivElement>) => void,
actionHandler: (
action: ActionCreatorWithoutPayload<string>,
actionWithPayload: ActionCreatorWithPayload<number, string> | undefined,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
payload: any,
ref?: React.RefObject<HTMLDivElement>,
) => void,
action: ActionCreatorWithoutPayload<string>,
actionWithPayload: ActionCreatorWithPayload<number, string> | undefined,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
payload: any,
tooltip: string,
ariaLabelText: string,
}
Expand All @@ -163,6 +225,8 @@ const CuttingActionsButton: React.FC<cuttingActionsButtonInterface> = ({
actionName,
actionHandler,
action,
actionWithPayload,
payload,
tooltip,
ariaLabelText,
}) => {
Expand All @@ -174,10 +238,10 @@ const CuttingActionsButton: React.FC<cuttingActionsButtonInterface> = ({
<div css={[basicButtonStyle(theme), cuttingActionButtonStyle]}
ref={ref}
role="button" tabIndex={0} aria-label={ariaLabelText}
onClick={() => actionHandler(action, ref)}
onClick={() => actionHandler(action, actionWithPayload, payload, ref)}
onKeyDown={(event: React.KeyboardEvent) => {
if (event.key === " " || event.key === "Enter") {
actionHandler(action);
actionHandler(action, actionWithPayload, payload);
}
}}
>
Expand All @@ -189,7 +253,13 @@ const CuttingActionsButton: React.FC<cuttingActionsButtonInterface> = ({
};

interface markAsDeleteButtonInterface {
actionHandler: (action: ActionCreatorWithoutPayload<string>, ref?: React.RefObject<HTMLDivElement>) => void,
actionHandler: (
action: ActionCreatorWithoutPayload<string> | undefined,
actionWithPayload: ActionCreatorWithPayload<number, string> | undefined,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
payload: any,
ref?: React.RefObject<HTMLDivElement>
) => void,
action: ActionCreatorWithoutPayload<string>,
hotKeyName: string,
}
Expand All @@ -214,10 +284,10 @@ const MarkAsDeletedButton: React.FC<markAsDeleteButtonInterface> = ({
ref={ref}
role="button" tabIndex={0}
aria-label={t("cuttingActions.delete-restore-tooltip-aria", { hotkeyName: hotKeyName })}
onClick={() => actionHandler(action, ref)}
onClick={() => actionHandler(action, undefined, undefined, ref)}
onKeyDown={(event: React.KeyboardEvent) => {
if (event.key === " " || event.key === "Enter") {
actionHandler(action);
actionHandler(action, undefined, undefined);
}
}}
>
Expand All @@ -228,4 +298,79 @@ const MarkAsDeletedButton: React.FC<markAsDeleteButtonInterface> = ({
);
};

interface ZoomSliderInterface {
actionHandler: (
action: ActionCreatorWithoutPayload<string> | undefined,
actionWithPayload: ActionCreatorWithPayload<number, string> | undefined,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
payload: any,
ref?: React.RefObject<HTMLDivElement>,
) => void,
tooltip: string,
ariaLabelText: string,
}

const ZoomSlider : React.FC<ZoomSliderInterface> = ({
actionHandler,
tooltip,
ariaLabelText,
}) => {

const { t } = useTranslation();
const theme = useTheme();
const timelineZoom = useAppSelector(selectTimelineZoom);
const timelineZoomMax = useAppSelector(selectTimelineZoomMax);

// Callback for the zoom slider
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const zoomSliderOnChange = (event: Event, newValue: number | number[]) => {
actionHandler(undefined, setTimelineZoom, newValue, undefined);
};

const zoomStyle = css({
display: "flex",
flexDirection: "row",
paddingLeft: "16px",
paddingRight: "16px",
gap: "15px",
justifyContent: "center",
alignItems: "center",
});


const sliderStyle = css({
width: "150px",
"& .MuiSlider-thumb": {
color: `${theme.slider_thumb_color}`,
"&:hover, &.Mui-focusVisible, &.Mui-active": {
boxShadow: `${theme.slider_thumb_shadow}`,
},
},
"& .MuiSlider-rail": {
color: `${theme.slider_track_color}`,
},
"& .MuiSlider-track": {
color: `${theme.slider_track_color}`,
},
});

return (
<ThemedTooltip title={tooltip}>
<div css={zoomStyle}>
<span>{t("cuttingActions.zoom")}</span>
<Slider
css={sliderStyle}
min={1}
max={timelineZoomMax}
step={0.1}
value={timelineZoom}
onChange={zoomSliderOnChange}
aria-label={ariaLabelText}
valueLabelDisplay="off"
/>
</div>
</ThemedTooltip>
);
};

export default CuttingActions;
5 changes: 3 additions & 2 deletions src/main/KeyboardControls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -131,11 +131,12 @@ const KeyboardControls: React.FC = () => {
const entries: { [groupName: string]: string[][]; } = {};
Object.entries(group).forEach(([, action]) => {
const sequences = action.key.split(",").map(item => item.trim());
const sequenceCombinationkey = action.combinationKey ? action.combinationKey : "+";
entries[action.name] = Object.entries(sequences).map(([, sequence]) => {
return sequence.split("+").map(item => rewriteKeys(item.trim()));
return sequence.split(sequenceCombinationkey).map(item => rewriteKeys(item.trim()));
});
});
groups.push(<Group name={getGroupName(groupName)} entries={entries} key={index} />);
groups.push(<Group name={getGroupName(groupName)} entries={entries} key={index}/>);
});

return (
Expand Down
4 changes: 2 additions & 2 deletions src/main/SubtitleTimeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ const SubtitleTimeline: React.FC = () => {
horizontal={true}
onEndScroll={onEndScroll}
// dom elements with this id in the container will not trigger scrolling when dragged
ignoreElements={"#no-scrolling"}
ignoreElements={".prevent-drag-scroll"}
>
{/* Container. Overflows. Width based on parent times zoom level*/}
<div ref={ref} css={timelineStyle}>
Expand Down Expand Up @@ -413,7 +413,7 @@ const TimelineSubtitleSegment: React.FC<{
// Fix most likely requires changes in one of those modules
resizeHandles={["w"]}
>
<div css={segmentStyle} ref={nodeRef} onClick={onClick} id="no-scrolling">
<div css={segmentStyle} ref={nodeRef} onClick={onClick} className="prevent-drag-scroll">
<span css={textStyle}>{props.cue.text}</span>
</div>
</Resizable>
Expand Down
Loading

0 comments on commit b321cf9

Please sign in to comment.