[improvement] adds option to keep style menu open (#697)

* Adds open to keep style menu open

* fix keyboard shortcuts, add button to menu
This commit is contained in:
Steve Ruiz 2022-05-20 13:56:16 +01:00 committed by GitHub
parent c3fe36c2e7
commit ba0795c595
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 96 additions and 52 deletions

View file

@ -1,7 +1,7 @@
import * as React from 'react' import * as React from 'react'
import { DMCheckboxItem, DMDivider, DMSubMenu } from '~components/Primitives/DropdownMenu' import { DMCheckboxItem, DMDivider, DMSubMenu } from '~components/Primitives/DropdownMenu'
import { useTldrawApp } from '~hooks' import { useTldrawApp } from '~hooks'
import type { TDSnapshot } from '~types' import { TDSnapshot } from '~types'
const settingsSelector = (s: TDSnapshot) => s.settings const settingsSelector = (s: TDSnapshot) => s.settings
@ -11,39 +11,43 @@ export function PreferencesMenu() {
const settings = app.useStore(settingsSelector) const settings = app.useStore(settingsSelector)
const toggleDebugMode = React.useCallback(() => { const toggleDebugMode = React.useCallback(() => {
app.setSetting('isDebugMode', (v) => !v) app.setSetting('isDebugMode', v => !v)
}, [app]) }, [app])
const toggleDarkMode = React.useCallback(() => { const toggleDarkMode = React.useCallback(() => {
app.setSetting('isDarkMode', (v) => !v) app.setSetting('isDarkMode', v => !v)
}, [app]) }, [app])
const toggleFocusMode = React.useCallback(() => { const toggleFocusMode = React.useCallback(() => {
app.setSetting('isFocusMode', (v) => !v) app.setSetting('isFocusMode', v => !v)
}, [app]) }, [app])
const toggleRotateHandle = React.useCallback(() => { const toggleRotateHandle = React.useCallback(() => {
app.setSetting('showRotateHandles', (v) => !v) app.setSetting('showRotateHandles', v => !v)
}, [app]) }, [app])
const toggleGrid = React.useCallback(() => { const toggleGrid = React.useCallback(() => {
app.setSetting('showGrid', (v) => !v) app.setSetting('showGrid', v => !v)
}, [app]) }, [app])
const toggleBoundShapesHandle = React.useCallback(() => { const toggleBoundShapesHandle = React.useCallback(() => {
app.setSetting('showBindingHandles', (v) => !v) app.setSetting('showBindingHandles', v => !v)
}, [app]) }, [app])
const toggleisSnapping = React.useCallback(() => { const toggleisSnapping = React.useCallback(() => {
app.setSetting('isSnapping', (v) => !v) app.setSetting('isSnapping', v => !v)
}, [app])
const toggleKeepStyleMenuOpen = React.useCallback(() => {
app.setSetting('keepStyleMenuOpen', v => !v)
}, [app]) }, [app])
const toggleCloneControls = React.useCallback(() => { const toggleCloneControls = React.useCallback(() => {
app.setSetting('showCloneHandles', (v) => !v) app.setSetting('showCloneHandles', v => !v)
}, [app]) }, [app])
const toggleCadSelectMode = React.useCallback(() => { const toggleCadSelectMode = React.useCallback(() => {
app.setSetting('isCadSelectMode', (v) => !v) app.setSetting('isCadSelectMode', v => !v)
}, [app]) }, [app])
return ( return (
@ -87,6 +91,13 @@ export function PreferencesMenu() {
> >
Use CAD Selection Use CAD Selection
</DMCheckboxItem> </DMCheckboxItem>
<DMCheckboxItem
checked={settings.keepStyleMenuOpen}
onCheckedChange={toggleKeepStyleMenuOpen}
id="TD-MenuItem-Preferences-Style_menu"
>
Keep Style Menu Open
</DMCheckboxItem>
<DMCheckboxItem <DMCheckboxItem
checked={settings.isSnapping} checked={settings.isSnapping}
onCheckedChange={toggleisSnapping} onCheckedChange={toggleisSnapping}

View file

@ -2,7 +2,12 @@ import * as React from 'react'
import * as DropdownMenu from '@radix-ui/react-dropdown-menu' import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
import { strokes, fills, defaultTextStyle } from '~state/shapes/shared/shape-styles' import { strokes, fills, defaultTextStyle } from '~state/shapes/shared/shape-styles'
import { useTldrawApp } from '~hooks' import { useTldrawApp } from '~hooks'
import { DMCheckboxItem, DMContent, DMRadioItem } from '~components/Primitives/DropdownMenu' import {
DMCheckboxItem,
DMContent,
DMDivider,
DMRadioItem,
} from '~components/Primitives/DropdownMenu'
import { import {
CircleIcon, CircleIcon,
DashDashedIcon, DashDashedIcon,
@ -63,6 +68,8 @@ const ALIGN_ICONS = {
const themeSelector = (s: TDSnapshot) => (s.settings.isDarkMode ? 'dark' : 'light') const themeSelector = (s: TDSnapshot) => (s.settings.isDarkMode ? 'dark' : 'light')
const keepOpenSelector = (s: TDSnapshot) => s.settings.keepStyleMenuOpen
const optionsSelector = (s: TDSnapshot) => { const optionsSelector = (s: TDSnapshot) => {
const { activeTool, currentPageId: pageId } = s.appState const { activeTool, currentPageId: pageId } = s.appState
switch (activeTool) { switch (activeTool) {
@ -104,6 +111,8 @@ export const StyleMenu = React.memo(function ColorMenu() {
const theme = app.useStore(themeSelector) const theme = app.useStore(themeSelector)
const keepOpen = app.useStore(keepOpenSelector)
const options = app.useStore(optionsSelector) const options = app.useStore(optionsSelector)
const currentStyle = app.useStore(currentStyleSelector) const currentStyle = app.useStore(currentStyleSelector)
@ -126,9 +135,9 @@ export const StyleMenu = React.memo(function ColorMenu() {
} else { } else {
const overrides = new Set<string>([]) const overrides = new Set<string>([])
app.selectedIds app.selectedIds
.map((id) => page.shapes[id]) .map(id => page.shapes[id])
.forEach((shape) => { .forEach(shape => {
STYLE_KEYS.forEach((key) => { STYLE_KEYS.forEach(key => {
if (overrides.has(key)) return if (overrides.has(key)) return
if (commonStyle[key] === undefined) { if (commonStyle[key] === undefined) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
@ -152,6 +161,10 @@ export const StyleMenu = React.memo(function ColorMenu() {
} }
}, [currentStyle, selectedIds]) }, [currentStyle, selectedIds])
const handleToggleKeepOpen = React.useCallback((checked: boolean) => {
app.setSetting('keepStyleMenuOpen', checked)
}, [])
const handleToggleFilled = React.useCallback((checked: boolean) => { const handleToggleFilled = React.useCallback((checked: boolean) => {
app.style({ isFilled: checked }) app.style({ isFilled: checked })
}, []) }, [])
@ -180,7 +193,11 @@ export const StyleMenu = React.memo(function ColorMenu() {
) )
return ( return (
<DropdownMenu.Root dir="ltr" onOpenChange={handleMenuOpenChange}> <DropdownMenu.Root
dir="ltr"
onOpenChange={handleMenuOpenChange}
open={keepOpen ? true : undefined}
>
<DropdownMenu.Trigger asChild id="TD-Styles"> <DropdownMenu.Trigger asChild id="TD-Styles">
<ToolButton variant="text"> <ToolButton variant="text">
Styles Styles
@ -240,7 +257,7 @@ export const StyleMenu = React.memo(function ColorMenu() {
<StyledRow id="TD-Styles-Dash-Container"> <StyledRow id="TD-Styles-Dash-Container">
Dash Dash
<StyledGroup dir="ltr" value={displayedStyle.dash} onValueChange={handleDashChange}> <StyledGroup dir="ltr" value={displayedStyle.dash} onValueChange={handleDashChange}>
{Object.values(DashStyle).map((style) => ( {Object.values(DashStyle).map(style => (
<DMRadioItem <DMRadioItem
key={style} key={style}
isActive={style === displayedStyle.dash} isActive={style === displayedStyle.dash}
@ -257,7 +274,7 @@ export const StyleMenu = React.memo(function ColorMenu() {
<StyledRow id="TD-Styles-Size-Container"> <StyledRow id="TD-Styles-Size-Container">
Size Size
<StyledGroup dir="ltr" value={displayedStyle.size} onValueChange={handleSizeChange}> <StyledGroup dir="ltr" value={displayedStyle.size} onValueChange={handleSizeChange}>
{Object.values(SizeStyle).map((sizeStyle) => ( {Object.values(SizeStyle).map(sizeStyle => (
<DMRadioItem <DMRadioItem
key={sizeStyle} key={sizeStyle}
isActive={sizeStyle === displayedStyle.size} isActive={sizeStyle === displayedStyle.size}
@ -277,7 +294,7 @@ export const StyleMenu = React.memo(function ColorMenu() {
<StyledRow id="TD-Styles-Font-Container"> <StyledRow id="TD-Styles-Font-Container">
Font Font
<StyledGroup dir="ltr" value={displayedStyle.font} onValueChange={handleFontChange}> <StyledGroup dir="ltr" value={displayedStyle.font} onValueChange={handleFontChange}>
{Object.values(FontStyle).map((fontStyle) => ( {Object.values(FontStyle).map(fontStyle => (
<DMRadioItem <DMRadioItem
key={fontStyle} key={fontStyle}
isActive={fontStyle === displayedStyle.font} isActive={fontStyle === displayedStyle.font}
@ -299,7 +316,7 @@ export const StyleMenu = React.memo(function ColorMenu() {
value={displayedStyle.textAlign} value={displayedStyle.textAlign}
onValueChange={handleTextAlignChange} onValueChange={handleTextAlignChange}
> >
{Object.values(AlignStyle).map((style) => ( {Object.values(AlignStyle).map(style => (
<DMRadioItem <DMRadioItem
key={style} key={style}
isActive={style === displayedStyle.textAlign} isActive={style === displayedStyle.textAlign}
@ -316,6 +333,15 @@ export const StyleMenu = React.memo(function ColorMenu() {
)} )}
</> </>
)} )}
<DMDivider />
<DMCheckboxItem
variant="styleMenu"
checked={keepOpen}
onCheckedChange={handleToggleKeepOpen}
id="TD-Styles-Keep-Open"
>
Keep Open
</DMCheckboxItem>
</DMContent> </DMContent>
</DropdownMenu.Root> </DropdownMenu.Root>
) )

View file

@ -9,7 +9,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
const canHandleEvent = React.useCallback( const canHandleEvent = React.useCallback(
(ignoreMenus = false) => { (ignoreMenus = false) => {
const elm = ref.current const elm = ref.current
if (ignoreMenus && app.isMenuOpen) return true if (ignoreMenus && (app.isMenuOpen || app.settings.keepStyleMenuOpen)) return true
return elm && (document.activeElement === elm || elm.contains(document.activeElement)) return elm && (document.activeElement === elm || elm.contains(document.activeElement))
}, },
[ref] [ref]
@ -159,7 +159,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
useHotkeys( useHotkeys(
'ctrl+shift+d,⌘+shift+d', 'ctrl+shift+d,⌘+shift+d',
(e) => { e => {
if (!canHandleEvent(true)) return if (!canHandleEvent(true)) return
app.toggleDarkMode() app.toggleDarkMode()
e.preventDefault() e.preventDefault()
@ -192,12 +192,17 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
// File System // File System
const { onNewProject, onOpenProject, onSaveProject, onSaveProjectAs, onOpenMedia } = const {
useFileSystemHandlers() onNewProject,
onOpenProject,
onSaveProject,
onSaveProjectAs,
onOpenMedia,
} = useFileSystemHandlers()
useHotkeys( useHotkeys(
'ctrl+n,⌘+n', 'ctrl+n,⌘+n',
(e) => { e => {
if (!canHandleEvent()) return if (!canHandleEvent()) return
onNewProject(e) onNewProject(e)
@ -207,7 +212,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
) )
useHotkeys( useHotkeys(
'ctrl+s,⌘+s', 'ctrl+s,⌘+s',
(e) => { e => {
if (!canHandleEvent()) return if (!canHandleEvent()) return
onSaveProject(e) onSaveProject(e)
@ -218,7 +223,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
useHotkeys( useHotkeys(
'ctrl+shift+s,⌘+shift+s', 'ctrl+shift+s,⌘+shift+s',
(e) => { e => {
if (!canHandleEvent()) return if (!canHandleEvent()) return
onSaveProjectAs(e) onSaveProjectAs(e)
@ -228,7 +233,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
) )
useHotkeys( useHotkeys(
'ctrl+o,⌘+o', 'ctrl+o,⌘+o',
(e) => { e => {
if (!canHandleEvent()) return if (!canHandleEvent()) return
onOpenProject(e) onOpenProject(e)
@ -238,7 +243,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
) )
useHotkeys( useHotkeys(
'ctrl+u,⌘+u', 'ctrl+u,⌘+u',
(e) => { e => {
if (!canHandleEvent()) return if (!canHandleEvent()) return
onOpenMedia(e) onOpenMedia(e)
}, },
@ -306,7 +311,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
useHotkeys( useHotkeys(
'ctrl+=,⌘+=,ctrl+num_subtract,⌘+num_subtract', 'ctrl+=,⌘+=,ctrl+num_subtract,⌘+num_subtract',
(e) => { e => {
if (!canHandleEvent(true)) return if (!canHandleEvent(true)) return
app.zoomIn() app.zoomIn()
e.preventDefault() e.preventDefault()
@ -317,7 +322,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
useHotkeys( useHotkeys(
'ctrl+-,⌘+-,ctrl+num_add,⌘+num_add', 'ctrl+-,⌘+-,ctrl+num_add,⌘+num_add',
(e) => { e => {
if (!canHandleEvent(true)) return if (!canHandleEvent(true)) return
app.zoomOut() app.zoomOut()
@ -361,7 +366,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
useHotkeys( useHotkeys(
'ctrl+d,⌘+d', 'ctrl+d,⌘+d',
(e) => { e => {
if (!canHandleEvent()) return if (!canHandleEvent()) return
app.duplicate() app.duplicate()
@ -536,7 +541,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
useHotkeys( useHotkeys(
'⌘+shift+c,ctrl+shift+c', '⌘+shift+c,ctrl+shift+c',
(e) => { e => {
if (!canHandleEvent()) return if (!canHandleEvent()) return
app.copySvg() app.copySvg()
@ -571,7 +576,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
useHotkeys( useHotkeys(
'⌘+g,ctrl+g', '⌘+g,ctrl+g',
(e) => { e => {
if (!canHandleEvent()) return if (!canHandleEvent()) return
app.group() app.group()
@ -583,7 +588,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
useHotkeys( useHotkeys(
'⌘+shift+g,ctrl+shift+g', '⌘+shift+g,ctrl+shift+g',
(e) => { e => {
if (!canHandleEvent()) return if (!canHandleEvent()) return
app.ungroup() app.ungroup()
@ -637,7 +642,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
useHotkeys( useHotkeys(
'ctrl+shift+backspace,⌘+shift+backspace', 'ctrl+shift+backspace,⌘+shift+backspace',
(e) => { e => {
if (!canHandleEvent()) return if (!canHandleEvent()) return
if (app.settings.isDebugMode) { if (app.settings.isDebugMode) {
app.resetDocument() app.resetDocument()
@ -652,7 +657,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
useHotkeys( useHotkeys(
'alt+command+l,alt+ctrl+l', 'alt+command+l,alt+ctrl+l',
(e) => { e => {
if (!canHandleEvent(true)) return if (!canHandleEvent(true)) return
app.style({ textAlign: AlignStyle.Start }) app.style({ textAlign: AlignStyle.Start })
e.preventDefault() e.preventDefault()
@ -663,7 +668,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
useHotkeys( useHotkeys(
'alt+command+t,alt+ctrl+t', 'alt+command+t,alt+ctrl+t',
(e) => { e => {
if (!canHandleEvent(true)) return if (!canHandleEvent(true)) return
app.style({ textAlign: AlignStyle.Middle }) app.style({ textAlign: AlignStyle.Middle })
e.preventDefault() e.preventDefault()
@ -674,7 +679,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
useHotkeys( useHotkeys(
'alt+command+r,alt+ctrl+r', 'alt+command+r,alt+ctrl+r',
(e) => { e => {
if (!canHandleEvent(true)) return if (!canHandleEvent(true)) return
app.style({ textAlign: AlignStyle.End }) app.style({ textAlign: AlignStyle.End })
e.preventDefault() e.preventDefault()

View file

@ -4086,6 +4086,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
isSnapping: false, isSnapping: false,
isDebugMode: false, isDebugMode: false,
isReadonlyMode: false, isReadonlyMode: false,
keepStyleMenuOpen: false,
nudgeDistanceLarge: 16, nudgeDistanceLarge: 16,
nudgeDistanceSmall: 1, nudgeDistanceSmall: 1,
showRotateHandles: true, showRotateHandles: true,

View file

@ -11,8 +11,8 @@ export function migrate(document: TDDocument, newVersion: number): TDDocument {
// Remove unused assets when loading a document // Remove unused assets when loading a document
const assetIdsInUse = new Set<string>() const assetIdsInUse = new Set<string>()
Object.values(document.pages).forEach((page) => Object.values(document.pages).forEach(page =>
Object.values(page.shapes).forEach((shape) => { Object.values(page.shapes).forEach(shape => {
const { parentId, children, assetId } = shape const { parentId, children, assetId } = shape
if (assetId) { if (assetId) {
@ -26,7 +26,7 @@ export function migrate(document: TDDocument, newVersion: number): TDDocument {
} }
if (shape.type === TDShapeType.Group && children) { if (shape.type === TDShapeType.Group && children) {
children.forEach((childId) => { children.forEach(childId => {
if (!page.shapes[childId]) { if (!page.shapes[childId]) {
console.warn('Encountered a parent with a missing child!', shape.id, childId) console.warn('Encountered a parent with a missing child!', shape.id, childId)
children?.splice(children.indexOf(childId), 1) children?.splice(children.indexOf(childId), 1)
@ -38,7 +38,7 @@ export function migrate(document: TDDocument, newVersion: number): TDDocument {
}) })
) )
Object.keys(document.assets).forEach((assetId) => { Object.keys(document.assets).forEach(assetId => {
if (!assetIdsInUse.has(assetId)) { if (!assetIdsInUse.has(assetId)) {
delete document.assets[assetId] delete document.assets[assetId]
} }
@ -47,22 +47,22 @@ export function migrate(document: TDDocument, newVersion: number): TDDocument {
if (version === newVersion) return document if (version === newVersion) return document
if (version < 14) { if (version < 14) {
Object.values(document.pages).forEach((page) => { Object.values(document.pages).forEach(page => {
Object.values(page.shapes) Object.values(page.shapes)
.filter((shape) => shape.type === TDShapeType.Text) .filter(shape => shape.type === TDShapeType.Text)
.forEach((shape) => (shape as TextShape).style.font === FontStyle.Script) .forEach(shape => (shape as TextShape).style.font === FontStyle.Script)
}) })
} }
// Lowercase styles, move binding meta to binding // Lowercase styles, move binding meta to binding
if (version <= 13) { if (version <= 13) {
Object.values(document.pages).forEach((page) => { Object.values(document.pages).forEach(page => {
Object.values(page.bindings).forEach((binding) => { Object.values(page.bindings).forEach(binding => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
Object.assign(binding, (binding as any).meta) Object.assign(binding, (binding as any).meta)
}) })
Object.values(page.shapes).forEach((shape) => { Object.values(page.shapes).forEach(shape => {
Object.entries(shape.style).forEach(([id, style]) => { Object.entries(shape.style).forEach(([id, style]) => {
if (typeof style === 'string') { if (typeof style === 'string') {
// @ts-ignore // @ts-ignore
@ -95,8 +95,8 @@ export function migrate(document: TDDocument, newVersion: number): TDDocument {
document.assets = {} document.assets = {}
} }
Object.values(document.pages).forEach((page) => { Object.values(document.pages).forEach(page => {
Object.values(page.shapes).forEach((shape) => { Object.values(page.shapes).forEach(shape => {
if (version < 15.2) { if (version < 15.2) {
if (shape.type === TDShapeType.Image || shape.type === TDShapeType.Video) { if (shape.type === TDShapeType.Image || shape.type === TDShapeType.Video) {
shape.style.isFilled = true shape.style.isFilled = true
@ -118,8 +118,8 @@ export function migrate(document: TDDocument, newVersion: number): TDDocument {
}) })
// Cleanup // Cleanup
Object.values(document.pageStates).forEach((pageState) => { Object.values(document.pageStates).forEach(pageState => {
pageState.selectedIds = pageState.selectedIds.filter((id) => { pageState.selectedIds = pageState.selectedIds.filter(id => {
return document.pages[pageState.id].shapes[id] !== undefined return document.pages[pageState.id].shapes[id] !== undefined
}) })
pageState.bindingId = undefined pageState.bindingId = undefined

View file

@ -85,6 +85,7 @@ export interface TDSnapshot {
isPenMode: boolean isPenMode: boolean
isReadonlyMode: boolean isReadonlyMode: boolean
isZoomSnap: boolean isZoomSnap: boolean
keepStyleMenuOpen: boolean
nudgeDistanceSmall: number nudgeDistanceSmall: number
nudgeDistanceLarge: number nudgeDistanceLarge: number
isFocusMode: boolean isFocusMode: boolean