import React, { useEffect, useLayoutEffect, useRef, useState } from "react"; import { DOCK_OVERLAY_COMMANDS, executeDockOverlayCommand, registerDockOverlayHost, type DockCommand, type DockPanelState, type DockSide, type DockState } from "./dock-overlay-adapter.js"; import { registerWebWindowHost, type WindowLayout, type WindowPanel } from "./web-window-host-bridge.js"; import { executeWebDesktop } from "./web-desktop-adapter.js"; import { WebViewTask } from "./web-view-task.js"; import type { WebModuleRouter } from "./module-router.js"; import type { ViewOwner } from "./part-view-facade.js"; import "./dock-overlay.css"; const sides: DockSide[] = ["left", "right", "top", "bottom"]; const overlayLabels: Record = { Std_DockOverlay: "Overlay docked panel", Std_DockOverlayAll: "Toggle all overlays", Std_DockOverlayTransparentAll: "Toggle transparent panels", Std_DockOverlayToggle: "Toggle panel overlay", Std_DockOverlayToggleTransparent: "Toggle panel transparency", Std_DockOverlayMouseTransparent: "Toggle mouse transparency", Std_DockOverlayToggleLeft: "Toggle left overlay", Std_DockOverlayToggleRight: "Toggle right overlay", Std_DockOverlayToggleTop: "Toggle top overlay", Std_DockOverlayToggleBottom: "Toggle bottom overlay", }; const initial = (): DockState => ({ active: "modules", mouseTransparent: false, panels: { modules: { side: "left", overlay: false, transparent: false, visible: true }, commands: { side: "right", overlay: false, transparent: false, visible: true }, } }); const storageKey = "freecad-workbench-dock-layout"; function restore(): DockState { const defaults = initial(); try { const saved = JSON.parse(localStorage.getItem(storageKey) || "null"); if (!saved || !Object.hasOwn(defaults.panels, saved.active) || typeof saved.mouseTransparent !== "boolean") return defaults; for (const name of Object.keys(defaults.panels)) { const panel = saved.panels?.[name]; if (!panel || !sides.includes(panel.side) || [panel.overlay, panel.transparent, panel.visible].some(v => typeof v !== "boolean")) return defaults; } if (saved.panels.modules.side === saved.panels.commands.side) return defaults; return { active: saved.active, mouseTransparent: saved.mouseTransparent, panels: { modules: { ...saved.panels.modules }, commands: { ...saved.panels.commands }, } }; } catch { return defaults; } } export function DockOverlayHost({ children, router, documentId, onSelection }: { children: React.ReactNode; router: WebModuleRouter; documentId: string; onSelection: (documentId: string, selection: Array<{ documentId: string; object: string; subelements?: string[] }>) => void; }) { const root = useRef(null); const [state, setState] = useState(restore); const current = useRef(state); const hoveredPanel = useRef(); const overlayQueue = useRef>(Promise.resolve()); const [overlayCommand, setOverlayCommand] = useState("Std_DockOverlayAll"); const [overlayPanel, setOverlayPanel] = useState("modules"); const [windowLayout, setWindowLayout] = useState(); const [windowOwner, setWindowOwner] = useState("PartBox"); const [locked, setLocked] = useState(true); const [toolbarPosition, setToolbarPosition] = useState<"top" | "bottom">("top"); const [windowBusy, setWindowBusy] = useState(false); const [windowReady, setWindowReady] = useState(false); const [windowError, setWindowError] = useState(""); const [dockMenu, setDockMenu] = useState([]); const windowBusyRef = useRef(false); const runWindow = async (command: string, args: Record = {}) => { if (windowBusyRef.current) return false; windowBusyRef.current = true; setWindowBusy(true); setWindowError(""); try { const response = await executeWebDesktop(router, command, { runtimeOwner: windowOwner, ...args }, documentId || "workbench-document", onSelection); if (response?.kind === "error") setWindowError(response.error.message); return response?.kind === "result"; } catch (error) { setWindowError(String(error)); return false; } finally { windowBusyRef.current = false; setWindowBusy(false); } }; const commit = (next: DockState) => { current.current = next; setState(next); try { localStorage.setItem(storageKey, JSON.stringify(next)); } catch { /* Layout remains usable without preference storage. */ } return next; }; useLayoutEffect(() => registerWebWindowHost({ snapshot: () => ({ toolbar: "window-commands", panels: Object.entries(current.current.panels).map(([objectName, panel]) => ({ objectName, title: objectName === "modules" ? "模块" : "命令与参数", visible: panel.visible, })) }), apply: (value, owner) => { setWindowOwner(owner); if (value.items) { const next = { ...current.current, panels: { ...current.current.panels } }; for (const item of value.items) { if (!Object.hasOwn(next.panels, item.objectName)) throw new Error("Dock panel is no longer mounted"); next.panels[item.objectName] = { ...next.panels[item.objectName], visible: item.visible }; } commit(next); setDockMenu(value.items); } if (value.locked !== undefined) setLocked(value.locked); if (value.windows && value.columns && value.rows) setWindowLayout({ windows: value.windows, columns: value.columns, rows: value.rows, activeDocument: value.activeDocument ?? null }); }, }), []); useEffect(() => { let disposed = false; setWindowReady(false); void (async () => { let ready = true; for (const command of ["Std_DockViewMenu", "Std_ToggleToolBarLock"]) { if (disposed) return; if (!await runWindow(command, { operation: "read" })) ready = false; } if (!disposed) setWindowReady(ready); })(); const close = (event: Event) => { const detail = (event as CustomEvent<{ owner: ViewOwner; documents: string[] }>).detail; if (detail?.owner !== windowOwner) return; setWindowLayout(previous => { if (!previous) return previous; const windows = previous.windows.filter(item => !detail.documents.includes(item.documentId)); return windows.length ? { ...previous, windows } : undefined; }); }; window.addEventListener("freecad-documents-closed", close); return () => { disposed = true; window.removeEventListener("freecad-documents-closed", close); }; }, [windowOwner]); useLayoutEffect(() => registerDockOverlayHost((requested, args) => { const run = async () => { let command = requested; if (requested === "Std_DockOverlay") { const child = args.childCommand ?? "Std_DockOverlayAll"; if (typeof child !== "string" || child === requested || !Object.hasOwn(DOCK_OVERLAY_COMMANDS, child)) throw new Error("Choose a dock overlay child command"); command = child as DockCommand; } const next: DockState = { ...current.current, panels: Object.fromEntries(Object.entries(current.current.panels).map(([key, panel]) => [key, { ...panel }])) }; if (args.panel !== undefined) { if (typeof args.panel !== "string" || !Object.hasOwn(next.panels, args.panel)) throw new Error("Unknown dock panel"); next.active = args.panel; } const operation = DOCK_OVERLAY_COMMANDS[command]; // Side toggles consume the current snapshot; WASM owns the visibility change. if (operation === "toggleActive" || operation === "transparentActive") { const focused = document.activeElement instanceof HTMLElement ? document.activeElement.closest("[data-dock-panel]") : null; const target = args.panel ?? hoveredPanel.current ?? (focused && root.current?.contains(focused) ? focused.dataset.dockPanel : undefined); // OverlayManager resolves the dock under the cursor, then the focused dock. // Clear a stale active dock so the native handler preserves the desktop // no-target behavior without toggling the previously focused panel. next.active = typeof target === "string" && Object.hasOwn(next.panels, target) ? target : ""; } if (operation === "undock") { next.panels[next.active] = { ...next.panels[next.active], overlay: false, visible: true }; return commit(next); } if (operation === "vr") { window.dispatchEvent(new CustomEvent("freecad-view-vr", { detail: { active: true, panel: next.active } })); return commit(next); } const response = await router.request("Web", "web.dockOverlay", { command: requested, args, state: next, }, documentId || "workbench-document"); if (response.kind === "error") throw new Error(response.error.message); const value = response.result as { command: string; state: DockState }; if (value.command !== requested || !value.state) throw new Error("Invalid native dock response"); return commit(value.state); }; const pending = overlayQueue.current.then(run); overlayQueue.current = pending.catch(() => {}); return pending; }), [router, documentId]); useLayoutEffect(() => { if (!root.current) return; const panels = Array.from(root.current.children).filter((element): element is HTMLElement => element instanceof HTMLElement && element.tagName === "ASIDE"); const cleanup: Array<() => void> = []; panels.forEach((element, index) => { const name = index === 0 ? "modules" : "commands"; const panel = state.panels[name]; element.dataset.dockPanel = name; element.dataset.dockSide = panel.side; element.dataset.overlay = String(panel.overlay); element.dataset.transparent = String(panel.transparent); element.dataset.mouseTransparent = String(state.mouseTransparent && panel.overlay); element.hidden = !panel.visible; element.style.display = panel.visible ? "" : "none"; const activate = () => { current.current = { ...current.current, active: name }; }; const enter = () => { hoveredPanel.current = name; activate(); }; const leave = () => { if (hoveredPanel.current === name) hoveredPanel.current = undefined; }; element.addEventListener("pointerenter", enter); element.addEventListener("pointerleave", leave); element.addEventListener("focusin", activate); cleanup.push(() => { element.removeEventListener("pointerenter", enter); element.removeEventListener("pointerleave", leave); element.removeEventListener("focusin", activate); }); }); return () => cleanup.forEach(release => release()); }, [state]); const patch = (name: string, patch: Partial) => { const previous = current.current; const panels = { ...previous.panels, [name]: { ...previous.panels[name], ...patch } }; if (patch.side) { for (const [other, panel] of Object.entries(previous.panels)) { if (other !== name && panel.side === patch.side) panels[other] = { ...panel, side: previous.panels[name].side }; } } return commit({ ...previous, active: name, panels }); }; const docked = (side: DockSide) => Object.values(state.panels).some(panel => panel.visible && !panel.overlay && panel.side === side); const style = { "--dock-left": docked("left") ? "260px" : "0px", "--dock-right": docked("right") ? "340px" : "0px" } as React.CSSProperties; const toolbar = ; return <> {toolbarPosition === "top" && toolbar} {windowError && {windowError}} {windowLayout &&
{windowLayout.windows.map(item =>
)}
}
面板布局 {Object.entries(state.panels).map(([name, panel]) =>
{name === "modules" ? "模块" : "命令与参数"}
)}
{children}
{toolbarPosition === "bottom" && toolbar} ; }