전략편집기 수정
This commit is contained in:
@@ -10,18 +10,24 @@ import {
|
||||
} from '../../utils/paletteDragSession';
|
||||
|
||||
/**
|
||||
* Tauri WebView — 드래그 중 전체 화면 overlay 가 pointer 이벤트·drop 을 수신
|
||||
* Tauri WebView — 드래그 overlay (라벨 위치는 ref 갱신, move 마다 React re-render 없음)
|
||||
*/
|
||||
export default function PaletteDragOverlay() {
|
||||
const [session, setSession] = useState<PaletteDragOverlaySession | null>(null);
|
||||
const [cursor, setCursor] = useState({ x: 0, y: 0 });
|
||||
const overlayRef = useRef<HTMLDivElement | null>(null);
|
||||
const labelRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
registerPaletteDragOverlay({
|
||||
mount: next => {
|
||||
setCursor({ x: next.x, y: next.y });
|
||||
setSession(next);
|
||||
requestAnimationFrame(() => {
|
||||
const label = labelRef.current;
|
||||
if (label) {
|
||||
label.style.left = `${next.x}px`;
|
||||
label.style.top = `${next.y}px`;
|
||||
}
|
||||
});
|
||||
},
|
||||
unmount: () => setSession(null),
|
||||
bindElement: el => {
|
||||
@@ -41,7 +47,7 @@ export default function PaletteDragOverlay() {
|
||||
try {
|
||||
overlayRef.current.setPointerCapture(session.pointerId);
|
||||
} catch {
|
||||
/* body capture fallback */
|
||||
/* ignore */
|
||||
}
|
||||
}, [session]);
|
||||
|
||||
@@ -49,7 +55,11 @@ export default function PaletteDragOverlay() {
|
||||
if (!session || e.pointerId !== session.pointerId) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setCursor({ x: e.clientX, y: e.clientY });
|
||||
const label = labelRef.current;
|
||||
if (label) {
|
||||
label.style.left = `${e.clientX}px`;
|
||||
label.style.top = `${e.clientY}px`;
|
||||
}
|
||||
notifyPaletteDragMove(e.clientX, e.clientY);
|
||||
}, [session]);
|
||||
|
||||
@@ -78,8 +88,9 @@ export default function PaletteDragOverlay() {
|
||||
onPointerCancel={onPointerCancel}
|
||||
>
|
||||
<div
|
||||
ref={labelRef}
|
||||
className="se-palette-drag-overlay-label"
|
||||
style={{ left: cursor.x, top: cursor.y }}
|
||||
style={{ left: session.x, top: session.y }}
|
||||
>
|
||||
{session.payload.label}
|
||||
</div>
|
||||
|
||||
@@ -1281,16 +1281,22 @@ function StrategyEditorCanvasInner({
|
||||
updatePreviewRef.current = updateDropPreview;
|
||||
const resolveDropFromClientRef = useRef(resolveDropFromClient);
|
||||
resolveDropFromClientRef.current = resolveDropFromClient;
|
||||
const lastPreviewAnchorRef = useRef<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!needsPointerPaletteDrag()) return;
|
||||
return subscribePaletteDrag((ev: PaletteDragEvent) => {
|
||||
if (ev.phase === 'move') {
|
||||
if (!isOverStrategyBuilder(ev.x, ev.y)) {
|
||||
if (lastPreviewAnchorRef.current != null) {
|
||||
lastPreviewAnchorRef.current = null;
|
||||
clearPreviewRef.current();
|
||||
}
|
||||
return;
|
||||
}
|
||||
const { flowPos, anchorId } = resolveDropFromClientRef.current(ev.x, ev.y);
|
||||
if (anchorId === lastPreviewAnchorRef.current) return;
|
||||
lastPreviewAnchorRef.current = anchorId;
|
||||
if (anchorId) {
|
||||
updatePreviewRef.current(anchorId, flowPos);
|
||||
} else {
|
||||
@@ -1298,6 +1304,9 @@ function StrategyEditorCanvasInner({
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (ev.phase === 'end' || ev.phase === 'cancel') {
|
||||
lastPreviewAnchorRef.current = null;
|
||||
}
|
||||
if (ev.phase === 'cancel') {
|
||||
clearPreviewRef.current();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||
import type { LogicNode } from '../../utils/strategyTypes';
|
||||
import {
|
||||
CondEditor,
|
||||
@@ -31,6 +31,15 @@ import {
|
||||
import StartTimeframeCheckboxes from './StartTimeframeCheckboxes';
|
||||
import LogicGateOpToggle from './LogicGateOpToggle';
|
||||
import { formatStartCandleTypesLabel } from '../../utils/strategyStartNodes';
|
||||
import { buildSidewaysFilterNode } from '../../utils/sidewaysFilterPaletteStorage';
|
||||
import { isOverListEditor } from '../../utils/flowScreenProjection';
|
||||
import {
|
||||
findPaletteDropKeyAtPoint,
|
||||
needsPointerPaletteDrag,
|
||||
setPaletteDragDropHandler,
|
||||
subscribePaletteDrag,
|
||||
type PaletteDragPayload,
|
||||
} from '../../utils/paletteDragSession';
|
||||
|
||||
const NODE_COLORS: Record<string, string> = {
|
||||
AND: '#4caf50',
|
||||
@@ -134,6 +143,7 @@ const TreeNodeComp: React.FC<TreeNodeProps> = ({
|
||||
<div
|
||||
className={`sp-tree-node${dragOverKey === nodeDropKey ? ' sp-tree-node--over' : ''}`}
|
||||
style={{ marginLeft: depth > 0 ? 12 : 0 }}
|
||||
data-palette-drop-key={isLogic ? nodeDropKey : undefined}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
@@ -327,6 +337,7 @@ function StartSectionBlock({
|
||||
|
||||
<div
|
||||
className={`sp-start-body${dragOverKey === sectionDropKey ? ' sp-start-body--over' : ''}`}
|
||||
data-palette-drop-key={sectionDropKey}
|
||||
onDragOver={e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
@@ -380,8 +391,15 @@ export default function StrategyListEditor({
|
||||
orphans = [],
|
||||
}: Props) {
|
||||
const [dragOverKey, setDragOverKey] = useState<string | null>(null);
|
||||
const dragOverKeyRef = useRef<string | null>(null);
|
||||
const sections = collectStartSections(editorState);
|
||||
|
||||
const setDragOverKeyThrottled = useCallback((key: string | null) => {
|
||||
if (dragOverKeyRef.current === key) return;
|
||||
dragOverKeyRef.current = key;
|
||||
setDragOverKey(key);
|
||||
}, []);
|
||||
|
||||
const handleAddStart = useCallback(() => {
|
||||
if (onAddStart) {
|
||||
onAddStart();
|
||||
@@ -406,6 +424,92 @@ export default function StrategyListEditor({
|
||||
}
|
||||
}, [editorState, onEditorStateChange, onOrphansChange, orphans]);
|
||||
|
||||
const resolvePaletteNode = useCallback((
|
||||
data: { type: string; value: string; label: string; composite?: boolean },
|
||||
) => {
|
||||
if (data.type === 'sidewaysFilter') {
|
||||
return buildSidewaysFilterNode(data.value, def);
|
||||
}
|
||||
return makeNode(data.type, data.value, signalTab, def, makeNodeOptionsFromPalette(data));
|
||||
}, [signalTab, def]);
|
||||
|
||||
const applyPaletteDropAtKey = useCallback((
|
||||
dropKey: string | null,
|
||||
data: PaletteDragPayload,
|
||||
) => {
|
||||
if (!dropKey) return;
|
||||
if (dropKey === LIST_DROP_KEY) {
|
||||
if (data.type === 'start') handleAddStart();
|
||||
return;
|
||||
}
|
||||
|
||||
const rootMatch = dropKey.match(/^([^:]+):__root__$/);
|
||||
const nodeMatch = dropKey.match(/^([^:]+):([^:]+)$/);
|
||||
const startId = rootMatch?.[1] ?? nodeMatch?.[1];
|
||||
const targetId = rootMatch ? null : nodeMatch?.[2];
|
||||
if (!startId) return;
|
||||
|
||||
if (data.type === 'start') {
|
||||
handleAddStart();
|
||||
return;
|
||||
}
|
||||
|
||||
const section = collectStartSections(editorState).find(s => s.startId === startId);
|
||||
if (!section) return;
|
||||
|
||||
const newNode = resolvePaletteNode(data);
|
||||
if (!newNode) return;
|
||||
|
||||
const root = section.root;
|
||||
if (!root) {
|
||||
handleRootChange(startId, newNode);
|
||||
return;
|
||||
}
|
||||
if (!targetId) {
|
||||
handleRootChange(startId, mergeAtRoot(root, newNode, data.type === 'operator'));
|
||||
return;
|
||||
}
|
||||
handleRootChange(startId, addChild(root, targetId, newNode));
|
||||
}, [
|
||||
editorState,
|
||||
handleAddStart,
|
||||
handleRootChange,
|
||||
resolvePaletteNode,
|
||||
]);
|
||||
|
||||
const applyDropRef = useRef(applyPaletteDropAtKey);
|
||||
applyDropRef.current = applyPaletteDropAtKey;
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!needsPointerPaletteDrag()) return undefined;
|
||||
setPaletteDragDropHandler(ev => {
|
||||
if (ev.phase !== 'end') return;
|
||||
dragOverKeyRef.current = null;
|
||||
setDragOverKey(null);
|
||||
if (!isOverListEditor(ev.x, ev.y)) return;
|
||||
applyDropRef.current(findPaletteDropKeyAtPoint(ev.x, ev.y), ev.payload);
|
||||
});
|
||||
return () => setPaletteDragDropHandler(null);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!needsPointerPaletteDrag()) return;
|
||||
return subscribePaletteDrag(ev => {
|
||||
if (ev.phase === 'move') {
|
||||
if (!isOverListEditor(ev.x, ev.y)) {
|
||||
setDragOverKeyThrottled(null);
|
||||
return;
|
||||
}
|
||||
setDragOverKeyThrottled(findPaletteDropKeyAtPoint(ev.x, ev.y));
|
||||
return;
|
||||
}
|
||||
if (ev.phase === 'cancel') {
|
||||
dragOverKeyRef.current = null;
|
||||
setDragOverKey(null);
|
||||
}
|
||||
});
|
||||
}, [setDragOverKeyThrottled]);
|
||||
|
||||
const handleListDragOver = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.dataTransfer.dropEffect = 'copy';
|
||||
@@ -429,6 +533,7 @@ export default function StrategyListEditor({
|
||||
return (
|
||||
<div
|
||||
className={`se-list-editor sp-dropzone${dragOverKey === LIST_DROP_KEY ? ' se-list-editor--over' : ''}`}
|
||||
data-palette-drop-key={LIST_DROP_KEY}
|
||||
onDragOver={handleListDragOver}
|
||||
onDragLeave={() => setDragOverKey(null)}
|
||||
onDrop={handleListDrop}
|
||||
|
||||
@@ -72,6 +72,14 @@ export function projectScreenToFlowPosition(
|
||||
);
|
||||
}
|
||||
|
||||
/** 목록 방식 편집기 위인지 */
|
||||
export function isOverListEditor(clientX: number, clientY: number): boolean {
|
||||
for (const el of elementsUnderDragPoint(clientX, clientY)) {
|
||||
if (el.closest('.se-list-editor')) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** 팔레트 드롭 — 전략 빌더 캔버스 위인지 */
|
||||
export function isOverStrategyBuilder(clientX: number, clientY: number): boolean {
|
||||
for (const el of elementsUnderDragPoint(clientX, clientY)) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/**
|
||||
* Desktop(Tauri) — HTML5 DnD 대신 pointer + 전체화면 overlay 드래그
|
||||
*/
|
||||
import { elementsUnderDragPoint } from './flowScreenProjection';
|
||||
import { isDesktop } from './platform';
|
||||
|
||||
export type PaletteDragPayload = Record<string, unknown> & {
|
||||
@@ -43,6 +44,8 @@ let overlayController: OverlayController | null = null;
|
||||
let overlayElement: HTMLElement | null = null;
|
||||
let listeners = new Set<PaletteDragListener>();
|
||||
let dropHandler: PaletteDropHandler | null = null;
|
||||
let moveRafId: number | null = null;
|
||||
let pendingMove: { x: number; y: number } | null = null;
|
||||
|
||||
export function needsPointerPaletteDrag(): boolean {
|
||||
return isDesktop();
|
||||
@@ -88,6 +91,11 @@ function dispatchEnd(xy: { x: number; y: number }, payload: PaletteDragPayload)
|
||||
function cleanupDrag() {
|
||||
activePayload = null;
|
||||
activePointerId = null;
|
||||
pendingMove = null;
|
||||
if (moveRafId != null) {
|
||||
cancelAnimationFrame(moveRafId);
|
||||
moveRafId = null;
|
||||
}
|
||||
document.body.classList.remove('se-palette-drag-active');
|
||||
overlayController?.unmount();
|
||||
document.querySelectorAll('.se-palette-drag-ghost').forEach(el => el.remove());
|
||||
@@ -107,7 +115,15 @@ function finishDrag(phase: 'end' | 'cancel', xy: { x: number; y: number }) {
|
||||
|
||||
export function notifyPaletteDragMove(clientX: number, clientY: number) {
|
||||
if (!activePayload) return;
|
||||
emit({ phase: 'move', x: clientX, y: clientY, payload: activePayload });
|
||||
pendingMove = { x: clientX, y: clientY };
|
||||
if (moveRafId != null) return;
|
||||
moveRafId = requestAnimationFrame(() => {
|
||||
moveRafId = null;
|
||||
if (!pendingMove || !activePayload) return;
|
||||
const { x, y } = pendingMove;
|
||||
pendingMove = null;
|
||||
emit({ phase: 'move', x, y, payload: activePayload });
|
||||
});
|
||||
}
|
||||
|
||||
export function notifyPaletteDragEnd(clientX: number, clientY: number) {
|
||||
@@ -229,8 +245,9 @@ export function readPaletteHtmlDragData(e: React.DragEvent): PaletteDragPayload
|
||||
}
|
||||
|
||||
export function findPaletteDropKeyAtPoint(x: number, y: number): string | null {
|
||||
const el = document.elementFromPoint(x, y);
|
||||
if (!el) return null;
|
||||
for (const el of elementsUnderDragPoint(x, y)) {
|
||||
const host = el.closest('[data-palette-drop-key]') as HTMLElement | null;
|
||||
return host?.dataset.paletteDropKey ?? null;
|
||||
if (host?.dataset.paletteDropKey) return host.dataset.paletteDropKey;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user