Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue 199 #201

Merged
merged 2 commits into from
Oct 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions client/src/Annotation/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { getImagesAnnotation } from "../utils/send-data-to-server"
import CircularProgress from "@mui/material/CircularProgress"
import Box from "@mui/material/Box"
import AlertDialog from "../AlertDialog"
import ShortcutsDialog from "../ShortcutsDialog"
import { clear_db, getSettings } from "../utils/get-data-from-server"
import colors from "../colors.js"
import { useTranslation } from "react-i18next"
Expand Down Expand Up @@ -58,6 +59,7 @@ const userReducer = (state, action) => {

export default () => {
const [selectedImageIndex, changeSelectedImageIndex] = useState(0)
const [openDialog, setOpenDialog] = useState(false)
const [open, setOpen] = useState(false)
const { t } = useTranslation()
const [showLabel, setShowLabel] = useState(true)
Expand Down Expand Up @@ -93,6 +95,14 @@ export default () => {
handleClose()
}

const handleOpenDialog = () => {
setOpenDialog(true)
}

const handleCloseDialog = () => {
setOpenDialog(false)
}

const [loading, setLoading] = useState(true) // Add loading state
const onSelectJumpHandle = (selectedImageName) => {
let selectedImage = imageNames.filter((image) => {
Expand Down Expand Up @@ -348,6 +358,7 @@ export default () => {
exitCancel={t("exit_alert_cancel")}
handleExit={handleExit}
/>
<ShortcutsDialog open={openDialog} handleClose={handleCloseDialog} />
<Annotator
taskDescription={
settings.taskDescription ||
Expand All @@ -367,6 +378,7 @@ export default () => {
onExit={(output) => {
handleClickOpen()
}}
onShortcutClick={() => handleOpenDialog()}
settings={settings}
onSelectJump={onSelectJumpHandle}
showTags={true}
Expand Down
6 changes: 5 additions & 1 deletion client/src/Annotator/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export const Annotator = ({
videoTime = 0,
videoName,
onExit,
onShortcutClick,
settings,
keypointDefinitions,
onSelectJump,
Expand Down Expand Up @@ -148,7 +149,7 @@ export const Annotator = ({

const dispatch = useEventCallback(async (action) => {
if (action.type === "HEADER_BUTTON_CLICKED") {
if (["Exit", "Done", "Save", "Complete"].includes(action.buttonName)) {
if (["Exit", "Done", "Save", "Complete", "Shortcuts"].includes(action.buttonName)) {
// save the current data
if (action.buttonName === "Save") {
const result = await preprocessDataBeforeSend(
Expand All @@ -163,6 +164,8 @@ export const Annotator = ({
payload: result,
})
return null
}else if(action.buttonName === "Shortcuts") {
return onShortcutClick()
} else {
return onExit(without(state, "history"))
}
Expand Down Expand Up @@ -219,6 +222,7 @@ export const Annotator = ({
hideSave={hideSave}
allImages={allImages}
onExit={onExit}
onShortcutClick={onShortcutClick}
enabledRegionProps={enabledRegionProps}
onSelectJump={onSelectJump}
saveActiveImage={saveCurrentData}
Expand Down
39 changes: 38 additions & 1 deletion client/src/FilesListMenu/index.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from "react"
import React, { useState, useEffect } from "react";
import { styled } from "@mui/material/styles"
import { createTheme, ThemeProvider } from "@mui/material/styles"
import Box from "@mui/material/Box"
Expand Down Expand Up @@ -67,6 +67,43 @@ export const FilesListMenu = ({
onClick,
}) => {
const { t } = useTranslation()

// Track the index of the selected image
const [selectedIndex, setSelectedIndex] = useState(
allImages.findIndex((img) => img.name === selectedImage) || 0
);

// Handle ArrowUp and ArrowDown key presses for navigation
const handleKeyDown = (event) => {
if (event.key === "ArrowUp") {
setSelectedIndex((prevIndex) =>
prevIndex > 0 ? prevIndex - 1 : allImages.length - 1
);
} else if (event.key === "ArrowDown") {
setSelectedIndex((prevIndex) =>
prevIndex < allImages.length - 1 ? prevIndex + 1 : 0
);
}
};

// Add keydown event listener when the component mounts
useEffect(() => {
window.addEventListener("keydown", handleKeyDown);

// Cleanup the event listener when the component unmounts
return () => {
window.removeEventListener("keydown", handleKeyDown);
};
}, []);

// Update selected image when selectedIndex changes
useEffect(() => {
if (allImages[selectedIndex]) {
onSelectJump(allImages[selectedIndex].name);
}
}, [selectedIndex]);


const handleClickLabel = (label) => {
onClick(getActiveImage(state))
onSelectJump(label)
Expand Down
3 changes: 3 additions & 0 deletions client/src/Localization/translation-de-DE.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const translationDeDE = {
"btn.previous": "Zurück",
"btn.save": "Speichern",
"btn.settings": "Einstellungen",
"btn.shortcuts": "Verknüpfungen",
"btn.exit": "Beenden",
"btn.clone": "Klonen",
"btn.play": "Abspielen",
Expand Down Expand Up @@ -97,6 +98,8 @@ const translationDeDE = {
"Möchten Sie wirklich beenden? Diese Aktion wird den Speicher löschen und alle Daten werden verloren gehen.",
exit_alert_cancel: "Abbrechen",
exit_alert_confirm: "Zustimmen",
short_key_up: "Nach oben navigieren",
short_key_down: "Nach unten navigieren",
}

export default translationDeDE
3 changes: 3 additions & 0 deletions client/src/Localization/translation-en-EN.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const translationEnEN = {
"btn.previous": "Prev",
"btn.save": "Save",
"btn.settings": "Settings",
"btn.shortcuts": "Shortcuts",
"btn.exit": "Exit",
"btn.clone": "Clone",
"btn.play": "Play",
Expand Down Expand Up @@ -96,6 +97,8 @@ const translationEnEN = {
"Do you really want to exit? This action will clear the storage and all data will be lost.",
exit_alert_cancel: "Cancel",
exit_alert_confirm: "Exit",
short_key_up: "Navigate Up",
short_key_down: "Navigate Down",
}

export default translationEnEN
28 changes: 26 additions & 2 deletions client/src/MainLayout/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import getHotkeyHelpText from "../utils/get-hotkey-help-text"
import { useKey } from "react-use"
import { useSettings } from "../SettingsProvider"
import { withHotKeys } from "react-hotkeys"
import { Save, ExitToApp } from "@mui/icons-material"
import { Save, ExitToApp, QuestionMark } from "@mui/icons-material"
import capitalize from "lodash/capitalize"
import { useTranslation } from "react-i18next"
import { useSnackbar } from "../SnackbarContext"
Expand All @@ -47,6 +47,7 @@ export const MainLayout = ({
hideHeader,
hideHeaderText,
onExit,
onShortcutClick,
hideClone = true,
hideSettings = false,
hideSave = false,
Expand Down Expand Up @@ -80,7 +81,27 @@ export const MainLayout = ({
nextImage = state.images[currentImageIndex + 1]
}

useKey("Escape", () => dispatch({ type: "CANCEL" }))

const handleKey = (key, actionType, selectedTool = null) => {
useKey(
(event) => event.ctrlKey && event.shiftKey && event.key.toLowerCase() === key,
() => {
const action = { type: actionType };
if (selectedTool) {
action.selectedTool = selectedTool;
}
dispatch(action);
},
{ event: 'keydown' }
);
};

handleKey('escape', 'CANCEL');
handleKey('b', 'SELECT_TOOL', 'create-box');
handleKey('z', 'SELECT_TOOL', 'zoom');
handleKey('p', 'SELECT_TOOL', 'create-polygon');
handleKey('c', 'SELECT_TOOL', 'create-circle');


const innerContainerRef = useRef()
const hotkeyHandlers = useDispatchHotkeyHandlers({ dispatch })
Expand Down Expand Up @@ -195,6 +216,8 @@ export const MainLayout = ({
const onClickHeaderItem = useEventCallback((item) => {
if (item.name === "Exit") {
onExit()
} else if (item.name === "Shortcuts") {
onShortcutClick()
} else {
dispatch({ type: "HEADER_BUTTON_CLICKED", buttonName: item.name })
}
Expand Down Expand Up @@ -266,6 +289,7 @@ export const MainLayout = ({
},
{ name: "Docs", label: t("btn.docs") },
!hideSettings && { name: "Settings", label: t("btn.settings") },
{ name: "Shortcuts", label: t("btn.shortcuts"), icon: <QuestionMark/> },
{ name: "Exit", label: t("btn.exit"), icon: <ExitToApp /> },
].filter(Boolean)}
onClickHeaderItem={onClickHeaderItem}
Expand Down
51 changes: 51 additions & 0 deletions client/src/ShortcutsDialog/ShortcutsDialog.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import React from "react"
import { render, screen, fireEvent } from "@testing-library/react"
import "@testing-library/jest-dom"
import ShortcutsDialog from "./index" // Adjust the import based on your file structure
import { useTranslation } from "react-i18next"

// Mock the useTranslation hook
jest.mock("react-i18next", () => ({
useTranslation: () => ({
t: (key) => key, // Simply return the key as the translation
}),
}))

describe("ShortcutsDialog", () => {
const mockHandleClose = jest.fn()

const defaultProps = {
open: true,
handleClose: mockHandleClose,
}

beforeEach(() => {
render(<ShortcutsDialog {...defaultProps} />)
})

afterEach(() => {
jest.clearAllMocks()
})

test("renders all shortcuts with corresponding actions", () => {
const shortcuts = [
{ key: "Ctrl + Shift + B", action: "helptext_boundingbox" },
{ key: "Ctrl + Shift + Z", action: "helptext_zoom" },
{ key: "Ctrl + Shift + P", action: "helptext_polypolygon" },
{ key: "Ctrl + Shift + C", action: "helptext_circle" },
{ key: "↑", action: "short_key_up" },
{ key: "↓", action: "short_key_down" },
]
shortcuts.forEach((shortcut, index) => {
expect(screen.getByTestId(`shortcut-key-${index}`)).toBeInTheDocument()
})
})

test("calls handleClose when close button is clicked", () => {
const closeButton = screen.getByTestId("close-button")
fireEvent.click(closeButton)
expect(mockHandleClose).toHaveBeenCalledTimes(1)
})


})
72 changes: 72 additions & 0 deletions client/src/ShortcutsDialog/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import * as React from "react"
import Button from "@mui/material/Button"
import Dialog from "@mui/material/Dialog"
import DialogActions from "@mui/material/DialogActions"
import DialogContent from "@mui/material/DialogContent"
import DialogTitle from "@mui/material/DialogTitle"
import Box from "@mui/material/Box"
import Typography from "@mui/material/Typography"
import { useTranslation } from "react-i18next"

export const ShortcutsDialog = ({ open, handleClose }) => {
const { t } = useTranslation()

const shortcuts = [
{ key: "Ctrl + Shift + B", action: t("helptext_boundingbox") },
{ key: "Ctrl + Shift + Z", action: t("helptext_zoom") },
{ key: "Ctrl + Shift + P", action: t("helptext_polypolygon") },
{ key: "Ctrl + Shift + C", action: t("helptext_circle") },
{ key: "↑", action: t("short_key_up") }, // Up arrow key for navigation
{ key: "↓", action: t("short_key_down") }, // Down arrow key for navigation
]

return (
<Dialog
open={open}
onClose={handleClose}
aria-labelledby="shortcuts-dialog-title"
aria-describedby="shortcuts-dialog-description"
data-testid="shortcuts-dialog"
fullWidth
maxWidth="sm"
PaperProps={{
style: {
minHeight: "40vh",
maxHeight: "80vh",
padding: "20px",
},
}}
>
<DialogTitle id="shortcuts-dialog-title" data-testid="shortcuts-dialog-title">
{t("Keyboard Shortcuts")}
</DialogTitle>
<DialogContent data-testid="shortcuts-dialog-content">
<Box display="flex" flexDirection="column" gap={2}>
{shortcuts.map((shortcut, index) => (
<Box
key={index}
display="flex"
justifyContent="space-between"
alignItems="center"
px={1}
>
<Typography variant="body1" data-testid={`shortcut-key-${index}`}>
{shortcut.key}
</Typography>
<Typography variant="body2" data-testid={`shortcut-action-${index}`}>
{shortcut.action}
</Typography>
</Box>
))}
</Box>
</DialogContent>
<DialogActions data-testid="shortcuts-dialog-actions">
<Button onClick={handleClose} data-testid="close-button">
{t("Close")}
</Button>
</DialogActions>
</Dialog>
)
}

export default ShortcutsDialog
Loading