From 56b8e2cdb2bdfca1fb3fb05d5b02ed523e2c7151 Mon Sep 17 00:00:00 2001 From: Ritchie Martori Date: Thu, 25 Aug 2022 15:48:53 -0700 Subject: [PATCH 01/11] looker3d hotkey support --- app/packages/looker-3d/src/Looker3d.tsx | 158 ++++++++++++------- app/packages/state/src/hooks/useJSONPanel.ts | 1 + 2 files changed, 106 insertions(+), 53 deletions(-) diff --git a/app/packages/looker-3d/src/Looker3d.tsx b/app/packages/looker-3d/src/Looker3d.tsx index d41b0f8240..b9f70c84ca 100644 --- a/app/packages/looker-3d/src/Looker3d.tsx +++ b/app/packages/looker-3d/src/Looker3d.tsx @@ -202,6 +202,7 @@ function Polyline({ selected, onClick, tooltip, + label }) { if (filled) { // filled not yet supported @@ -291,7 +292,9 @@ function Looker3dCore({ sampleOverride: sample }) { // @ts-ignore const src = fos.getSampleSrc(sample[mediaField]); const points = useLoader(PCDLoader, src); - const selectedLabels = recoil.useRecoilValue(fos.selectedLabels); + const [selectedLabels, setSelectedLabels] = recoil.useRecoilState( + fos.selectedLabels + ); const pathFilter = usePathFilter(); const labelAlpha = recoil.useRecoilValue(fos.alpha(modal)); const onSelectLabel = fos.useOnSelectLabel(); @@ -314,36 +317,45 @@ function Looker3dCore({ sampleOverride: sample }) { const colorBy = recoil.useRecoilValue(pcState.colorBy); - function onChangeView(view) { - const camera = cameraRef.current as any; - const controls = controlsRef.current as any; - if (camera) { - if (controls) { + const onChangeView = useCallback( + (view) => { + const camera = cameraRef.current as any; + const controls = controlsRef.current as any; + if (camera && controls) { + const origTarget = controls.target.clone(); + const origCamPos = camera.position.clone(); controls.target.set(0, 0, 0); - } - switch (view) { - case "top": - if (settings.defaultCameraPosition) { - camera.position.set( - settings.defaultCameraPosition.x, - settings.defaultCameraPosition.y, - settings.defaultCameraPosition.z - ); - } else { - const maxZ = pointCloudBounds ? pointCloudBounds.max.z : null; - if (maxZ !== null) { - camera.position.set(0, 0, 20 * maxZ); + switch (view) { + case "top": + if (settings.defaultCameraPosition) { + camera.position.set( + settings.defaultCameraPosition.x, + settings.defaultCameraPosition.y, + settings.defaultCameraPosition.z + ); + } else { + const maxZ = pointCloudBounds ? pointCloudBounds.max.z : null; + if (maxZ !== null) { + camera.position.set(0, 0, 20 * maxZ); + } } - } - break; - case "pov": - camera.position.set(0, -10, 1); - break; + break; + case "pov": + camera.position.set(0, -10, 1); + break; + } + controls.update(); + camera.updateProjectionMatrix(); + return ( + origTarget.equals(controls.target) && + origCamPos.equals(camera.position) + ); } - controls.update(); - camera.updateProjectionMatrix(); - } - } + }, + [cameraRef, controlsRef, settings, pointCloudBounds] + ); + + const jsonPanel = fos.useJSONPanel(); const pcRotationSetting = _.get(settings, "pointCloud.rotation", [0, 0, 0]); const pcRotation = toEulerFromDegreesArray(pcRotationSetting); @@ -376,6 +388,28 @@ function Looker3dCore({ sampleOverride: sample }) { }, [clear, hovering]); const hoveringRef = useRef(false); const tooltip = fos.useTooltip(); + useHotkey("KeyT", () => onChangeView("top")); + useHotkey("KeyE", () => onChangeView("pov")); + useHotkey( + "Escape", + ({ get, set }) => { + const changed = onChangeView("top"); + if (changed) return; + + const selectedLabels = get(fos.selectedLabels); + if (selectedLabels && Object.keys(selectedLabels).length > 0) { + set(fos.selectedLabels, {}); + return; + } + if (jsonPanel.isOpen) return; + set(fos.modal, null); + }, + [jsonPanel.isOpen, selectedLabels, hovering] + ); + + useEffect(() => { + onChangeView("top"); + }, [cameraRef, controlsRef]); return ( @@ -424,29 +458,31 @@ function Looker3dCore({ sampleOverride: sample }) { /> - {(hoveringRef.current || hovering) && ( - (hoveringRef.current = true)} - onMouseOut={() => (hoveringRef.current = false)} - > - - - - - - - - )} + { + /*(hoveringRef.current || hovering)*/ true && ( + (hoveringRef.current = true)} + onMouseLeave={() => (hoveringRef.current = false)} + > + + + + + + + + ) + } ); } @@ -602,8 +638,7 @@ function Choice({ label, value }) { ); } -function ViewJSON({ sample }) { - const jsonPanel = fos.useJSONPanel(); +function ViewJSON({ sample, jsonPanel }) { const [currentAction, setAction] = recoil.useRecoilState( pcState.currentAction ); @@ -709,3 +744,20 @@ class ErrorBoundary extends React.Component< return this.props.children; } } +function useHotkey(code, fn, deps) { + const EVENT_NAME = "keydown"; + const cb = recoil.useRecoilTransaction_UNSTABLE((ctx) => () => fn(ctx), deps); + function handle(e) { + if (e.code === code) { + cb(); + } + } + function unlisten() { + window.removeEventListener(EVENT_NAME, handle); + } + useEffect(() => { + window.addEventListener(EVENT_NAME, handle); + + return unlisten; + }, []); +} diff --git a/app/packages/state/src/hooks/useJSONPanel.ts b/app/packages/state/src/hooks/useJSONPanel.ts index d0bddc029a..dd6b42b1c0 100644 --- a/app/packages/state/src/hooks/useJSONPanel.ts +++ b/app/packages/state/src/hooks/useJSONPanel.ts @@ -37,6 +37,7 @@ export default function useJSONPanel() { } function handleEscape(e) { + console.log("ev", e); if (e.key === "Escape") close(); } From ac29fee88d9b0f9bbaa700cdbf49c7c42dc70655 Mon Sep 17 00:00:00 2001 From: Ritchie Martori Date: Fri, 26 Aug 2022 10:37:17 -0700 Subject: [PATCH 02/11] fix ordering in looker3d escape --- .../app/src/components/Modal/Looker.tsx | 3 +- app/packages/looker-3d/src/Looker3d.tsx | 61 +++++++++---------- app/packages/state/src/hooks/useJSONPanel.ts | 1 - 3 files changed, 32 insertions(+), 33 deletions(-) diff --git a/app/packages/app/src/components/Modal/Looker.tsx b/app/packages/app/src/components/Modal/Looker.tsx index 8266b73131..5d00ddd853 100644 --- a/app/packages/app/src/components/Modal/Looker.tsx +++ b/app/packages/app/src/components/Modal/Looker.tsx @@ -107,7 +107,6 @@ const Looker = ({ lookerRef, onClose, onNext, onPrevious }: LookerProps) => { onNext && useEventHandler(looker, "next", onNext); onPrevious && useEventHandler(looker, "previous", onPrevious); - onClose && useEventHandler(looker, "close", onClose); useEventHandler(looker, "select", useOnSelectLabel()); useEventHandler(looker, "error", (event) => handleError(event.detail)); const jsonPanel = fos.useJSONPanel(); @@ -121,6 +120,8 @@ const Looker = ({ lookerRef, onClose, onNext, onPrevious }: LookerProps) => { } }); + onClose && useEventHandler(looker, "close", onClose); + useEffect(() => { initialRef.current = false; }, []); diff --git a/app/packages/looker-3d/src/Looker3d.tsx b/app/packages/looker-3d/src/Looker3d.tsx index b9f70c84ca..2a48267ecf 100644 --- a/app/packages/looker-3d/src/Looker3d.tsx +++ b/app/packages/looker-3d/src/Looker3d.tsx @@ -344,11 +344,11 @@ function Looker3dCore({ sampleOverride: sample }) { camera.position.set(0, -10, 1); break; } - controls.update(); + camera.updateProjectionMatrix(); - return ( + return !( origTarget.equals(controls.target) && - origCamPos.equals(camera.position) + origCamPos.distanceTo(camera.position) < 1 ); } }, @@ -393,14 +393,15 @@ function Looker3dCore({ sampleOverride: sample }) { useHotkey( "Escape", ({ get, set }) => { - const changed = onChangeView("top"); - if (changed) return; - const selectedLabels = get(fos.selectedLabels); if (selectedLabels && Object.keys(selectedLabels).length > 0) { set(fos.selectedLabels, {}); return; } + + const changed = onChangeView("top"); + if (changed) return; + if (jsonPanel.isOpen) return; set(fos.modal, null); }, @@ -458,31 +459,29 @@ function Looker3dCore({ sampleOverride: sample }) { /> - { - /*(hoveringRef.current || hovering)*/ true && ( - (hoveringRef.current = true)} - onMouseLeave={() => (hoveringRef.current = false)} - > - - - - - - - - ) - } + {(hoveringRef.current || hovering) && ( + (hoveringRef.current = true)} + onMouseLeave={() => (hoveringRef.current = false)} + > + + + + + + + + )} ); } diff --git a/app/packages/state/src/hooks/useJSONPanel.ts b/app/packages/state/src/hooks/useJSONPanel.ts index dd6b42b1c0..d0bddc029a 100644 --- a/app/packages/state/src/hooks/useJSONPanel.ts +++ b/app/packages/state/src/hooks/useJSONPanel.ts @@ -37,7 +37,6 @@ export default function useJSONPanel() { } function handleEscape(e) { - console.log("ev", e); if (e.key === "Escape") close(); } From 6b3cd6365aa54c9d35d655ce1c8deab059dc538d Mon Sep 17 00:00:00 2001 From: Ritchie Martori Date: Fri, 26 Aug 2022 12:54:56 -0700 Subject: [PATCH 03/11] looker3d help panel --- .../app/src/components/Modal/Modal.tsx | 9 +- .../src/components/HelpPanel/HelpPanel.tsx | 67 ++++++++++ .../src/components/HelpPanel/index.ts | 1 + .../src/components/HelpPanel/panel.module.css | 126 ++++++++++++++++++ .../components/src/components/index.ts | 1 + app/packages/components/src/icons/help.svg | 1 + app/packages/components/src/index.ts | 3 +- app/packages/looker-3d/src/Looker3d.tsx | 44 ++++++ app/packages/state/src/hooks/index.ts | 1 + app/packages/state/src/hooks/useHelpPanel.ts | 51 +++++++ 10 files changed, 302 insertions(+), 2 deletions(-) create mode 100644 app/packages/components/src/components/HelpPanel/HelpPanel.tsx create mode 100644 app/packages/components/src/components/HelpPanel/index.ts create mode 100644 app/packages/components/src/components/HelpPanel/panel.module.css create mode 100644 app/packages/components/src/icons/help.svg create mode 100644 app/packages/state/src/hooks/useHelpPanel.ts diff --git a/app/packages/app/src/components/Modal/Modal.tsx b/app/packages/app/src/components/Modal/Modal.tsx index 62160f66a7..1817711b0b 100644 --- a/app/packages/app/src/components/Modal/Modal.tsx +++ b/app/packages/app/src/components/Modal/Modal.tsx @@ -10,7 +10,7 @@ import Sidebar, { Entries } from "../Sidebar"; import Group from "./Group"; import Sample from "./Sample"; import { lookerPanelOverlayContainer } from "./Group.module.css"; -import { JSONPanel } from "@fiftyone/components"; +import { HelpPanel, JSONPanel } from "@fiftyone/components"; const ModalWrapper = styled.div` position: fixed; @@ -185,6 +185,7 @@ const SampleModal = () => { const wrapperRef = useRef(null); const isGroup = useRecoilValue(fos.isGroup); const jsonPanel = fos.useJSONPanel(); + const helpPanel = fos.useHelpPanel(); return ReactDOM.createPortal( @@ -202,6 +203,12 @@ const SampleModal = () => { onCopy={() => jsonPanel.copy()} /> )} + {helpPanel.isOpen && ( + helpPanel.close()} + items={helpPanel.items} + /> + )} diff --git a/app/packages/components/src/components/HelpPanel/HelpPanel.tsx b/app/packages/components/src/components/HelpPanel/HelpPanel.tsx new file mode 100644 index 0000000000..1d422025df --- /dev/null +++ b/app/packages/components/src/components/HelpPanel/HelpPanel.tsx @@ -0,0 +1,67 @@ +/** + * Copyright 2017-2022, Voxel51, Inc. + */ +import { + lookerPanel, + lookerPanelContainer, + lookerPanelVerticalContainer, + lookerPanelClose, + lookerHelpPanelItems, + lookerShortcutValue, + lookerShortcutTitle, + lookerShortcutDetail, + lookerPanelFlex, + lookerPanelHeader, +} from "./panel.module.css"; +import closeIcon from "../../icons/close.svg"; +import { Fragment } from "react"; + +function Close({ onClick }) { + return ( + + ); +} + +export default function HelpPanel({ onClose, items }) { + return ( +
+
+
e.stopPropagation()}> + +
Help
+ + {items.map((item) => ( + + ))} + +
+ +
+
+
+ ); +} + +function Header({ children }) { + return
{children}
; +} +function Scroll({ children }) { + return
{children}
; +} +function Items({ children }) { + return
{children}
; +} +function Item({ shortcut, title, detail }) { + return ( + +
{shortcut}
+
{title}
+
{detail}
+
+ ); +} diff --git a/app/packages/components/src/components/HelpPanel/index.ts b/app/packages/components/src/components/HelpPanel/index.ts new file mode 100644 index 0000000000..881f6550c8 --- /dev/null +++ b/app/packages/components/src/components/HelpPanel/index.ts @@ -0,0 +1 @@ +export { default } from "./HelpPanel"; diff --git a/app/packages/components/src/components/HelpPanel/panel.module.css b/app/packages/components/src/components/HelpPanel/panel.module.css new file mode 100644 index 0000000000..38678a0d09 --- /dev/null +++ b/app/packages/components/src/components/HelpPanel/panel.module.css @@ -0,0 +1,126 @@ +/** + * Copyright 2017-2022, Voxel51, Inc. + */ + +.lookerPanel { + padding: 1rem; + line-height: 1; + font-weight: bold; + color: #eee; + background-color: hsl(210, 11%, 11%); + border: 1px solid #191c1f; + box-shadow: 0 8px 15px 0 rgba(0, 0, 0, 0.43); + border-radius: 3px; + overflow: auto; + scrollbar-width: none; + position: relative; + pointer-events: all; +} + +.lookerPanel::-webkit-scrollbar { + width: 0px; + background: transparent; + display: none; +} +.lookerPanel::-webkit-scrollbar-thumb { + width: 0px; + display: none; +} + +.lookerPanelContainer { + top: 0; + left: 0; + height: 100%; + width: 100%; + display: flex; + justify-content: center; + position: absolute; + overflow: hidden; + z-index: 10000; + pointer-events: none; +} + +.lookerPanelVerticalContainer { + display: flex; + flex-direction: column; + justify-content: center; + position: relative; + margin: 3.5rem; +} + +.lookerPanelHeader { + font-size: 18px; + padding-bottom: 0.5rem; + border-bottom: 2px solid rgb(225, 100, 40); +} + +.lookerPanelClose { + position: absolute; + top: 0; + right: 0; + cursor: pointer; + margin: 3px; + border-radius: 3px; + + -webkit-transition: 0.2s ease-in-out; + -moz-transition: 0.2s ease-in-out; + -o-transition: 0.2s ease-in-out; + transition: 0.2s ease-in-out; +} + +.lookerPanelClose:hover { + background-color: rgb(225, 100, 40); + transform: translate(0, -1px); +} + +.lookerPanelFlex { + display: flex; + max-height: 100%; + flex-direction: column; +} + +/** + * Copyright 2017-2022, Voxel51, Inc. + */ + +.lookerHelpPanelItems { + padding: 1rem 0; + margin: 0; + display: grid; + grid-template-columns: auto auto auto auto auto auto; + grid-row-gap: 2rem; + grid-column-gap: 2rem; + font-size: 1rem; + width: 100%; + scrollbar-width: none; + flex-grow: 1; + height: 100%; + overflow-y: auto; +} + +@media only screen and (max-width: 1000px) { + .lookerHelpPanelItems { + grid-template-columns: auto auto auto; + } +} + +.lookerHelpPanelItems::-webkit-scrollbar { + width: 0px; + background: transparent; + display: none; +} + +.lookerShortcutValue { + font-weight: normal; + color: rgb(225, 100, 40); +} + +.lookerShortcutTitle { + color: rgb(238, 238, 238); + font-weight: bold; +} + +.lookerShortcutDetail { + font-weight: normal; + color: hsl(220, 2%, 68%); +} diff --git a/app/packages/components/src/components/index.ts b/app/packages/components/src/components/index.ts index c9c8883936..e15b4ba73c 100644 --- a/app/packages/components/src/components/index.ts +++ b/app/packages/components/src/components/index.ts @@ -2,6 +2,7 @@ export { default as Bar } from "./Bar"; export { default as Button } from "./Button"; export { default as ErrorBoundary } from "./ErrorBoundary"; export { default as Header } from "./Header"; +export { default as HelpPanel } from "./HelpPanel"; export * from "./Icons"; export { default as JSONPanel } from "./JSONPanel"; export { default as Link } from "./Link"; diff --git a/app/packages/components/src/icons/help.svg b/app/packages/components/src/icons/help.svg new file mode 100644 index 0000000000..4c5de40837 --- /dev/null +++ b/app/packages/components/src/icons/help.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/packages/components/src/index.ts b/app/packages/components/src/index.ts index f6555c1ab4..b8219177a5 100644 --- a/app/packages/components/src/index.ts +++ b/app/packages/components/src/index.ts @@ -3,5 +3,6 @@ export * from "./components"; export { scrollable } from "./scrollable.module.css"; import jsonIcon from "./icons/json.svg"; +import helpIcon from "./icons/help.svg"; -export { jsonIcon }; +export { jsonIcon, helpIcon }; diff --git a/app/packages/looker-3d/src/Looker3d.tsx b/app/packages/looker-3d/src/Looker3d.tsx index 2a48267ecf..a6de551770 100644 --- a/app/packages/looker-3d/src/Looker3d.tsx +++ b/app/packages/looker-3d/src/Looker3d.tsx @@ -31,6 +31,7 @@ import { PopoutSectionTitle, TabOption, jsonIcon, + helpIcon, } from "@fiftyone/components"; import { colorMap } from "@fiftyone/state"; import { removeListener } from "process"; @@ -356,6 +357,7 @@ function Looker3dCore({ sampleOverride: sample }) { ); const jsonPanel = fos.useJSONPanel(); + const helpPanel = fos.useHelpPanel(); const pcRotationSetting = _.get(settings, "pointCloud.rotation", [0, 0, 0]); const pcRotation = toEulerFromDegreesArray(pcRotationSetting); @@ -479,6 +481,7 @@ function Looker3dCore({ sampleOverride: sample }) { hint="Ego View" /> + )} @@ -663,6 +666,47 @@ function ViewJSON({ sample, jsonPanel }) { ); } +const LOOKER3D_HELP_ITEMS = [ + { shortcut: "Wheel", title: "Zoom", detail: "Zoom in and out" }, + { shortcut: "Drag", title: "Rotate", detail: "Rotate the camera" }, + { + shortcut: "Shift + drag", + title: "Translate", + detail: "Translate the camera", + }, + { shortcut: "T", title: "Top-down", detail: "Reset camera to top-down view" }, + { shortcut: "E", title: "Ego-view", detail: "Reset the camera to ego view" }, + { shortcut: "C", title: "Controls", detail: "Toggle controls" }, + { shortcut: "?", title: "Display help", detail: "Display this help window" }, + { shortcut: "ESC", title: "Escape ", detail: "Escape the current context" }, +]; + +function ViewHelp({ helpPanel }) { + const [currentAction, setAction] = recoil.useRecoilState( + pcState.currentAction + ); + + return ( + + + { + const targetAction = "help"; + const nextAction = + currentAction === targetAction ? null : targetAction; + setAction(nextAction); + helpPanel.toggle(LOOKER3D_HELP_ITEMS); + e.stopPropagation(); + e.preventDefault(); + return false; + }} + /> + + + ); +} + function load3dOverlays(sample, selectedLabels, currentPath = []) { let overlays = []; const labels = Array.isArray(sample) ? sample : Object.values(sample); diff --git a/app/packages/state/src/hooks/index.ts b/app/packages/state/src/hooks/index.ts index 81a831d0b7..011859997c 100644 --- a/app/packages/state/src/hooks/index.ts +++ b/app/packages/state/src/hooks/index.ts @@ -1,6 +1,7 @@ export { default as useClearModal } from "./useClearModal"; export { default as useCreateLooker } from "./useCreateLooker"; export { default as useExpandSample } from "./useExpandSample"; +export { default as useHelpPanel } from "./useHelpPanel"; export { default as useJSONPanel } from "./useJSONPanel"; export { default as useLookerStore } from "./useLookerStore"; export * from "./useLookerStore"; diff --git a/app/packages/state/src/hooks/useHelpPanel.ts b/app/packages/state/src/hooks/useHelpPanel.ts new file mode 100644 index 0000000000..f4e49d94c4 --- /dev/null +++ b/app/packages/state/src/hooks/useHelpPanel.ts @@ -0,0 +1,51 @@ +import { useState, useMemo, useEffect } from "react"; +import { atom, useRecoilState } from "recoil"; + +type HelpItem = { + shortcut: string; + title: string; + detail: string; +}; +type HelpPanelState = { + isOpen: boolean; + items: Array; +}; +const helpPanelState = atom({ + key: "HelpPanelState", + default: { isOpen: false, items: [] }, +}); + +export default function useHelpPanel() { + const [{ isOpen, items }, setState] = useRecoilState(helpPanelState); + function close() { + setState((s) => ({ ...s, isOpen: false })); + } + + function handleEscape(e) { + if (e.key === "Escape") close(); + } + + useEffect(() => { + if (isOpen) { + window.addEventListener("keydown", handleEscape); + } + return () => window.removeEventListener("keydown", handleEscape); + }, [isOpen]); + + return { + open(items) { + setState((s) => ({ ...s, items, isOpen: true })); + }, + close, + toggle(items) { + setState((s) => { + if (s.isOpen) { + return { ...s, items: null, isOpen: false }; + } + return { ...s, items, isOpen: true }; + }); + }, + items, + isOpen, + }; +} From b70af82d5ef121358448fbb3d93919074a546584 Mon Sep 17 00:00:00 2001 From: Ritchie Martori Date: Mon, 29 Aug 2022 11:41:59 -0700 Subject: [PATCH 04/11] fix escape issues --- app/packages/app/src/components/Modal/Looker.tsx | 3 --- app/packages/looker-3d/src/Looker3d.tsx | 14 +++++++++++--- app/packages/state/src/hooks/useClearModal.ts | 7 ++++++- app/packages/state/src/hooks/useHelpPanel.ts | 1 + app/packages/state/src/hooks/useJSONPanel.ts | 12 +----------- 5 files changed, 19 insertions(+), 18 deletions(-) diff --git a/app/packages/app/src/components/Modal/Looker.tsx b/app/packages/app/src/components/Modal/Looker.tsx index 5d00ddd853..cbc8f91872 100644 --- a/app/packages/app/src/components/Modal/Looker.tsx +++ b/app/packages/app/src/components/Modal/Looker.tsx @@ -115,9 +115,6 @@ const Looker = ({ lookerRef, onClose, onNext, onPrevious }: LookerProps) => { if (showJSON === true) { jsonPanel.open(sample); } - if (showJSON === false) { - jsonPanel.close(); - } }); onClose && useEventHandler(looker, "close", onClose); diff --git a/app/packages/looker-3d/src/Looker3d.tsx b/app/packages/looker-3d/src/Looker3d.tsx index a6de551770..f5b24e8257 100644 --- a/app/packages/looker-3d/src/Looker3d.tsx +++ b/app/packages/looker-3d/src/Looker3d.tsx @@ -203,7 +203,7 @@ function Polyline({ selected, onClick, tooltip, - label + label, }) { if (filled) { // filled not yet supported @@ -395,6 +395,15 @@ function Looker3dCore({ sampleOverride: sample }) { useHotkey( "Escape", ({ get, set }) => { + if (get(jsonPanel.stateAtom).isOpen) { + set(jsonPanel.stateAtom, (s) => ({ ...s, isOpen: false })); + return false; + } + if (get(helpPanel.stateAtom).isOpen) { + set(helpPanel.stateAtom, (s) => ({ ...s, isOpen: false })); + return false; + } + const selectedLabels = get(fos.selectedLabels); if (selectedLabels && Object.keys(selectedLabels).length > 0) { set(fos.selectedLabels, {}); @@ -404,10 +413,9 @@ function Looker3dCore({ sampleOverride: sample }) { const changed = onChangeView("top"); if (changed) return; - if (jsonPanel.isOpen) return; set(fos.modal, null); }, - [jsonPanel.isOpen, selectedLabels, hovering] + [jsonPanel.isOpen, helpPanel.isOpen, selectedLabels, hovering] ); useEffect(() => { diff --git a/app/packages/state/src/hooks/useClearModal.ts b/app/packages/state/src/hooks/useClearModal.ts index fc6683c87d..0d1be62661 100644 --- a/app/packages/state/src/hooks/useClearModal.ts +++ b/app/packages/state/src/hooks/useClearModal.ts @@ -1,11 +1,16 @@ import { useRecoilTransaction_UNSTABLE } from "recoil"; import * as fos from "../recoil"; - +import useJSONPanel from "./useJSONPanel"; +import useHelpPanel from "./useHelpPanel"; export default () => { + const jsonPanel = useJSONPanel(); + const helpPanel = useHelpPanel(); return useRecoilTransaction_UNSTABLE( ({ set, get }) => () => { + if (get(jsonPanel.stateAtom).isOpen) return; + if (get(helpPanel.stateAtom).isOpen) return; const fullscreen = get(fos.fullscreen); if (fullscreen) { return; diff --git a/app/packages/state/src/hooks/useHelpPanel.ts b/app/packages/state/src/hooks/useHelpPanel.ts index f4e49d94c4..27991e3cb9 100644 --- a/app/packages/state/src/hooks/useHelpPanel.ts +++ b/app/packages/state/src/hooks/useHelpPanel.ts @@ -47,5 +47,6 @@ export default function useHelpPanel() { }, items, isOpen, + stateAtom: helpPanelState, }; } diff --git a/app/packages/state/src/hooks/useJSONPanel.ts b/app/packages/state/src/hooks/useJSONPanel.ts index d0bddc029a..b3471ba11e 100644 --- a/app/packages/state/src/hooks/useJSONPanel.ts +++ b/app/packages/state/src/hooks/useJSONPanel.ts @@ -36,17 +36,6 @@ export default function useJSONPanel() { setState((s) => ({ ...s, isOpen: false })); } - function handleEscape(e) { - if (e.key === "Escape") close(); - } - - useEffect(() => { - if (isOpen) { - window.addEventListener("keydown", handleEscape); - } - return () => window.removeEventListener("keydown", handleEscape); - }, [isOpen]); - return { open(sample) { setState((s) => ({ ...s, sample, isOpen: true })); @@ -67,5 +56,6 @@ export default function useJSONPanel() { sample, json, jsonHTML, + stateAtom: jsonPanelState, }; } From 594d6373ea5a377a9926280afb9c7cfe2dcbfa2c Mon Sep 17 00:00:00 2001 From: Ritchie Martori Date: Tue, 30 Aug 2022 10:59:03 -0700 Subject: [PATCH 05/11] fix various looker hotkey issues --- .../app/src/components/Modal/Group.tsx | 26 ++++++++++++++++--- .../app/src/components/Modal/Looker.tsx | 14 +++++++++- .../app/src/components/Modal/Sample.tsx | 5 ++-- 3 files changed, 38 insertions(+), 7 deletions(-) diff --git a/app/packages/app/src/components/Modal/Group.tsx b/app/packages/app/src/components/Modal/Group.tsx index fdbf1bf130..ffb3f64564 100644 --- a/app/packages/app/src/components/Modal/Group.tsx +++ b/app/packages/app/src/components/Modal/Group.tsx @@ -48,8 +48,18 @@ const GroupSample: React.FC< slice: string; pinned: boolean; onClick: MouseEventHandler; + onMouseEnter: MouseEventHandler; + onMouseLeave: MouseEventHandler; }> -> = ({ children, onClick, pinned, sampleId, slice }) => { +> = ({ + children, + onClick, + pinned, + sampleId, + slice, + onMouseEnter, + onMouseLeave, +}) => { const [hovering, setHovering] = useState(false); const timeout: MutableRefObject = useRef(null); @@ -73,9 +83,15 @@ const GroupSample: React.FC< className={ pinned ? classNames(groupSample, groupSampleActive) : groupSample } - onMouseEnter={update} + onMouseEnter={(e) => { + update(); + onMouseEnter(e); + }} onMouseMove={update} - onMouseLeave={clear} + onMouseLeave={(e) => { + clear(); + onMouseLeave(e); + }} onClickCapture={onClick} > {children} @@ -110,6 +126,7 @@ const MainSample: React.FC<{ const pinned = !useRecoilValue(sidebarOverride); const reset = useResetRecoilState(sidebarOverride); const slice = useSlice(sample); + const hover = fos.useHoveredSample(sample); return ( { const [pinned, setPinned] = useRecoilState(sidebarOverride); const slice = useRecoilValue(pinnedSlice) as string; + const hover = fos.useHoveredSample(sample.sample); if (sample.__typename === "%other") { throw new Error("bad sample"); @@ -199,6 +218,7 @@ const PinnedSample: React.FC = () => { pinned={Boolean(pinned)} onClick={() => setPinned(sample.sample._id)} slice={slice} + {...hover.handlers} > diff --git a/app/packages/app/src/components/Modal/Looker.tsx b/app/packages/app/src/components/Modal/Looker.tsx index cbc8f91872..9efab8e2b1 100644 --- a/app/packages/app/src/components/Modal/Looker.tsx +++ b/app/packages/app/src/components/Modal/Looker.tsx @@ -17,9 +17,10 @@ import { useEventHandler } from "../../utils/hooks"; import { useErrorHandler } from "react-error-boundary"; import { useTheme } from "@fiftyone/components"; import * as fos from "@fiftyone/state"; -import { useOnSelectLabel } from "@fiftyone/state"; +import { lookerOptions, useOnSelectLabel } from "@fiftyone/state"; import { TooltipInfo } from "./TooltipInfo"; import { Tooltip } from "@material-ui/core"; +import { sample } from "lodash"; type EventCallback = (event: CustomEvent) => void; @@ -115,6 +116,9 @@ const Looker = ({ lookerRef, onClose, onNext, onPrevious }: LookerProps) => { if (showJSON === true) { jsonPanel.open(sample); } + if (showJSON === false) { + jsonPanel.close(); + } }); onClose && useEventHandler(looker, "close", onClose); @@ -135,6 +139,14 @@ const Looker = ({ lookerRef, onClose, onNext, onPrevious }: LookerProps) => { e.detail && tooltip.setCoords(e.detail.coordinates); }); + const hoveredSample = useRecoilValue(fos.hoveredSample); + useEffect(() => { + looker.updater((state) => ({ + ...state, + shouldHandleKeyEvents: hoveredSample._id === sample._id, + })); + }, [hoveredSample, sample, looker]); + return (
{ }; }, [clear, hovering]); const hoveringRef = useRef(false); + const hover = fos.useHoveredSample(data.sample, { update, clear }); return (
Date: Tue, 30 Aug 2022 11:00:29 -0700 Subject: [PATCH 06/11] more hotkey fixes --- .../src/components/HelpPanel/HelpPanel.tsx | 4 ++-- app/packages/looker-3d/src/Looker3d.tsx | 9 +++++++- .../looker/src/elements/common/looker.ts | 9 ++++---- app/packages/state/src/hooks/index.ts | 1 + .../state/src/hooks/useHoveredSample.ts | 22 +++++++++++++++++++ app/packages/state/src/recoil/atoms.ts | 5 +++++ 6 files changed, 43 insertions(+), 7 deletions(-) create mode 100644 app/packages/state/src/hooks/useHoveredSample.ts diff --git a/app/packages/components/src/components/HelpPanel/HelpPanel.tsx b/app/packages/components/src/components/HelpPanel/HelpPanel.tsx index 1d422025df..fde41eac1f 100644 --- a/app/packages/components/src/components/HelpPanel/HelpPanel.tsx +++ b/app/packages/components/src/components/HelpPanel/HelpPanel.tsx @@ -35,8 +35,8 @@ export default function HelpPanel({ onClose, items }) {
Help
- {items.map((item) => ( - + {items.map((item, idx) => ( + ))}
diff --git a/app/packages/looker-3d/src/Looker3d.tsx b/app/packages/looker-3d/src/Looker3d.tsx index f5b24e8257..d51b0c360e 100644 --- a/app/packages/looker-3d/src/Looker3d.tsx +++ b/app/packages/looker-3d/src/Looker3d.tsx @@ -33,12 +33,16 @@ import { jsonIcon, helpIcon, } from "@fiftyone/components"; -import { colorMap } from "@fiftyone/state"; +import { colorMap, dataset } from "@fiftyone/state"; import { removeListener } from "process"; THREE.Object3D.DefaultUp = new THREE.Vector3(0, 0, 1); const deg2rad = (degrees) => degrees * (Math.PI / 180); +const hasFocusAtom = recoil.atom({ + key: "looker3dHasFocus", + default: false, +}); function PointCloudMesh({ minZ, colorBy, points, rotation, onLoad }) { const colorMinMaxRef = React.useRef(); @@ -395,6 +399,8 @@ function Looker3dCore({ sampleOverride: sample }) { useHotkey( "Escape", ({ get, set }) => { + console.log("get(fos.hoveredSample)", get(fos.hoveredSample)); + if (get(fos.hoveredSample)?._id !== sample._id) return; if (get(jsonPanel.stateAtom).isOpen) { set(jsonPanel.stateAtom, (s) => ({ ...s, isOpen: false })); return false; @@ -413,6 +419,7 @@ function Looker3dCore({ sampleOverride: sample }) { const changed = onChangeView("top"); if (changed) return; + console.log("Looker3D Closing Modal"); set(fos.modal, null); }, [jsonPanel.isOpen, helpPanel.isOpen, selectedLabels, hovering] diff --git a/app/packages/looker/src/elements/common/looker.ts b/app/packages/looker/src/elements/common/looker.ts index 20d000db88..00fb9e2568 100644 --- a/app/packages/looker/src/elements/common/looker.ts +++ b/app/packages/looker/src/elements/common/looker.ts @@ -23,8 +23,9 @@ export class LookerElement extends BaseElement< } const e = event as KeyboardEvent; - update(({ SHORTCUTS, error }) => { - if (!error && e.key in SHORTCUTS) { + update((state) => { + const { SHORTCUTS, error, shouldHandleKeyEvents } = state; + if (shouldHandleKeyEvents && !error && e.key in SHORTCUTS) { const matchedControl = SHORTCUTS[e.key] as Control; matchedControl.action(update, dispatchEvent, e.key, e.shiftKey); } @@ -38,8 +39,8 @@ export class LookerElement extends BaseElement< } const e = event as KeyboardEvent; - update(({ SHORTCUTS, error }) => { - if (!error && e.key in SHORTCUTS) { + update(({ SHORTCUTS, error, shouldHandleKeyEvents }) => { + if (shouldHandleKeyEvents && !error && e.key in SHORTCUTS) { const matchedControl = SHORTCUTS[e.key] as Control; if (matchedControl.eventKeyType === ControlEventKeyType.HOLD) { matchedControl.afterAction( diff --git a/app/packages/state/src/hooks/index.ts b/app/packages/state/src/hooks/index.ts index 011859997c..e8b2597cc9 100644 --- a/app/packages/state/src/hooks/index.ts +++ b/app/packages/state/src/hooks/index.ts @@ -2,6 +2,7 @@ export { default as useClearModal } from "./useClearModal"; export { default as useCreateLooker } from "./useCreateLooker"; export { default as useExpandSample } from "./useExpandSample"; export { default as useHelpPanel } from "./useHelpPanel"; +export { default as useHoveredSample } from "./useHoveredSample"; export { default as useJSONPanel } from "./useJSONPanel"; export { default as useLookerStore } from "./useLookerStore"; export * from "./useLookerStore"; diff --git a/app/packages/state/src/hooks/useHoveredSample.ts b/app/packages/state/src/hooks/useHoveredSample.ts new file mode 100644 index 0000000000..592aef5b94 --- /dev/null +++ b/app/packages/state/src/hooks/useHoveredSample.ts @@ -0,0 +1,22 @@ +import { AppSample } from "../recoil"; +import * as fos from "../.."; +import { useRecoilState } from "recoil"; + +export default function useHoveredSample(sample: AppSample, auxHandlers: any) { + const [hoveredSample, setSample] = useRecoilState(fos.hoveredSample); + const { update, clear } = auxHandlers; + function onMouseEnter() { + setSample(sample); + update && update(); + } + function onMouseLeave() { + setSample(null); + clear && clear(); + } + function onMouseMove() { + setSample(sample); + update && update(); + } + + return { handlers: { onMouseEnter, onMouseLeave } }; +} diff --git a/app/packages/state/src/recoil/atoms.ts b/app/packages/state/src/recoil/atoms.ts index b5f3819806..542988e2b8 100644 --- a/app/packages/state/src/recoil/atoms.ts +++ b/app/packages/state/src/recoil/atoms.ts @@ -218,3 +218,8 @@ export const modalTopBarVisible = atom({ key: "modalTopBarVisible", default: true, }); + +export const hoveredSample = atom({ + key: "hoveredSample", + default: null, +}); From ecfc9ab6abf69624d81266676f46a2a286cac3c1 Mon Sep 17 00:00:00 2001 From: Ritchie Martori Date: Tue, 30 Aug 2022 11:11:48 -0700 Subject: [PATCH 07/11] fix missing default --- app/packages/state/src/hooks/useHelpPanel.ts | 9 ++++++++- app/packages/state/src/hooks/useHoveredSample.ts | 5 ++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/app/packages/state/src/hooks/useHelpPanel.ts b/app/packages/state/src/hooks/useHelpPanel.ts index 27991e3cb9..8123597329 100644 --- a/app/packages/state/src/hooks/useHelpPanel.ts +++ b/app/packages/state/src/hooks/useHelpPanel.ts @@ -24,12 +24,19 @@ export default function useHelpPanel() { function handleEscape(e) { if (e.key === "Escape") close(); } + function handleClick() { + close(); + } useEffect(() => { if (isOpen) { window.addEventListener("keydown", handleEscape); + window.addEventListener("mousedown", handleClick); } - return () => window.removeEventListener("keydown", handleEscape); + return () => { + window.removeEventListener("keydown", handleEscape); + window.removeEventListener("mousedown", handleClick); + }; }, [isOpen]); return { diff --git a/app/packages/state/src/hooks/useHoveredSample.ts b/app/packages/state/src/hooks/useHoveredSample.ts index 592aef5b94..93a649ee99 100644 --- a/app/packages/state/src/hooks/useHoveredSample.ts +++ b/app/packages/state/src/hooks/useHoveredSample.ts @@ -2,7 +2,10 @@ import { AppSample } from "../recoil"; import * as fos from "../.."; import { useRecoilState } from "recoil"; -export default function useHoveredSample(sample: AppSample, auxHandlers: any) { +export default function useHoveredSample( + sample: AppSample, + auxHandlers: any = {} +) { const [hoveredSample, setSample] = useRecoilState(fos.hoveredSample); const { update, clear } = auxHandlers; function onMouseEnter() { From 89ab7de72c7e0c1ce9c8900fffacd6ba2f2d7f4e Mon Sep 17 00:00:00 2001 From: Ritchie Martori Date: Tue, 30 Aug 2022 12:54:42 -0700 Subject: [PATCH 08/11] fix consistency with all looker panels --- .../app/src/components/Modal/Looker.tsx | 32 ++++- .../src/components/HelpPanel/HelpPanel.tsx | 7 +- .../src/components/JSONPanel/JSONPanel.tsx | 7 +- app/packages/looker-3d/src/Looker3d.tsx | 21 +-- .../looker/src/elements/common/actions.ts | 130 ++---------------- .../looker/src/elements/common/index.ts | 7 +- .../looker/src/elements/common/looker.ts | 17 ++- app/packages/looker/src/elements/index.ts | 9 -- app/packages/looker/src/state.ts | 1 + app/packages/state/src/hooks/useClearModal.ts | 4 +- app/packages/state/src/hooks/useHelpPanel.ts | 50 +++++-- app/packages/state/src/hooks/useJSONPanel.ts | 56 ++++++-- app/packages/state/src/recoil/atoms.ts | 8 ++ 13 files changed, 162 insertions(+), 187 deletions(-) diff --git a/app/packages/app/src/components/Modal/Looker.tsx b/app/packages/app/src/components/Modal/Looker.tsx index 9efab8e2b1..1b7108511e 100644 --- a/app/packages/app/src/components/Modal/Looker.tsx +++ b/app/packages/app/src/components/Modal/Looker.tsx @@ -15,7 +15,7 @@ import { ContentDiv, ContentHeader } from "../utils"; import { useEventHandler } from "../../utils/hooks"; import { useErrorHandler } from "react-error-boundary"; -import { useTheme } from "@fiftyone/components"; +import { HelpPanel, useTheme } from "@fiftyone/components"; import * as fos from "@fiftyone/state"; import { lookerOptions, useOnSelectLabel } from "@fiftyone/state"; import { TooltipInfo } from "./TooltipInfo"; @@ -106,19 +106,37 @@ const Looker = ({ lookerRef, onClose, onNext, onPrevious }: LookerProps) => { useEventHandler(looker, "fullscreen", useFullscreen()); useEventHandler(looker, "showOverlays", useShowOverlays()); - onNext && useEventHandler(looker, "next", onNext); - onPrevious && useEventHandler(looker, "previous", onPrevious); + onNext && + useEventHandler(looker, "next", (e) => { + jsonPanel.close(); + helpPanel.close(); + return onNext(e); + }); + onPrevious && + useEventHandler(looker, "previous", (e) => { + jsonPanel.close(); + helpPanel.close(); + return onPrevious(e); + }); useEventHandler(looker, "select", useOnSelectLabel()); useEventHandler(looker, "error", (event) => handleError(event.detail)); const jsonPanel = fos.useJSONPanel(); - useEventHandler(looker, "options", ({ detail }) => { - const { showJSON } = detail || {}; + const helpPanel = fos.useHelpPanel(); + useEventHandler(looker, "options", (e) => { + const { detail } = e; + const { showJSON, showHelp, SHORTCUTS } = detail || {}; if (showJSON === true) { jsonPanel.open(sample); } if (showJSON === false) { jsonPanel.close(); } + if (showHelp === true) { + helpPanel.open(shortcutToHelpItems(SHORTCUTS)); + } + if (showHelp === false) { + helpPanel.close(); + } }); onClose && useEventHandler(looker, "close", onClose); @@ -163,3 +181,7 @@ const Looker = ({ lookerRef, onClose, onNext, onPrevious }: LookerProps) => { }; export default React.memo(Looker); + +function shortcutToHelpItems(SHORTCUTS) { + return Object.values(SHORTCUTS); +} diff --git a/app/packages/components/src/components/HelpPanel/HelpPanel.tsx b/app/packages/components/src/components/HelpPanel/HelpPanel.tsx index fde41eac1f..2a5b3761de 100644 --- a/app/packages/components/src/components/HelpPanel/HelpPanel.tsx +++ b/app/packages/components/src/components/HelpPanel/HelpPanel.tsx @@ -29,9 +29,12 @@ function Close({ onClick }) { export default function HelpPanel({ onClose, items }) { return ( -
+
e.stopPropagation()} + >
-
e.stopPropagation()}> +
Help
diff --git a/app/packages/components/src/components/JSONPanel/JSONPanel.tsx b/app/packages/components/src/components/JSONPanel/JSONPanel.tsx index 3b324753db..3f54345842 100644 --- a/app/packages/components/src/components/JSONPanel/JSONPanel.tsx +++ b/app/packages/components/src/components/JSONPanel/JSONPanel.tsx @@ -39,9 +39,12 @@ function Copy({ onClick }) { export default function JSONPanel({ jsonHTML, onClose, onCopy }) { return ( -
+
e.stopPropagation()} + >
-
e.stopPropagation()}> +
{jsonHTML &&
}
         
diff --git a/app/packages/looker-3d/src/Looker3d.tsx b/app/packages/looker-3d/src/Looker3d.tsx index d51b0c360e..f2b33603aa 100644 --- a/app/packages/looker-3d/src/Looker3d.tsx +++ b/app/packages/looker-3d/src/Looker3d.tsx @@ -399,15 +399,17 @@ function Looker3dCore({ sampleOverride: sample }) { useHotkey( "Escape", ({ get, set }) => { - console.log("get(fos.hoveredSample)", get(fos.hoveredSample)); if (get(fos.hoveredSample)?._id !== sample._id) return; - if (get(jsonPanel.stateAtom).isOpen) { - set(jsonPanel.stateAtom, (s) => ({ ...s, isOpen: false })); - return false; - } - if (get(helpPanel.stateAtom).isOpen) { - set(helpPanel.stateAtom, (s) => ({ ...s, isOpen: false })); - return false; + const panels = get(fos.lookerPanels); + let lookerPanelUpdate; + for (let panel of ["help", "json"]) { + if (panels[panel].isOpen) { + set(fos.lookerPanels, { + ...panels, + [panel]: { ...panels[panel], isOpen: false }, + }); + return; + } } const selectedLabels = get(fos.selectedLabels); @@ -419,10 +421,9 @@ function Looker3dCore({ sampleOverride: sample }) { const changed = onChangeView("top"); if (changed) return; - console.log("Looker3D Closing Modal"); set(fos.modal, null); }, - [jsonPanel.isOpen, helpPanel.isOpen, selectedLabels, hovering] + [jsonPanel, helpPanel, selectedLabels, hovering] ); useEffect(() => { diff --git a/app/packages/looker/src/elements/common/actions.ts b/app/packages/looker/src/elements/common/actions.ts index 06c9d19333..e2a4b3cfbf 100644 --- a/app/packages/looker/src/elements/common/actions.ts +++ b/app/packages/looker/src/elements/common/actions.ts @@ -128,6 +128,7 @@ export const next: Control = { shortcut: "→", eventKeys: "ArrowRight", detail: "Go to the next sample", + alwaysHandle: true, action: (_, dispatchEvent) => { dispatchEvent("next"); }, @@ -138,6 +139,7 @@ export const previous: Control = { shortcut: "←", eventKeys: "ArrowLeft", detail: "Go to the previous sample", + alwaysHandle: true, action: (_, dispatchEvent) => { dispatchEvent("previous"); }, @@ -220,14 +222,18 @@ export const help: Control = { shortcut: "?", detail: "Display this help window", action: (update, dispatchEvent) => { - update(({ showHelp, config: { thumbnail } }) => { + update(({ showHelp, SHORTCUTS, config: { thumbnail } }) => { if (thumbnail) { return {}; } if (!showHelp) { - dispatchEvent("options", { showJSON: false }); - return { showHelp: true, options: { showJSON: false } }; + dispatchEvent("options", { + showJSON: false, + showHelp: true, + SHORTCUTS, + }); + return { showHelp: true, options: { showJSON: false, showHelp: true } }; } return { showHelp: false }; @@ -440,6 +446,7 @@ export const nextFrame: Control = { eventKeys: [".", ">"], shortcut: ">", detail: "Seek to the next frame", + alwaysHandle: true, action: (update, dispatchEvent) => { update( ({ @@ -470,6 +477,7 @@ export const previousFrame: Control = { eventKeys: [",", "<"], shortcut: "<", detail: "Seek to the previous frame", + alwaysHandle: true, action: (update, dispatchEvent) => { update( ({ @@ -691,122 +699,6 @@ export const VIDEO = { export const VIDEO_SHORTCUTS = readActions(VIDEO); -export class HelpPanelElement< - State extends BaseState -> extends BaseElement { - private showHelp?: boolean; - protected items?: HTMLDivElement; - - getEvents(): Events { - return { - click: ({ event, update }) => { - event.stopPropagation(); - event.preventDefault(); - update({ showHelp: false }); - }, - dblclick: ({ event }) => { - event.stopPropagation(); - event.preventDefault(); - }, - }; - } - - createHTMLElement(update) { - return this.createHelpPanel(update, COMMON); - } - - isShown({ thumbnail }: Readonly) { - return !thumbnail; - } - - renderSelf({ showHelp }: Readonly) { - if (this.showHelp === showHelp) { - return this.element; - } - if (showHelp) { - this.element.style.opacity = "0.9"; - this.element.style.display = "flex"; - } else { - this.element.style.opacity = "0.0"; - this.element.style.display = "none"; - } - this.showHelp = showHelp; - return this.element; - } - - protected createHelpPanel( - update: StateUpdate, - controls: ControlMap - ): HTMLElement { - const element = document.createElement("div"); - const header = document.createElement("div"); - header.innerText = "Help"; - header.classList.add(lookerPanelHeader); - element.classList.add(lookerPanel); - - const container = document.createElement("div"); - container.classList.add(lookerPanelContainer); - - const vContainer = document.createElement("div"); - vContainer.classList.add(lookerPanelVerticalContainer); - - vContainer.appendChild(element); - - container.appendChild(vContainer); - const scroll = document.createElement("div"); - scroll.classList.add(lookerPanelFlex); - - const items = document.createElement("div"); - items.classList.add(lookerHelpPanelItems); - this.items = items; - - const close = document.createElement("img"); - close.src = closeIcon; - close.classList.add(lookerPanelClose); - close.onclick = () => update({ showHelp: false }); - element.appendChild(close); - - const first = ["Wheel", "Esc"]; - - Object.values(controls) - .sort((a, b) => - a.shortcut.length !== b.shortcut.length - ? first.includes(b.shortcut) - ? 1 - : first.includes(a.shortcut) - ? -1 - : b.shortcut.length - a.shortcut.length - : a.shortcut > b.shortcut - ? 1 - : -1 - ) - .forEach(addItem(items)); - - scroll.appendChild(header); - scroll.appendChild(items); - element.append(scroll); - - return container; - } -} - -export class VideoHelpPanelElement extends HelpPanelElement { - createHTMLElement(update) { - let config = null; - update(({ config: _config }) => { - config = _config; - return {}; - }); - - return this.createHelpPanel( - update, - Object.fromEntries( - Object.entries(VIDEO).filter(([k, v]) => !v.filter || v.filter(config)) - ) - ); - } -} - const addItem = (items: HTMLDivElement) => (value: Control) => { diff --git a/app/packages/looker/src/elements/common/index.ts b/app/packages/looker/src/elements/common/index.ts index 3d5e9d45fc..0dcd2f25b2 100644 --- a/app/packages/looker/src/elements/common/index.ts +++ b/app/packages/looker/src/elements/common/index.ts @@ -5,12 +5,7 @@ export { CanvasElement } from "./canvas"; export * from "./controls"; export { ErrorElement } from "./error"; -export { - HelpPanelElement, - VideoHelpPanelElement, - COMMON_SHORTCUTS, - VIDEO_SHORTCUTS, -} from "./actions"; +export { COMMON_SHORTCUTS, VIDEO_SHORTCUTS } from "./actions"; export { LookerElement } from "./looker"; export * from "./options"; export { TagsElement } from "./tags"; diff --git a/app/packages/looker/src/elements/common/looker.ts b/app/packages/looker/src/elements/common/looker.ts index 00fb9e2568..e1fa3a5a67 100644 --- a/app/packages/looker/src/elements/common/looker.ts +++ b/app/packages/looker/src/elements/common/looker.ts @@ -25,9 +25,13 @@ export class LookerElement extends BaseElement< const e = event as KeyboardEvent; update((state) => { const { SHORTCUTS, error, shouldHandleKeyEvents } = state; - if (shouldHandleKeyEvents && !error && e.key in SHORTCUTS) { + if (!error && e.key in SHORTCUTS) { const matchedControl = SHORTCUTS[e.key] as Control; - matchedControl.action(update, dispatchEvent, e.key, e.shiftKey); + const enabled = + shouldHandleKeyEvents || matchedControl.alwaysHandle; + if (enabled) { + matchedControl.action(update, dispatchEvent, e.key, e.shiftKey); + } } return {}; @@ -40,9 +44,14 @@ export class LookerElement extends BaseElement< const e = event as KeyboardEvent; update(({ SHORTCUTS, error, shouldHandleKeyEvents }) => { - if (shouldHandleKeyEvents && !error && e.key in SHORTCUTS) { + if (!error && e.key in SHORTCUTS) { const matchedControl = SHORTCUTS[e.key] as Control; - if (matchedControl.eventKeyType === ControlEventKeyType.HOLD) { + const enabled = + shouldHandleKeyEvents || matchedControl.alwaysHandle; + if ( + enabled && + matchedControl.eventKeyType === ControlEventKeyType.HOLD + ) { matchedControl.afterAction( update, dispatchEvent, diff --git a/app/packages/looker/src/elements/index.ts b/app/packages/looker/src/elements/index.ts index 172800fb1f..214353f57d 100644 --- a/app/packages/looker/src/elements/index.ts +++ b/app/packages/looker/src/elements/index.ts @@ -42,9 +42,6 @@ export const getFrameElements: GetElements = ( { node: common.ThumbnailSelectorElement, }, - { - node: common.HelpPanelElement, - }, { node: common.ControlsElement, children: [ @@ -107,9 +104,6 @@ export const getImageElements: GetElements = ( { node: common.ThumbnailSelectorElement, }, - { - node: common.HelpPanelElement, - }, { node: common.ControlsElement, children: [ @@ -175,9 +169,6 @@ export const getVideoElements: GetElements = ( { node: video.LoaderBar, }, - { - node: common.VideoHelpPanelElement, - }, { node: common.ControlsElement, children: [ diff --git a/app/packages/looker/src/state.ts b/app/packages/looker/src/state.ts index 51ea567ba7..7c3cfab632 100644 --- a/app/packages/looker/src/state.ts +++ b/app/packages/looker/src/state.ts @@ -71,6 +71,7 @@ export interface Control { detail: string; action: Action; afterAction?: Action; + alwaysHandle?: boolean; } export interface ControlMap { diff --git a/app/packages/state/src/hooks/useClearModal.ts b/app/packages/state/src/hooks/useClearModal.ts index 0d1be62661..fc13da4f0e 100644 --- a/app/packages/state/src/hooks/useClearModal.ts +++ b/app/packages/state/src/hooks/useClearModal.ts @@ -9,8 +9,8 @@ export default () => { return useRecoilTransaction_UNSTABLE( ({ set, get }) => () => { - if (get(jsonPanel.stateAtom).isOpen) return; - if (get(helpPanel.stateAtom).isOpen) return; + if (get(jsonPanel.stateAtom).json.isOpen) return; + if (get(helpPanel.stateAtom).help.isOpen) return; const fullscreen = get(fos.fullscreen); if (fullscreen) { return; diff --git a/app/packages/state/src/hooks/useHelpPanel.ts b/app/packages/state/src/hooks/useHelpPanel.ts index 8123597329..aec63ab8c5 100644 --- a/app/packages/state/src/hooks/useHelpPanel.ts +++ b/app/packages/state/src/hooks/useHelpPanel.ts @@ -1,5 +1,6 @@ import { useState, useMemo, useEffect } from "react"; import { atom, useRecoilState } from "recoil"; +import * as fos from "../../"; type HelpItem = { shortcut: string; @@ -10,13 +11,15 @@ type HelpPanelState = { isOpen: boolean; items: Array; }; -const helpPanelState = atom({ - key: "HelpPanelState", - default: { isOpen: false, items: [] }, -}); export default function useHelpPanel() { - const [{ isOpen, items }, setState] = useRecoilState(helpPanelState); + const [state, setFullState] = useRecoilState(fos.lookerPanels); + const setState = (update) => + setFullState((fullState) => ({ + ...fullState, + help: update(fullState.help), + })); + const { isOpen, items } = state.help || {}; function close() { setState((s) => ({ ...s, isOpen: false })); } @@ -31,29 +34,46 @@ export default function useHelpPanel() { useEffect(() => { if (isOpen) { window.addEventListener("keydown", handleEscape); - window.addEventListener("mousedown", handleClick); + window.addEventListener("click", handleClick); } return () => { window.removeEventListener("keydown", handleEscape); - window.removeEventListener("mousedown", handleClick); + window.removeEventListener("click", handleClick); }; }, [isOpen]); return { open(items) { - setState((s) => ({ ...s, items, isOpen: true })); + setFullState((s) => ({ + ...s, + json: { + ...s.json, + isOpen: false, + }, + help: { + ...s.help, + items, + isOpen: true, + }, + })); }, close, toggle(items) { - setState((s) => { - if (s.isOpen) { - return { ...s, items: null, isOpen: false }; - } - return { ...s, items, isOpen: true }; - }); + setFullState((s) => ({ + ...s, + json: { + ...s.json, + isOpen: false, + }, + help: { + ...s.help, + items, + isOpen: !s.help.isOpen, + }, + })); }, items, isOpen, - stateAtom: helpPanelState, + stateAtom: fos.lookerPanels, }; } diff --git a/app/packages/state/src/hooks/useJSONPanel.ts b/app/packages/state/src/hooks/useJSONPanel.ts index b3471ba11e..6e6144c12c 100644 --- a/app/packages/state/src/hooks/useJSONPanel.ts +++ b/app/packages/state/src/hooks/useJSONPanel.ts @@ -3,15 +3,12 @@ import { useState, useMemo, useEffect } from "react"; import { atom, useRecoilState } from "recoil"; import copyToClipboard from "copy-to-clipboard"; import highlightJSON from "json-format-highlight"; +import * as fos from "../../"; type JSONPanelState = { sample?: Sample; isOpen: boolean; }; -const jsonPanelState = atom({ - key: "jsonPanelState", - default: { isOpen: false }, -}); export const JSON_COLORS = { keyColor: "rgb(138, 138, 138)", @@ -23,7 +20,13 @@ export const JSON_COLORS = { }; export default function useJSONPanel() { - const [{ isOpen, sample }, setState] = useRecoilState(jsonPanelState); + const [state, setFullState] = useRecoilState(fos.lookerPanels); + const { sample, isOpen } = state.json || {}; + const setState = (update) => + setFullState((fullState) => ({ + ...fullState, + json: update(fullState.json), + })); const json = useMemo( () => (sample ? JSON.stringify(sample, null, 2) : null), [sample] @@ -36,18 +39,45 @@ export default function useJSONPanel() { setState((s) => ({ ...s, isOpen: false })); } + function handleClick() { + close(); + } + + useEffect(() => { + window.addEventListener("click", handleClick); + return () => { + window.removeEventListener("click", handleClick); + }; + }, []); + return { open(sample) { - setState((s) => ({ ...s, sample, isOpen: true })); + setFullState((s) => ({ + ...s, + json: { + sample, + isOpen: true, + }, + help: { + ...s.help, + isOpen: false, + }, + })); }, close, toggle(sample) { - setState((s) => { - if (s.isOpen) { - return { ...s, sample: null, isOpen: false }; - } - return { ...s, sample, isOpen: true }; - }); + setFullState((s) => ({ + ...s, + help: { + ...s.help, + isOpen: false, + }, + json: { + ...s.json, + sample, + isOpen: !s.json.isOpen, + }, + })); }, copy() { copyToClipboard(json); @@ -56,6 +86,6 @@ export default function useJSONPanel() { sample, json, jsonHTML, - stateAtom: jsonPanelState, + stateAtom: fos.lookerPanels, }; } diff --git a/app/packages/state/src/recoil/atoms.ts b/app/packages/state/src/recoil/atoms.ts index 542988e2b8..ca3ae7f6eb 100644 --- a/app/packages/state/src/recoil/atoms.ts +++ b/app/packages/state/src/recoil/atoms.ts @@ -223,3 +223,11 @@ export const hoveredSample = atom({ key: "hoveredSample", default: null, }); + +export const lookerPanels = atom({ + key: "lookerPanels", + default: { + json: { isOpen: false }, + help: { isOpen: false }, + }, +}); From 9f4f14dce71cfbe401a8c033961cec7fa2cc146c Mon Sep 17 00:00:00 2001 From: Ritchie Martori Date: Tue, 30 Aug 2022 12:56:14 -0700 Subject: [PATCH 09/11] fix help panel opacity --- .../components/src/components/HelpPanel/panel.module.css | 1 + 1 file changed, 1 insertion(+) diff --git a/app/packages/components/src/components/HelpPanel/panel.module.css b/app/packages/components/src/components/HelpPanel/panel.module.css index 38678a0d09..711cd45f5f 100644 --- a/app/packages/components/src/components/HelpPanel/panel.module.css +++ b/app/packages/components/src/components/HelpPanel/panel.module.css @@ -15,6 +15,7 @@ scrollbar-width: none; position: relative; pointer-events: all; + opacity: 0.9; } .lookerPanel::-webkit-scrollbar { From 18661da5665d5ab71295836ea33438deec85b0ee Mon Sep 17 00:00:00 2001 From: Ritchie Martori Date: Tue, 30 Aug 2022 14:18:47 -0700 Subject: [PATCH 10/11] fix json panel not closing --- app/packages/looker-3d/src/Looker3d.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/packages/looker-3d/src/Looker3d.tsx b/app/packages/looker-3d/src/Looker3d.tsx index f2b33603aa..7fc813e145 100644 --- a/app/packages/looker-3d/src/Looker3d.tsx +++ b/app/packages/looker-3d/src/Looker3d.tsx @@ -399,7 +399,6 @@ function Looker3dCore({ sampleOverride: sample }) { useHotkey( "Escape", ({ get, set }) => { - if (get(fos.hoveredSample)?._id !== sample._id) return; const panels = get(fos.lookerPanels); let lookerPanelUpdate; for (let panel of ["help", "json"]) { @@ -411,6 +410,7 @@ function Looker3dCore({ sampleOverride: sample }) { return; } } + if (get(fos.hoveredSample)?._id !== sample._id) return; const selectedLabels = get(fos.selectedLabels); if (selectedLabels && Object.keys(selectedLabels).length > 0) { From 57313cdaa7483e42bf1d3b3b754742a8f61fc0fc Mon Sep 17 00:00:00 2001 From: Ritchie Martori Date: Tue, 30 Aug 2022 14:27:41 -0700 Subject: [PATCH 11/11] fix first click panel issue --- app/packages/app/src/components/Modal/Looker.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/packages/app/src/components/Modal/Looker.tsx b/app/packages/app/src/components/Modal/Looker.tsx index 1b7108511e..75309cc28e 100644 --- a/app/packages/app/src/components/Modal/Looker.tsx +++ b/app/packages/app/src/components/Modal/Looker.tsx @@ -162,8 +162,13 @@ const Looker = ({ lookerRef, onClose, onNext, onPrevious }: LookerProps) => { looker.updater((state) => ({ ...state, shouldHandleKeyEvents: hoveredSample._id === sample._id, + options: { + ...state.options, + showJSON: jsonPanel.isOpen, + showHelp: helpPanel.isOpen, + }, })); - }, [hoveredSample, sample, looker]); + }, [hoveredSample, sample, looker, jsonPanel, helpPanel]); return (