daedalOS/hooks/useGlobalKeyboardShortcuts.ts
2024-01-15 21:38:02 -08:00

201 lines
5.8 KiB
TypeScript

import { useEffect, useRef } from "react";
import {
SEARCH_BUTTON_TITLE,
START_BUTTON_TITLE,
} from "components/system/Taskbar/functions";
import { useProcesses } from "contexts/process";
import { useSession } from "contexts/session";
import { useViewport } from "contexts/viewport";
import { useProcessesRef } from "hooks/useProcessesRef";
import { KEYPRESS_DEBOUNCE_MS } from "utils/constants";
import { haltEvent, toggleShowDesktop, viewHeight } from "utils/functions";
declare global {
interface Window {
globalKeyStates?: {
altKey: boolean;
ctrlKey: boolean;
metaKey: boolean;
shiftKey: boolean;
};
}
}
const getByTitle = (title: string): HTMLButtonElement | null =>
document.querySelector(
`main > nav > div[title='${title}']`
) as HTMLButtonElement;
let metaDown = false;
let metaComboUsed = false;
let triggeringBinding = false;
const haltAndDebounceBinding = (event: KeyboardEvent): boolean => {
haltEvent(event);
if (triggeringBinding) return true;
triggeringBinding = true;
setTimeout(() => {
triggeringBinding = false;
}, KEYPRESS_DEBOUNCE_MS);
return false;
};
const metaCombos = new Set(["ARROWDOWN", "ARROWUP", "D", "E", "R", "S", "X"]);
const updateKeyStates = (event: KeyboardEvent): void => {
const { altKey, ctrlKey, shiftKey, metaKey } = event;
window.globalKeyStates = { altKey, ctrlKey, metaKey, shiftKey };
};
const useGlobalKeyboardShortcuts = (): void => {
const { closeWithTransition, maximize, minimize, open } = useProcesses();
const processesRef = useProcessesRef();
const { foregroundId, stackOrder } = useSession();
const { fullscreenElement, toggleFullscreen } = useViewport();
const altBindingsRef = useRef<Record<string, () => void>>({});
const shiftBindingsRef = useRef<Record<string, () => void>>({
E: () => open("FileExplorer"),
ESCAPE: () => getByTitle(START_BUTTON_TITLE)?.click(),
F10: () => open("Terminal"),
F12: () => open("DevTools"),
F5: () => window.location.reload(),
R: () => open("Run"),
S: () => getByTitle(SEARCH_BUTTON_TITLE)?.click(),
X: () =>
getByTitle(START_BUTTON_TITLE)?.dispatchEvent(
new MouseEvent("contextmenu", {
clientX: 1,
clientY: viewHeight() - 1,
})
),
});
useEffect(() => {
const onKeyDown = (event: KeyboardEvent): void => {
updateKeyStates(event);
const { altKey, ctrlKey, key, shiftKey } = event;
const keyName = key?.toUpperCase();
if (!keyName) return;
if (shiftKey) {
if (
(ctrlKey || !metaCombos.has(keyName)) &&
shiftBindingsRef.current?.[keyName] &&
!haltAndDebounceBinding(event)
) {
shiftBindingsRef.current[keyName]();
}
} else if (keyName === "F11") {
haltEvent(event);
toggleFullscreen();
} else if (
document.activeElement === document.body &&
keyName.startsWith("ARROW")
) {
document.body.querySelector("main ol li button")?.dispatchEvent(
new MouseEvent("mousedown", {
bubbles: true,
})
);
} else if (ctrlKey && altKey && altBindingsRef.current?.[keyName]) {
altBindingsRef.current?.[keyName]?.();
} else if (fullscreenElement === document.documentElement) {
if (keyName === "META") metaDown = true;
else if (altKey && altBindingsRef.current?.[keyName]) {
haltEvent(event);
altBindingsRef.current?.[keyName]?.();
} else if (keyName === "ESCAPE") {
if (document.pointerLockElement) document.exitPointerLock();
else toggleFullscreen();
} else if (
metaDown &&
metaCombos.has(keyName) &&
shiftBindingsRef.current?.[keyName] &&
!haltAndDebounceBinding(event)
) {
metaComboUsed = true;
shiftBindingsRef.current[keyName]();
}
}
};
const onKeyUp = (event: KeyboardEvent): void => {
updateKeyStates(event);
if (
metaDown &&
fullscreenElement === document.documentElement &&
event.key?.toUpperCase() === "META"
) {
metaDown = false;
if (metaComboUsed) metaComboUsed = false;
else getByTitle(START_BUTTON_TITLE)?.click();
}
};
document.addEventListener("keydown", onKeyDown, {
capture: true,
});
document.addEventListener("keyup", onKeyUp, {
capture: true,
passive: true,
});
return () => {
document.removeEventListener("keydown", onKeyDown, {
capture: true,
});
document.removeEventListener("keyup", onKeyUp, {
capture: true,
});
};
}, [fullscreenElement, toggleFullscreen]);
useEffect(() => {
altBindingsRef.current = {
...altBindingsRef.current,
F4: () => closeWithTransition(foregroundId),
};
}, [closeWithTransition, foregroundId]);
useEffect(() => {
shiftBindingsRef.current = {
...shiftBindingsRef.current,
ARROWDOWN: () => {
const {
hideMinimizeButton = false,
maximized,
minimized,
} = processesRef.current[foregroundId];
if (maximized) {
maximize(foregroundId);
} else if (!minimized && !hideMinimizeButton) {
minimize(foregroundId);
}
},
ARROWUP: () => {
const {
allowResizing = true,
hideMaximizeButton = false,
maximized,
minimized,
} = processesRef.current[foregroundId];
if (minimized) {
minimize(foregroundId);
} else if (!maximized && allowResizing && !hideMaximizeButton) {
maximize(foregroundId);
}
},
D: () => toggleShowDesktop(processesRef.current, stackOrder, minimize),
};
}, [foregroundId, maximize, minimize, processesRef, stackOrder]);
};
export default useGlobalKeyboardShortcuts;