Skip to content

Commit

Permalink
Merge pull request #1318 from dennis531/context-menu
Browse files Browse the repository at this point in the history
Add context menu for cutting actions
  • Loading branch information
lkiesow authored May 6, 2024
2 parents a805572 + 7257551 commit ffbcbe0
Show file tree
Hide file tree
Showing 5 changed files with 214 additions and 16 deletions.
2 changes: 1 addition & 1 deletion src/i18n/locales/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"cut-tooltip-aria": "Cut. Split the segment at the current timeline marker position. Hotkey: {{hotkeyName}}.",
"delete-button": "Delete",
"delete-restore-tooltip": "Mark or unmark the segment at the current position as to be deleted. Hotkey: {{hotkeyName}}",
"delete-restore-tooltip-aria": "Delete and Restore. Mark or unmark the segment at the current position as to be deleted. Hotkey: {{hotKeyName}}.",
"delete-restore-tooltip-aria": "Delete and Restore. Mark or unmark the segment at the current position as to be deleted. Hotkey: {{hotkeyName}}.",
"merge-all-button": "Merge All",
"merge-all-tooltip": "Combine all segments into a single segment.",
"merge-all-tooltip-aria": "Merge All. Combine all segments into a single segment.",
Expand Down
118 changes: 118 additions & 0 deletions src/main/ContextMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { Menu, MenuItem } from "@mui/material";
import React, { MouseEventHandler } from "react";
import { IconType } from "react-icons";

import { useTheme } from "../themes";
import { customIconStyle } from "../cssStyles";

export interface ContextMenuItem {
name: string,
action: MouseEventHandler,
ariaLabel: string,
icon?: IconType | React.FunctionComponent,
hotKey?: string,
}

/**
* Context menu component
*
* @param menuItems Menu items
* @param children Content between the opening and the closing tag where the context menu should be triggered
*/
export const ThemedContextMenu: React.FC<{
menuItems: ContextMenuItem[],
children: React.ReactNode,
}> = ({
menuItems,
children,
}) => {

const theme = useTheme();

// Init state variables
const [contextMenuPosition, setContextMenuPosition] = React.useState<{
left: number,
top: number,
} | null>(null);

const handleContextMenu = (e: React.MouseEvent) => {
e.preventDefault();

setContextMenuPosition(contextMenuPosition === null
? { left: e.clientX + 5, top: e.clientY }
: null // Prevent relocation of context menu outside the element
);
};

const handleClose = () => {
setContextMenuPosition(null);
};

/**
* Handles the click on a menu item
*
* @param e mouse event
* @param action menu item action
*/
const handleAction = (e: React.MouseEvent, action: MouseEventHandler) => {
action(e);

// Immediately close menu after action
handleClose();
};

const renderMenuItems = () => {
return menuItems.map((menuItem, i) => (
<MenuItem
key={i}
onClick={e => handleAction(e, menuItem.action)}
sx={{
fontFamily: "inherit",
gap: "15px",
}}
aria-label={menuItem.ariaLabel}
>
{menuItem.icon &&
<menuItem.icon css={customIconStyle}/>
}
<div css={{ flexGrow: 1 }}>
{menuItem.name}
</div>
{menuItem.hotKey &&
<div css={{
fontSize: "0.875em",
opacity: 0.7,
}}>
{menuItem.hotKey}
</div>
}
</MenuItem>
));
};

return (
<div
onContextMenu={handleContextMenu}
>
{children}

<Menu
open={contextMenuPosition !== null}
onClose={handleClose}
anchorReference="anchorPosition"
anchorPosition={contextMenuPosition ?? undefined}
slotProps={{
paper: {
sx: {
backgroundColor: `${theme.contextMenu}`,
color: `${theme.text}`,
},
},
}}
transitionDuration={0} // Allow quick re-opening of menu elsewhere by double-clicking the secondary button
>
{ renderMenuItems() }
</Menu>
</div>
);
};
72 changes: 72 additions & 0 deletions src/main/CuttingActionsContextMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import React from "react";
import { useTranslation } from "react-i18next";

import { LuChevronLeft, LuChevronRight, LuScissors, LuTrash } from "react-icons/lu";
import TrashRestore from "../img/trash-restore.svg?react";

import { ContextMenuItem, ThemedContextMenu } from "./ContextMenu";
import { KEYMAP, rewriteKeys } from "../globalKeys";
import { useAppDispatch, useAppSelector } from "../redux/store";
import { cut, markAsDeletedOrAlive, mergeLeft, mergeRight, selectIsCurrentSegmentAlive } from "../redux/videoSlice";

