Added context menu hook
This commit is contained in:
parent
72165ad770
commit
7510d1bbf4
11 changed files with 140 additions and 74 deletions
|
|
@ -1,56 +1,14 @@
|
||||||
import { useState } from "react";
|
import { useShortcuts } from "../../../hooks/utils/keyboard.js";
|
||||||
import { removeFromArray } from "../../../features/utils/array.js";
|
|
||||||
import { useKeyboardListener } from "../../../hooks/utils/keyboard.js";
|
|
||||||
import { DropdownButton } from "../../utils/DropdownButton.jsx";
|
import { DropdownButton } from "../../utils/DropdownButton.jsx";
|
||||||
import styles from "./HeaderMenu.module.css";
|
import styles from "./HeaderMenu.module.css";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Object} props
|
* @param {Object} props
|
||||||
* @param {Object<String, Object<string, function>>} props.options
|
* @param {Object<string, Object<string, Function>>} props.options
|
||||||
* @param {Object<String, Object<string, string[]>>} props.shortcuts
|
* @param {Object<string, Object<string, string[]>>} props.shortcuts
|
||||||
*/
|
*/
|
||||||
export function HeaderMenu({ options, shortcuts }) {
|
export function HeaderMenu({ options, shortcuts }) {
|
||||||
const [activeKeys, setActiveKeys] = useState([]);
|
useShortcuts({ options, shortcuts });
|
||||||
|
|
||||||
const checkShortcuts = (event, allowExecution = true) => {
|
|
||||||
for (const [category, group] of Object.entries(shortcuts)) {
|
|
||||||
for (const [name, shortcut] of Object.entries(group)) {
|
|
||||||
let active = true;
|
|
||||||
|
|
||||||
shortcut.forEach((key) => {
|
|
||||||
if (!activeKeys.includes(key) && event.key != key)
|
|
||||||
return active = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (active) {
|
|
||||||
event.preventDefault();
|
|
||||||
if (shortcut.includes(event.key) && allowExecution) {
|
|
||||||
options?.[category]?.[name]?.();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const onKeyDown = (event) => {
|
|
||||||
const isRepeated = activeKeys.includes(event.key);
|
|
||||||
checkShortcuts(event, isRepeated);
|
|
||||||
|
|
||||||
if (!isRepeated)
|
|
||||||
setActiveKeys(activeKeys.concat([event.key]));
|
|
||||||
};
|
|
||||||
|
|
||||||
const onKeyUp = (event) => {
|
|
||||||
checkShortcuts(event);
|
|
||||||
|
|
||||||
if (activeKeys.includes(event.key)) {
|
|
||||||
const keys = [...activeKeys];
|
|
||||||
removeFromArray(event.key, keys);
|
|
||||||
setActiveKeys(keys);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useKeyboardListener({ onKeyDown, onKeyUp });
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.Container}>
|
<div className={styles.Container}>
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,7 @@ export function TextEditor({ file, setTitle, close, mode, app }) {
|
||||||
"New": newText,
|
"New": newText,
|
||||||
"Save": saveText,
|
"Save": saveText,
|
||||||
// "Save As": saveTextAs,
|
// "Save As": saveTextAs,
|
||||||
"Exit": () => {
|
"Quit": () => {
|
||||||
close();
|
close();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -116,7 +116,7 @@ export function TextEditor({ file, setTitle, close, mode, app }) {
|
||||||
"File": {
|
"File": {
|
||||||
"New": ["Control", "e"],
|
"New": ["Control", "e"],
|
||||||
"Save": ["Control", "s"],
|
"Save": ["Control", "s"],
|
||||||
"Exit": ["Control", "x"],
|
"Quit": ["Control", "q"],
|
||||||
},
|
},
|
||||||
"View": {
|
"View": {
|
||||||
"Zoom In": ["Control", "+"],
|
"Zoom In": ["Control", "+"],
|
||||||
|
|
|
||||||
|
|
@ -3,18 +3,23 @@ import { SettingsManager } from "../../features/settings/settings.js";
|
||||||
import { useSettings } from "../../hooks/settings/SettingsContext.js";
|
import { useSettings } from "../../hooks/settings/SettingsContext.js";
|
||||||
import styles from "./Desktop.module.css";
|
import styles from "./Desktop.module.css";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useModals } from "../../hooks/modals/ModalsContext.js";
|
import { useModals } from "../../hooks/modals/Modals.js";
|
||||||
import { ModalsView } from "../modals/ModalsView.jsx";
|
import { ModalsView } from "../modals/ModalsView.jsx";
|
||||||
import Vector2 from "../../features/math/vector2.js";
|
|
||||||
import { ContextMenu } from "../modals/context-menu/ContextMenu.jsx";
|
|
||||||
import Modal from "../../features/modals/modal.js";
|
|
||||||
import { useWindowsManager } from "../../hooks/windows/WindowsManagerContext.js";
|
import { useWindowsManager } from "../../hooks/windows/WindowsManagerContext.js";
|
||||||
|
import { useContextMenu } from "../../hooks/modals/ContextMenu.js";
|
||||||
|
|
||||||
export function Desktop() {
|
export function Desktop() {
|
||||||
const settingsManager = useSettings();
|
const settingsManager = useSettings();
|
||||||
const [wallpaper, setWallpaper] = useState(null);
|
const [wallpaper, setWallpaper] = useState(null);
|
||||||
const [modalsManager, modals] = useModals();
|
const [modalsManager, modals] = useModals();
|
||||||
const windowsManager = useWindowsManager();
|
const windowsManager = useWindowsManager();
|
||||||
|
const { onContextMenu } = useContextMenu({
|
||||||
|
modalsManager,
|
||||||
|
options: {
|
||||||
|
"Change appearance": () => { windowsManager.open("settings", { initialTabIndex: 0 }); },
|
||||||
|
"Open in Files": () => { windowsManager.open("file-explorer", { startPath: "~/Desktop" }); }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
|
|
@ -23,18 +28,6 @@ export function Desktop() {
|
||||||
})();
|
})();
|
||||||
}, [settingsManager]);
|
}, [settingsManager]);
|
||||||
|
|
||||||
const onContextMenu = (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
modalsManager.open(new Modal(ContextMenu)
|
|
||||||
.setPosition(new Vector2(event.clientX, event.clientY))
|
|
||||||
.setProps({
|
|
||||||
options: {
|
|
||||||
"Change appearance": () => { windowsManager.open("settings", { initialTabIndex: 0 }); },
|
|
||||||
"Open in Files": () => { windowsManager.open("file-explorer", { startPath: "~/Desktop" }); }
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
return (<>
|
return (<>
|
||||||
<div className={styles.Container} style={{ backgroundImage: `url(${wallpaper})` }} onContextMenu={onContextMenu}>
|
<div className={styles.Container} style={{ backgroundImage: `url(${wallpaper})` }} onContextMenu={onContextMenu}>
|
||||||
<ModalsView modalsManager={modalsManager} modals={modals}/>
|
<ModalsView modalsManager={modalsManager} modals={modals}/>
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,18 @@
|
||||||
import { Modal } from "../../features/modals/modal.js";
|
import { Modal } from "../../features/modals/modal.js";
|
||||||
import ModalsManager from "../../features/modals/modals.js";
|
import ModalsManager from "../../features/modals/modals.js";
|
||||||
import { ModalView } from "./ModalView.jsx";
|
import { ModalView } from "./ModalView.jsx";
|
||||||
|
import styles from "./ModalsView.module.css";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {object} root
|
* @param {object} root
|
||||||
* @param {ModalsManager} root.modalsManager
|
* @param {ModalsManager} root.modalsManager
|
||||||
* @param {Modal[]} root.modals
|
* @param {Modal[]} root.modals
|
||||||
|
* @param {import("react").CSSProperties} root.style
|
||||||
|
* @param {import("react").className} root.className
|
||||||
*/
|
*/
|
||||||
export function ModalsView({ modalsManager, modals }) {
|
export function ModalsView({ modalsManager, modals, style, className }) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div style={style} className={`${styles.Container} ${className}`}>
|
||||||
{modals?.map((modal) =>
|
{modals?.map((modal) =>
|
||||||
<ModalView key={modal.id} modal={modal}/>
|
<ModalView key={modal.id} modal={modal}/>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
3
src/components/modals/ModalsView.module.css
Normal file
3
src/components/modals/ModalsView.module.css
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
.Container {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
@ -11,7 +11,7 @@ import styles from "./ContextMenu.module.css";
|
||||||
export function ContextMenu({ modal, options, shortcuts }) {
|
export function ContextMenu({ modal, options, shortcuts }) {
|
||||||
return (<div className={styles.Container}>
|
return (<div className={styles.Container}>
|
||||||
{Object.entries(options).map(([label, callback]) =>
|
{Object.entries(options).map(([label, callback]) =>
|
||||||
<button title={label} className={styles.Button} key={label} tabIndex={0} onClick={() => {
|
<button className={styles.Button} key={label} tabIndex={0} onClick={() => {
|
||||||
modal.close();
|
modal.close();
|
||||||
callback();
|
callback();
|
||||||
}}>
|
}}>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
.Container {
|
.Container {
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
border-top-left-radius: 0;
|
border-top-left-radius: 0;
|
||||||
|
background-color: var(--background-color-b);
|
||||||
}
|
}
|
||||||
|
|
||||||
.Button {
|
.Button {
|
||||||
|
|
@ -23,7 +24,7 @@
|
||||||
background-color: rgba(255, 255, 255, 5%);
|
background-color: rgba(255, 255, 255, 5%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.Label {
|
.Label, .Shortcut {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,9 @@ import Application from "../../features/applications/application.js";
|
||||||
import Vector2 from "../../features/math/vector2.js";
|
import Vector2 from "../../features/math/vector2.js";
|
||||||
import { faWindowMaximize } from "@fortawesome/free-regular-svg-icons";
|
import { faWindowMaximize } from "@fortawesome/free-regular-svg-icons";
|
||||||
import utilStyles from "../../styles/utils.module.css";
|
import utilStyles from "../../styles/utils.module.css";
|
||||||
|
import { useModals } from "../../hooks/modals/Modals.js";
|
||||||
|
import { ModalsView } from "../modals/ModalsView.jsx";
|
||||||
|
import { useContextMenu } from "../../hooks/modals/ContextMenu.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {object} props
|
* @param {object} props
|
||||||
|
|
@ -23,6 +26,7 @@ import utilStyles from "../../styles/utils.module.css";
|
||||||
export const WindowView = memo(function Window({ id, app, size, position, focused = false, onInteract, options }) {
|
export const WindowView = memo(function Window({ id, app, size, position, focused = false, onInteract, options }) {
|
||||||
const windowsManager = useWindowsManager();
|
const windowsManager = useWindowsManager();
|
||||||
const nodeRef = useRef(null);
|
const nodeRef = useRef(null);
|
||||||
|
const [modalsManager, modals] = useModals();
|
||||||
|
|
||||||
const [initialised, setInitialised] = useState(false);
|
const [initialised, setInitialised] = useState(false);
|
||||||
const [startSize, setStartSize] = useState(size);
|
const [startSize, setStartSize] = useState(size);
|
||||||
|
|
@ -36,6 +40,18 @@ export const WindowView = memo(function Window({ id, app, size, position, focuse
|
||||||
|
|
||||||
const [title, setTitle] = useState(app.name);
|
const [title, setTitle] = useState(app.name);
|
||||||
|
|
||||||
|
const { onContextMenu } = useContextMenu({
|
||||||
|
modalsManager,
|
||||||
|
options: {
|
||||||
|
"Maximize": () => { setMaximized(!maximized); },
|
||||||
|
"Close": () => { close(); }
|
||||||
|
},
|
||||||
|
shortcuts: {
|
||||||
|
"Maximize": ["F11"],
|
||||||
|
"Close": ["Control", "q"]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const resizeObserver = new ResizeObserver((event) => {
|
const resizeObserver = new ResizeObserver((event) => {
|
||||||
setScreenWidth(event[0].contentBoxSize[0].inlineSize);
|
setScreenWidth(event[0].contentBoxSize[0].inlineSize);
|
||||||
|
|
@ -88,9 +104,8 @@ export const WindowView = memo(function Window({ id, app, size, position, focuse
|
||||||
if (minimized)
|
if (minimized)
|
||||||
classNames.push(styles.Minimized);
|
classNames.push(styles.Minimized);
|
||||||
|
|
||||||
// console.log(`Rendering window: ${id}`);
|
return (<>
|
||||||
|
<ModalsView modalsManager={modalsManager} modals={modals} style={{ zIndex: 1 }}/>
|
||||||
return (
|
|
||||||
<Draggable
|
<Draggable
|
||||||
key={id}
|
key={id}
|
||||||
axis="both"
|
axis="both"
|
||||||
|
|
@ -118,7 +133,7 @@ export const WindowView = memo(function Window({ id, app, size, position, focuse
|
||||||
}}
|
}}
|
||||||
onClick={focus}
|
onClick={focus}
|
||||||
>
|
>
|
||||||
<div className={`${styles.Header} Handle`}>
|
<div className={`${styles.Header} Handle`} onContextMenu={onContextMenu}>
|
||||||
<ReactSVG
|
<ReactSVG
|
||||||
className={styles["Window-icon"]}
|
className={styles["Window-icon"]}
|
||||||
src={process.env.PUBLIC_URL + `/media/applications/icons/${app.id}.svg`}
|
src={process.env.PUBLIC_URL + `/media/applications/icons/${app.id}.svg`}
|
||||||
|
|
@ -150,5 +165,5 @@ export const WindowView = memo(function Window({ id, app, size, position, focuse
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Draggable>
|
</Draggable>
|
||||||
);
|
</>);
|
||||||
});
|
});
|
||||||
24
src/hooks/modals/ContextMenu.js
Normal file
24
src/hooks/modals/ContextMenu.js
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { ContextMenu } from "../../components/modals/context-menu/ContextMenu.jsx";
|
||||||
|
import Vector2 from "../../features/math/vector2.js";
|
||||||
|
import Modal from "../../features/modals/modal.js";
|
||||||
|
import ModalsManager from "../../features/modals/modals.js";
|
||||||
|
import { useShortcuts } from "../utils/keyboard.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {object} props
|
||||||
|
* @param {ModalsManager} props.modalsManager
|
||||||
|
* @param {Object<string, Object<string, Function>>} props.options
|
||||||
|
* @param {Object<string, Object<string, string[]>>} props.shortcuts
|
||||||
|
* @returns {{ onContextMenu: Function }}
|
||||||
|
*/
|
||||||
|
export function useContextMenu({ modalsManager, options, shortcuts }) {
|
||||||
|
const onContextMenu = (event) => {
|
||||||
|
modalsManager.open(new Modal(ContextMenu)
|
||||||
|
.setPosition(new Vector2(event.clientX, event.clientY))
|
||||||
|
.setProps({ options, shortcuts }));
|
||||||
|
};
|
||||||
|
|
||||||
|
useShortcuts({ options, shortcuts, useCategories: false });
|
||||||
|
|
||||||
|
return { onContextMenu };
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { useEffect } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
import { removeFromArray } from "../../features/utils/array.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {object} params
|
* @param {object} params
|
||||||
|
|
@ -20,3 +21,71 @@ export function useKeyboardListener({ onKeyDown, onKeyUp }) {
|
||||||
};
|
};
|
||||||
}, [onKeyDown, onKeyUp]);
|
}, [onKeyDown, onKeyUp]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {object} props
|
||||||
|
* @param {Object<string, Object<string, Function>>} props.options
|
||||||
|
* @param {Object<string, Object<string, string[]>>} props.shortcuts
|
||||||
|
* @param {boolean} props.useCategories
|
||||||
|
*/
|
||||||
|
export function useShortcuts({ options, shortcuts, useCategories = true }) {
|
||||||
|
const [activeKeys, setActiveKeys] = useState([]);
|
||||||
|
|
||||||
|
const checkShortcuts = (event, allowExecution = true) => {
|
||||||
|
if (!shortcuts)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const checkGroup = (group, category) => {
|
||||||
|
for (const [name, shortcut] of Object.entries(group)) {
|
||||||
|
let active = true;
|
||||||
|
|
||||||
|
shortcut.forEach((key) => {
|
||||||
|
if (!activeKeys.includes(key) && event.key !== key)
|
||||||
|
return active = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!active)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
if (!shortcut.includes(event.key) || !allowExecution)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (category != null) {
|
||||||
|
options?.[category]?.[name]?.();
|
||||||
|
} else {
|
||||||
|
options?.[name]?.();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (useCategories) {
|
||||||
|
for (const [category, group] of Object.entries(shortcuts)) {
|
||||||
|
checkGroup(group, category);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
checkGroup(shortcuts);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onKeyDown = (event) => {
|
||||||
|
const isRepeated = activeKeys.includes(event.key);
|
||||||
|
checkShortcuts(event, isRepeated);
|
||||||
|
|
||||||
|
if (!isRepeated)
|
||||||
|
setActiveKeys(activeKeys.concat([event.key]));
|
||||||
|
};
|
||||||
|
|
||||||
|
const onKeyUp = (event) => {
|
||||||
|
checkShortcuts(event);
|
||||||
|
|
||||||
|
if (activeKeys.includes(event.key)) {
|
||||||
|
const keys = [...activeKeys];
|
||||||
|
removeFromArray(event.key, keys);
|
||||||
|
setActiveKeys(keys);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useKeyboardListener({ onKeyDown, onKeyUp });
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue