[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:
parent
c3fe36c2e7
commit
ba0795c595
6 changed files with 96 additions and 52 deletions
|
@ -1,7 +1,7 @@
|
|||
import * as React from 'react'
|
||||
import { DMCheckboxItem, DMDivider, DMSubMenu } from '~components/Primitives/DropdownMenu'
|
||||
import { useTldrawApp } from '~hooks'
|
||||
import type { TDSnapshot } from '~types'
|
||||
import { TDSnapshot } from '~types'
|
||||
|
||||
const settingsSelector = (s: TDSnapshot) => s.settings
|
||||
|
||||
|
@ -11,39 +11,43 @@ export function PreferencesMenu() {
|
|||
const settings = app.useStore(settingsSelector)
|
||||
|
||||
const toggleDebugMode = React.useCallback(() => {
|
||||
app.setSetting('isDebugMode', (v) => !v)
|
||||
app.setSetting('isDebugMode', v => !v)
|
||||
}, [app])
|
||||
|
||||
const toggleDarkMode = React.useCallback(() => {
|
||||
app.setSetting('isDarkMode', (v) => !v)
|
||||
app.setSetting('isDarkMode', v => !v)
|
||||
}, [app])
|
||||
|
||||
const toggleFocusMode = React.useCallback(() => {
|
||||
app.setSetting('isFocusMode', (v) => !v)
|
||||
app.setSetting('isFocusMode', v => !v)
|
||||
}, [app])
|
||||
|
||||
const toggleRotateHandle = React.useCallback(() => {
|
||||
app.setSetting('showRotateHandles', (v) => !v)
|
||||
app.setSetting('showRotateHandles', v => !v)
|
||||
}, [app])
|
||||
|
||||
const toggleGrid = React.useCallback(() => {
|
||||
app.setSetting('showGrid', (v) => !v)
|
||||
app.setSetting('showGrid', v => !v)
|
||||
}, [app])
|
||||
|
||||
const toggleBoundShapesHandle = React.useCallback(() => {
|
||||
app.setSetting('showBindingHandles', (v) => !v)
|
||||
app.setSetting('showBindingHandles', v => !v)
|
||||
}, [app])
|
||||
|
||||
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])
|
||||
|
||||
const toggleCloneControls = React.useCallback(() => {
|
||||
app.setSetting('showCloneHandles', (v) => !v)
|
||||
app.setSetting('showCloneHandles', v => !v)
|
||||
}, [app])
|
||||
|
||||
const toggleCadSelectMode = React.useCallback(() => {
|
||||
app.setSetting('isCadSelectMode', (v) => !v)
|
||||
app.setSetting('isCadSelectMode', v => !v)
|
||||
}, [app])
|
||||
|
||||
return (
|
||||
|
@ -87,6 +91,13 @@ export function PreferencesMenu() {
|
|||
>
|
||||
Use CAD Selection
|
||||
</DMCheckboxItem>
|
||||
<DMCheckboxItem
|
||||
checked={settings.keepStyleMenuOpen}
|
||||
onCheckedChange={toggleKeepStyleMenuOpen}
|
||||
id="TD-MenuItem-Preferences-Style_menu"
|
||||
>
|
||||
Keep Style Menu Open
|
||||
</DMCheckboxItem>
|
||||
<DMCheckboxItem
|
||||
checked={settings.isSnapping}
|
||||
onCheckedChange={toggleisSnapping}
|
||||
|
|
|
@ -2,7 +2,12 @@ import * as React from 'react'
|
|||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
||||
import { strokes, fills, defaultTextStyle } from '~state/shapes/shared/shape-styles'
|
||||
import { useTldrawApp } from '~hooks'
|
||||
import { DMCheckboxItem, DMContent, DMRadioItem } from '~components/Primitives/DropdownMenu'
|
||||
import {
|
||||
DMCheckboxItem,
|
||||
DMContent,
|
||||
DMDivider,
|
||||
DMRadioItem,
|
||||
} from '~components/Primitives/DropdownMenu'
|
||||
import {
|
||||
CircleIcon,
|
||||
DashDashedIcon,
|
||||
|
@ -63,6 +68,8 @@ const ALIGN_ICONS = {
|
|||
|
||||
const themeSelector = (s: TDSnapshot) => (s.settings.isDarkMode ? 'dark' : 'light')
|
||||
|
||||
const keepOpenSelector = (s: TDSnapshot) => s.settings.keepStyleMenuOpen
|
||||
|
||||
const optionsSelector = (s: TDSnapshot) => {
|
||||
const { activeTool, currentPageId: pageId } = s.appState
|
||||
switch (activeTool) {
|
||||
|
@ -104,6 +111,8 @@ export const StyleMenu = React.memo(function ColorMenu() {
|
|||
|
||||
const theme = app.useStore(themeSelector)
|
||||
|
||||
const keepOpen = app.useStore(keepOpenSelector)
|
||||
|
||||
const options = app.useStore(optionsSelector)
|
||||
|
||||
const currentStyle = app.useStore(currentStyleSelector)
|
||||
|
@ -126,9 +135,9 @@ export const StyleMenu = React.memo(function ColorMenu() {
|
|||
} else {
|
||||
const overrides = new Set<string>([])
|
||||
app.selectedIds
|
||||
.map((id) => page.shapes[id])
|
||||
.forEach((shape) => {
|
||||
STYLE_KEYS.forEach((key) => {
|
||||
.map(id => page.shapes[id])
|
||||
.forEach(shape => {
|
||||
STYLE_KEYS.forEach(key => {
|
||||
if (overrides.has(key)) return
|
||||
if (commonStyle[key] === undefined) {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
|
@ -152,6 +161,10 @@ export const StyleMenu = React.memo(function ColorMenu() {
|
|||
}
|
||||
}, [currentStyle, selectedIds])
|
||||
|
||||
const handleToggleKeepOpen = React.useCallback((checked: boolean) => {
|
||||
app.setSetting('keepStyleMenuOpen', checked)
|
||||
}, [])
|
||||
|
||||
const handleToggleFilled = React.useCallback((checked: boolean) => {
|
||||
app.style({ isFilled: checked })
|
||||
}, [])
|
||||
|
@ -180,7 +193,11 @@ export const StyleMenu = React.memo(function ColorMenu() {
|
|||
)
|
||||
|
||||
return (
|
||||
<DropdownMenu.Root dir="ltr" onOpenChange={handleMenuOpenChange}>
|
||||
<DropdownMenu.Root
|
||||
dir="ltr"
|
||||
onOpenChange={handleMenuOpenChange}
|
||||
open={keepOpen ? true : undefined}
|
||||
>
|
||||
<DropdownMenu.Trigger asChild id="TD-Styles">
|
||||
<ToolButton variant="text">
|
||||
Styles
|
||||
|
@ -240,7 +257,7 @@ export const StyleMenu = React.memo(function ColorMenu() {
|
|||
<StyledRow id="TD-Styles-Dash-Container">
|
||||
Dash
|
||||
<StyledGroup dir="ltr" value={displayedStyle.dash} onValueChange={handleDashChange}>
|
||||
{Object.values(DashStyle).map((style) => (
|
||||
{Object.values(DashStyle).map(style => (
|
||||
<DMRadioItem
|
||||
key={style}
|
||||
isActive={style === displayedStyle.dash}
|
||||
|
@ -257,7 +274,7 @@ export const StyleMenu = React.memo(function ColorMenu() {
|
|||
<StyledRow id="TD-Styles-Size-Container">
|
||||
Size
|
||||
<StyledGroup dir="ltr" value={displayedStyle.size} onValueChange={handleSizeChange}>
|
||||
{Object.values(SizeStyle).map((sizeStyle) => (
|
||||
{Object.values(SizeStyle).map(sizeStyle => (
|
||||
<DMRadioItem
|
||||
key={sizeStyle}
|
||||
isActive={sizeStyle === displayedStyle.size}
|
||||
|
@ -277,7 +294,7 @@ export const StyleMenu = React.memo(function ColorMenu() {
|
|||
<StyledRow id="TD-Styles-Font-Container">
|
||||
Font
|
||||
<StyledGroup dir="ltr" value={displayedStyle.font} onValueChange={handleFontChange}>
|
||||
{Object.values(FontStyle).map((fontStyle) => (
|
||||
{Object.values(FontStyle).map(fontStyle => (
|
||||
<DMRadioItem
|
||||
key={fontStyle}
|
||||
isActive={fontStyle === displayedStyle.font}
|
||||
|
@ -299,7 +316,7 @@ export const StyleMenu = React.memo(function ColorMenu() {
|
|||
value={displayedStyle.textAlign}
|
||||
onValueChange={handleTextAlignChange}
|
||||
>
|
||||
{Object.values(AlignStyle).map((style) => (
|
||||
{Object.values(AlignStyle).map(style => (
|
||||
<DMRadioItem
|
||||
key={style}
|
||||
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>
|
||||
</DropdownMenu.Root>
|
||||
)
|
||||
|
|
|
@ -9,7 +9,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
|||
const canHandleEvent = React.useCallback(
|
||||
(ignoreMenus = false) => {
|
||||
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))
|
||||
},
|
||||
[ref]
|
||||
|
@ -159,7 +159,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
|||
|
||||
useHotkeys(
|
||||
'ctrl+shift+d,⌘+shift+d',
|
||||
(e) => {
|
||||
e => {
|
||||
if (!canHandleEvent(true)) return
|
||||
app.toggleDarkMode()
|
||||
e.preventDefault()
|
||||
|
@ -192,12 +192,17 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
|||
|
||||
// File System
|
||||
|
||||
const { onNewProject, onOpenProject, onSaveProject, onSaveProjectAs, onOpenMedia } =
|
||||
useFileSystemHandlers()
|
||||
const {
|
||||
onNewProject,
|
||||
onOpenProject,
|
||||
onSaveProject,
|
||||
onSaveProjectAs,
|
||||
onOpenMedia,
|
||||
} = useFileSystemHandlers()
|
||||
|
||||
useHotkeys(
|
||||
'ctrl+n,⌘+n',
|
||||
(e) => {
|
||||
e => {
|
||||
if (!canHandleEvent()) return
|
||||
|
||||
onNewProject(e)
|
||||
|
@ -207,7 +212,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
|||
)
|
||||
useHotkeys(
|
||||
'ctrl+s,⌘+s',
|
||||
(e) => {
|
||||
e => {
|
||||
if (!canHandleEvent()) return
|
||||
|
||||
onSaveProject(e)
|
||||
|
@ -218,7 +223,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
|||
|
||||
useHotkeys(
|
||||
'ctrl+shift+s,⌘+shift+s',
|
||||
(e) => {
|
||||
e => {
|
||||
if (!canHandleEvent()) return
|
||||
|
||||
onSaveProjectAs(e)
|
||||
|
@ -228,7 +233,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
|||
)
|
||||
useHotkeys(
|
||||
'ctrl+o,⌘+o',
|
||||
(e) => {
|
||||
e => {
|
||||
if (!canHandleEvent()) return
|
||||
|
||||
onOpenProject(e)
|
||||
|
@ -238,7 +243,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
|||
)
|
||||
useHotkeys(
|
||||
'ctrl+u,⌘+u',
|
||||
(e) => {
|
||||
e => {
|
||||
if (!canHandleEvent()) return
|
||||
onOpenMedia(e)
|
||||
},
|
||||
|
@ -306,7 +311,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
|||
|
||||
useHotkeys(
|
||||
'ctrl+=,⌘+=,ctrl+num_subtract,⌘+num_subtract',
|
||||
(e) => {
|
||||
e => {
|
||||
if (!canHandleEvent(true)) return
|
||||
app.zoomIn()
|
||||
e.preventDefault()
|
||||
|
@ -317,7 +322,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
|||
|
||||
useHotkeys(
|
||||
'ctrl+-,⌘+-,ctrl+num_add,⌘+num_add',
|
||||
(e) => {
|
||||
e => {
|
||||
if (!canHandleEvent(true)) return
|
||||
|
||||
app.zoomOut()
|
||||
|
@ -361,7 +366,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
|||
|
||||
useHotkeys(
|
||||
'ctrl+d,⌘+d',
|
||||
(e) => {
|
||||
e => {
|
||||
if (!canHandleEvent()) return
|
||||
|
||||
app.duplicate()
|
||||
|
@ -536,7 +541,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
|||
|
||||
useHotkeys(
|
||||
'⌘+shift+c,ctrl+shift+c',
|
||||
(e) => {
|
||||
e => {
|
||||
if (!canHandleEvent()) return
|
||||
|
||||
app.copySvg()
|
||||
|
@ -571,7 +576,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
|||
|
||||
useHotkeys(
|
||||
'⌘+g,ctrl+g',
|
||||
(e) => {
|
||||
e => {
|
||||
if (!canHandleEvent()) return
|
||||
|
||||
app.group()
|
||||
|
@ -583,7 +588,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
|||
|
||||
useHotkeys(
|
||||
'⌘+shift+g,ctrl+shift+g',
|
||||
(e) => {
|
||||
e => {
|
||||
if (!canHandleEvent()) return
|
||||
|
||||
app.ungroup()
|
||||
|
@ -637,7 +642,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
|||
|
||||
useHotkeys(
|
||||
'ctrl+shift+backspace,⌘+shift+backspace',
|
||||
(e) => {
|
||||
e => {
|
||||
if (!canHandleEvent()) return
|
||||
if (app.settings.isDebugMode) {
|
||||
app.resetDocument()
|
||||
|
@ -652,7 +657,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
|||
|
||||
useHotkeys(
|
||||
'alt+command+l,alt+ctrl+l',
|
||||
(e) => {
|
||||
e => {
|
||||
if (!canHandleEvent(true)) return
|
||||
app.style({ textAlign: AlignStyle.Start })
|
||||
e.preventDefault()
|
||||
|
@ -663,7 +668,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
|||
|
||||
useHotkeys(
|
||||
'alt+command+t,alt+ctrl+t',
|
||||
(e) => {
|
||||
e => {
|
||||
if (!canHandleEvent(true)) return
|
||||
app.style({ textAlign: AlignStyle.Middle })
|
||||
e.preventDefault()
|
||||
|
@ -674,7 +679,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
|||
|
||||
useHotkeys(
|
||||
'alt+command+r,alt+ctrl+r',
|
||||
(e) => {
|
||||
e => {
|
||||
if (!canHandleEvent(true)) return
|
||||
app.style({ textAlign: AlignStyle.End })
|
||||
e.preventDefault()
|
||||
|
|
|
@ -4086,6 +4086,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
isSnapping: false,
|
||||
isDebugMode: false,
|
||||
isReadonlyMode: false,
|
||||
keepStyleMenuOpen: false,
|
||||
nudgeDistanceLarge: 16,
|
||||
nudgeDistanceSmall: 1,
|
||||
showRotateHandles: true,
|
||||
|
|
|
@ -11,8 +11,8 @@ export function migrate(document: TDDocument, newVersion: number): TDDocument {
|
|||
// Remove unused assets when loading a document
|
||||
const assetIdsInUse = new Set<string>()
|
||||
|
||||
Object.values(document.pages).forEach((page) =>
|
||||
Object.values(page.shapes).forEach((shape) => {
|
||||
Object.values(document.pages).forEach(page =>
|
||||
Object.values(page.shapes).forEach(shape => {
|
||||
const { parentId, children, assetId } = shape
|
||||
|
||||
if (assetId) {
|
||||
|
@ -26,7 +26,7 @@ export function migrate(document: TDDocument, newVersion: number): TDDocument {
|
|||
}
|
||||
|
||||
if (shape.type === TDShapeType.Group && children) {
|
||||
children.forEach((childId) => {
|
||||
children.forEach(childId => {
|
||||
if (!page.shapes[childId]) {
|
||||
console.warn('Encountered a parent with a missing child!', shape.id, childId)
|
||||
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)) {
|
||||
delete document.assets[assetId]
|
||||
}
|
||||
|
@ -47,22 +47,22 @@ export function migrate(document: TDDocument, newVersion: number): TDDocument {
|
|||
if (version === newVersion) return document
|
||||
|
||||
if (version < 14) {
|
||||
Object.values(document.pages).forEach((page) => {
|
||||
Object.values(document.pages).forEach(page => {
|
||||
Object.values(page.shapes)
|
||||
.filter((shape) => shape.type === TDShapeType.Text)
|
||||
.forEach((shape) => (shape as TextShape).style.font === FontStyle.Script)
|
||||
.filter(shape => shape.type === TDShapeType.Text)
|
||||
.forEach(shape => (shape as TextShape).style.font === FontStyle.Script)
|
||||
})
|
||||
}
|
||||
|
||||
// Lowercase styles, move binding meta to binding
|
||||
if (version <= 13) {
|
||||
Object.values(document.pages).forEach((page) => {
|
||||
Object.values(page.bindings).forEach((binding) => {
|
||||
Object.values(document.pages).forEach(page => {
|
||||
Object.values(page.bindings).forEach(binding => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
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]) => {
|
||||
if (typeof style === 'string') {
|
||||
// @ts-ignore
|
||||
|
@ -95,8 +95,8 @@ export function migrate(document: TDDocument, newVersion: number): TDDocument {
|
|||
document.assets = {}
|
||||
}
|
||||
|
||||
Object.values(document.pages).forEach((page) => {
|
||||
Object.values(page.shapes).forEach((shape) => {
|
||||
Object.values(document.pages).forEach(page => {
|
||||
Object.values(page.shapes).forEach(shape => {
|
||||
if (version < 15.2) {
|
||||
if (shape.type === TDShapeType.Image || shape.type === TDShapeType.Video) {
|
||||
shape.style.isFilled = true
|
||||
|
@ -118,8 +118,8 @@ export function migrate(document: TDDocument, newVersion: number): TDDocument {
|
|||
})
|
||||
|
||||
// Cleanup
|
||||
Object.values(document.pageStates).forEach((pageState) => {
|
||||
pageState.selectedIds = pageState.selectedIds.filter((id) => {
|
||||
Object.values(document.pageStates).forEach(pageState => {
|
||||
pageState.selectedIds = pageState.selectedIds.filter(id => {
|
||||
return document.pages[pageState.id].shapes[id] !== undefined
|
||||
})
|
||||
pageState.bindingId = undefined
|
||||
|
|
|
@ -85,6 +85,7 @@ export interface TDSnapshot {
|
|||
isPenMode: boolean
|
||||
isReadonlyMode: boolean
|
||||
isZoomSnap: boolean
|
||||
keepStyleMenuOpen: boolean
|
||||
nudgeDistanceSmall: number
|
||||
nudgeDistanceLarge: number
|
||||
isFocusMode: boolean
|
||||
|
|
Loading…
Reference in a new issue