From 21effe2818f10636fd217b1cf3a0d17254953fdc Mon Sep 17 00:00:00 2001 From: Ben Wolfram Date: Wed, 3 Jul 2024 12:25:42 -0500 Subject: [PATCH] Add Manage Panels to Toolbar (#6004) **Problem:** We'd like to have a more prominent, easily-accessible way to manage panel visibility located in the toolbar **Fix:** As a v1, leveraging a lot of similarity to the insert mode's dropdown in the toolbar. Will iterate to use leverage a newly created button + dropdown menu component in the future. **Manual Tests:** I hereby swear that: - [x] I opened a hydrogen project and it loaded - [x] I could navigate to various routes in Preview mode Fixes https://github.com/concrete-utopia/utopia/issues/5892 --------- Co-authored-by: Balazs Bajorics <2226774+balazsbajorics@users.noreply.github.com> --- .../canvas/canvas-floating-toolbars.tsx | 9 +- .../canvas-strategies/canvas-strategies.tsx | 1 + .../controls/panels-mode/panels-mode-hooks.ts | 12 ++ .../select-mode/select-mode-hooks.tsx | 4 + .../editor/canvas-toolbar-states.tsx | 5 + .../src/components/editor/canvas-toolbar.tsx | 31 +++ .../src/components/editor/closed-panels.tsx | 204 ------------------ editor/src/components/editor/editor-modes.ts | 13 +- .../store/store-deep-equality-instances.ts | 12 ++ .../navigator-item/panels-picker.tsx | 87 ++++++++ editor/src/components/titlebar/test-menu.tsx | 2 + editor/src/templates/editor-canvas.tsx | 7 +- 12 files changed, 173 insertions(+), 214 deletions(-) create mode 100644 editor/src/components/canvas/controls/panels-mode/panels-mode-hooks.ts delete mode 100644 editor/src/components/editor/closed-panels.tsx create mode 100644 editor/src/components/navigator/navigator-item/panels-picker.tsx diff --git a/editor/src/components/canvas/canvas-floating-toolbars.tsx b/editor/src/components/canvas/canvas-floating-toolbars.tsx index eac19c39c880..87e1e7041f26 100644 --- a/editor/src/components/canvas/canvas-floating-toolbars.tsx +++ b/editor/src/components/canvas/canvas-floating-toolbars.tsx @@ -1,11 +1,11 @@ import React from 'react' import { FlexColumn, FlexRow } from '../../uuiui' import { CanvasToolbar } from '../editor/canvas-toolbar' -import { ClosedPanels } from '../editor/closed-panels' import { Substores, useEditorState } from '../editor/store/store-hook' import { ErrorOverlayComponent } from './canvas-error-overlay' import { SafeModeErrorOverlay } from './canvas-wrapper-component' import { CanvasStrategyPicker } from './controls/select-mode/canvas-strategy-picker' +import { TestMenu } from '../titlebar/test-menu' export const CanvasFloatingToolbars = React.memo((props: { style: React.CSSProperties }) => { const safeMode = useEditorState( @@ -32,21 +32,20 @@ export const CanvasFloatingToolbars = React.memo((props: { style: React.CSSPrope position: 'absolute', top: 0, alignItems: 'flex-start', - justifyContent: 'space-between', + justifyContent: 'center', padding: 6, width: '100%', height: '100%', }} > - - - {/* The error overlays are deliberately the last here so they hide other canvas UI */} + {/* The error overlays are deliberately the last here so they hide other canvas UI, except the test menu */} {safeMode ? : } + ) }) diff --git a/editor/src/components/canvas/canvas-strategies/canvas-strategies.tsx b/editor/src/components/canvas/canvas-strategies/canvas-strategies.tsx index c8c59d5527e3..ac434d214eef 100644 --- a/editor/src/components/canvas/canvas-strategies/canvas-strategies.tsx +++ b/editor/src/components/canvas/canvas-strategies/canvas-strategies.tsx @@ -229,6 +229,7 @@ function getInteractionTargetFromEditorState(editor: EditorState): InteractionTa case 'textEdit': case 'comment': case 'follow': + case 'panels': return targetPaths(editor.selectedViews) default: assertNever(editor.mode) diff --git a/editor/src/components/canvas/controls/panels-mode/panels-mode-hooks.ts b/editor/src/components/canvas/controls/panels-mode/panels-mode-hooks.ts new file mode 100644 index 000000000000..0f642ea90f76 --- /dev/null +++ b/editor/src/components/canvas/controls/panels-mode/panels-mode-hooks.ts @@ -0,0 +1,12 @@ +import type { MouseCallbacks } from '../select-mode/select-mode-hooks' +import { NO_OP } from '../../../../core/shared/utils' + +const noop: MouseCallbacks = { + onMouseMove: NO_OP, + onMouseDown: NO_OP, + onMouseUp: NO_OP, +} + +export function usePanelsModeSelectAndHover(): MouseCallbacks { + return noop +} diff --git a/editor/src/components/canvas/controls/select-mode/select-mode-hooks.tsx b/editor/src/components/canvas/controls/select-mode/select-mode-hooks.tsx index a226f17ffdcf..fc368de424f8 100644 --- a/editor/src/components/canvas/controls/select-mode/select-mode-hooks.tsx +++ b/editor/src/components/canvas/controls/select-mode/select-mode-hooks.tsx @@ -64,6 +64,7 @@ import { getAllLockedElementPaths } from '../../../../core/shared/element-lockin import { treatElementAsGroupLike } from '../../canvas-strategies/strategies/group-helpers' import { useCommentModeSelectAndHover } from '../comment-mode/comment-mode-hooks' import { useFollowModeSelectAndHover } from '../follow-mode/follow-mode-hooks' +import { usePanelsModeSelectAndHover } from '../panels-mode/panels-mode-hooks' const DRAG_START_THRESHOLD = 2 @@ -805,6 +806,7 @@ export function useSelectAndHover( mode?.type === 'comment' ? mode.comment : null, ) const followModeCallbacks = useFollowModeSelectAndHover() + const panelsModeCallbacks = usePanelsModeSelectAndHover() if (hasInteractionSession) { return { @@ -826,6 +828,8 @@ export function useSelectAndHover( return commentModeCallbacks case 'follow': return followModeCallbacks + case 'panels': + return panelsModeCallbacks default: const _exhaustiveCheck: never = modeType throw new Error(`Unhandled editor mode ${JSON.stringify(modeType)}`) diff --git a/editor/src/components/editor/canvas-toolbar-states.tsx b/editor/src/components/editor/canvas-toolbar-states.tsx index 091326327fbe..01121a624967 100644 --- a/editor/src/components/editor/canvas-toolbar-states.tsx +++ b/editor/src/components/editor/canvas-toolbar-states.tsx @@ -27,6 +27,7 @@ type ToolbarMode = | { primary: 'play' } | { primary: 'comment' } | { primary: 'zoom' } + | { primary: 'panels' } export function useToolbarMode(): ToolbarMode { const editorMode = useEditorState( @@ -124,6 +125,10 @@ export function useToolbarMode(): ToolbarMode { } } + if (editorMode.type === 'panels') { + return { primary: 'panels' } + } + // Edit Mode if (nothingSelected) { return { primary: 'edit', secondary: 'nothing-selected' } diff --git a/editor/src/components/editor/canvas-toolbar.tsx b/editor/src/components/editor/canvas-toolbar.tsx index fbac92139647..801c4ce49268 100644 --- a/editor/src/components/editor/canvas-toolbar.tsx +++ b/editor/src/components/editor/canvas-toolbar.tsx @@ -61,6 +61,7 @@ import { insertComponentPickerItem } from '../navigator/navigator-item/component import { useAtom } from 'jotai' import { ActiveRemixSceneAtom } from '../canvas/remix/utopia-remix-root-component' import * as EP from '../../core/shared/element-path' +import { PanelsPicker } from '../navigator/navigator-item/panels-picker' export const InsertMenuButtonTestId = 'insert-menu-button' export const PlayModeButtonTestId = 'canvas-toolbar-play-mode' @@ -267,6 +268,14 @@ export const CanvasToolbar = React.memo(() => { } }, [canvasToolbarMode.primary, dispatch, dispatchSwitchToSelectModeCloseMenus]) + const togglePanelsButtonClicked = React.useCallback(() => { + if (canvasToolbarMode.primary === 'panels') { + dispatchSwitchToSelectModeCloseMenus() + } else { + dispatch([switchEditorMode(EditorModes.panelsMode())]) + } + }, [canvasToolbarMode.primary, dispatch, dispatchSwitchToSelectModeCloseMenus]) + const currentStrategyState = useEditorState( Substores.restOfStore, (store) => store.strategyState, @@ -423,6 +432,16 @@ export const CanvasToolbar = React.memo(() => { , )} + + + { : null} {/* Live Mode */} {showRemixNavBar ? wrapInSubmenu() : null} + {/* Panels Mode */} + {canvasToolbarMode.primary === 'panels' + ? wrapInSubmenu( + + + + + + + , + ) + : null} ) }) diff --git a/editor/src/components/editor/closed-panels.tsx b/editor/src/components/editor/closed-panels.tsx deleted file mode 100644 index a37a6072f3f9..000000000000 --- a/editor/src/components/editor/closed-panels.tsx +++ /dev/null @@ -1,204 +0,0 @@ -/** @jsxRuntime classic */ -/** @jsx jsx */ -/** @jsxFrag React.Fragment */ -import { jsx } from '@emotion/react' -import React, { useState } from 'react' -import type { TooltipProps } from '../../uuiui' -import { UtopiaStyles, UtopiaTheme, colorTheme } from '../../uuiui' -import { FlexColumn, Icn, SquareButton, Tooltip as TooltipWithoutSpanFixme } from '../../uuiui' -import { useDispatch } from './store/dispatch-context' -import { Substores, useEditorState } from './store/store-hook' -import { togglePanel } from './actions/action-creators' -import { stopPropagation } from '../inspector/common/inspector-utils' -import { setFocus } from '../common/actions' -import { useAtom } from 'jotai' -import { GridPanelsStateAtom } from '../canvas/grid-panels-state' -import type { StoredLayout, StoredPanel } from '../canvas/stored-layout' -import { assertNever } from '../../core/shared/utils' -import { TestMenu } from '../titlebar/test-menu' -import { ToastRenderer } from './editor-component' - -function getPanelColumn(side: 'left' | 'right', panels: StoredLayout): Array { - switch (side) { - case 'left': - return [...panels[0].panels, ...panels[1].panels] - case 'right': - return [...panels[2].panels, ...panels[3].panels] - default: - assertNever(side) - } -} - -export const ClosedPanels = React.memo((props: { side: 'left' | 'right' }) => { - const dispatch = useDispatch() - const [panelState] = useAtom(GridPanelsStateAtom) - const thisColumn = getPanelColumn(props.side, panelState) - - const inspectorInvisible = useEditorState( - Substores.restOfEditor, - (store) => { - const theInspectorInvisible = !store.editor.rightMenu.visible - const inspectorOnThisSide = thisColumn.some((panel) => panel.name === 'inspector') - return theInspectorInvisible && inspectorOnThisSide - }, - 'SettingsPanel inspector.minimized', - ) - - const toggleInspectorVisible = React.useCallback(() => { - dispatch([togglePanel('rightmenu')]) - }, [dispatch]) - - const navigatorInvisible = useEditorState( - Substores.restOfEditor, - (store) => { - const theNavigatorInvisible = !store.editor.leftMenu.visible - const navigatorOnThisSide = thisColumn.some((panel) => panel.name === 'navigator') - return theNavigatorInvisible && navigatorOnThisSide - }, - 'SettingsPanel navigator.minimised', - ) - - const toggleNavigatorVisible = React.useCallback(() => { - dispatch([togglePanel('leftmenu')]) - }, [dispatch]) - - const editorInvisible = useEditorState( - Substores.restOfEditor, - (store) => { - const theEditorInvisible = !store.editor.interfaceDesigner.codePaneVisible - const codeEditorOnThisSide = thisColumn.some((panel) => panel.name === 'code-editor') - return theEditorInvisible && codeEditorOnThisSide - }, - 'SettingsPanel navigator.minimised', - ) - - const toggleCodeEditorVisible = React.useCallback( - () => dispatch([togglePanel('codeEditor')]), - [dispatch], - ) - - const focusCanvasOnMouseDown = React.useCallback( - (event: React.MouseEvent) => { - stopPropagation(event) - dispatch([setFocus('canvas')], 'everyone') - }, - [dispatch], - ) - - const [isVisible, setIsVisible] = useState(false) - const setIsVisibleTrue = React.useCallback(() => { - setIsVisible(true) - }, []) - - const setIsVisibleFalse = React.useCallback(() => { - setIsVisible(false) - }, []) - - return ( - - - {navigatorInvisible ? ( - - - - ) : null} - {editorInvisible ? ( - - - - ) : null} - {inspectorInvisible ? ( - - - - ) : null} - - {props.side === 'left' ? : } - - ) -}) -ClosedPanels.displayName = 'ClosedPanels' - -interface ClosedPanelButtonProps { - iconType: string - visible?: boolean - onClick: (event: React.MouseEvent) => void -} -const ClosedPanelButton = React.memo((props: ClosedPanelButtonProps) => { - const [isHovered, setIsHovered] = useState(false) - const setIsHoveredTrue = React.useCallback(() => { - setIsHovered(true) - }, []) - - const setIsHoveredFalse = React.useCallback(() => { - setIsHovered(false) - }, []) - - return ( - - - - ) -}) - -const Tooltip = (props: TooltipProps) => { - return ( - - {/* TODO why do we need to wrap the children in a span? */} - {props.children} - - ) -} diff --git a/editor/src/components/editor/editor-modes.ts b/editor/src/components/editor/editor-modes.ts index 94323918a1b6..8c56b148b70b 100644 --- a/editor/src/components/editor/editor-modes.ts +++ b/editor/src/components/editor/editor-modes.ts @@ -165,7 +165,7 @@ export interface CommentMode { isDragging: IsDragging } -export type SelectModeToolbarMode = 'none' | 'pseudo-insert' +export type SelectModeToolbarMode = 'none' | 'pseudo-insert' | 'panels' export interface SelectMode { type: 'select' @@ -200,6 +200,10 @@ export interface FollowMode { connectionId: number // the connection ID of the followed player } +export interface PanelsMode { + type: 'panels' +} + export type Mode = | InsertMode | SelectMode @@ -207,6 +211,7 @@ export type Mode = | TextEditMode | CommentMode | FollowMode + | PanelsMode export type PersistedMode = SelectMode | LiveCanvasMode export const EditorModes = { @@ -262,6 +267,11 @@ export const EditorModes = { connectionId: connectionId, } }, + panelsMode: function (): PanelsMode { + return { + type: 'panels', + } + }, } export function isInsertMode(value: Mode): value is InsertMode { @@ -295,6 +305,7 @@ export function convertModeToSavedMode(mode: Mode): PersistedMode { case 'textEdit': case 'comment': case 'follow': + case 'panels': return EditorModes.selectMode(null, false, 'none') } } diff --git a/editor/src/components/editor/store/store-deep-equality-instances.ts b/editor/src/components/editor/store/store-deep-equality-instances.ts index 8f5cef751b70..7bae85303aa3 100644 --- a/editor/src/components/editor/store/store-deep-equality-instances.ts +++ b/editor/src/components/editor/store/store-deep-equality-instances.ts @@ -530,6 +530,7 @@ import type { SelectModeToolbarMode, CommentMode, FollowMode, + PanelsMode, CommentId, NewComment, ExistingComment, @@ -4018,6 +4019,12 @@ export const FollowModeKeepDeepEquality: KeepDeepEqualityCall = comb EditorModes.followMode, ) +export const PanelsModeKeepDeepEquality: KeepDeepEqualityCall = combine1EqualityCall( + (mode) => mode.type, + StringKeepDeepEquality, + EditorModes.panelsMode, +) + export const ModeKeepDeepEquality: KeepDeepEqualityCall = (oldValue, newValue) => { switch (oldValue.type) { case 'insert': @@ -4050,6 +4057,11 @@ export const ModeKeepDeepEquality: KeepDeepEqualityCall = (oldValue, newVa return FollowModeKeepDeepEquality(newValue, oldValue) } break + case 'panels': + if (newValue.type === oldValue.type) { + return PanelsModeKeepDeepEquality(oldValue, newValue) + } + break default: assertNever(oldValue) } diff --git a/editor/src/components/navigator/navigator-item/panels-picker.tsx b/editor/src/components/navigator/navigator-item/panels-picker.tsx new file mode 100644 index 000000000000..7f6af34440bd --- /dev/null +++ b/editor/src/components/navigator/navigator-item/panels-picker.tsx @@ -0,0 +1,87 @@ +/** @jsxRuntime classic */ +/** @jsx jsx */ +import { jsx } from '@emotion/react' +import React from 'react' +import { CheckboxInput, colorTheme } from '../../../uuiui' +import { FlexRow } from 'utopia-api' +import { Substores, useEditorState } from '../../../components/editor/store/store-hook' +import { togglePanel } from '../../../components/editor/actions/action-creators' +import { useDispatch } from '../../../components/editor/store/dispatch-context' + +export const PanelsPicker = React.memo(() => { + const dispatch = useDispatch() + + const { codeEditorVisible, navigatorVisible, inspectorVisible } = useEditorState( + Substores.restOfEditor, + (store) => { + return { + codeEditorVisible: store.editor.interfaceDesigner.codePaneVisible, + navigatorVisible: store.editor.leftMenu.visible, + inspectorVisible: store.editor.rightMenu.visible, + } + }, + 'storedLayoutToResolvedPanels panel visibility', + ) + + const toggleNavigator = React.useCallback(() => { + dispatch([togglePanel('leftmenu')]) + }, [dispatch]) + + const toggleCodeEditor = React.useCallback(() => { + dispatch([togglePanel('codeEditor')]) + }, [dispatch]) + + const toggleInspector = React.useCallback(() => { + dispatch([togglePanel('rightmenu')]) + }, [dispatch]) + + return ( +
+ + + +
+ ) +}) + +interface CheckboxRowProps { + checked: boolean + label: string + onChange: () => void +} + +const CheckboxRow = React.memo((props: CheckboxRowProps) => { + const { checked, label, onChange } = props + + const id = `input-${label}` + + return ( + + + + + + + ) +}) diff --git a/editor/src/components/titlebar/test-menu.tsx b/editor/src/components/titlebar/test-menu.tsx index f79b959ab2d0..017c9fb325a1 100644 --- a/editor/src/components/titlebar/test-menu.tsx +++ b/editor/src/components/titlebar/test-menu.tsx @@ -93,6 +93,8 @@ export const TestMenu = React.memo(() => { return (