fix-editor-sync-context-menu-selection-snapshot
This commit is contained in:
parent
9d95f34bd9
commit
ab84000919
@ -9,7 +9,7 @@ import {
|
|||||||
Tabs,
|
Tabs,
|
||||||
Typography,
|
Typography,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import { useCallback, useEffect, useMemo, useReducer, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react';
|
||||||
import { bindEditorHotkeys } from './hotkeys';
|
import { bindEditorHotkeys } from './hotkeys';
|
||||||
import { Canvas } from './Canvas';
|
import { Canvas } from './Canvas';
|
||||||
import { ContextMenu, type ContextMenuState } from './ContextMenu';
|
import { ContextMenu, type ContextMenuState } from './ContextMenu';
|
||||||
@ -48,17 +48,26 @@ export function EditorApp() {
|
|||||||
const selected = state.doc.screen.nodes.find((n) => n.id === state.selection.ids[0]);
|
const selected = state.doc.screen.nodes.find((n) => n.id === state.selection.ids[0]);
|
||||||
const hasSelection = state.selection.ids.length > 0;
|
const hasSelection = state.selection.ids.length > 0;
|
||||||
|
|
||||||
const selection = useMemo(() => {
|
// Note: selection lock/visibility flags for the context menu are computed from `selectionIdsForMenu` below.
|
||||||
const ids = new Set(state.selection.ids);
|
|
||||||
|
// Context-menu parity: right-click selection updates happen via reducer dispatch,
|
||||||
|
// but the context menu opens immediately.
|
||||||
|
// Use the captured selectionIds from the context menu state to avoid a one-frame mismatch.
|
||||||
|
const selectionIdsForMenu = ctxMenu?.selectionIds ?? state.selection.ids;
|
||||||
|
|
||||||
|
const selectionForMenu = useMemo(() => {
|
||||||
|
const ids = new Set(selectionIdsForMenu);
|
||||||
return state.doc.screen.nodes.filter((n) => ids.has(n.id));
|
return state.doc.screen.nodes.filter((n) => ids.has(n.id));
|
||||||
}, [state.doc.screen.nodes, state.selection.ids]);
|
}, [selectionIdsForMenu, state.doc.screen.nodes]);
|
||||||
|
|
||||||
const selectionAllLocked = selection.length > 0 && selection.every((n) => n.locked);
|
const menuSelectionAllLocked = selectionForMenu.length > 0 && selectionForMenu.every((n) => n.locked);
|
||||||
const selectionSomeLocked = selection.length > 0 && selection.some((n) => n.locked) && !selectionAllLocked;
|
const menuSelectionSomeLocked =
|
||||||
const selectionHasUnlocked = selection.length > 0 && selection.some((n) => !n.locked);
|
selectionForMenu.length > 0 && selectionForMenu.some((n) => n.locked) && !menuSelectionAllLocked;
|
||||||
|
const menuSelectionHasUnlocked = selectionForMenu.length > 0 && selectionForMenu.some((n) => !n.locked);
|
||||||
|
|
||||||
const selectionAllHidden = selection.length > 0 && selection.every((n) => n.hidden);
|
const menuSelectionAllHidden = selectionForMenu.length > 0 && selectionForMenu.every((n) => n.hidden);
|
||||||
const selectionSomeHidden = selection.length > 0 && selection.some((n) => n.hidden) && !selectionAllHidden;
|
const menuSelectionSomeHidden =
|
||||||
|
selectionForMenu.length > 0 && selectionForMenu.some((n) => n.hidden) && !menuSelectionAllHidden;
|
||||||
|
|
||||||
const bounds = useMemo(
|
const bounds = useMemo(
|
||||||
() => ({ w: state.doc.screen.width, h: state.doc.screen.height }),
|
() => ({ w: state.doc.screen.width, h: state.doc.screen.height }),
|
||||||
@ -77,10 +86,29 @@ export function EditorApp() {
|
|||||||
|
|
||||||
const closeContextMenu = useCallback(() => setCtxMenu(null), []);
|
const closeContextMenu = useCallback(() => setCtxMenu(null), []);
|
||||||
|
|
||||||
// Selection parity: if selection changes via hotkeys/toolbar, close any open context menu.
|
const ctxMenuSyncedRef = useRef(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Reset the sync gate whenever a new context menu opens/closes.
|
||||||
|
ctxMenuSyncedRef.current = false;
|
||||||
|
}, [ctxMenu?.selectionKey]);
|
||||||
|
|
||||||
|
// Selection parity: if selection changes via hotkeys/toolbar *after* the context menu has opened,
|
||||||
|
// close it. But don't close immediately during the one-frame gap where right-click selection
|
||||||
|
// has not been reduced into state yet.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!ctxMenu) return;
|
if (!ctxMenu) return;
|
||||||
|
|
||||||
const currentKey = state.selection.ids.join('|');
|
const currentKey = state.selection.ids.join('|');
|
||||||
|
|
||||||
|
// Wait until reducer state has caught up with the context menu's intended selection.
|
||||||
|
if (!ctxMenuSyncedRef.current) {
|
||||||
|
if (currentKey === ctxMenu.selectionKey) {
|
||||||
|
ctxMenuSyncedRef.current = true;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (currentKey !== ctxMenu.selectionKey) {
|
if (currentKey !== ctxMenu.selectionKey) {
|
||||||
setCtxMenu(null);
|
setCtxMenu(null);
|
||||||
}
|
}
|
||||||
@ -522,12 +550,12 @@ export function EditorApp() {
|
|||||||
|
|
||||||
<ContextMenu
|
<ContextMenu
|
||||||
state={ctxMenu}
|
state={ctxMenu}
|
||||||
selectionIds={state.selection.ids}
|
selectionIds={selectionIdsForMenu}
|
||||||
selectionAllLocked={selectionAllLocked}
|
selectionAllLocked={menuSelectionAllLocked}
|
||||||
selectionSomeLocked={selectionSomeLocked}
|
selectionSomeLocked={menuSelectionSomeLocked}
|
||||||
selectionHasUnlocked={selectionHasUnlocked}
|
selectionHasUnlocked={menuSelectionHasUnlocked}
|
||||||
selectionAllHidden={selectionAllHidden}
|
selectionAllHidden={menuSelectionAllHidden}
|
||||||
selectionSomeHidden={selectionSomeHidden}
|
selectionSomeHidden={menuSelectionSomeHidden}
|
||||||
hasAnyNodes={state.doc.screen.nodes.length > 0}
|
hasAnyNodes={state.doc.screen.nodes.length > 0}
|
||||||
onClose={closeContextMenu}
|
onClose={closeContextMenu}
|
||||||
onAddTextAt={(x, y) => dispatch({ type: 'addTextAt', x, y })}
|
onAddTextAt={(x, y) => dispatch({ type: 'addTextAt', x, y })}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user