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
import os
import platform
import shutil
import subprocess
import sys
from pathlib import Path
......@@ -99,6 +100,35 @@ class AppState(QObject):
self._current_tab = idx
self.currentTabChanged.emit()
@Slot(str)
def openExternalFile(self, path: str) -> None:
"""用系统默认应用打开文件 (双击图片预览的统一入口).
比 Qt.openUrlExternally 可靠: 直接 subprocess 把 path 交给系统命令,
绕开 Qt URL 解析 + macOS launch services 双层间接. 旧版 QML 用
Qt.openUrlExternally("file:///" + path) 在 macOS 26+ 不弹 Preview.app
(用户反馈"双击预览大图功能没了"的根因).
与 HistoryBridge.revealInExplorer 同款 subprocess.Popen + open 路径,
prod 已验证.
"""
if not path:
return
p = Path(path)
if not p.exists():
logger.warning(f"openExternalFile: 文件不存在 {path}")
return
try:
system = platform.system()
if system == "Darwin":
subprocess.Popen(["open", str(p)])
elif system == "Windows":
os.startfile(str(p)) # type: ignore[attr-defined]
else:
subprocess.Popen(["xdg-open", str(p)])
except Exception:
logger.exception(f"openExternalFile 失败 {path}")
# -------------------- 启动期 helpers --------------------
......
......@@ -350,7 +350,7 @@ Item {
cursorShape: Qt.PointingHandCursor
onDoubleClicked: {
if (tab.selectedItem.generatedImagePath) {
Qt.openUrlExternally("file:///" + tab.selectedItem.generatedImagePath)
appState.openExternalFile(tab.selectedItem.generatedImagePath)
}
}
}
......@@ -414,7 +414,7 @@ Item {
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onDoubleClicked: Qt.openUrlExternally("file:///" + modelData)
onDoubleClicked: appState.openExternalFile(modelData)
}
}
}
......
......@@ -418,7 +418,7 @@ Item {
// 一旦超 8px 阈值进入 drag,TapHandler 自动放弃。
TapHandler {
acceptedButtons: Qt.LeftButton
onDoubleTapped: Qt.openUrlExternally("file:///" + modelData)
onDoubleTapped: appState.openExternalFile(modelData)
}
DragHandler {
id: dragHandler
......@@ -816,7 +816,7 @@ Item {
cursorShape: Qt.PointingHandCursor
onDoubleClicked: {
if (tab.lastResultPath) {
Qt.openUrlExternally("file:///" + tab.lastResultPath)
appState.openExternalFile(tab.lastResultPath)
}
}
}
......
......@@ -509,7 +509,7 @@ Item {
cursorShape: Qt.PointingHandCursor
onDoubleClicked: {
if (tab.lastResultPath) {
Qt.openUrlExternally("file:///" + tab.lastResultPath)
appState.openExternalFile(tab.lastResultPath)
}
}
}
......