diff --git a/resources/preload.js b/resources/preload.js index 2aca3969..25cf17e7 100644 --- a/resources/preload.js +++ b/resources/preload.js @@ -539,7 +539,10 @@ window.ztools = { // 显示文件保存对话框 showSaveDialog: (options) => electron.ipcRenderer.sendSync('show-save-dialog', options), // 显示文件打开对话框 - showOpenDialog: (options) => electron.ipcRenderer.sendSync('show-open-dialog', options), + showOpenDialog: (options) => { + const data = electron.ipcRenderer.sendSync('show-open-dialog', options); + return data; + }, // 屏幕截图 screenCapture: async (callback) => { const { image, bounds } = await electron.ipcRenderer.invoke('screen-capture') diff --git a/src/main/api/plugin/dialog.ts b/src/main/api/plugin/dialog.ts index c88bfaf2..b7288b56 100644 --- a/src/main/api/plugin/dialog.ts +++ b/src/main/api/plugin/dialog.ts @@ -76,10 +76,15 @@ export class PluginDialogAPI { event.returnValue = undefined return } - const result = windowManager.withBlurHideSuppressedSync(() => - dialog.showSaveDialogSync(targetWindow, options) - ) - event.returnValue = result + windowManager + .withBlurHideSuppressed(() => dialog.showSaveDialog(targetWindow, options)) + .then((data: Electron.SaveDialogReturnValue) => { + event.returnValue = data.canceled ? undefined : data.filePath + }) + .catch((error: Error) => { + console.error('[PluginDialog] 显示文件保存对话框失败:', error) + event.returnValue = undefined + }) } catch (error) { console.error('[PluginDialog] 显示文件保存对话框失败:', error) event.returnValue = undefined @@ -97,10 +102,15 @@ export class PluginDialogAPI { event.returnValue = [] return } - const result = windowManager.withBlurHideSuppressedSync(() => - dialog.showOpenDialogSync(targetWindow, options) - ) - event.returnValue = result || [] + windowManager + .withBlurHideSuppressed(() => dialog.showOpenDialog(targetWindow, options)) + .then((data: Electron.OpenDialogReturnValue) => { + event.returnValue = data.canceled ? [] : data.filePaths + }) + .catch((error: Error) => { + console.error('[PluginDialog] 显示文件打开对话框失败:', error) + event.returnValue = [] + }) } catch (error) { console.error('[PluginDialog] 显示文件打开对话框失败:', error) event.returnValue = [] diff --git a/src/main/api/plugin/shell.ts b/src/main/api/plugin/shell.ts index 87b28fa7..422174e4 100644 --- a/src/main/api/plugin/shell.ts +++ b/src/main/api/plugin/shell.ts @@ -290,7 +290,7 @@ export class PluginShellAPI { 'FESearchHost.exe', 'prevhost.exe' ] - if (!EXPLORER_APPS.includes(windowInfo.app)) { + if (!windowInfo.app || !EXPLORER_APPS.includes(windowInfo.app)) { console.log( `[PluginShell] readCurrentFolderPath: 当前窗口非 Explorer (app=${windowInfo.app})` ) diff --git a/src/main/api/renderer/systemCommands.ts b/src/main/api/renderer/systemCommands.ts index f4425ff1..2ff078b4 100644 --- a/src/main/api/renderer/systemCommands.ts +++ b/src/main/api/renderer/systemCommands.ts @@ -736,23 +736,25 @@ function handleAddToWakeupBlacklist(ctx: SystemCommandContext): any { const blacklist: Array<{ app: string; bundleId?: string; label?: string }> = settings.wakeupBlacklist ?? [] + const appName = winInfo.app + // 去重:macOS 按 bundleId,Windows 按 app 名称 const isDuplicate = process.platform === 'darwin' && winInfo.bundleId ? blacklist.some((item) => item.bundleId === winInfo.bundleId) - : blacklist.some((item) => item.app.toLowerCase() === winInfo.app.toLowerCase()) + : blacklist.some((item) => item.app.toLowerCase() === appName.toLowerCase()) if (isDuplicate) { ctx.mainWindow?.hide() if (Notification.isSupported()) { - new Notification({ title: 'ZTools', body: `${winInfo.app} 已在唤醒黑名单中` }).show() + new Notification({ title: 'ZTools', body: `${appName} 已在唤醒黑名单中` }).show() } return { success: false, error: '该应用已在唤醒黑名单中' } } - const label = winInfo.app.replace(/\.(exe|app)$/i, '') + const label = appName.replace(/\.(exe|app)$/i, '') blacklist.push({ - app: winInfo.app, + app: appName, bundleId: winInfo.bundleId, label }) diff --git a/src/main/core/native/index.ts b/src/main/core/native/index.ts index fae968fa..9fff9c0c 100644 --- a/src/main/core/native/index.ts +++ b/src/main/core/native/index.ts @@ -26,6 +26,24 @@ interface UwpAppInfo { installLocation: string } +export interface FileLocationWindowInfo { + platform?: 'win32' | 'darwin' + kind?: 'windows-explorer' | 'windows-file-dialog' | 'mac-finder' | 'mac-file-dialog' + preciseTarget?: boolean + hwnd?: number + windowId?: number + finderId?: number + pid?: number + bundleId?: string + app?: string + title?: string + className?: string + axRole?: string + axSubrole?: string + path?: string + url?: string +} + interface NativeAddon { startMonitor: (callback: () => void) => void stopMonitor: () => void @@ -60,6 +78,12 @@ interface NativeAddon { unicodeType: (segment: string) => boolean /** Windows: 通过 COM IShellWindows 查询指定窗口句柄对应的 Explorer 文件夹路径 */ getExplorerFolderPath: (hwnd: number) => string | null + /** Windows/macOS: 获取文件管理器窗口 */ + getAllExplorerWindows: () => Array + /** Windows/macOS: 设置文件管理器或文件选择对话框地址栏路径 */ + setAddressBar: (identifier: number | string | FileLocationWindowInfo, address: string) => boolean + /** Windows: 判断窗口是否是可安全修改地址栏的文件定位窗口 */ + isFileLocationWindow?: (hwnd: number) => boolean /** Windows: 读取指定浏览器窗口的当前 URL,结果通过 callback 返回 */ readBrowserWindowUrl: ( browserName: string, @@ -82,6 +106,9 @@ interface NativeAddon { interface WindowInfo { app: string // 应用名称(如 "Finder.app") + platform?: 'win32' | 'darwin' + kind?: FileLocationWindowInfo['kind'] + preciseTarget?: boolean bundleId?: string // macOS 独有 pid?: number // 进程ID (macOS 和 Windows 都有) title?: string // 窗口标题 @@ -92,6 +119,12 @@ interface WindowInfo { appPath?: string // 应用路径 className?: string // Windows 窗口类名(用于区分 CabinetWClass/Progman/WorkerW 等) hwnd?: number // Windows 窗口句柄(用于 COM 查询 Explorer 路径) + windowId?: number + finderId?: number + axRole?: string + axSubrole?: string + path?: string + url?: string } interface ActiveWindowResult { @@ -435,6 +468,69 @@ export class WindowManager { return (addon as NativeAddon).getExplorerFolderPath(hwnd) } + /** + * Windows/macOS: 获取所有文件管理器窗口 + * @returns 文件管理器窗口列表 + */ + static getAllExplorerWindows(): Array { + if (platform !== 'win32' && platform !== 'darwin') { + throw new Error('getAllExplorerWindows is only available on Windows and macOS') + } + return (addon as NativeAddon).getAllExplorerWindows() + } + + static isFileLocationWindow(hwnd: number): boolean { + if (platform !== 'win32') { + throw new Error('isFileLocationWindow is only available on Windows') + } + if (typeof hwnd !== 'number' || !Number.isFinite(hwnd) || hwnd <= 0) { + throw new TypeError('hwnd must be a positive number') + } + return Boolean((addon as NativeAddon).isFileLocationWindow?.(hwnd)) + } + + /** + * Windows/macOS: 设置文件管理器或文件选择对话框地址栏路径 + * @param target 目标窗口标识或窗口信息 + * @param address 要跳转的路径或地址 + * @returns 是否设置成功 + */ + static setAddressBar( + target: number | string | FileLocationWindowInfo, + address: string + ): boolean { + if (platform !== 'win32' && platform !== 'darwin') { + throw new Error('setAddressBar is only available on Windows and macOS') + } + if (typeof address !== 'string' || address.trim() === '') { + throw new TypeError('address must be a non-empty string') + } + + const identifier = + typeof target === 'object' && target !== null + ? platform === 'darwin' + ? target + : target.hwnd + : target + + if (platform === 'win32') { + if (typeof identifier !== 'number' || !Number.isFinite(identifier) || identifier <= 0) { + throw new TypeError('target must include a valid hwnd') + } + } else if (typeof identifier === 'object' && identifier !== null) { + if (!identifier.preciseTarget) { + throw new TypeError('target must include precise macOS window identity') + } + } else if ( + (typeof identifier !== 'number' || !Number.isFinite(identifier) || identifier <= 0) && + (typeof identifier !== 'string' || identifier.trim() === '') + ) { + throw new TypeError('target must include a valid bundleId or pid') + } + + return Boolean((addon as NativeAddon).setAddressBar(identifier, address)) + } + /** * Windows: 读取指定浏览器窗口的当前 URL * @param browserName 浏览器标识(如 chrome/msedge/firefox) diff --git a/src/main/core/superPanelManager.ts b/src/main/core/superPanelManager.ts index 61f7c66d..42c4f901 100644 --- a/src/main/core/superPanelManager.ts +++ b/src/main/core/superPanelManager.ts @@ -1,7 +1,7 @@ import { BrowserWindow, ipcMain, screen } from 'electron' import path from 'path' import { is } from '@electron-toolkit/utils' -import { MouseMonitor, WindowManager, type MouseMonitorResult } from './native/index.js' +import { MouseMonitor, WindowManager, type FileLocationWindowInfo, type MouseMonitorResult } from './native/index.js' import { launchApp } from './commandLauncher/index.js' import databaseAPI from '../api/shared/database.js' import pluginsAPI from '../api/renderer/plugins.js' @@ -10,6 +10,7 @@ import clipboardManager, { type LastCopiedContent } from '../managers/clipboardM import { applyWindowMaterial, getDefaultWindowMaterial } from '../utils/windowUtils.js' import translationManager from './translationManager.js' import { filterSuperPanelPinnedCommands } from './superPanelPinnedCommands.js' +import { decodeFileUrlToPath } from '../utils/common' // 超级面板窗口尺寸 const SUPER_PANEL_WIDTH = 250 @@ -48,6 +49,8 @@ class SuperPanelManager { private mainWindow: BrowserWindow | null = null private windowReady = false private pendingMessages: Array<{ channel: string; data: any }> = [] + private windowCommandRequestId = 0 + private activeWindowCommandRequestId: number | null = null private config: SuperPanelConfig = { enabled: false, mouseButton: 'middle', @@ -142,6 +145,42 @@ class SuperPanelManager { return false } + private enrichFileLocationWindowInfo< + T extends { className?: string; hwnd?: number | null; path?: string; url?: string } + >(windowInfo: T | null): T | null { + if (process.platform !== 'win32' || !windowInfo) { + return windowInfo + } + + if (windowInfo.className !== 'CabinetWClass' && windowInfo.className !== 'ExploreWClass') { + return windowInfo + } + + if ( + typeof windowInfo.hwnd !== 'number' || + !Number.isFinite(windowInfo.hwnd) || + windowInfo.hwnd <= 0 + ) { + return windowInfo + } + + try { + const folderUrl = WindowManager.getExplorerFolderPath(windowInfo.hwnd) + if (!folderUrl) { + return windowInfo + } + + return { + ...windowInfo, + url: folderUrl, + path: decodeFileUrlToPath(folderUrl) + } + } catch (error) { + console.error('[SuperPanel] Explorer 当前目录读取异常:', error) + return windowInfo + } + } + /** * 启动鼠标监听 */ @@ -176,19 +215,13 @@ class SuperPanelManager { // 当前剪贴板内容(在模拟复制后读取) private currentClipboardContent: ClipboardContent | null = null // 触发时的完整窗口信息 - private currentWindowInfo: { - app: string - bundleId?: string - pid?: number - title?: string + private currentWindowInfo: (FileLocationWindowInfo & { x?: number y?: number width?: number height?: number appPath?: string - className?: string - hwnd?: number - } | null = null + }) | null = null /** * 将剪贴板管理器返回的数据转换为超级面板使用的结构 @@ -228,13 +261,16 @@ class SuperPanelManager { const cachedWindow = clipboardManager.getCurrentWindow() const activeWindow = WindowManager.getActiveWindow() const windowInfo = activeWindow ? { ...cachedWindow, ...activeWindow } : cachedWindow - this.currentWindowInfo = windowInfo ?? null + this.currentWindowInfo = this.enrichFileLocationWindowInfo(windowInfo ?? null) // 1.6. 检查当前窗口是否被屏蔽 const windowToCheck = activeWindow || cachedWindow - if (windowToCheck && this.isWindowBlocked(windowToCheck)) { - console.log('[SuperPanel] 当前窗口被屏蔽,跳过触发:', windowToCheck.app) - return { shouldBlock: false } + if (windowToCheck?.app) { + const blockedWindow = { ...windowToCheck, app: windowToCheck.app } + if (this.isWindowBlocked(blockedWindow)) { + console.log('[SuperPanel] 当前窗口被屏蔽,跳过触发:', blockedWindow.app) + return { shouldBlock: false } + } } // 异步部分:模拟复制、读取剪贴板、显示面板 @@ -808,10 +844,81 @@ class SuperPanelManager { } }) + // 超级面板文件位置快捷跳转 + ipcMain.handle('super-panel:get-file-location-windows', async () => { + if (process.platform !== 'win32' && process.platform !== 'darwin') { + return [] + } + + try { + const windows = WindowManager.getAllExplorerWindows() + console.log('[SuperPanel] 获取文件位置窗口成功:', { + platform: process.platform, + count: windows.length + }) + return windows + } catch (error) { + console.error('[SuperPanel] 获取文件位置窗口失败:', error) + return [] + } + }) + + ipcMain.handle('super-panel:is-file-location-window', async (_event, hwnd: number) => { + if (process.platform !== 'win32' || typeof hwnd !== 'number' || !Number.isFinite(hwnd) || hwnd <= 0) { + return { supported: false, isFileLocation: false } + } + + try { + return { + supported: true, + isFileLocation: WindowManager.isFileLocationWindow(hwnd) + } + } catch (error) { + console.error('[SuperPanel] 判断文件位置窗口失败:', error) + return { supported: false, isFileLocation: false } + } + }) + + ipcMain.handle( + 'super-panel:set-file-location-address-bar', + async ( + _event, + target: number | string | FileLocationWindowInfo, + address: string + ) => { + if ((process.platform !== 'win32' && process.platform !== 'darwin') || !target || !address) { + return false + } + + try { + const success = WindowManager.setAddressBar(target, address) + console.log('[SuperPanel] 设置文件位置地址栏结果:', { + platform: process.platform, + target, + address, + success + }) + return success + } catch (error) { + console.error('[SuperPanel] 设置文件位置地址栏失败:', error) + return false + } + } + ) + // 超级面板请求窗口匹配搜索 → 转发给主渲染进程 ipcMain.handle( 'super-panel:search-window-commands', - (_event, windowInfo: { app?: string; title?: string }) => { + (_event, windowInfo: { + app?: string + title?: string + className?: string + hwnd?: number + bundleId?: string + pid?: number + path?: string + url?: string + }) => { // 设置触发前的窗口信息到主窗口管理器 if (this.currentWindowInfo) { windowManager.setPreviousActiveWindow(this.currentWindowInfo) @@ -820,14 +927,25 @@ class SuperPanelManager { this.sendToSuperPanel('super-panel-window-commands-data', { results: [] }) return } - this.mainWindow.webContents.send('super-panel-search-window-commands', windowInfo) + const requestId = ++this.windowCommandRequestId + this.activeWindowCommandRequestId = requestId + this.mainWindow.webContents.send('super-panel-search-window-commands', { + requestId, + windowInfo + }) } ) // 主渲染进程返回窗口匹配结果 → 转发到超级面板 - ipcMain.on('super-panel-window-commands-result', (_event, data: { results: any[] }) => { - this.sendToSuperPanel('super-panel-window-commands-data', data) - }) + ipcMain.on( + 'super-panel-window-commands-result', + (_event, data: { requestId?: number; results: any[] }) => { + if (data.requestId !== this.activeWindowCommandRequestId) { + return + } + this.sendToSuperPanel('super-panel-window-commands-data', data) + } + ) } } diff --git a/src/main/managers/clipboardManager.ts b/src/main/managers/clipboardManager.ts index 52de6a69..5b2e15bc 100644 --- a/src/main/managers/clipboardManager.ts +++ b/src/main/managers/clipboardManager.ts @@ -50,7 +50,10 @@ interface ClipboardItem { // 窗口激活信息 interface WindowActivationInfo { - app: string + app?: string + platform?: 'win32' | 'darwin' + kind?: 'windows-explorer' | 'windows-file-dialog' | 'mac-finder' | 'mac-file-dialog' + preciseTarget?: boolean bundleId?: string pid?: number title?: string @@ -61,6 +64,12 @@ interface WindowActivationInfo { appPath?: string className?: string // Windows 窗口类名(CabinetWClass/Progman/WorkerW 等) hwnd?: number // Windows 窗口句柄(用于 COM 查询 Explorer 路径) + windowId?: number + finderId?: number + path?: string + url?: string + axRole?: string + axSubrole?: string } // 配置 diff --git a/src/main/managers/windowManager.ts b/src/main/managers/windowManager.ts index fc7d0d44..1f060ca6 100644 --- a/src/main/managers/windowManager.ts +++ b/src/main/managers/windowManager.ts @@ -65,7 +65,10 @@ class WindowManager { private static readonly MODIFIER_NAMES = ['Command', 'Ctrl', 'Alt', 'Option', 'Shift'] private isQuitting = false // 是否正在退出应用 private previousActiveWindow: { - app: string + app?: string + platform?: 'win32' | 'darwin' + kind?: 'windows-explorer' | 'windows-file-dialog' | 'mac-finder' | 'mac-file-dialog' + preciseTarget?: boolean bundleId?: string pid?: number title?: string @@ -76,6 +79,12 @@ class WindowManager { appPath?: string className?: string hwnd?: number + windowId?: number + finderId?: number + path?: string + url?: string + axRole?: string + axSubrole?: string } | null = null // 打开应用前激活的窗口 // private _shouldRestoreFocus = true // TODO: 是否在隐藏窗口时恢复焦点(待实现) private windowPositionsByDisplay: Record = {} @@ -690,21 +699,7 @@ class WindowManager { return ret } - public setPreviousActiveWindow( - windowInfo: { - app: string - bundleId?: string - pid?: number - title?: string - x?: number - y?: number - width?: number - height?: number - appPath?: string - className?: string - hwnd?: number - } | null - ): void { + public setPreviousActiveWindow(windowInfo: typeof this.previousActiveWindow): void { this.previousActiveWindow = windowInfo } @@ -864,9 +859,12 @@ class WindowManager { this.previousActiveWindow = currentWindow // 唤醒黑名单检查:当前活动窗口在黑名单中时不弹出 - if (this.isAppInWakeupBlacklist(currentWindow)) { - this.isRestoringFocus = false - return + if (currentWindow.app) { + const wakeupWindow = { ...currentWindow, app: currentWindow.app } + if (this.isAppInWakeupBlacklist(wakeupWindow)) { + this.isRestoringFocus = false + return + } } } @@ -1032,19 +1030,7 @@ class WindowManager { /** * 获取打开窗口前激活的窗口 */ - public getPreviousActiveWindow(): { - app: string - bundleId?: string - pid?: number - title?: string - x?: number - y?: number - width?: number - height?: number - appPath?: string - className?: string - hwnd?: number - } | null { + public getPreviousActiveWindow(): typeof this.previousActiveWindow { return this.previousActiveWindow } @@ -1079,21 +1065,27 @@ class WindowManager { return false } + const previousApp = this.previousActiveWindow.app + if (!previousApp) { + console.log('[Window] 前一个激活窗口缺少应用标识') + return false + } + // 忽略同类启动器工具,避免激活冲突 const ignoredApps = ['uTools', 'Alfred', 'Raycast', 'Wox', 'Listary'] - if (ignoredApps.includes(this.previousActiveWindow.app)) { - console.log(`跳过恢复同类工具: ${this.previousActiveWindow.app}`) + if (ignoredApps.includes(previousApp)) { + console.log(`跳过恢复同类工具: ${previousApp}`) return false } try { const success = clipboardManager.activateApp(this.previousActiveWindow) if (success) { - console.log(`已恢复激活窗口: ${this.previousActiveWindow.app}`) + console.log(`已恢复激活窗口: ${previousApp}`) return true } else { // 静默失败,不报错(可能进程已关闭或窗口已销毁) - console.log(`无法恢复窗口: ${this.previousActiveWindow.app}`) + console.log(`无法恢复窗口: ${previousApp}`) return false } } catch (error) { diff --git a/src/preload/index.ts b/src/preload/index.ts index 9ec45b26..1c6a1f7f 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -417,7 +417,14 @@ const api = { }, superPanelAddBlockedApp: () => ipcRenderer.invoke('super-panel:add-blocked-app'), // 超级面板窗口匹配 - superPanelSearchWindowCommands: (windowInfo: { app?: string; title?: string }) => + superPanelGetFileLocationWindows: () => ipcRenderer.invoke('super-panel:get-file-location-windows'), + superPanelIsFileLocationWindow: (hwnd: number) => + ipcRenderer.invoke('super-panel:is-file-location-window', hwnd), + superPanelSetFileLocationAddressBar: ( + target: number | FileLocationWindowInfo, + address: string + ) => ipcRenderer.invoke('super-panel:set-file-location-address-bar', target, address), + superPanelSearchWindowCommands: (windowInfo: SuperPanelWindowInfo) => ipcRenderer.invoke('super-panel:search-window-commands', windowInfo), onSuperPanelWindowCommandsData: (callback: (data: { results: any[] }) => void) => { ipcRenderer.on('super-panel-window-commands-data', (_event, data) => callback(data)) @@ -426,11 +433,9 @@ const api = { ipcRenderer.on('super-panel-translation', (_event, data) => callback(data)) }, onSuperPanelSearchWindowCommands: ( - callback: (windowInfo: { app?: string; title?: string }) => void + callback: (data: { requestId: number; windowInfo: SuperPanelWindowInfo }) => void ) => { - ipcRenderer.on('super-panel-search-window-commands', (_event, windowInfo) => - callback(windowInfo) - ) + ipcRenderer.on('super-panel-search-window-commands', (_event, data) => callback(data)) }, sendSuperPanelWindowCommandsResult: (data: { results: any[] }) => { ipcRenderer.send('super-panel-window-commands-result', data) @@ -454,6 +459,26 @@ contextBridge.exposeInMainWorld('electron', { }) // TypeScript 类型定义 +type SuperPanelWindowInfo = { + platform?: 'win32' | 'darwin' + kind?: 'windows-explorer' | 'windows-file-dialog' | 'mac-finder' | 'mac-file-dialog' + preciseTarget?: boolean + app?: string + title?: string + className?: string + hwnd?: number + windowId?: number + finderId?: number + bundleId?: string + pid?: number + path?: string + url?: string + axRole?: string + axSubrole?: string +} + +type FileLocationWindowInfo = SuperPanelWindowInfo + declare global { interface Window { electron: { @@ -717,6 +742,20 @@ declare global { path: string, featureCode?: string ) => Promise<{ success: boolean; error?: string }> + superPanelGetFileLocationWindows: () => Promise> + superPanelIsFileLocationWindow: ( + hwnd: number + ) => Promise<{ supported: boolean; isFileLocation: boolean }> + superPanelSetFileLocationAddressBar: ( + target: number | FileLocationWindowInfo, + address: string + ) => Promise + superPanelSearchWindowCommands: (windowInfo: SuperPanelWindowInfo) => Promise + onSuperPanelWindowCommandsData: (callback: (data: { requestId?: number; results: any[] }) => void) => void + onSuperPanelSearchWindowCommands: ( + callback: (data: { requestId: number; windowInfo: SuperPanelWindowInfo }) => void + ) => void + sendSuperPanelWindowCommandsResult: (data: { requestId: number; results: any[] }) => void // AI 模型管理 aiModels: { getAll: () => Promise<{ success: boolean; data?: any[]; error?: string }> diff --git a/src/renderer/src/App.vue b/src/renderer/src/App.vue index 40f8063d..b141f692 100644 --- a/src/renderer/src/App.vue +++ b/src/renderer/src/App.vue @@ -943,12 +943,27 @@ onMounted(async () => { }) // 监听超级面板窗口匹配搜索请求 - window.ztools.onSuperPanelSearchWindowCommands((windowInfo: { app?: string; title?: string }) => { - const results = commandDataStore.searchWindowCommands(windowInfo) - window.ztools.sendSuperPanelWindowCommandsResult({ - results: JSON.parse(JSON.stringify(results)) - }) - }) + window.ztools.onSuperPanelSearchWindowCommands( + (data: { + requestId: number + windowInfo: { + app?: string + title?: string + className?: string + hwnd?: number + bundleId?: string + pid?: number + } + }) => { + console.log('[超级面板窗口匹配] 收到搜索请求:', data.windowInfo) + const results = commandDataStore.searchWindowCommands(data.windowInfo) + console.log('[超级面板窗口匹配] 返回结果数:', results.length) + window.ztools.sendSuperPanelWindowCommandsResult({ + requestId: data.requestId, + results: JSON.parse(JSON.stringify(results)) + }) + } + ) // 监听超级面板启动事件(由主进程从超级面板转发) window.ztools.onSuperPanelLaunch( diff --git a/src/renderer/src/components/SuperPanel.vue b/src/renderer/src/components/SuperPanel.vue index 175b9bc4..44de4cbb 100644 --- a/src/renderer/src/components/SuperPanel.vue +++ b/src/renderer/src/components/SuperPanel.vue @@ -324,30 +324,59 @@
-
- -
- {{ item.name.charAt(0).toUpperCase() }} +
+
快捷跳转
+
+
+
+ {{ formatFileLocationJumpPath(item) }} + {{ item.title }} +
-
- {{ item.name }} - {{ item.pluginExplain }} +
+
+
窗口命令
+
+ +
+ {{ item.name.charAt(0).toUpperCase() }} +
+
+ {{ item.name }} + {{ item.pluginExplain }} +
-
+
无匹配结果
@@ -408,6 +437,40 @@ interface ClipboardContent { files?: Array<{ path: string; name: string; isDirectory: boolean }> } +interface CurrentWindowInfo { + platform?: 'win32' | 'darwin' + kind?: 'windows-explorer' | 'windows-file-dialog' | 'mac-finder' | 'mac-file-dialog' + preciseTarget?: boolean + app?: string + title?: string + className?: string + hwnd?: number + windowId?: number + finderId?: number + bundleId?: string + pid?: number + path?: string + url?: string + axRole?: string + axSubrole?: string +} + +interface FileLocationJumpTarget extends CurrentWindowInfo {} + +type FileLocationAddressBarTarget = Pick< + CurrentWindowInfo, + | 'platform' + | 'kind' + | 'preciseTarget' + | 'hwnd' + | 'windowId' + | 'finderId' + | 'bundleId' + | 'pid' + | 'axRole' + | 'axSubrole' +> + const mode = ref<'pinned' | 'search' | 'loading'>('loading') const pinnedCommands = ref([]) const searchResults = ref([]) @@ -428,7 +491,10 @@ const customColor = ref('#db2777') const showWindowMatch = ref(false) const windowMatchResults = ref([]) const windowMatchSelectedIndex = ref(0) -const currentWindowInfo = ref<{ app?: string; title?: string } | null>(null) +const currentWindowInfo = ref(null) +const fileLocationJumpTargets = ref([]) +const fileLocationJumpLoading = ref(false) +let fileLocationJumpRequestId = 0 // 窗口匹配图标闪动 const windowMatchBlink = ref(false) @@ -774,6 +840,8 @@ function openWindowMatch(): void { showWindowMatch.value = true windowMatchBlink.value = false windowMatchSelectedIndex.value = 0 + fileLocationJumpTargets.value = [] + loadFileLocationJumpTargets() // 如果已有结果(自动搜索过),直接使用;否则发起搜索 if (windowMatchResults.value.length === 0 && currentWindowInfo.value) { window.ztools.superPanelSearchWindowCommands( @@ -785,6 +853,247 @@ function openWindowMatch(): void { // 关闭窗口匹配面板 function closeWindowMatch(): void { showWindowMatch.value = false + fileLocationJumpRequestId++ +} + +// 判断当前触发窗口是否为可精确定位的文件位置窗口 +async function getCurrentFileLocationWindowKind(): Promise { + const info = currentWindowInfo.value + console.log('object', info); + if (!info) return null + + const platform = window.ztools.getPlatform() + if (info.preciseTarget && info.kind) { + if (info.kind === 'windows-explorer' && platform === 'win32' && hasWindowsAddressBarTarget(info)) { + return info.kind + } + if (info.kind === 'windows-file-dialog' && platform === 'win32' && hasWindowsAddressBarTarget(info)) { + return info.kind + } + if (info.kind === 'mac-finder' && platform === 'darwin' && hasMacAddressBarTarget(info)) { + return info.kind + } + if (info.kind === 'mac-file-dialog' && platform === 'darwin' && hasMacAddressBarTarget(info)) { + return info.kind + } + } + + if (platform === 'win32' && hasWindowsAddressBarTarget(info)) { + if (info.className === 'CabinetWClass' || info.className === 'ExploreWClass') { + return 'windows-explorer' + } + if (await isWindowsFileDialogWindow(info)) { + return 'windows-file-dialog' + } + } + + return null +} + +// 判断 Windows 当前窗口是否为可跳转的文件对话框 +async function isWindowsFileDialogWindow(info: CurrentWindowInfo): Promise { + if (info.className !== '#32770' || !info.hwnd) return false + + try { + const result = await window.ztools.superPanelIsFileLocationWindow(info.hwnd) + return result.supported + } catch (error) { + console.error('[SuperPanel] 判断 Windows 文件对话框失败:', error) + } + + return isCommonWindowsFileDialogTitle(info.title) +} + +// 判断 Windows 文件对话框的常见标题 +function isCommonWindowsFileDialogTitle(title?: string): boolean { + return /^(打开|开启|选择|另存为|保存|Open|Choose|Select|Save|Save As)$/i.test( + title?.trim() || '' + ) +} + +// 判断 Windows 当前窗口是否有可传给地址栏设置的窗口句柄 +function hasWindowsAddressBarTarget(info: CurrentWindowInfo): boolean { + return typeof info.hwnd === 'number' && Number.isFinite(info.hwnd) && info.hwnd > 0 +} + +function hasMacAddressBarTarget(info: CurrentWindowInfo): boolean { + if (!info.preciseTarget) return false + if (info.kind === 'mac-finder') { + return typeof info.finderId === 'number' && Number.isFinite(info.finderId) && info.finderId > 0 + } + if (info.kind === 'mac-file-dialog') { + return typeof info.pid === 'number' && Number.isFinite(info.pid) && info.pid > 0 + } + return false +} + +// 标准化文件位置字符串用于比较 +function normalizeFileLocation(value?: string): string | null { + if (!value) return null + let normalized = value.trim() + if (!normalized) return null + + normalized = normalized.replace(/^file:\/\/\//i, '') + normalized = normalized.replace(/^file:\/\//i, '') + normalized = normalized.replace(/^\/([A-Za-z]:\/)/, '$1') + + try { + normalized = decodeURIComponent(normalized) + } catch { + // 保留原值用于后续比较 + } + + normalized = normalized.replace(/\\/g, '/') + if (window.ztools.getPlatform() === 'win32') { + normalized = normalized.toLowerCase() + } + return normalized.replace(/\/+$/, '') +} + +// 比较两个文件位置字段是否指向同一地址 +function isSameFileLocation(a?: string, b?: string): boolean { + const left = normalizeFileLocation(a) + const right = normalizeFileLocation(b) + return Boolean(left && right && left === right) +} + +// 生成快捷跳转列表项的稳定 key +function getFileLocationJumpKey(target: FileLocationJumpTarget, index: number): string { + return [target.path, target.url, target.hwnd, target.title, target.bundleId, target.pid, index] + .filter((part) => part !== undefined && part !== null && part !== '') + .join('|') +} + +// 判断快捷跳转目标是否为当前窗口 +function isCurrentFileLocationTarget( + target: FileLocationJumpTarget, + currentWindow: CurrentWindowInfo +): boolean { + if (currentWindow.hwnd && target.hwnd) { + return target.hwnd === currentWindow.hwnd + } + if (currentWindow.finderId && target.finderId) { + return target.finderId === currentWindow.finderId + } + if (currentWindow.windowId && target.windowId) { + return target.windowId === currentWindow.windowId + } + return ( + isSameFileLocation(target.path, currentWindow.path) || + isSameFileLocation(target.url, currentWindow.url) || + isSameFileLocation(target.path, currentWindow.url) || + isSameFileLocation(target.url, currentWindow.path) + ) +} + +// 加载其它文件位置窗口作为快捷跳转目标 +async function loadFileLocationJumpTargets(): Promise { + fileLocationJumpTargets.value = [] + const requestId = ++fileLocationJumpRequestId + const currentKind = await getCurrentFileLocationWindowKind() + if (requestId !== fileLocationJumpRequestId) { + return + } + if (!currentKind) { + console.log('[SuperPanel] 当前窗口不是文件位置窗口,跳过快捷跳转目标加载:', currentWindowInfo.value) + return + } + + const currentWindow = currentWindowInfo.value + fileLocationJumpLoading.value = true + + try { + const rawWindows = (await window.ztools.superPanelGetFileLocationWindows()) as Array< + FileLocationJumpTarget | string + > + const windows = (rawWindows || []).map((item) => + typeof item === 'string' ? { path: item, url: item } : item + ) + if (requestId !== fileLocationJumpRequestId) { + return + } + + fileLocationJumpTargets.value = (windows || []) + .filter((item) => item?.path || item?.url) + .filter((item) => currentWindow && !isCurrentFileLocationTarget(item, currentWindow)) + windowMatchSelectedIndex.value = 0 + console.log('[SuperPanel] 文件位置快捷跳转目标已加载:', { + currentKind, + currentWindow, + total: windows?.length || 0, + targets: fileLocationJumpTargets.value.length + }) + } catch (error) { + console.error('[SuperPanel] 加载文件位置快捷跳转失败:', error) + fileLocationJumpTargets.value = [] + } finally { + if (requestId === fileLocationJumpRequestId) { + fileLocationJumpLoading.value = false + } + } +} + +// 格式化快捷跳转路径展示文本 +function formatFileLocationJumpPath(target: FileLocationJumpTarget): string { + const rawPath = target.path || target.url || target.title || '' + if (/^file:\/\/\//i.test(rawPath)) { + const withoutScheme = rawPath.replace(/^file:\/\/\//i, '') + return withoutScheme.replace(/^\/([A-Za-z]:\/)/, '$1') + } + return rawPath.replace(/^file:\/\//i, '') +} + +// 生成可安全传递给 IPC 的文件位置窗口标识 +function toFileLocationAddressBarTarget( + windowInfo: CurrentWindowInfo +): FileLocationAddressBarTarget | null { + const platform = window.ztools.getPlatform() + if (platform === 'win32') { + return hasWindowsAddressBarTarget(windowInfo) ? { hwnd: windowInfo.hwnd } : null + } + if (platform === 'darwin' && hasMacAddressBarTarget(windowInfo)) { + return { + platform: 'darwin', + kind: windowInfo.kind, + preciseTarget: windowInfo.preciseTarget, + windowId: windowInfo.windowId, + finderId: windowInfo.finderId, + bundleId: windowInfo.bundleId, + pid: windowInfo.pid, + axRole: windowInfo.axRole, + axSubrole: windowInfo.axSubrole + } + } + return null +} + +// 将当前文件位置窗口跳转到目标路径 +async function jumpToFileLocationTarget(target: FileLocationJumpTarget): Promise { + const currentWindow = currentWindowInfo.value + const address = target.path || target.url + if (!currentWindow || !address) return + + try { + const addressBarTarget = toFileLocationAddressBarTarget(currentWindow) + if (!addressBarTarget) return + + console.log('[SuperPanel] 准备执行文件位置快捷跳转:', { + currentWindow: addressBarTarget, + target, + address + }) + const success = await window.ztools.superPanelSetFileLocationAddressBar( + addressBarTarget, + address + ) + console.log('[SuperPanel] 文件位置快捷跳转结果:', success) + if (success) { + closeWindowMatch() + window.close() + } + } catch (error) { + console.error('[SuperPanel] 文件位置快捷跳转失败:', error) + } } // 从窗口匹配面板启动指令 @@ -793,6 +1102,26 @@ async function launchWindowMatch(cmd: CommandItem): Promise { await launch(cmd) } +// 获取窗口匹配面板可操作项总数 +function getWindowMatchItemCount(): number { + return fileLocationJumpTargets.value.length + windowMatchResults.value.length +} + +// 启动当前选中的窗口匹配面板项 +function launchSelectedWindowMatchItem(): void { + const jumpTarget = fileLocationJumpTargets.value[windowMatchSelectedIndex.value] + if (jumpTarget) { + jumpToFileLocationTarget(jumpTarget) + return + } + + const commandIndex = windowMatchSelectedIndex.value - fileLocationJumpTargets.value.length + const command = windowMatchResults.value[commandIndex] + if (command) { + launchWindowMatch(command) + } +} + function getItemKey(item: GridItem): string { if (isFolder(item)) { return `folder-${item.id}` @@ -825,6 +1154,34 @@ async function launch(cmd: CommandItem): Promise { // 键盘导航 function handleKeydown(event: KeyboardEvent): void { + if (showWindowMatch.value) { + const itemCount = getWindowMatchItemCount() + switch (event.key) { + case 'ArrowUp': + event.preventDefault() + if (itemCount > 0 && windowMatchSelectedIndex.value > 0) { + windowMatchSelectedIndex.value-- + } + break + case 'ArrowDown': + event.preventDefault() + if (itemCount > 0 && windowMatchSelectedIndex.value < itemCount - 1) { + windowMatchSelectedIndex.value++ + } + break + case 'Enter': + event.preventDefault() + launchSelectedWindowMatchItem() + break + case 'Escape': + event.preventDefault() + closeWindowMatch() + break + } + scrollToSelected() + return + } + const list = getCurrentList() if (list.length === 0) return @@ -946,13 +1303,14 @@ onMounted(() => { data.commands?.length || data.results?.length || 0 ) // 保存窗口信息 - if (data.windowInfo) { - currentWindowInfo.value = data.windowInfo - } + currentWindowInfo.value = data.windowInfo ?? null // 收到新数据时关闭窗口匹配面板、重置闪动状态 showWindowMatch.value = false + fileLocationJumpRequestId++ windowMatchBlink.value = false windowMatchResults.value = [] + fileLocationJumpTargets.value = [] + fileLocationJumpLoading.value = false // 自动发起窗口匹配搜索(用于判断是否需要闪动图标) if (data.windowInfo) { @@ -1572,6 +1930,38 @@ onUnmounted(() => { min-height: 0; } +.window-match-section { + display: flex; + flex-direction: column; + gap: 1px; +} + +.window-match-section + .window-match-section { + margin-top: 6px; +} + +.window-match-section-title { + padding: 6px 8px 4px; + font-size: 11px; + color: var(--text-secondary); +} + +.file-location-jump-item { + align-items: flex-start; +} + +.file-location-jump-item .list-name { + font-family: ui-monospace, SFMono-Regular, Consolas, 'Liberation Mono', monospace; + font-size: 11px; + font-weight: 400; + line-height: 1.35; + overflow: visible; + text-overflow: clip; + white-space: normal; + overflow-wrap: anywhere; + word-break: break-all; +} + .window-match-empty { min-height: 60px; } diff --git a/src/renderer/src/env.d.ts b/src/renderer/src/env.d.ts index 271f7345..d1e94ecc 100644 --- a/src/renderer/src/env.d.ts +++ b/src/renderer/src/env.d.ts @@ -11,6 +11,26 @@ interface LastMatchState { timestamp: number } +interface SuperPanelWindowInfo { + platform?: 'win32' | 'darwin' + kind?: 'windows-explorer' | 'windows-file-dialog' | 'mac-finder' | 'mac-file-dialog' + preciseTarget?: boolean + app?: string + title?: string + className?: string + hwnd?: number + windowId?: number + finderId?: number + bundleId?: string + pid?: number + path?: string + url?: string + axRole?: string + axSubrole?: string +} + +interface FileLocationWindowInfo extends SuperPanelWindowInfo {} + declare global { interface Window { electron: { @@ -391,7 +411,7 @@ declare global { commands?: any[] results?: any[] clipboardContent?: any - windowInfo?: { app?: string; title?: string } + windowInfo?: SuperPanelWindowInfo | null }) => void ) => void superPanelLaunch: (command: any) => Promise<{ success: boolean; error?: string }> @@ -412,18 +432,25 @@ declare global { ) => void superPanelAddBlockedApp: () => Promise<{ success: boolean; app?: string; error?: string }> // 超级面板窗口匹配 - superPanelSearchWindowCommands: (windowInfo: { - app?: string - title?: string - }) => Promise - onSuperPanelWindowCommandsData: (callback: (data: { results: any[] }) => void) => void + superPanelGetFileLocationWindows: () => Promise> + superPanelIsFileLocationWindow: ( + hwnd: number + ) => Promise<{ supported: boolean; isFileLocation: boolean }> + superPanelSetFileLocationAddressBar: ( + target: number | FileLocationWindowInfo, + address: string + ) => Promise + superPanelSearchWindowCommands: (windowInfo: SuperPanelWindowInfo) => Promise + onSuperPanelWindowCommandsData: ( + callback: (data: { requestId?: number; results: any[] }) => void + ) => void onSuperPanelTranslation: ( callback: (data: { text: string; sourceText?: string }) => void ) => void onSuperPanelSearchWindowCommands: ( - callback: (windowInfo: { app?: string; title?: string }) => void + callback: (data: { requestId: number; windowInfo: SuperPanelWindowInfo }) => void ) => void - sendSuperPanelWindowCommandsResult: (data: { results: any[] }) => void + sendSuperPanelWindowCommandsResult: (data: { requestId: number; results: any[] }) => void onFloatingBallFiles: ( callback: (files: Array<{ path: string; name: string; isDirectory: boolean }>) => void ) => void