diff --git a/src/app.tsx b/src/app.tsx index 18dbd26a..fce9adde 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -35,7 +35,7 @@ import { WithDialogs } from "dialogs"; import { useInit, usePageLocation } from "hooks"; import { superuser } from "superuser"; -import { FilesContext, usePath } from "./common.ts"; +import { FilesContext, testIsAppleDevice, usePath } from "./common.ts"; import type { ClipboardInfo, FolderFileInfo } from "./common.ts"; import { FilesBreadcrumbs } from "./files-breadcrumbs.tsx"; import { FilesFolderView } from "./files-folder-view.tsx"; @@ -113,8 +113,20 @@ export const Application = () => { ); useInit(() => { + const isApple = testIsAppleDevice(); + const whichkey = isApple ? "J" : "L"; + + const editPathModifiers = (e: KeyboardEvent) => { + if (isApple) { + // use both meta and ctrl key on apple devices to support external non-apple keyboards + return ((e.ctrlKey || e.metaKey) && e.shiftKey && !e.altKey); + } else { + return (e.ctrlKey && e.shiftKey && !e.altKey && !e.metaKey); + } + }; + const onKeyboardNav = (e: KeyboardEvent) => { - if (e.key === "L" && e.ctrlKey && !e.altKey) { + if (e.key === whichkey && editPathModifiers(e)) { e.preventDefault(); document.dispatchEvent(new Event("manual-change-dir")); } diff --git a/src/common.ts b/src/common.ts index c4869a8c..42833a07 100644 --- a/src/common.ts +++ b/src/common.ts @@ -149,3 +149,7 @@ export function debug(...args: unknown[]) { console.debug("files:", ...args); } } + +export function testIsAppleDevice() { + return /Mac|iPhone|iPad|iPod/.test(navigator.platform); +} diff --git a/src/dialogs/keyboardShortcutsHelp.tsx b/src/dialogs/keyboardShortcutsHelp.tsx index f213ba73..64ac6a11 100644 --- a/src/dialogs/keyboardShortcutsHelp.tsx +++ b/src/dialogs/keyboardShortcutsHelp.tsx @@ -30,9 +30,15 @@ import { Flex, } from "@patternfly/react-core/dist/esm/layouts/Flex"; import cockpit from 'cockpit'; import { DialogResult, Dialogs } from 'dialogs'; +import { testIsAppleDevice } from '../common.ts'; + const _ = cockpit.gettext; const KeyboardShortcutsHelp = ({ dialogResult } : { dialogResult: DialogResult }) => { + const isApple = testIsAppleDevice(); + const ctrlString = isApple ? "Command" : "Ctrl"; + const altString = isApple ? "Command" : "Alt"; + const footer = ( ); @@ -53,25 +59,25 @@ const KeyboardShortcutsHelp = ({ dialogResult } : { dialogResult: DialogResult = [ [ - Alt + {'\u{2191}'} + {altString} + {'\u{2191}'} , _("Go up a directory"), "go-up", ], [ - Alt + {'\u{2190}'} + {altString} + {'\u{2190}'} , _("Go back"), "go-back", ], [ - Alt + {'\u{2192}'} + {altString} + {'\u{2192}'} , _("Go forward"), "go-forward", ], [ - Alt + {'\u{2193}'} + {altString} + {'\u{2193}'} , _("Activate selected item, enter directory"), "activate", @@ -83,9 +89,9 @@ const KeyboardShortcutsHelp = ({ dialogResult } : { dialogResult: DialogResult - Ctrl + + {ctrlString} + Shift + - L + {isApple ? "J" : "L"} , _("Edit path"), "edit-path", @@ -94,7 +100,9 @@ const KeyboardShortcutsHelp = ({ dialogResult } : { dialogResult: DialogResult = [ [ - F2, + + Fn + F2 + , _("Rename selected file or directory"), "rename", ], [ @@ -106,19 +114,19 @@ const KeyboardShortcutsHelp = ({ dialogResult } : { dialogResult: DialogResult - Ctrl + C + {ctrlString} + C , _("Copy selected file or directory"), "copy", ], [ - Ctrl + V + {ctrlString} + V , _("Paste file or directory"), "paste", ], [ - Ctrl + A + {ctrlString} + A , _("Select all"), "select-all", diff --git a/src/files-card-body.tsx b/src/files-card-body.tsx index 88845181..2a9fbf35 100644 --- a/src/files-card-body.tsx +++ b/src/files-card-body.tsx @@ -35,7 +35,7 @@ import { dirname } from "cockpit-path.ts"; import { useDialogs } from "dialogs"; import * as timeformat from "timeformat"; -import { get_permissions, permissionShortStr, useFilesContext } from "./common.ts"; +import { get_permissions, permissionShortStr, testIsAppleDevice, useFilesContext } from "./common.ts"; import type { ClipboardInfo, FolderFileInfo } from "./common.ts"; import { confirm_delete } from "./dialogs/delete.tsx"; import { show_create_directory_dialog } from "./dialogs/mkdir.tsx"; @@ -207,6 +207,7 @@ export const FilesCardBody = ({ useEffect(() => { let folderViewElem: HTMLDivElement | null = null; + const isApple = testIsAppleDevice(); const resetSelected = (e: MouseEvent) => { if ((e.target instanceof HTMLElement)) { @@ -273,14 +274,38 @@ export const FilesCardBody = ({ } }; - const hasNoKeydownModifiers = (event: KeyboardEvent) => { + const hasNoModifiers = (event: KeyboardEvent) => { return !event.shiftKey && !event.ctrlKey && !event.altKey && !event.metaKey; }; + const dirNavigationModifiers = (e: KeyboardEvent) => { + if (isApple) { + // use both meta and alt key on apple devices to support external non-apple keyboards + return !e.ctrlKey && !e.shiftKey && (e.altKey || e.metaKey); + } else { + return !e.ctrlKey && !e.shiftKey && e.altKey && !e.metaKey; + } + }; + + // Modifiers used for copy, paste and select all shortcuts + const editingModifiers = (e: KeyboardEvent) => { + // Keep standard text editing behavior by excluding input fields + if ((e.target instanceof HTMLInputElement)) { + return false; + } + + if (isApple) { + // use both meta and ctrl key on apple devices to support external non-apple keyboards + return !e.shiftKey && !e.altKey && (e.ctrlKey || e.metaKey); + } else { + return !e.shiftKey && !e.altKey && e.ctrlKey && !e.metaKey; + } + }; + const onKeyboardNav = (e: KeyboardEvent) => { switch (e.key) { case "ArrowRight": - if (hasNoKeydownModifiers(e)) { + if (hasNoModifiers(e) && sortedFiles.length > 0) { setSelected(_selected => { const firstSelectedName = _selected?.[0]?.name; const selectedIdx = sortedFiles?.findIndex(file => file.name === firstSelectedName); @@ -294,7 +319,7 @@ export const FilesCardBody = ({ break; case "ArrowLeft": - if (hasNoKeydownModifiers(e)) { + if (hasNoModifiers(e) && sortedFiles.length > 0) { setSelected(_selected => { const firstSelectedName = _selected?.[0]?.name; const selectedIdx = sortedFiles?.findIndex(file => file.name === firstSelectedName); @@ -308,9 +333,9 @@ export const FilesCardBody = ({ break; case "ArrowUp": - if (e.altKey && !e.shiftKey && !e.ctrlKey) { + if (dirNavigationModifiers(e)) { goUpOneDir(); - } else if (hasNoKeydownModifiers(e)) { + } else if (hasNoModifiers(e) && sortedFiles.length > 0) { setSelected(_selected => { const firstSelectedName = _selected?.[0]?.name; const selectedIdx = sortedFiles?.findIndex(file => file.name === firstSelectedName); @@ -318,13 +343,14 @@ export const FilesCardBody = ({ return [sortedFiles[newIdx]]; }); + e.preventDefault(); } break; case "ArrowDown": - if (e.altKey && !e.shiftKey && !e.ctrlKey && selected.length === 1) { + if (selected.length === 1 && dirNavigationModifiers(e)) { onDoubleClickNavigate(selected[0]); - } else if (hasNoKeydownModifiers(e)) { + } else if (hasNoModifiers(e) && sortedFiles.length > 0) { setSelected(_selected => { const firstSelectedName = _selected?.[0]?.name; const selectedIdx = sortedFiles?.findIndex(file => file.name === firstSelectedName); @@ -332,54 +358,51 @@ export const FilesCardBody = ({ return [sortedFiles[newIdx]]; }); + e.preventDefault(); } break; case "Enter": - if (hasNoKeydownModifiers(e) && selected.length === 1) { + if (hasNoModifiers(e) && selected.length === 1) { onDoubleClickNavigate(selected[0]); } break; case "Delete": - if (hasNoKeydownModifiers(e) && selected.length !== 0) { + if (hasNoModifiers(e) && selected.length !== 0) { confirm_delete(dialogs, path, selected, setSelected); } break; case "F2": - if (hasNoKeydownModifiers(e) && selected.length === 1) { + if (hasNoModifiers(e) && selected.length === 1) { show_rename_dialog(dialogs, path, selected[0]); } break; case "a": - // Keep standard text editing behavior by excluding input fields - if (e.ctrlKey && !e.shiftKey && !e.altKey && !(e.target instanceof HTMLInputElement)) { + if (editingModifiers(e)) { e.preventDefault(); setSelected(sortedFiles); } break; case "c": - // Keep standard text editing behavior by excluding input fields - if (e.ctrlKey && !e.shiftKey && !e.altKey && !(e.target instanceof HTMLInputElement)) { + if (editingModifiers(e)) { e.preventDefault(); setClipboard({ path, files: selected }); } break; case "v": - // Keep standard text editing behavior by excluding input fields - if (e.ctrlKey && !e.shiftKey && !e.altKey && clipboard.files.length > 0 && - !(e.target instanceof HTMLInputElement)) { + if (editingModifiers(e) && clipboard.files.length > 0) { e.preventDefault(); pasteFromClipboard(clipboard, cwdInfo, path, dialogs, addAlert); } break; case "N": - if (!e.ctrlKey && !e.altKey) { + if (e.shiftKey && !e.ctrlKey && !e.altKey && !e.metaKey) { e.preventDefault(); show_create_directory_dialog(dialogs, path); } diff --git a/test/check-application b/test/check-application index 8deff09f..09716be1 100755 --- a/test/check-application +++ b/test/check-application @@ -1948,14 +1948,14 @@ class TestFiles(testlib.MachineCase): b.wait_visible("[data-item='testdir']") # Manually edit path - b.key("L", modifiers=["Control"]) + b.key("L", modifiers=["Control", "Shift"]) b.input_text("/home/admin/anotherdir") b.key("Enter") self.assert_last_breadcrumb("anotherdir") b.go("/files#/?path=/home/admin") self.assert_last_breadcrumb("admin") - b.key("N") + b.key("N", modifiers=["Shift"]) b.set_input_text("#create-directory-input", "foodir") b.click("button.pf-m-primary") b.wait_visible("[data-item='foodir']")