f2d92337 by shady

:bug: 双击图片预览改走 subprocess open: 5 处统一调系统默认应用

旧实现 Qt.openUrlExternally("file:///" + path) 在 macOS 26+ 不弹
Preview.app — 用户反馈"双击预览大图功能没了"的根因 (Qt URL 解析
+ launch services 双层间接). 中间过渡试过应用内 ImagePreviewDialog,
但缺缩放 / 全屏 / 方向键浏览, 不如系统预览方便.

修法: AppState 加 openExternalFile(path) Slot, 内部 subprocess.Popen
(macOS open / Windows os.startfile / Linux xdg-open). 与
HistoryBridge.revealInExplorer 同款路径, prod 已验.

5 处双击全切到 appState.openExternalFile:
  - ImageGenTab 参考图 (TapHandler.onDoubleTapped) + 生成图 (MouseArea)
  - StyleDesignerTab 生成图
  - HistoryTab 详情大图 + 参考图卡片

中间过渡产物 components/ImagePreviewDialog.qml 已删 (从未 git track),
3 tab 顶部各一个实例化删. 系统 Preview.app 自带缩放 / 全屏 / 方向键 /
标记 / 分享, 远胜自实现 Dialog. Linus 消除特殊情况.

闪退维度复核:
  - subprocess.Popen fire-and-forget, 不阻塞主线程
  - 不传 bytes, 不动 Qt clipboard / NSPasteboard / NSImage
  - path 不存在仅 logger.warning, 不抛
  - macOS 用户已实测全部 5 处弹出 Preview.app

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent b8a3ce35
...@@ -20,6 +20,7 @@ import logging ...@@ -20,6 +20,7 @@ import logging
20 import os 20 import os
21 import platform 21 import platform
22 import shutil 22 import shutil
23 import subprocess
23 import sys 24 import sys
24 from pathlib import Path 25 from pathlib import Path
25 26
...@@ -99,6 +100,35 @@ class AppState(QObject): ...@@ -99,6 +100,35 @@ class AppState(QObject):
99 self._current_tab = idx 100 self._current_tab = idx
100 self.currentTabChanged.emit() 101 self.currentTabChanged.emit()
101 102
103 @Slot(str)
104 def openExternalFile(self, path: str) -> None:
105 """用系统默认应用打开文件 (双击图片预览的统一入口).
106
107 比 Qt.openUrlExternally 可靠: 直接 subprocess 把 path 交给系统命令,
108 绕开 Qt URL 解析 + macOS launch services 双层间接. 旧版 QML 用
109 Qt.openUrlExternally("file:///" + path) 在 macOS 26+ 不弹 Preview.app
110 (用户反馈"双击预览大图功能没了"的根因).
111
112 与 HistoryBridge.revealInExplorer 同款 subprocess.Popen + open 路径,
113 prod 已验证.
114 """
115 if not path:
116 return
117 p = Path(path)
118 if not p.exists():
119 logger.warning(f"openExternalFile: 文件不存在 {path}")
120 return
121 try:
122 system = platform.system()
123 if system == "Darwin":
124 subprocess.Popen(["open", str(p)])
125 elif system == "Windows":
126 os.startfile(str(p)) # type: ignore[attr-defined]
127 else:
128 subprocess.Popen(["xdg-open", str(p)])
129 except Exception:
130 logger.exception(f"openExternalFile 失败 {path}")
131
102 132
103 # -------------------- 启动期 helpers -------------------- 133 # -------------------- 启动期 helpers --------------------
104 134
......
...@@ -350,7 +350,7 @@ Item { ...@@ -350,7 +350,7 @@ Item {
350 cursorShape: Qt.PointingHandCursor 350 cursorShape: Qt.PointingHandCursor
351 onDoubleClicked: { 351 onDoubleClicked: {
352 if (tab.selectedItem.generatedImagePath) { 352 if (tab.selectedItem.generatedImagePath) {
353 Qt.openUrlExternally("file:///" + tab.selectedItem.generatedImagePath) 353 appState.openExternalFile(tab.selectedItem.generatedImagePath)
354 } 354 }
355 } 355 }
356 } 356 }
...@@ -414,7 +414,7 @@ Item { ...@@ -414,7 +414,7 @@ Item {
414 MouseArea { 414 MouseArea {
415 anchors.fill: parent 415 anchors.fill: parent
416 cursorShape: Qt.PointingHandCursor 416 cursorShape: Qt.PointingHandCursor
417 onDoubleClicked: Qt.openUrlExternally("file:///" + modelData) 417 onDoubleClicked: appState.openExternalFile(modelData)
418 } 418 }
419 } 419 }
420 } 420 }
......
...@@ -418,7 +418,7 @@ Item { ...@@ -418,7 +418,7 @@ Item {
418 // 一旦超 8px 阈值进入 drag,TapHandler 自动放弃。 418 // 一旦超 8px 阈值进入 drag,TapHandler 自动放弃。
419 TapHandler { 419 TapHandler {
420 acceptedButtons: Qt.LeftButton 420 acceptedButtons: Qt.LeftButton
421 onDoubleTapped: Qt.openUrlExternally("file:///" + modelData) 421 onDoubleTapped: appState.openExternalFile(modelData)
422 } 422 }
423 DragHandler { 423 DragHandler {
424 id: dragHandler 424 id: dragHandler
...@@ -816,7 +816,7 @@ Item { ...@@ -816,7 +816,7 @@ Item {
816 cursorShape: Qt.PointingHandCursor 816 cursorShape: Qt.PointingHandCursor
817 onDoubleClicked: { 817 onDoubleClicked: {
818 if (tab.lastResultPath) { 818 if (tab.lastResultPath) {
819 Qt.openUrlExternally("file:///" + tab.lastResultPath) 819 appState.openExternalFile(tab.lastResultPath)
820 } 820 }
821 } 821 }
822 } 822 }
......
...@@ -509,7 +509,7 @@ Item { ...@@ -509,7 +509,7 @@ Item {
509 cursorShape: Qt.PointingHandCursor 509 cursorShape: Qt.PointingHandCursor
510 onDoubleClicked: { 510 onDoubleClicked: {
511 if (tab.lastResultPath) { 511 if (tab.lastResultPath) {
512 Qt.openUrlExternally("file:///" + tab.lastResultPath) 512 appState.openExternalFile(tab.lastResultPath)
513 } 513 }
514 } 514 }
515 } 515 }
......