Add "paste at cursor" option, which toggles how cmd + v and cmd + shift + v work (#4088)

Add an option to make paste at cursor the default. 

Not sure if we also want to expose this on tldraw.com? For now I did,
but happy to remove if we'd want to keep the preferences simple.

We could also add this to the `TldrawOptions`, but it felt like some
apps might actually allow this customization on a per user level.

Solves https://github.com/tldraw/tldraw/issues/4066

### Change type

- [ ] `bugfix`
- [ ] `improvement`
- [x] `feature`
- [ ] `api`
- [ ] `other`

### Test plan

1. Copy / pasting should still work as it works now: `⌘ + v` pastes on
top of the shape, `⌘ + ⇧ + v` pastes at cursor.
2. There's now a new option under Preferences to paste at cursor. This
just swaps the logic between the two shortcuts: `⌘ + v` then pastes at
cursor and `⌘ + ⇧ + v` pastes on top of the shape.

### Release notes

- Allow users and sdk users to make pasting at the cursor a default
instead of only being available with `⌘ + ⇧ + v`.
This commit is contained in:
Mitja Bezenšek 2024-07-09 11:09:34 +02:00 committed by GitHub
parent 7d5a6cbe3b
commit a85c215ffc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 61 additions and 3 deletions

View file

@ -83,6 +83,8 @@
"action.toggle-auto-size": "Toggle auto size", "action.toggle-auto-size": "Toggle auto size",
"action.toggle-dark-mode.menu": "Dark mode", "action.toggle-dark-mode.menu": "Dark mode",
"action.toggle-dark-mode": "Toggle dark mode", "action.toggle-dark-mode": "Toggle dark mode",
"action.toggle-paste-at-cursor.menu": "Paste at cursor",
"action.toggle-paste-at-cursor": "Toggle paste at cursor",
"action.toggle-wrap-mode.menu": "Select on wrap", "action.toggle-wrap-mode.menu": "Select on wrap",
"action.toggle-wrap-mode": "Toggle Select on wrap", "action.toggle-wrap-mode": "Toggle Select on wrap",
"action.toggle-reduce-motion.menu": "Reduce motion", "action.toggle-reduce-motion.menu": "Reduce motion",

View file

@ -724,6 +724,7 @@ export const defaultUserPreferences: Readonly<{
isWrapMode: false; isWrapMode: false;
locale: "ar" | "ca" | "cs" | "da" | "de" | "en" | "es" | "fa" | "fi" | "fr" | "gl" | "he" | "hi-in" | "hr" | "hu" | "id" | "it" | "ja" | "ko-kr" | "ku" | "my" | "ne" | "no" | "pl" | "pt-br" | "pt-pt" | "ro" | "ru" | "sl" | "sv" | "te" | "th" | "tr" | "uk" | "vi" | "zh-cn" | "zh-tw"; locale: "ar" | "ca" | "cs" | "da" | "de" | "en" | "es" | "fa" | "fi" | "fr" | "gl" | "he" | "hi-in" | "hr" | "hu" | "id" | "it" | "ja" | "ko-kr" | "ku" | "my" | "ne" | "no" | "pl" | "pt-br" | "pt-pt" | "ro" | "ru" | "sl" | "sv" | "te" | "th" | "tr" | "uk" | "vi" | "zh-cn" | "zh-tw";
name: "New User"; name: "New User";
pasteAtCursor: false;
}>; }>;
// @public // @public
@ -3371,6 +3372,8 @@ export interface TLUserPreferences {
locale?: null | string; locale?: null | string;
// (undocumented) // (undocumented)
name?: null | string; name?: null | string;
// (undocumented)
pasteAtCursor?: boolean | null;
} }
// @public (undocumented) // @public (undocumented)
@ -3480,6 +3483,8 @@ export class UserPreferencesManager {
// (undocumented) // (undocumented)
getName(): string; getName(): string;
// (undocumented) // (undocumented)
getPasteAtCursor(): boolean;
// (undocumented)
getUserPreferences(): { getUserPreferences(): {
animationSpeed: number; animationSpeed: number;
color: string; color: string;

View file

@ -22,6 +22,7 @@ export interface TLUserPreferences {
isSnapMode?: boolean | null isSnapMode?: boolean | null
isWrapMode?: boolean | null isWrapMode?: boolean | null
isDynamicSizeMode?: boolean | null isDynamicSizeMode?: boolean | null
pasteAtCursor?: boolean | null
} }
interface UserDataSnapshot { interface UserDataSnapshot {
@ -46,6 +47,7 @@ const userTypeValidator: T.Validator<TLUserPreferences> = T.object<TLUserPrefere
isSnapMode: T.boolean.nullable().optional(), isSnapMode: T.boolean.nullable().optional(),
isWrapMode: T.boolean.nullable().optional(), isWrapMode: T.boolean.nullable().optional(),
isDynamicSizeMode: T.boolean.nullable().optional(), isDynamicSizeMode: T.boolean.nullable().optional(),
pasteAtCursor: T.boolean.nullable().optional(),
}) })
const Versions = { const Versions = {
@ -56,6 +58,7 @@ const Versions = {
AddExcalidrawSelectMode: 5, AddExcalidrawSelectMode: 5,
AddDynamicSizeMode: 6, AddDynamicSizeMode: 6,
AllowSystemColorScheme: 7, AllowSystemColorScheme: 7,
AddPasteAtCursor: 8,
} as const } as const
const CURRENT_VERSION = Math.max(...Object.values(Versions)) const CURRENT_VERSION = Math.max(...Object.values(Versions))
@ -88,6 +91,9 @@ function migrateSnapshot(data: { version: number; user: any }) {
if (data.version < Versions.AddDynamicSizeMode) { if (data.version < Versions.AddDynamicSizeMode) {
data.user.isDynamicSizeMode = false data.user.isDynamicSizeMode = false
} }
if (data.version < Versions.AddPasteAtCursor) {
data.user.pasteAtCursor = false
}
// finally // finally
data.version = CURRENT_VERSION data.version = CURRENT_VERSION
@ -131,6 +137,7 @@ export const defaultUserPreferences = Object.freeze({
isSnapMode: false, isSnapMode: false,
isWrapMode: false, isWrapMode: false,
isDynamicSizeMode: false, isDynamicSizeMode: false,
pasteAtCursor: false,
}) satisfies Readonly<Omit<TLUserPreferences, 'id'>> }) satisfies Readonly<Omit<TLUserPreferences, 'id'>>
/** @public */ /** @public */

