diff --git a/src/i18n/locales/en-US.json b/src/i18n/locales/en-US.json
index b4bb8b1ed..f468b2512 100644
--- a/src/i18n/locales/en-US.json
+++ b/src/i18n/locales/en-US.json
@@ -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.",
diff --git a/src/main/ContextMenu.tsx b/src/main/ContextMenu.tsx
new file mode 100644
index 000000000..c98d46d01
--- /dev/null
+++ b/src/main/ContextMenu.tsx
@@ -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) => (
+
+ ));
+ };
+
+ return (
+
+ {children}
+
+
+
+ );
+};
diff --git a/src/main/CuttingActionsContextMenu.tsx b/src/main/CuttingActionsContextMenu.tsx
new file mode 100644
index 000000000..f5a0138ec
--- /dev/null
+++ b/src/main/CuttingActionsContextMenu.tsx
@@ -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 (
+
+ {children}
+
+ );
+};
+
+export default CuttingActionsContextMenu;
diff --git a/src/main/Timeline.tsx b/src/main/Timeline.tsx
index 4c98241f5..428590dfd 100644
--- a/src/main/Timeline.tsx
+++ b/src/main/Timeline.tsx
@@ -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";
@@ -71,25 +72,27 @@ const Timeline: React.FC<{
};
return (
- setCurrentlyAtToClick(e)}>
-
-
-
-
+ setCurrentlyAtToClick(e)}>
+
+
+
+
+
-
+
);
};
diff --git a/src/themes.ts b/src/themes.ts
index 7ac7a090f..470966b5f 100644
--- a/src/themes.ts
+++ b/src/themes.ts
@@ -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;
@@ -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}`,
@@ -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}`,
@@ -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",
@@ -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",