diff --git a/assets/webui/ofdgo.js b/assets/webui/ofdgo.js index c0ae1b1..a1c5c86 100644 --- a/assets/webui/ofdgo.js +++ b/assets/webui/ofdgo.js @@ -2631,7 +2631,15 @@ async function closeWorkspace() { async function closeApplicationWindow() { window.removeEventListener("beforeunload", warnUnsaved); window.removeEventListener("beforeunload", warnBatch); - await nativeHost.call("window.close", {}); + let closed = false; + try { + closed = await nativeHost.call("window.close", {}); + } finally { + if (!closed) { + window.addEventListener("beforeunload", warnUnsaved); + window.addEventListener("beforeunload", warnBatch); + } + } } function createDocumentTab(value = createDocumentState()) { @@ -2848,18 +2856,20 @@ async function activateDocumentTab(tab, closing = false) { const focused = el.documentTitleBar.contains(document.activeElement); workspace.switching = true; try { - if (!documentTab().busy && !await commitDocumentView()) return false; - detachDocumentView(); - attachDocumentTab(tab); - if (focused) el.documentTabs.querySelector('[data-selected="true"]')?.focus({preventScroll: true}); - workspace.switching = false; + try { + if (!documentTab().busy && !await commitDocumentView()) return false; + detachDocumentView(); + attachDocumentTab(tab); + if (focused) el.documentTabs.querySelector('[data-selected="true"]')?.focus({preventScroll: true}); + } finally { + workspace.switching = false; + } await mountDocumentTab(tab); return true; } catch (error) { if (isForegroundDocument(tab)) showError(error, !state.doc); return false; } finally { - if (workspace.active === tab) workspace.switching = false; const next = workspace.nextTab; workspace.nextTab = null; if (next && next !== workspace.active && !next.closed) queueMicrotask(() => activateDocumentTab(next)); @@ -5163,7 +5173,25 @@ async function boot() { } } -async function openNativeFiles(files) { +async function openNativeDocument(file) { + let retained = false; + try { + retained = await openOFD(file); + } catch (error) { + showError(error, !state.doc); + } finally { + if (!retained) await nativeHost.discardFiles([file]); + } +} + +async function openNativeFiles(files, launch = false) { + if (launch) { + for (const file of files) { + while (workspace.closing || workspace.switching || batchElements.Panel.open || documentDialogs?.applicationOpen()) await new Promise(resolve => setTimeout(resolve, 100)); + void openNativeDocument(file).catch(error => showError(error, !state.doc)); + } + return; + } let retained = []; try { if (workspace.closing || batchElements.Panel.open && batch.running) { setStatus("请等待当前操作完成"); return; } diff --git a/assets/webui/ofdgo_host.js b/assets/webui/ofdgo_host.js index 109a62d..92dbe21 100644 --- a/assets/webui/ofdgo_host.js +++ b/assets/webui/ofdgo_host.js @@ -726,6 +726,13 @@ class NativeHost { } async start(onFiles, onError) { + let launching = Promise.resolve(); + this.bridge.on("ofdgo.launch", values => { + launching = launching.then(async () => { + try { await onFiles(values.map(value => new NativeFile(value)), true); } + finally { await this.call("launch.done", {}); } + }).catch(onError); + }); this.bridge.on("ofdgo.files", async values => { try { await onFiles(values.map(value => new NativeFile(value))); } catch (error) { onError(error); } diff --git a/go.mod b/go.mod index ef01586..841831b 100644 --- a/go.mod +++ b/go.mod @@ -8,12 +8,13 @@ require ( github.com/go-text/typesetting v0.3.5 github.com/tdewolff/canvas v0.0.0-20260923214215-09804640d00c github.com/tdewolff/font v0.0.0-20261010204837-88a7f6b56918 - github.com/xiaoqidun/guigo v0.0.0-20261025075751-73659d2a5eba + github.com/xiaoqidun/guigo v0.0.0-20261025093134-18d57dc4973b github.com/xiaoqidun/j2kgo v1.0.2 github.com/xiaoqidun/jbig2 v1.1.8 github.com/xiaoqidun/pdfgo v0.0.0-20261023171626-3473b442eed5 golang.org/x/crypto v0.58.0 golang.org/x/image v0.47.0 + golang.org/x/sys v0.49.0 golang.org/x/text v0.43.0 ) @@ -35,7 +36,6 @@ require ( github.com/tdewolff/parse/v2 v2.8.16 // indirect github.com/yuin/goldmark v1.8.5 // indirect golang.org/x/net v0.60.0 // indirect - golang.org/x/sys v0.49.0 // indirect modernc.org/knuth v0.6.0 // indirect modernc.org/token v1.1.0 // indirect star-tex.org/x/tex v0.7.1 // indirect diff --git a/go.sum b/go.sum index 9054755..21d1861 100644 --- a/go.sum +++ b/go.sum @@ -60,8 +60,8 @@ github.com/tdewolff/parse/v2 v2.8.16 h1:bLk5svUOQRkW/Y2SJ+DeENSIkZBcTIkq+Atyv5D8 github.com/tdewolff/parse/v2 v2.8.16/go.mod h1:XdsoSFThlVIRIajAuqz1evNY7bagZS8LBOPA3aVopwQ= github.com/tdewolff/test v1.0.12 h1:7F21DqIajswxuche0geHdrUZRCWE4oko4b7bcmkkrxk= github.com/tdewolff/test v1.0.12/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8= -github.com/xiaoqidun/guigo v0.0.0-20261025075751-73659d2a5eba h1:8gQ9K2aIpYl72TdPAYWDju4YDhkrc3EyqIapWJd0AO8= -github.com/xiaoqidun/guigo v0.0.0-20261025075751-73659d2a5eba/go.mod h1:bXz+dPMVntPIVwePTTuMXt8Q0z1YoUGuROQSsXyUEfY= +github.com/xiaoqidun/guigo v0.0.0-20261025093134-18d57dc4973b h1:GlNcsrDSkahmjrQdA/q5yjkM5qWQw7pOBqY1qv625tU= +github.com/xiaoqidun/guigo v0.0.0-20261025093134-18d57dc4973b/go.mod h1:bXz+dPMVntPIVwePTTuMXt8Q0z1YoUGuROQSsXyUEfY= github.com/xiaoqidun/j2kgo v1.0.2 h1:OAjjEOfaBaQzBNcj9wx+eM68UI5pewR6SzNZa1R1DIw= github.com/xiaoqidun/j2kgo v1.0.2/go.mod h1:DCwAfuDzCDYqUiAlZ7BDwigTjaXw7o9L1FtEma/D1aA= github.com/xiaoqidun/jbig2 v1.1.8 h1:dERPW7F8gs18a/DOD9gXHR3fP1Vl6KhCNsLnq5l3BIo= diff --git a/internal/webview/webview.go b/internal/webview/webview.go index e999063..4999641 100644 --- a/internal/webview/webview.go +++ b/internal/webview/webview.go @@ -63,6 +63,10 @@ type Host struct { fontFiles map[string]string fontError error pendingPaths []string + launchMu sync.Mutex + launchActivate bool + launchClosed bool + launchPending int frontendReady bool closeAllowed bool } @@ -93,11 +97,38 @@ func Run(ctx context.Context, paths []string, dataDir string) error { if err != nil { return err } + paths = append([]string(nil), paths...) + for index, path := range paths { + paths[index], err = filepath.Abs(path) + if err != nil { + return err + } + } + h := &Host{pendingPaths: paths} + data, err := json.Marshal(paths) + if err != nil { + return err + } + if err := os.MkdirAll(options.UserDataDir, 0700); err != nil { + return err + } + key, err := instanceDirectory(options.UserDataDir) + if err != nil { + return err + } + instance, err := guigo.OpenInstance(ctx, "OFDGo:"+key, data, h.receiveLaunch) + if err != nil || instance == nil { + return err + } + defer instance.Close() files, err := newFileStore() if err != nil { return err } - h := &Host{files: files, engines: make(map[string]*webui.NativeEngine), cancels: make(map[string]context.CancelFunc), fontFiles: make(map[string]string), pendingPaths: append([]string(nil), paths...)} + h.files = files + h.engines = make(map[string]*webui.NativeEngine) + h.cancels = make(map[string]context.CancelFunc) + h.fontFiles = make(map[string]string) cacheBytes := int64(256 << 20) if memory := conversionMemory; memory != nil { cacheBytes = max(32<<20, memory()/8) @@ -110,7 +141,7 @@ func Run(ctx context.Context, paths []string, dataDir string) error { h.conversionPool = h.resources.ConversionPool() defer h.close() bindings := make(map[string]guigo.HandlerFunc) - for _, name := range []string{"engine.open", "engine.close", "engine.call", "engine.start", "engine.cancel", "engine.finish", "batch", "open", "save", "directory", "file", "release", "drop.release", "child", "remove", "write.begin", "write.data", "write.commit", "write.abort", "fonts", "ready", "title", "window.info", "window.drag", "window.close", "update.check", "update.open", "association.query", "association.register", "association.remove", "print.capabilities", "print.dialog", "print.printers", "print.query", "print.properties", "print"} { + for _, name := range []string{"engine.open", "engine.close", "engine.call", "engine.start", "engine.cancel", "engine.finish", "batch", "open", "save", "directory", "file", "release", "drop.release", "child", "remove", "write.begin", "write.data", "write.commit", "write.abort", "fonts", "ready", "launch.done", "title", "window.info", "window.drag", "window.close", "update.check", "update.open", "association.query", "association.register", "association.remove", "print.capabilities", "print.dialog", "print.printers", "print.query", "print.properties", "print"} { bindings["ofdgo.host."+name] = func(ctx context.Context, args any) (any, error) { return h.call(ctx, name, args) } } static := http.FileServerFS(webuiassets.NativeFS) @@ -136,6 +167,9 @@ func Run(ctx context.Context, paths []string, dataDir string) error { h.mu.Unlock() } options.OnClose = func() { + h.launchMu.Lock() + h.launchClosed = true + h.launchMu.Unlock() h.mu.Lock() for _, cancel := range h.cancels { cancel() @@ -278,6 +312,13 @@ func (h *Host) emit(name string, value any) { // openPaths 将操作系统提供的文件登记后交给界面 // 入参: names 文件路径列表 func (h *Host) openPaths(names []string) { + _ = h.sendPaths(names, "ofdgo.files") +} + +// sendPaths 将文件登记后发送到指定界面入口 +// 入参: names 文件路径列表, event 界面事件名称 +// 返回: error 投递错误 +func (h *Host) sendPaths(names []string, event string) error { files := make([]any, 0, len(names)) for _, name := range names { file, err := h.files.addDrop(name) @@ -290,12 +331,72 @@ func (h *Host) openPaths(names []string) { h.mu.Lock() w := h.window h.mu.Unlock() - if w != nil && w.Emit("ofdgo.files", files) == nil { - return + var err error + if w == nil { + err = guigo.ErrClosed + } else { + err = w.Emit(event, files) + } + if err == nil { + return nil } for _, file := range files { h.files.discardDrop(file.(map[string]any)["nativeFile"].(string)) } + return err +} + +// receiveLaunch 接收已有进程转交的文件及激活请求 +// 入参: ctx 请求上下文, data 文件路径消息 +// 返回: error 接收错误 +func (h *Host) receiveLaunch(ctx context.Context, data []byte) error { + var paths []string + if err := json.Unmarshal(data, &paths); err != nil { + return err + } + for _, path := range paths { + if !filepath.IsAbs(path) { + return errors.New("file path must be absolute") + } + } + h.launchMu.Lock() + defer h.launchMu.Unlock() + if h.launchClosed { + return guigo.ErrClosed + } + if err := ctx.Err(); err != nil { + return err + } + h.pendingPaths = append(h.pendingPaths, paths...) + h.launchActivate = true + return h.flushLaunch() +} + +// flushLaunch 在界面就绪后按接收顺序投递外部打开请求,调用方持有launchMu +// 返回: error 投递错误 +func (h *Host) flushLaunch() error { + h.mu.Lock() + ready, window := h.frontendReady, h.window + h.mu.Unlock() + if !ready || window == nil { + return nil + } + if h.launchActivate { + if err := window.Activate(); err != nil { + return err + } + h.launchActivate = false + } + if len(h.pendingPaths) == 0 { + return nil + } + paths := h.pendingPaths + h.pendingPaths = nil + if err := h.sendPaths(paths, "ofdgo.launch"); err != nil { + return err + } + h.launchPending++ + return nil } // call 执行宿主协议并保留业务结果结构 @@ -603,10 +704,16 @@ func (h *Host) call(ctx context.Context, name string, value any) (any, error) { return window.TitleBarInfo() } if name == "window.close" { + h.launchMu.Lock() + defer h.launchMu.Unlock() + if h.launchPending != 0 || len(h.pendingPaths) != 0 { + return false, nil + } + h.launchClosed = true h.mu.Lock() h.closeAllowed = true h.mu.Unlock() - return nil, window.Close() + return true, window.Close() } values, ok := args["regions"].([]any) if !ok { @@ -628,16 +735,20 @@ func (h *Host) call(ctx context.Context, name string, value any) (any, error) { regions = append(regions, guigo.Rect{X: x, Y: y, Width: width, Height: height}) } return nil, window.SetDragRegions(regions) + case "launch.done": + h.launchMu.Lock() + if h.launchPending > 0 { + h.launchPending-- + } + h.launchMu.Unlock() + return nil, nil case "ready": + h.launchMu.Lock() + defer h.launchMu.Unlock() h.mu.Lock() - paths := h.pendingPaths - h.pendingPaths = nil h.frontendReady = true h.mu.Unlock() - if len(paths) != 0 { - h.openPaths(paths) - } - return nil, nil + return nil, h.flushLaunch() } return nil, fmt.Errorf("未知宿主操作") } @@ -1019,6 +1130,9 @@ func (h *Host) closeEngine(id string) error { // close 取消任务并释放当前窗口持有的会话 func (h *Host) close() { + h.launchMu.Lock() + h.launchClosed = true + h.launchMu.Unlock() h.mu.Lock() for _, cancel := range h.cancels { cancel() diff --git a/internal/webview/webview_linux.go b/internal/webview/webview_linux.go index 06a21af..e54e697 100644 --- a/internal/webview/webview_linux.go +++ b/internal/webview/webview_linux.go @@ -16,12 +16,20 @@ package webview import ( "os" + "path/filepath" "strings" ) // conversionMemory 未提供系统内存探测时仅按并行数调度 var conversionMemory func() int64 +// instanceDirectory 解析数据目录的实际路径,使符号链接复用同一实例 +// 入参: path 数据目录路径 +// 返回: string 规范路径, error 路径解析错误 +func instanceDirectory(path string) (string, error) { + return filepath.EvalSymlinks(path) +} + // fontNameLanguage 获取Linux区域设置对应的OpenType名称表标识 // 返回: uint16 名称表平台标识, uint16 显示语言标识 func fontNameLanguage() (uint16, uint16) { diff --git a/internal/webview/webview_other.go b/internal/webview/webview_other.go index 5935b4f..403b198 100644 --- a/internal/webview/webview_other.go +++ b/internal/webview/webview_other.go @@ -16,9 +16,18 @@ package webview +import "path/filepath" + // conversionMemory 未提供系统内存探测时仅按并行数调度 var conversionMemory func() int64 +// instanceDirectory 解析数据目录的实际路径 +// 入参: path 数据目录路径 +// 返回: string 规范路径, error 路径解析错误 +func instanceDirectory(path string) (string, error) { + return filepath.EvalSymlinks(path) +} + // fontNameLanguage 未指定系统显示语言时沿用字体名称表顺序 // 返回: uint16 未指定平台标识, uint16 未指定语言标识 func fontNameLanguage() (uint16, uint16) { diff --git a/internal/webview/webview_windows.go b/internal/webview/webview_windows.go index 5bf4606..a27ac0e 100644 --- a/internal/webview/webview_windows.go +++ b/internal/webview/webview_windows.go @@ -15,13 +15,39 @@ package webview import ( + "os" + "strings" "syscall" "unsafe" + + "golang.org/x/sys/windows" ) // memoryStatusProc 查询系统物理内存和提交额度 var memoryStatusProc = syscall.NewLazyDLL("kernel32.dll").NewProc("GlobalMemoryStatusEx") +// instanceDirectory 解析数据目录的实际路径,使目录联接及大小写别名复用同一实例 +// 入参: path 数据目录路径 +// 返回: string 规范路径, error 路径解析错误 +func instanceDirectory(path string) (string, error) { + dir, err := os.Open(path) + if err != nil { + return "", err + } + defer dir.Close() + buffer := make([]uint16, 256) + for { + n, err := windows.GetFinalPathNameByHandle(windows.Handle(dir.Fd()), &buffer[0], uint32(len(buffer)), 0) + if err != nil { + return "", err + } + if n < uint32(len(buffer)) { + return strings.ToLower(windows.UTF16ToString(buffer[:n])), nil + } + buffer = make([]uint16, n+1) + } +} + // fontNameLanguage 获取系统显示语言对应的OpenType名称表标识 // 返回: uint16 名称表平台标识, uint16 系统显示语言标识 func fontNameLanguage() (uint16, uint16) {