View file

@ -97,4 +97,8 @@ export class UserPreferencesManager {
this.user.userPreferences.get().isDynamicSizeMode ?? defaultUserPreferences.isDynamicSizeMode this.user.userPreferences.get().isDynamicSizeMode ?? defaultUserPreferences.isDynamicSizeMode
) )
} }
@computed getPasteAtCursor() {
return this.user.userPreferences.get().pasteAtCursor ?? defaultUserPreferences.pasteAtCursor
}
} }

File diff suppressed because one or more lines are too long

View file

@ -21,6 +21,7 @@ import {
ToggleFocusModeItem, ToggleFocusModeItem,
ToggleGridItem, ToggleGridItem,
ToggleLockMenuItem, ToggleLockMenuItem,
TogglePasteAtCursorItem,
ToggleReduceMotionItem, ToggleReduceMotionItem,
ToggleSnapModeItem, ToggleSnapModeItem,
ToggleToolLockItem, ToggleToolLockItem,
@ -174,6 +175,7 @@ export function PreferencesGroup() {
<ToggleEdgeScrollingItem /> <ToggleEdgeScrollingItem />
<ToggleReduceMotionItem /> <ToggleReduceMotionItem />
<ToggleDynamicSizeModeItem /> <ToggleDynamicSizeModeItem />
<TogglePasteAtCursorItem />
<ToggleDebugModeItem /> <ToggleDebugModeItem />
</TldrawUiMenuGroup> </TldrawUiMenuGroup>
<TldrawUiMenuGroup id="color-scheme"> <TldrawUiMenuGroup id="color-scheme">

View file

@ -626,6 +626,14 @@ export function ToggleDynamicSizeModeItem() {
) )
} }
/** @public @react */
export function TogglePasteAtCursorItem() {
const actions = useActions()
const editor = useEditor()
const pasteAtCursor = useValue('paste at cursor', () => editor.user.getPasteAtCursor(), [editor])
return <TldrawUiMenuCheckboxItem {...actions['toggle-paste-at-cursor']} checked={pasteAtCursor} />
}
/* ---------------------- Print --------------------- */ /* ---------------------- Print --------------------- */
/** @public @react */ /** @public @react */
export function PrintItem() { export function PrintItem() {

View file

@ -1149,6 +1149,21 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
}, },
checkbox: true, checkbox: true,
}, },
{
id: 'toggle-paste-at-cursor',
label: {
default: 'action.toggle-paste-at-cursor',
menu: 'action.toggle-paste-at-cursor.menu',
},
readonlyOk: false,
onSelect(source) {
trackEvent('toggle-paste-at-cursor', { source })
editor.user.updateUserPreferences({
pasteAtCursor: !editor.user.getPasteAtCursor(),
})
},
checkbox: true,
},
{ {
id: 'toggle-reduce-motion', id: 'toggle-reduce-motion',
label: { label: {

View file

@ -88,6 +88,7 @@ export interface TLUiEventMap {
'toggle-focus-mode': null 'toggle-focus-mode': null
'toggle-debug-mode': null 'toggle-debug-mode': null
'toggle-dynamic-size-mode': null 'toggle-dynamic-size-mode': null
'toggle-paste-at-cursor': null
'toggle-lock': null 'toggle-lock': null
'toggle-reduce-motion': null 'toggle-reduce-motion': null
'toggle-edge-scrolling': null 'toggle-edge-scrolling': null

View file

@ -671,12 +671,20 @@ export function useNativeClipboardEvents() {
// First try to use the clipboard data on the event // First try to use the clipboard data on the event
if (e.clipboardData && !editor.inputs.shiftKey) { if (e.clipboardData && !editor.inputs.shiftKey) {
handlePasteFromEventClipboardData(editor, e.clipboardData) if (editor.user.getPasteAtCursor()) {
handlePasteFromEventClipboardData(editor, e.clipboardData, editor.inputs.currentPagePoint)
} else {
handlePasteFromEventClipboardData(editor, e.clipboardData)
}
} else { } else {
// Or else use the clipboard API // Or else use the clipboard API
navigator.clipboard.read().then((clipboardItems) => { navigator.clipboard.read().then((clipboardItems) => {
if (Array.isArray(clipboardItems) && clipboardItems[0] instanceof ClipboardItem) { if (Array.isArray(clipboardItems) && clipboardItems[0] instanceof ClipboardItem) {
handlePasteFromClipboardApi(editor, clipboardItems, editor.inputs.currentPagePoint) if (e.clipboardData && editor.user.getPasteAtCursor()) {
handlePasteFromClipboardApi(editor, clipboardItems)
} else {
handlePasteFromClipboardApi(editor, clipboardItems, editor.inputs.currentPagePoint)
}
} }
}) })
} }

View file

@ -87,6 +87,8 @@ export type TLUiTranslationKey =
| 'action.toggle-auto-size' | 'action.toggle-auto-size'
| 'action.toggle-dark-mode.menu' | 'action.toggle-dark-mode.menu'
| 'action.toggle-dark-mode' | 'action.toggle-dark-mode'
| 'action.toggle-paste-at-cursor.menu'
| 'action.toggle-paste-at-cursor'
| 'action.toggle-wrap-mode.menu' | 'action.toggle-wrap-mode.menu'
| 'action.toggle-wrap-mode' | 'action.toggle-wrap-mode'
| 'action.toggle-reduce-motion.menu' | 'action.toggle-reduce-motion.menu'

View file

@ -87,6 +87,8 @@ export const DEFAULT_TRANSLATION = {
'action.toggle-auto-size': 'Toggle auto size', 'action.toggle-auto-size': 'Toggle auto size',
'action.toggle-dark-mode.menu': 'Dark mode', 'action.toggle-dark-mode.menu': 'Dark mode',
'action.toggle-dark-mode': 'Toggle dark mode', 'action.toggle-dark-mode': 'Toggle dark mode',
'action.toggle-paste-at-cursor.menu': 'Paste at cursor',
'action.toggle-paste-at-cursor': 'Toggle paste at cursor',
'action.toggle-wrap-mode.menu': 'Select on wrap', 'action.toggle-wrap-mode.menu': 'Select on wrap',
'action.toggle-wrap-mode': 'Toggle Select on wrap', 'action.toggle-wrap-mode': 'Toggle Select on wrap',
'action.toggle-reduce-motion.menu': 'Reduce motion', 'action.toggle-reduce-motion.menu': 'Reduce motion',