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 { 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}>
|
||||
|
|
|
|||
|
|
@ -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", "+"],
|
||||
|
|
|
|||
|
|
@ -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}/>
|
||||
|
|
|
|||
|
|
@ -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}/>
|
||||
)}
|
||||
|
|
|
|||
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 }) {
|
||||
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();
|
||||
}}>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
</>);
|
||||
});
|
||||
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
|
||||
|
|
@ -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 });
|
||||
}
|
||||
Loading…
Reference in a new issue