const CuttingActionsContextMenu: React.FC<{
children: React.ReactNode,
}> = ({
children,
}) => {

const { t } = useTranslation();

// Init redux variables
const dispatch = useAppDispatch();
const isCurrentSegmentAlive = useAppSelector(selectIsCurrentSegmentAlive);

const cuttingContextMenuItems: ContextMenuItem[] = [
{
name: t("cuttingActions.cut-button"),
action: () => dispatch(cut()),
icon: LuScissors,
hotKey: KEYMAP.cutting.cut.key,
ariaLabel: t("cuttingActions.cut-tooltip-aria", {
hotkeyName: rewriteKeys(KEYMAP.cutting.cut.key),
}),
},
{
name: isCurrentSegmentAlive ? t("cuttingActions.delete-button") : t("cuttingActions.restore-button"),
action: () => dispatch(markAsDeletedOrAlive()),
icon: isCurrentSegmentAlive ? LuTrash : TrashRestore,
hotKey: KEYMAP.cutting.delete.key,
ariaLabel: t("cuttingActions.delete-restore-tooltip-aria", {
hotkeyName: rewriteKeys(KEYMAP.cutting.delete.key),
}),
},
{
name: t("cuttingActions.mergeLeft-button"),
action: () => dispatch(mergeLeft()),
icon: LuChevronLeft,
hotKey: KEYMAP.cutting.mergeLeft.key,
ariaLabel: t("cuttingActions.mergeLeft-tooltip-aria", {
hotkeyName: rewriteKeys(KEYMAP.cutting.mergeLeft.key),
}),
},
{
name: t("cuttingActions.mergeRight-button"),
action: () => dispatch(mergeRight()),
icon: LuChevronRight,
hotKey: KEYMAP.cutting.mergeRight.key,
ariaLabel: t("cuttingActions.mergeRight-tooltip-aria", {
hotkeyName: rewriteKeys(KEYMAP.cutting.mergeRight.key),
}),
},
];

return (
<ThemedContextMenu
menuItems={cuttingContextMenuItems}
>
{children}
</ThemedContextMenu>
);
};

export default CuttingActionsContextMenu;
33 changes: 18 additions & 15 deletions src/main/Timeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { ActionCreatorWithPayload } from "@reduxjs/toolkit";
import { RootState } from "../redux/store";
import { useTheme } from "../themes";
import { ThemedTooltip } from "./Tooltip";
import CuttingActionsContextMenu from "./CuttingActionsContextMenu";
import { useHotkeys } from "react-hotkeys-hook";
import { spinningStyle } from "../cssStyles";

Expand Down Expand Up @@ -71,25 +72,27 @@ const Timeline: React.FC<{
};

return (
<div ref={ref} css={timelineStyle} onMouseDown={e => setCurrentlyAtToClick(e)}>
<Scrubber
timelineWidth={width}
timelineHeight={timelineHeight}
selectCurrentlyAt={selectCurrentlyAt}
selectIsPlaying={selectIsPlaying}
setCurrentlyAt={setCurrentlyAt}
setIsPlaying={setIsPlaying}
/>
<div css={{ position: "relative", height: timelineHeight + "px" }} >
<Waveforms timelineHeight={timelineHeight} />
<SegmentsList
<CuttingActionsContextMenu>
<div ref={ref} css={timelineStyle} onMouseDown={e => setCurrentlyAtToClick(e)}>
<Scrubber
timelineWidth={width}
timelineHeight={timelineHeight}
styleByActiveSegment={styleByActiveSegment}
tabable={true}
selectCurrentlyAt={selectCurrentlyAt}
selectIsPlaying={selectIsPlaying}
setCurrentlyAt={setCurrentlyAt}
setIsPlaying={setIsPlaying}
/>
<div css={{ position: "relative", height: timelineHeight + "px" }}>
<Waveforms timelineHeight={timelineHeight}/>
<SegmentsList
timelineWidth={width}
timelineHeight={timelineHeight}
styleByActiveSegment={styleByActiveSegment}
tabable={true}
/>
</div>
</div>
</div>
</CuttingActionsContextMenu>
);
};

Expand Down
5 changes: 5 additions & 0 deletions src/themes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export interface Theme {
inverted_text: string;
tooltip: string;
tooltip_text: string;
contextMenu: string;
element_outline: string;
selected_text: string;
dropdown_border: string;
Expand Down Expand Up @@ -90,6 +91,7 @@ export const lightMode: Theme = {
inverted_text: COLORS.neutral90,
tooltip: COLORS.neutral80,
tooltip_text: COLORS.neutral05,
contextMenu: COLORS.neutral10,
element_outline: "2px solid transparent",
selected_text: COLORS.neutral90,
dropdown_border: `1px solid ${COLORS.neutral40}`,
Expand Down Expand Up @@ -149,6 +151,7 @@ export const darkMode: Theme = {
inverted_text: COLORS.neutral90,
tooltip: COLORS.neutral80,
tooltip_text: COLORS.neutral05,
contextMenu: COLORS.neutral20,
element_outline: "2px solid transparent",
selected_text: COLORS.neutral90,
dropdown_border: `1px solid ${COLORS.neutral40}`,
Expand Down Expand Up @@ -207,6 +210,7 @@ export const highContrastDarkMode: Theme = {
inverted_text: "#000",
tooltip: "#fff",
tooltip_text: "#000",
contextMenu: "#000",
element_outline: "2px solid #fff",
selected_text: "#000",
dropdown_border: "2px solid #fff",
Expand Down Expand Up @@ -263,6 +267,7 @@ export const highContrastLightMode: Theme = {
inverted_text: "#fff",
tooltip: "#000",
tooltip_text: "#fff",
contextMenu: "snow",
element_outline: "2px solid #000",
selected_text: "#fff",
dropdown_border: "2px solid #000",
Expand Down

0 comments on commit ffbcbe0

Please sign in to comment.