From b7a89a4a4a830d4071ea286f6cd2d74fff5d2b50 Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Wed, 30 Sep 2020 13:26:39 +1000 Subject: [PATCH] Refactored keyboard shortcuts to be global and not dependent on map interaction --- src/App.js | 61 +++++---- src/components/map/MapFog.js | 62 ++++----- src/components/map/MapInteraction.js | 127 +++++++----------- .../map/controls/DrawingToolSettings.js | 78 +++++------ .../map/controls/FogToolSettings.js | 85 ++++++------ .../map/controls/MeasureToolSettings.js | 29 ++-- src/contexts/KeyboardContext.js | 42 ++++++ src/helpers/useKeyboard.js | 26 ++++ 8 files changed, 258 insertions(+), 252 deletions(-) create mode 100644 src/contexts/KeyboardContext.js create mode 100644 src/helpers/useKeyboard.js diff --git a/src/App.js b/src/App.js index d6bd778..e82e3d2 100644 --- a/src/App.js +++ b/src/App.js @@ -15,7 +15,8 @@ import { DatabaseProvider } from "./contexts/DatabaseContext"; import { MapDataProvider } from "./contexts/MapDataContext"; import { TokenDataProvider } from "./contexts/TokenDataContext"; import { MapLoadingProvider } from "./contexts/MapLoadingContext"; -import { SettingsProvider } from "./contexts/SettingsContext.js"; +import { SettingsProvider } from "./contexts/SettingsContext"; +import { KeyboardProvider } from "./contexts/KeyboardContext"; function App() { return ( @@ -23,34 +24,36 @@ function App() { - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/map/MapFog.js b/src/components/map/MapFog.js index 96bc1df..be9e043 100644 --- a/src/components/map/MapFog.js +++ b/src/components/map/MapFog.js @@ -20,6 +20,7 @@ import { getRelativePointerPositionNormalized, Tick, } from "../../helpers/konva"; +import useKeyboard from "../../helpers/useKeyboard"; function MapFog({ map, @@ -248,44 +249,37 @@ function MapFog({ }, [toolSettings, drawingShape, onShapeSubtract, onShapeAdd]); // Add keyboard shortcuts - useEffect(() => { - function handleKeyDown({ key }) { - if (key === "Enter" && toolSettings.type === "polygon" && drawingShape) { - finishDrawingPolygon(); - } - if (key === "Escape" && drawingShape) { - setDrawingShape(null); - } - if (key === "Alt" && drawingShape) { - updateShapeColor(); - } + function handleKeyDown({ key }) { + if (key === "Enter" && toolSettings.type === "polygon" && drawingShape) { + finishDrawingPolygon(); } + if (key === "Escape" && drawingShape) { + setDrawingShape(null); + } + if (key === "Alt" && drawingShape) { + updateShapeColor(); + } + } - function handleKeyUp({ key }) { - if (key === "Alt" && drawingShape) { - updateShapeColor(); + function handleKeyUp({ key }) { + if (key === "Alt" && drawingShape) { + updateShapeColor(); + } + } + + function updateShapeColor() { + setDrawingShape((prevShape) => { + if (!prevShape) { + return; } - } + return { + ...prevShape, + color: toolSettings.useFogSubtract ? "black" : "red", + }; + }); + } - function updateShapeColor() { - setDrawingShape((prevShape) => { - if (!prevShape) { - return; - } - return { - ...prevShape, - color: toolSettings.useFogSubtract ? "black" : "red", - }; - }); - } - - interactionEmitter.on("keyDown", handleKeyDown); - interactionEmitter.on("keyUp", handleKeyUp); - return () => { - interactionEmitter.off("keyDown", handleKeyDown); - interactionEmitter.off("keyUp", handleKeyUp); - }; - }, [finishDrawingPolygon, interactionEmitter, drawingShape, toolSettings]); + useKeyboard(handleKeyDown, handleKeyUp); function handleShapeOver(shape, isDown) { if (shouldHover && isDown) { diff --git a/src/components/map/MapInteraction.js b/src/components/map/MapInteraction.js index 0b1aba5..7194d93 100644 --- a/src/components/map/MapInteraction.js +++ b/src/components/map/MapInteraction.js @@ -9,6 +9,7 @@ import normalizeWheel from "normalize-wheel"; import usePreventOverscroll from "../../helpers/usePreventOverscroll"; import useDataSource from "../../helpers/useDataSource"; +import useKeyboard from "../../helpers/useKeyboard"; import { mapSources as defaultMapSources } from "../../maps"; @@ -18,6 +19,7 @@ import MapStageContext, { } from "../../contexts/MapStageContext"; import AuthContext from "../../contexts/AuthContext"; import SettingsContext from "../../contexts/SettingsContext"; +import KeyboardContext from "../../contexts/KeyboardContext"; const wheelZoomSpeed = -0.001; const touchZoomSpeed = 0.005; @@ -206,88 +208,49 @@ function MapInteraction({ stageHeightRef.current = height; } - // Added key events to interaction emitter - useEffect(() => { - function handleKeyDown(event) { - // Ignore text input - if (event.target instanceof HTMLInputElement) { - return; - } - interactionEmitter.emit("keyDown", event); + function handleKeyDown(event) { + // Change to pan tool when pressing space + if (event.key === " " && selectedToolId === "pan") { + // Stop active state on pan icon from being selected + event.preventDefault(); + } + if ( + event.key === " " && + selectedToolId !== "pan" && + !disabledControls.includes("pan") + ) { + event.preventDefault(); + previousSelectedToolRef.current = selectedToolId; + onSelectedToolChange("pan"); } - function handleKeyUp(event) { - // Ignore text input - if (event.target instanceof HTMLInputElement) { - return; - } - interactionEmitter.emit("keyUp", event); + // Basic keyboard shortcuts + if (event.key === "w" && !disabledControls.includes("pan")) { + onSelectedToolChange("pan"); } - - document.body.addEventListener("keydown", handleKeyDown); - document.body.addEventListener("keyup", handleKeyUp); - document.body.tabIndex = 1; - return () => { - document.body.removeEventListener("keydown", handleKeyDown); - document.body.removeEventListener("keyup", handleKeyUp); - document.body.tabIndex = 0; - }; - }, [interactionEmitter]); - - // Create default keyboard shortcuts - useEffect(() => { - function handleKeyDown(event) { - // Change to pan tool when pressing space - if (event.key === " " && selectedToolId === "pan") { - // Stop active state on pan icon from being selected - event.preventDefault(); - } - if ( - event.key === " " && - selectedToolId !== "pan" && - !disabledControls.includes("pan") - ) { - event.preventDefault(); - previousSelectedToolRef.current = selectedToolId; - onSelectedToolChange("pan"); - } - - // Basic keyboard shortcuts - if (event.key === "w" && !disabledControls.includes("pan")) { - onSelectedToolChange("pan"); - } - if (event.key === "d" && !disabledControls.includes("drawing")) { - onSelectedToolChange("drawing"); - } - if (event.key === "f" && !disabledControls.includes("fog")) { - onSelectedToolChange("fog"); - } - if (event.key === "m" && !disabledControls.includes("measure")) { - onSelectedToolChange("measure"); - } - if (event.key === "q" && !disabledControls.includes("pointer")) { - onSelectedToolChange("pointer"); - } + if (event.key === "d" && !disabledControls.includes("drawing")) { + onSelectedToolChange("drawing"); } - - function handleKeyUp(event) { - if (event.key === " " && selectedToolId === "pan") { - onSelectedToolChange(previousSelectedToolRef.current); - } + if (event.key === "f" && !disabledControls.includes("fog")) { + onSelectedToolChange("fog"); } + if (event.key === "m" && !disabledControls.includes("measure")) { + onSelectedToolChange("measure"); + } + if (event.key === "q" && !disabledControls.includes("pointer")) { + onSelectedToolChange("pointer"); + } + } - interactionEmitter.on("keyDown", handleKeyDown); - interactionEmitter.on("keyUp", handleKeyUp); - return () => { - interactionEmitter.off("keyDown", handleKeyDown); - interactionEmitter.off("keyUp", handleKeyUp); - }; - }, [ - interactionEmitter, - onSelectedToolChange, - disabledControls, - selectedToolId, - ]); + function handleKeyUp(event) { + if (event.key === " " && selectedToolId === "pan") { + onSelectedToolChange(previousSelectedToolRef.current); + } + } + + useKeyboard(handleKeyDown, handleKeyUp); + // Get keyboard context to pass to Konva + const keyboardValue = useContext(KeyboardContext); function getCursorForTool(tool) { switch (tool) { @@ -360,11 +323,13 @@ function MapInteraction({ {/* Forward auth context to konva elements */} - - - {mapLoaded && children} - - + + + + {mapLoaded && children} + + + diff --git a/src/components/map/controls/DrawingToolSettings.js b/src/components/map/controls/DrawingToolSettings.js index 9f0bb6f..1ff7274 100644 --- a/src/components/map/controls/DrawingToolSettings.js +++ b/src/components/map/controls/DrawingToolSettings.js @@ -1,4 +1,4 @@ -import React, { useEffect, useContext } from "react"; +import React, { useEffect } from "react"; import { Flex, IconButton } from "theme-ui"; import { useMedia } from "react-media"; @@ -21,7 +21,7 @@ import RedoButton from "./RedoButton"; import Divider from "../../Divider"; -import MapInteractionContext from "../../../contexts/MapInteractionContext"; +import useKeyboard from "../../../helpers/useKeyboard"; function DrawingToolSettings({ settings, @@ -29,49 +29,41 @@ function DrawingToolSettings({ onToolAction, disabledActions, }) { - const { interactionEmitter } = useContext(MapInteractionContext); - // Keyboard shotcuts - useEffect(() => { - function handleKeyDown({ key, ctrlKey, metaKey, shiftKey }) { - if (key === "b") { - onSettingChange({ type: "brush" }); - } else if (key === "p") { - onSettingChange({ type: "paint" }); - } else if (key === "l") { - onSettingChange({ type: "line" }); - } else if (key === "r") { - onSettingChange({ type: "rectangle" }); - } else if (key === "c") { - onSettingChange({ type: "circle" }); - } else if (key === "t") { - onSettingChange({ type: "triangle" }); - } else if (key === "e") { - onSettingChange({ type: "erase" }); - } else if (key === "o") { - onSettingChange({ useBlending: !settings.useBlending }); - } else if ( - (key === "z" || key === "Z") && - (ctrlKey || metaKey) && - shiftKey && - !disabledActions.includes("redo") - ) { - onToolAction("mapRedo"); - } else if ( - key === "z" && - (ctrlKey || metaKey) && - !shiftKey && - !disabledActions.includes("undo") - ) { - onToolAction("mapUndo"); - } + function handleKeyDown({ key, ctrlKey, metaKey, shiftKey }) { + if (key === "b") { + onSettingChange({ type: "brush" }); + } else if (key === "p") { + onSettingChange({ type: "paint" }); + } else if (key === "l") { + onSettingChange({ type: "line" }); + } else if (key === "r") { + onSettingChange({ type: "rectangle" }); + } else if (key === "c") { + onSettingChange({ type: "circle" }); + } else if (key === "t") { + onSettingChange({ type: "triangle" }); + } else if (key === "e") { + onSettingChange({ type: "erase" }); + } else if (key === "o") { + onSettingChange({ useBlending: !settings.useBlending }); + } else if ( + (key === "z" || key === "Z") && + (ctrlKey || metaKey) && + shiftKey && + !disabledActions.includes("redo") + ) { + onToolAction("mapRedo"); + } else if ( + key === "z" && + (ctrlKey || metaKey) && + !shiftKey && + !disabledActions.includes("undo") + ) { + onToolAction("mapUndo"); } - - interactionEmitter.on("keyDown", handleKeyDown); - return () => { - interactionEmitter.off("keyDown", handleKeyDown); - }; - }); + } + useKeyboard(handleKeyDown); // Change to brush if on erase and it gets disabled useEffect(() => { diff --git a/src/components/map/controls/FogToolSettings.js b/src/components/map/controls/FogToolSettings.js index 09ece28..9db94c3 100644 --- a/src/components/map/controls/FogToolSettings.js +++ b/src/components/map/controls/FogToolSettings.js @@ -1,4 +1,4 @@ -import React, { useContext, useEffect } from "react"; +import React from "react"; import { Flex } from "theme-ui"; import { useMedia } from "react-media"; @@ -15,11 +15,11 @@ import FogSubtractIcon from "../../../icons/FogSubtractIcon"; import UndoButton from "./UndoButton"; import RedoButton from "./RedoButton"; +import ToolSection from "./ToolSection"; import Divider from "../../Divider"; -import MapInteractionContext from "../../../contexts/MapInteractionContext"; -import ToolSection from "./ToolSection"; +import useKeyboard from "../../../helpers/useKeyboard"; function BrushToolSettings({ settings, @@ -27,55 +27,46 @@ function BrushToolSettings({ onToolAction, disabledActions, }) { - const { interactionEmitter } = useContext(MapInteractionContext); - // Keyboard shortcuts - useEffect(() => { - function handleKeyDown({ key, ctrlKey, metaKey, shiftKey }) { - if (key === "Alt") { - onSettingChange({ useFogSubtract: !settings.useFogSubtract }); - } else if (key === "p") { - onSettingChange({ type: "polygon" }); - } else if (key === "b") { - onSettingChange({ type: "brush" }); - } else if (key === "t") { - onSettingChange({ type: "toggle" }); - } else if (key === "r") { - onSettingChange({ type: "remove" }); - } else if (key === "s") { - onSettingChange({ useEdgeSnapping: !settings.useEdgeSnapping }); - } else if (key === "f") { - onSettingChange({ preview: !settings.preview }); - } else if ( - (key === "z" || key === "Z") && - (ctrlKey || metaKey) && - shiftKey && - !disabledActions.includes("redo") - ) { - onToolAction("fogRedo"); - } else if ( - key === "z" && - (ctrlKey || metaKey) && - !shiftKey && - !disabledActions.includes("undo") - ) { - onToolAction("fogUndo"); - } + function handleKeyDown({ key, ctrlKey, metaKey, shiftKey }) { + if (key === "Alt") { + onSettingChange({ useFogSubtract: !settings.useFogSubtract }); + } else if (key === "p") { + onSettingChange({ type: "polygon" }); + } else if (key === "b") { + onSettingChange({ type: "brush" }); + } else if (key === "t") { + onSettingChange({ type: "toggle" }); + } else if (key === "r") { + onSettingChange({ type: "remove" }); + } else if (key === "s") { + onSettingChange({ useEdgeSnapping: !settings.useEdgeSnapping }); + } else if (key === "f") { + onSettingChange({ preview: !settings.preview }); + } else if ( + (key === "z" || key === "Z") && + (ctrlKey || metaKey) && + shiftKey && + !disabledActions.includes("redo") + ) { + onToolAction("fogRedo"); + } else if ( + key === "z" && + (ctrlKey || metaKey) && + !shiftKey && + !disabledActions.includes("undo") + ) { + onToolAction("fogUndo"); } + } - function handleKeyUp({ key }) { - if (key === "Alt") { - onSettingChange({ useFogSubtract: !settings.useFogSubtract }); - } + function handleKeyUp({ key }) { + if (key === "Alt") { + onSettingChange({ useFogSubtract: !settings.useFogSubtract }); } + } - interactionEmitter.on("keyDown", handleKeyDown); - interactionEmitter.on("keyUp", handleKeyUp); - return () => { - interactionEmitter.off("keyDown", handleKeyDown); - interactionEmitter.off("keyUp", handleKeyUp); - }; - }); + useKeyboard(handleKeyDown, handleKeyUp); const isSmallScreen = useMedia({ query: "(max-width: 799px)" }); const drawTools = [ diff --git a/src/components/map/controls/MeasureToolSettings.js b/src/components/map/controls/MeasureToolSettings.js index 8886dbc..40be3ca 100644 --- a/src/components/map/controls/MeasureToolSettings.js +++ b/src/components/map/controls/MeasureToolSettings.js @@ -1,4 +1,4 @@ -import React, { useEffect, useContext } from "react"; +import React from "react"; import { Flex, Input, Text } from "theme-ui"; import ToolSection from "./ToolSection"; @@ -8,28 +8,21 @@ import MeasureManhattanIcon from "../../../icons/MeasureManhattanIcon"; import Divider from "../../Divider"; -import MapInteractionContext from "../../../contexts/MapInteractionContext"; +import useKeyboard from "../../../helpers/useKeyboard"; function MeasureToolSettings({ settings, onSettingChange }) { - const { interactionEmitter } = useContext(MapInteractionContext); - // Keyboard shortcuts - useEffect(() => { - function handleKeyDown({ key }) { - if (key === "g") { - onSettingChange({ type: "chebyshev" }); - } else if (key === "l") { - onSettingChange({ type: "euclidean" }); - } else if (key === "c") { - onSettingChange({ type: "manhattan" }); - } + function handleKeyDown({ key }) { + if (key === "g") { + onSettingChange({ type: "chebyshev" }); + } else if (key === "l") { + onSettingChange({ type: "euclidean" }); + } else if (key === "c") { + onSettingChange({ type: "manhattan" }); } - interactionEmitter.on("keyDown", handleKeyDown); + } - return () => { - interactionEmitter.off("keyDown", handleKeyDown); - }; - }); + useKeyboard(handleKeyDown); const tools = [ { diff --git a/src/contexts/KeyboardContext.js b/src/contexts/KeyboardContext.js new file mode 100644 index 0000000..867c28d --- /dev/null +++ b/src/contexts/KeyboardContext.js @@ -0,0 +1,42 @@ +import React, { useEffect, useState } from "react"; +import { EventEmitter } from "events"; + +const KeyboardContext = React.createContext({ keyEmitter: new EventEmitter() }); + +export function KeyboardProvider({ children }) { + const [keyEmitter] = useState(new EventEmitter()); + useEffect(() => { + function handleKeyDown(event) { + // Ignore text input + if (event.target instanceof HTMLInputElement) { + return; + } + keyEmitter.emit("keyDown", event); + } + + function handleKeyUp(event) { + // Ignore text input + if (event.target instanceof HTMLInputElement) { + return; + } + keyEmitter.emit("keyUp", event); + } + + document.body.addEventListener("keydown", handleKeyDown); + document.body.addEventListener("keyup", handleKeyUp); + document.body.tabIndex = 1; + return () => { + document.body.removeEventListener("keydown", handleKeyDown); + document.body.removeEventListener("keyup", handleKeyUp); + document.body.tabIndex = 0; + }; + }, [keyEmitter]); + + return ( + + {children} + + ); +} + +export default KeyboardContext; diff --git a/src/helpers/useKeyboard.js b/src/helpers/useKeyboard.js new file mode 100644 index 0000000..19958d4 --- /dev/null +++ b/src/helpers/useKeyboard.js @@ -0,0 +1,26 @@ +import { useEffect, useContext } from "react"; + +import KeyboardContext from "../contexts/KeyboardContext"; + +function useKeyboard(onKeyDown, onKeyUp) { + const { keyEmitter } = useContext(KeyboardContext); + useEffect(() => { + if (onKeyDown) { + keyEmitter.on("keyDown", onKeyDown); + } + if (onKeyUp) { + keyEmitter.on("keyUp", onKeyUp); + } + + return () => { + if (onKeyDown) { + keyEmitter.off("keyDown", onKeyDown); + } + if (onKeyUp) { + keyEmitter.off("keyUp", onKeyUp); + } + }; + }); +} + +export default useKeyboard;