diff --git a/src/atoms/gridInteractionStateAtom.ts b/src/atoms/gridInteractionStateAtom.ts index 1c73ec366a..a41e378638 100644 --- a/src/atoms/gridInteractionStateAtom.ts +++ b/src/atoms/gridInteractionStateAtom.ts @@ -1,6 +1,14 @@ import { atom } from 'recoil'; import { Coordinate } from '../gridGL/types/size'; + +export enum PanMode { + Disabled = 'DISABLED', + Enabled = 'ENABLED', + Dragging = 'DRAGGING', +} + export interface GridInteractionState { + panMode: PanMode; keyboardMovePosition: Coordinate; cursorPosition: Coordinate; showMultiCursor: boolean; @@ -13,6 +21,7 @@ export interface GridInteractionState { } export const gridInteractionStateDefault: GridInteractionState = { + panMode: PanMode.Disabled, keyboardMovePosition: { x: 0, y: 0 }, cursorPosition: { x: 0, y: 0 }, showMultiCursor: false, diff --git a/src/constants/app.ts b/src/constants/app.ts new file mode 100644 index 0000000000..c96e1eea24 --- /dev/null +++ b/src/constants/app.ts @@ -0,0 +1,3 @@ +import { isMobile } from 'react-device-detect'; + +export const IS_READONLY_MODE = isMobile; diff --git a/src/gridGL/QuadraticGrid.tsx b/src/gridGL/QuadraticGrid.tsx index 8fff958d7a..cb76baf8e0 100644 --- a/src/gridGL/QuadraticGrid.tsx +++ b/src/gridGL/QuadraticGrid.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import { useLoading } from '../contexts/LoadingContext'; import { gridInteractionStateAtom } from '../atoms/gridInteractionStateAtom'; import { editorInteractionStateAtom } from '../atoms/editorInteractionStateAtom'; @@ -9,6 +9,7 @@ import { ensureVisible } from './interaction/viewportHelper'; import { CellInput } from './interaction/CellInput'; import { SheetController } from '../grid/controller/sheetController'; import { FloatingContextMenu } from '../ui/menus/ContextMenu/FloatingContextMenu'; +import { PanMode } from '../atoms/gridInteractionStateAtom'; interface IProps { sheetController: SheetController; @@ -34,13 +35,23 @@ export default function QuadraticGrid(props: IProps) { // Interaction State hook const [interactionState, setInteractionState] = useRecoilState(gridInteractionStateAtom); + let prevPanModeRef = useRef(interactionState.panMode); useEffect(() => { props.app?.settings.updateInteractionState(interactionState, setInteractionState); - ensureVisible({ - sheet: props.sheetController.sheet, - app: props.app, - interactionState, - }); + + // If we're not dealing with a change in pan mode, ensure the cursor stays + // visible on screen (if we did have a change in pan mode, the user is + // panning and we don't want to change the visibility of the screen when + // they’re done) + if (prevPanModeRef.current === interactionState.panMode) { + ensureVisible({ + sheet: props.sheetController.sheet, + app: props.app, + interactionState, + }); + } + // Store the previous state for our check above + prevPanModeRef.current = interactionState.panMode; }, [props.app, props.app?.settings, interactionState, setInteractionState, props.sheetController.sheet]); const [editorInteractionState, setEditorInteractionState] = useRecoilState(editorInteractionStateAtom); @@ -51,7 +62,45 @@ export default function QuadraticGrid(props: IProps) { // Right click menu const [showContextMenu, setShowContextMenu] = useState(false); - const { onKeyDown } = useKeyboard({ + // Pan mode + const [mouseIsDown, setMouseIsDown] = useState(false); + const [spaceIsDown, setSpaceIsDown] = useState(false); + const onMouseDown = () => { + setMouseIsDown(true); + if (interactionState.panMode === PanMode.Enabled) { + setInteractionState({ ...interactionState, panMode: PanMode.Dragging }); + } + }; + const onMouseUp = () => { + setMouseIsDown(false); + if (interactionState.panMode !== PanMode.Disabled) { + setInteractionState({ ...interactionState, panMode: spaceIsDown ? PanMode.Enabled : PanMode.Disabled }); + } + }; + const onKeyDown = (e: React.KeyboardEvent) => { + if (e.code === 'Space') { + setSpaceIsDown(true); + if (interactionState.panMode === PanMode.Disabled) { + setInteractionState({ + ...interactionState, + panMode: PanMode.Enabled, + }); + } + } + }; + const onKeyUp = (e: React.KeyboardEvent) => { + if (e.code === 'Space') { + setSpaceIsDown(false); + if (interactionState.panMode !== PanMode.Disabled && !mouseIsDown) { + setInteractionState({ + ...interactionState, + panMode: PanMode.Disabled, + }); + } + } + }; + + const { onKeyDown: onKeyDownFromUseKeyboard } = useKeyboard({ sheetController: props.sheetController, interactionState, setInteractionState, @@ -71,6 +120,12 @@ export default function QuadraticGrid(props: IProps) { outline: 'none', overflow: 'hidden', WebkitTapHighlightColor: 'transparent', + cursor: + interactionState.panMode === PanMode.Enabled + ? 'grab' + : interactionState.panMode === PanMode.Dragging + ? 'grabbing' + : 'unset', }} onContextMenu={(event) => { event.preventDefault(); @@ -86,7 +141,13 @@ export default function QuadraticGrid(props: IProps) { setShowContextMenu(false); } }} - onKeyDown={onKeyDown} + onMouseDown={onMouseDown} + onMouseUp={onMouseUp} + onKeyDown={(e) => { + onKeyDown(e); + onKeyDownFromUseKeyboard(e); + }} + onKeyUp={onKeyUp} > { closeInput({ x: 0, y: 1 }); } else if ((event.metaKey || event.ctrlKey) && event.code === 'KeyP') { event.preventDefault(); + } else if (event.code === 'Space') { + // Don't propagate so panning mode doesn't get triggered + event.stopPropagation(); } }} > diff --git a/src/gridGL/interaction/keyboard/keyboardCell.ts b/src/gridGL/interaction/keyboard/keyboardCell.ts index abc1d99344..24ab096617 100644 --- a/src/gridGL/interaction/keyboard/keyboardCell.ts +++ b/src/gridGL/interaction/keyboard/keyboardCell.ts @@ -142,8 +142,8 @@ export function keyboardCell(options: { event.preventDefault(); } - // if key is a letter number or space start taking input - if (isAlphaNumeric(event.key) || event.key === ' ' || event.key === '.') { + // if key is a letter number start taking input + if (isAlphaNumeric(event.key) || event.key === '.') { setInteractionState({ ...interactionState, ...{ diff --git a/src/gridGL/interaction/pointer/Pointer.ts b/src/gridGL/interaction/pointer/Pointer.ts index 2c61a4ee47..ef88e278ad 100644 --- a/src/gridGL/interaction/pointer/Pointer.ts +++ b/src/gridGL/interaction/pointer/Pointer.ts @@ -29,7 +29,6 @@ export class Pointer { } private handlePointerDown = (e: InteractionEvent): void => { - this.app.canvas.style.cursor = 'auto'; const world = this.app.viewport.toWorld(e.data.global); const event = e.data.originalEvent; this.headingResize.pointerDown(world, event) || this.pointerDown.pointerDown(world, event as PointerEvent); diff --git a/src/gridGL/interaction/pointer/PointerDown.ts b/src/gridGL/interaction/pointer/PointerDown.ts index 8c085e0926..a3496a3485 100644 --- a/src/gridGL/interaction/pointer/PointerDown.ts +++ b/src/gridGL/interaction/pointer/PointerDown.ts @@ -1,9 +1,10 @@ import { Point } from 'pixi.js'; -import { isMobile } from 'react-device-detect'; +import { IS_READONLY_MODE } from '../../../constants/app'; import { Sheet } from '../../../grid/sheet/Sheet'; import { PixiApp } from '../../pixiApp/PixiApp'; import { doubleClickCell } from './doubleClickCell'; import { DOUBLE_CLICK_TIME } from './pointerUtils'; +import { PanMode } from '../../../atoms/gridInteractionStateAtom'; const MINIMUM_MOVE_POSITION = 5; @@ -42,7 +43,8 @@ export class PointerDown { } pointerDown(world: Point, event: PointerEvent): void { - if (isMobile) return; + if (IS_READONLY_MODE) return; + if (this.app.settings.interactionState.panMode !== PanMode.Disabled) return; const { settings, cursor } = this.app; const { interactionState, setInteractionState } = settings; @@ -156,6 +158,8 @@ export class PointerDown { } pointerMove(world: Point): void { + if (this.app.settings.interactionState.panMode !== PanMode.Disabled) return; + const { viewport, settings, cursor } = this.app; const { gridOffsets } = this.sheet; @@ -208,6 +212,7 @@ export class PointerDown { if (column === this.position.x && row === this.position.y) { // hide multi cursor when only selecting one cell settings.setInteractionState({ + ...settings.interactionState, keyboardMovePosition: { x: this.position.x, y: this.position.y }, cursorPosition: { x: this.position.x, y: this.position.y }, multiCursorPosition: { @@ -240,6 +245,7 @@ export class PointerDown { if (hasMoved) { // update multiCursor settings.setInteractionState({ + ...settings.interactionState, keyboardMovePosition: { x: column, y: row }, cursorPosition: { x: this.position.x, y: this.position.y }, multiCursorPosition: { diff --git a/src/gridGL/interaction/pointer/PointerHeading.ts b/src/gridGL/interaction/pointer/PointerHeading.ts index 0fcf53805e..8c2f63731a 100644 --- a/src/gridGL/interaction/pointer/PointerHeading.ts +++ b/src/gridGL/interaction/pointer/PointerHeading.ts @@ -4,6 +4,7 @@ import { zoomToFit } from '../../helpers/zoom'; import { PixiApp } from '../../pixiApp/PixiApp'; import { DOUBLE_CLICK_TIME } from './pointerUtils'; import { HeadingSize } from '../../../grid/sheet/useHeadings'; +import { PanMode } from '../../../atoms/gridInteractionStateAtom'; const MINIMUM_COLUMN_SIZE = 20; @@ -127,13 +128,16 @@ export class PointerHeading { } pointerMove(world: Point): boolean { - const { canvas, headings, cells, gridLines, cursor } = this.app; + const { canvas, headings, cells, gridLines, cursor, settings } = this.app; const { gridOffsets } = this.sheet; - const headingResize = headings.intersectsHeadingGridLine(world); - if (headingResize) { - canvas.style.cursor = headingResize.column !== undefined ? 'col-resize' : 'row-resize'; - } else { - canvas.style.cursor = headings.intersectsHeadings(world) ? 'pointer' : 'auto'; + // Only style the heading resize cursor if panning mode is disabled + if (settings.interactionState.panMode === PanMode.Disabled) { + const headingResize = headings.intersectsHeadingGridLine(world); + if (headingResize) { + canvas.style.cursor = headingResize.column !== undefined ? 'col-resize' : 'row-resize'; + } else { + canvas.style.cursor = headings.intersectsHeadings(world) ? 'pointer' : 'unset'; + } } if (!this.active) { return false; diff --git a/src/gridGL/pixiApp/PixiApp.ts b/src/gridGL/pixiApp/PixiApp.ts index 6822124c76..bf3f5bb470 100644 --- a/src/gridGL/pixiApp/PixiApp.ts +++ b/src/gridGL/pixiApp/PixiApp.ts @@ -1,6 +1,5 @@ import { Renderer, Container, Graphics } from 'pixi.js'; import { Viewport } from 'pixi-viewport'; -import { isMobileOnly } from 'react-device-detect'; import { PixiAppSettings } from './PixiAppSettings'; import { Pointer } from '../interaction/pointer/Pointer'; import { Update } from './Update'; @@ -19,6 +18,7 @@ import { SheetController } from '../../grid/controller/sheetController'; import { HEADING_SIZE } from '../../constants/gridConstants'; import { editorInteractionStateDefault } from '../../atoms/editorInteractionStateAtom'; import { gridInteractionStateDefault } from '../../atoms/gridInteractionStateAtom'; +import { IS_READONLY_MODE } from '../../constants/app'; export class PixiApp { private parent?: HTMLDivElement; @@ -73,7 +73,10 @@ export class PixiApp { this.viewport = new Viewport({ interaction: this.renderer.plugins.interaction }); this.stage.addChild(this.viewport); this.viewport - .drag({ pressDrag: isMobileOnly }) // enable drag on mobile, no where else + .drag({ + pressDrag: true, + ...(IS_READONLY_MODE ? {} : { keyToPress: ['Space'] }), + }) .decelerate() .pinch() .wheel({ trackpadPinch: true, wheelZoom: false, percent: 1.5 }) @@ -82,6 +85,9 @@ export class PixiApp { maxScale: 10, }); + // hack to ensure pointermove works outside of canvas + this.viewport.off('pointerout'); + // this holds the viewport's contents so it can be reused in Quadrants this.viewportContents = this.viewport.addChild(new Container()); diff --git a/src/quadratic/QuadraticApp.tsx b/src/quadratic/QuadraticApp.tsx index 83eee7add3..2e438cf72d 100644 --- a/src/quadratic/QuadraticApp.tsx +++ b/src/quadratic/QuadraticApp.tsx @@ -7,7 +7,7 @@ import { loadPython } from '../grid/computations/python/loadPython'; import { FileLoadingComponent } from './FileLoadingComponent'; import { AnalyticsProvider } from './AnalyticsProvider'; import { loadAssets } from '../gridGL/loadAssets'; -import { isMobileOnly } from 'react-device-detect'; +import { IS_READONLY_MODE } from '../constants/app'; import { debugSkipPythonLoad } from '../debugFlags'; import { GetCellsDBSetSheet } from '../grid/sheet/Cells/GetCellsDB'; import { localFiles } from '../grid/sheet/localFiles'; @@ -22,13 +22,11 @@ export const QuadraticApp = () => { // Loading Effect useEffect(() => { if (loading) { - if (!isMobileOnly && !debugSkipPythonLoad) { - // Load Python on desktop + if (!IS_READONLY_MODE && !debugSkipPythonLoad) { loadPython().then(() => { incrementLoadingCount(); }); } else { - // Don't load python on mobile incrementLoadingCount(); } loadAssets().then(() => { diff --git a/src/ui/menus/TopBar/SubMenus/QuadraticMenu.tsx b/src/ui/menus/TopBar/SubMenus/QuadraticMenu.tsx index 623ac559a5..7475985bd5 100644 --- a/src/ui/menus/TopBar/SubMenus/QuadraticMenu.tsx +++ b/src/ui/menus/TopBar/SubMenus/QuadraticMenu.tsx @@ -4,7 +4,7 @@ import { useCallback, useEffect, useState } from 'react'; import Button from '@mui/material/Button'; import KeyboardArrowDown from '@mui/icons-material/KeyboardArrowDown'; import { Menu, MenuItem, SubMenu, MenuDivider, MenuHeader } from '@szhsin/react-menu'; -import { isMobileOnly } from 'react-device-detect'; +import { IS_READONLY_MODE } from '../../../../constants/app'; import { useGridSettings } from './useGridSettings'; import { useAuth0 } from '@auth0/auth0-react'; @@ -50,9 +50,9 @@ export const QuadraticMenu = (props: Props) => { const { isAuthenticated, user, logout } = useAuth0(); - // On Mobile set Headers to not visible by default + // For readonly, set Headers to not visible by default useEffect(() => { - if (isMobileOnly) { + if (IS_READONLY_MODE) { settings.setShowHeadings(false); } // eslint-disable-next-line diff --git a/src/ui/menus/TopBar/TopBar.tsx b/src/ui/menus/TopBar/TopBar.tsx index 59871e2431..807f6c4d6d 100644 --- a/src/ui/menus/TopBar/TopBar.tsx +++ b/src/ui/menus/TopBar/TopBar.tsx @@ -9,7 +9,7 @@ import { DataMenu } from './SubMenus/DataMenu'; import { NumberFormatMenu } from './SubMenus/NumberFormatMenu'; import { ZoomDropdown } from './ZoomDropdown'; import { electronMaximizeCurrentWindow } from '../../../helpers/electronMaximizeCurrentWindow'; -import { isMobileOnly } from 'react-device-detect'; +import { IS_READONLY_MODE } from '../../../constants/app'; import { PixiApp } from '../../../gridGL/pixiApp/PixiApp'; import { useLocalFiles } from '../../../hooks/useLocalFiles'; import { SheetController } from '../../../grid/controller/sheetController'; @@ -64,7 +64,7 @@ export const TopBar = (props: IProps) => { }} > - {!isMobileOnly && ( + {!IS_READONLY_MODE && ( <> @@ -73,7 +73,7 @@ export const TopBar = (props: IProps) => { )} - {isMobileOnly ? ( + {IS_READONLY_MODE ? ( { WebkitAppRegion: 'no-drag', }} > - {!isMobileOnly && ( + {!IS_READONLY_MODE && ( <> {/* {user !== undefined && (