Added context menu hook

This commit is contained in:
Prozilla 2023-08-11 16:27:12 +02:00
parent 72165ad770
commit 7510d1bbf4
No known key found for this signature in database
GPG key ID: 5858DFE71CAF31EE
11 changed files with 140 additions and 74 deletions

View file

@ -1,56 +1,14 @@
import { useState } from "react";
import { removeFromArray } from "../../../features/utils/array.js";
import { useKeyboardListener } from "../../../hooks/utils/keyboard.js";
import { useShortcuts } from "../../../hooks/utils/keyboard.js";
import { DropdownButton } from "../../utils/DropdownButton.jsx";
import styles from "./HeaderMenu.module.css";
/**
* @param {Object} props
* @param {Object<String, Object<string, function>>} props.options
* @param {Object<String, Object<string, string[]>>} props.shortcuts
* @param {Object<string, Object<string, Function>>} props.options
* @param {Object<string, Object<string, string[]>>} props.shortcuts
*/
export function HeaderMenu({ options, shortcuts }) {
const [activeKeys, setActiveKeys] = useState([]);
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 });
useShortcuts({ options, shortcuts });
return (
<div className={styles.Container}>

View file

@ -93,7 +93,7 @@ export function TextEditor({ file, setTitle, close, mode, app }) {
"New": newText,
"Save": saveText,
// "Save As": saveTextAs,
"Exit": () => {
"Quit": () => {
close();
},
},
@ -116,7 +116,7 @@ export function TextEditor({ file, setTitle, close, mode, app }) {
"File": {
"New": ["Control", "e"],
"Save": ["Control", "s"],
"Exit": ["Control", "x"],
"Quit": ["Control", "q"],
},
"View": {
"Zoom In": ["Control", "+"],

View file

@ -3,18 +3,23 @@ import { SettingsManager } from "../../features/settings/settings.js";
import { useSettings } from "../../hooks/settings/SettingsContext.js";
import styles from "./Desktop.module.css";
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 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 { useContextMenu } from "../../hooks/modals/ContextMenu.js";
export function Desktop() {
const settingsManager = useSettings();
const [wallpaper, setWallpaper] = useState(null);
const [modalsManager, modals] = useModals();
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(() => {
(async () => {
@ -23,18 +28,6 @@ export function Desktop() {
})();
}, [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 (<>
<div className={styles.Container} style={{ backgroundImage: `url(${wallpaper})` }} onContextMenu={onContextMenu}>
<ModalsView modalsManager={modalsManager} modals={modals}/>

View file

@ -1,15 +1,18 @@
import { Modal } from "../../features/modals/modal.js";
import ModalsManager from "../../features/modals/modals.js";
import { ModalView } from "./ModalView.jsx";
import styles from "./ModalsView.module.css";
/**
* @param {object} root
* @param {ModalsManager} root.modalsManager
* @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 (
<div>
<div style={style} className={`${styles.Container} ${className}`}>
{modals?.map((modal) =>
<ModalView key={modal.id} modal={modal}/>
)}

View file

@ -0,0 +1,3 @@
.Container {
position: relative;
}

View file

@ -11,7 +11,7 @@ import styles from "./ContextMenu.module.css";
export function ContextMenu({ modal, options, shortcuts }) {
return (<div className={styles.Container}>
{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();
callback();
}}>

View file

@ -1,6 +1,7 @@
.Container {
padding: 0.5rem;
border-top-left-radius: 0;
background-color: var(--background-color-b);
}
.Button {
@ -23,7 +24,7 @@
background-color: rgba(255, 255, 255, 5%);
}
.Label {
.Label, .Shortcut {
margin: 0;
}

View file

@ -9,6 +9,9 @@ import Application from "../../features/applications/application.js";
import Vector2 from "../../features/math/vector2.js";
import { faWindowMaximize } from "@fortawesome/free-regular-svg-icons";
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
@ -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 }) {
const windowsManager = useWindowsManager();
const nodeRef = useRef(null);
const [modalsManager, modals] = useModals();
const [initialised, setInitialised] = useState(false);
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 { onContextMenu } = useContextMenu({
modalsManager,
options: {
"Maximize": () => { setMaximized(!maximized); },
"Close": () => { close(); }
},
shortcuts: {
"Maximize": ["F11"],
"Close": ["Control", "q"]
}
});
useEffect(() => {
const resizeObserver = new ResizeObserver((event) => {
setScreenWidth(event[0].contentBoxSize[0].inlineSize);
@ -88,9 +104,8 @@ export const WindowView = memo(function Window({ id, app, size, position, focuse
if (minimized)
classNames.push(styles.Minimized);
// console.log(`Rendering window: ${id}`);
return (
return (<>
<ModalsView modalsManager={modalsManager} modals={modals} style={{ zIndex: 1 }}/>
<Draggable
key={id}
axis="both"
@ -118,7 +133,7 @@ export const WindowView = memo(function Window({ id, app, size, position, focuse
}}
onClick={focus}
>
<div className={`${styles.Header} Handle`}>
<div className={`${styles.Header} Handle`} onContextMenu={onContextMenu}>
<ReactSVG
className={styles["Window-icon"]}
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>
</Draggable>
);
</>);
});

View 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 };
}

View file

@ -1,4 +1,5 @@
import { useEffect } from "react";
import { useEffect, useState } from "react";
import { removeFromArray } from "../../features/utils/array.js";
/**
* @param {object} params
@ -19,4 +20,72 @@ export function useKeyboardListener({ onKeyDown, onKeyUp }) {
document.removeEventListener("keyup", 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 });
}