diff --git a/packages/shared/IpcChannel.ts b/packages/shared/IpcChannel.ts index c1725968f4..6f350ceb9a 100644 --- a/packages/shared/IpcChannel.ts +++ b/packages/shared/IpcChannel.ts @@ -82,8 +82,6 @@ export enum IpcChannel { Windows_ResetMinimumSize = 'window:reset-minimum-size', Windows_SetMinimumSize = 'window:set-minimum-size', - SelectionMenu_Action = 'selection-menu:action', - KnowledgeBase_Create = 'knowledge-base:create', KnowledgeBase_Reset = 'knowledge-base:reset', KnowledgeBase_Delete = 'knowledge-base:delete', @@ -136,7 +134,6 @@ export enum IpcChannel { System_GetHostname = 'system:getHostname', // events - SelectionAction = 'selection-action', BackupProgress = 'backup-progress', ThemeChange = 'theme:change', UpdateDownloadedCancelled = 'update-downloaded-cancelled', diff --git a/resources/textMonitor.swift b/resources/textMonitor.swift deleted file mode 100644 index 3c8b97bd92..0000000000 --- a/resources/textMonitor.swift +++ /dev/null @@ -1,117 +0,0 @@ -import Cocoa -import Foundation - -class TextSelectionObserver: NSObject { - let workspace = NSWorkspace.shared - var lastSelectedText: String? - - override init() { - super.init() - - // 注册通知观察者 - let observer = NSWorkspace.shared.notificationCenter - observer.addObserver( - self, - selector: #selector(handleSelectionChange), - name: NSWorkspace.didActivateApplicationNotification, - object: nil - ) - - // 监听选择变化通知 - var axObserver: AXObserver? - let error = AXObserverCreate(getpid(), { observer, element, notification, userData in - let selfPointer = userData!.load(as: TextSelectionObserver.self) - selfPointer.checkSelectedText() - }, &axObserver) - - if error == .success, let axObserver = axObserver { - CFRunLoopAddSource( - RunLoop.main.getCFRunLoop(), - AXObserverGetRunLoopSource(axObserver), - .defaultMode - ) - - // 当前活动应用添加监听 - updateActiveAppObserver(axObserver) - } - } - - @objc func handleSelectionChange(_ notification: Notification) { - // 应用切换时更新监听 - var axObserver: AXObserver? - let error = AXObserverCreate(getpid(), { _, _, _, _ in }, &axObserver) - if error == .success, let axObserver = axObserver { - updateActiveAppObserver(axObserver) - } - } - - func updateActiveAppObserver(_ axObserver: AXObserver) { - guard let app = workspace.frontmostApplication else { return } - let pid = app.processIdentifier - let element = AXUIElementCreateApplication(pid) - - // 添加选择变化通知监听 - AXObserverAddNotification( - axObserver, - element, - kAXSelectedTextChangedNotification as CFString, - UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque()) - ) - } - - func checkSelectedText() { - if let text = getSelectedText() { - if text.count > 0 && text != lastSelectedText { - print(text) - fflush(stdout) - lastSelectedText = text - } - } - } - - func getSelectedText() -> String? { - guard let app = NSWorkspace.shared.frontmostApplication else { return nil } - let pid = app.processIdentifier - - let axApp = AXUIElementCreateApplication(pid) - var focusedElement: AnyObject? - - // Get focused element - let result = AXUIElementCopyAttributeValue(axApp, kAXFocusedUIElementAttribute as CFString, &focusedElement) - guard result == .success else { return nil } - - // Try different approaches to get selected text - var selectedText: AnyObject? - - // First try: Direct selected text - var textResult = AXUIElementCopyAttributeValue(focusedElement as! AXUIElement, kAXSelectedTextAttribute as CFString, &selectedText) - - // Second try: Selected text in text area - if textResult != .success { - var selectedTextRange: AnyObject? - textResult = AXUIElementCopyAttributeValue(focusedElement as! AXUIElement, kAXSelectedTextRangeAttribute as CFString, &selectedTextRange) - if textResult == .success { - textResult = AXUIElementCopyAttributeValue(focusedElement as! AXUIElement, kAXValueAttribute as CFString, &selectedText) - } - } - - // Third try: Get selected text from parent element - if textResult != .success { - var parent: AnyObject? - if AXUIElementCopyAttributeValue(focusedElement as! AXUIElement, kAXParentAttribute as CFString, &parent) == .success { - textResult = AXUIElementCopyAttributeValue(parent as! AXUIElement, kAXSelectedTextAttribute as CFString, &selectedText) - } - } - - guard textResult == .success, let text = selectedText as? String else { return nil } - return text - } -} - -let observer = TextSelectionObserver() - -signal(SIGINT) { _ in - exit(0) -} - -RunLoop.main.run() \ No newline at end of file diff --git a/src/main/services/ClipboardMonitor.ts b/src/main/services/ClipboardMonitor.ts deleted file mode 100644 index 8a4ff540c6..0000000000 --- a/src/main/services/ClipboardMonitor.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { debounce, getResourcePath } from '@main/utils' -import { exec } from 'child_process' -import { screen } from 'electron' -import path from 'path' - -import { windowService } from './WindowService' - -export default class ClipboardMonitor { - private platform: string - private lastText: string - private user32: any - private observer: any - public onTextSelected: (text: string) => void - - constructor() { - this.platform = process.platform - this.lastText = '' - this.onTextSelected = debounce((text: string) => this.handleTextSelected(text), 550) - - if (this.platform === 'win32') { - this.setupWindows() - } else if (this.platform === 'darwin') { - this.setupMacOS() - } - } - - setupMacOS() { - // 使用 Swift 脚本来监听文本选择 - const scriptPath = path.join(getResourcePath(), 'textMonitor.swift') - - // 启动 Swift 进程来监听文本选择 - const process = exec(`swift ${scriptPath}`) - - process?.stdout?.on('data', (data: string) => { - console.log('[ClipboardMonitor] MacOS data:', data) - const text = data.toString().trim() - if (text && text !== this.lastText) { - this.lastText = text - this.onTextSelected(text) - } - }) - - process.on('error', (error) => { - console.error('[ClipboardMonitor] MacOS error:', error) - }) - } - - setupWindows() { - // 使用 Windows API 监听文本选择事件 - const ffi = require('ffi-napi') - const ref = require('ref-napi') - - this.user32 = new ffi.Library('user32', { - SetWinEventHook: ['pointer', ['uint32', 'uint32', 'pointer', 'pointer', 'uint32', 'uint32', 'uint32']], - UnhookWinEvent: ['bool', ['pointer']] - }) - - // 定义事件常量 - const EVENT_OBJECT_SELECTION = 0x8006 - const WINEVENT_OUTOFCONTEXT = 0x0000 - const WINEVENT_SKIPOWNTHREAD = 0x0001 - const WINEVENT_SKIPOWNPROCESS = 0x0002 - - // 创建回调函数 - const callback = ffi.Callback('void', ['pointer', 'uint32', 'pointer', 'long', 'long', 'uint32', 'uint32'], () => { - this.getSelectedText() - }) - - // 设置事件钩子 - this.observer = this.user32.SetWinEventHook( - EVENT_OBJECT_SELECTION, - EVENT_OBJECT_SELECTION, - ref.NULL, - callback, - 0, - 0, - WINEVENT_OUTOFCONTEXT | WINEVENT_SKIPOWNTHREAD | WINEVENT_SKIPOWNPROCESS - ) - } - - getSelectedText() { - // Get selected text - if (this.platform === 'win32') { - const ref = require('ref-napi') - if (this.user32.OpenClipboard(ref.NULL)) { - // Get clipboard content - const text = this.user32.GetClipboardData(1) // CF_TEXT = 1 - this.user32.CloseClipboard() - - if (text && text !== this.lastText) { - this.lastText = text - this.onTextSelected(text) - } - } - } - } - - private handleTextSelected(text: string) { - if (!text) return - - console.log('[ClipboardMonitor] handleTextSelected', text) - - windowService.setLastSelectedText(text) - - const mousePosition = screen.getCursorScreenPoint() - - windowService.showSelectionMenu({ - x: mousePosition.x, - y: mousePosition.y + 10 - }) - } - - dispose() { - if (this.platform === 'win32' && this.observer) { - this.user32.UnhookWinEvent(this.observer) - } - } -} diff --git a/src/main/services/WindowService.ts b/src/main/services/WindowService.ts index e65e10ad3b..585f7c6eea 100644 --- a/src/main/services/WindowService.ts +++ b/src/main/services/WindowService.ts @@ -22,8 +22,6 @@ export class WindowService { //hacky-fix: store the focused status of mainWindow before miniWindow shows //to restore the focus status when miniWindow hides private wasMainWindowFocused: boolean = false - private selectionMenuWindow: BrowserWindow | null = null - private lastSelectedText: string = '' private contextMenu: Menu | null = null private lastRendererProcessCrashTime: number = 0 @@ -508,10 +506,6 @@ export class WindowService { return } - if (this.selectionMenuWindow && !this.selectionMenuWindow.isDestroyed()) { - this.selectionMenuWindow.hide() - } - if (this.miniWindow && !this.miniWindow.isDestroyed()) { this.wasMainWindowFocused = this.mainWindow?.isFocused() || false @@ -558,74 +552,6 @@ export class WindowService { public setPinMiniWindow(isPinned) { this.isPinnedMiniWindow = isPinned } - - public showSelectionMenu(bounds: { x: number; y: number }) { - if (this.selectionMenuWindow && !this.selectionMenuWindow.isDestroyed()) { - this.selectionMenuWindow.setPosition(bounds.x, bounds.y) - this.selectionMenuWindow.show() - return - } - - const theme = configManager.getTheme() - - this.selectionMenuWindow = new BrowserWindow({ - width: 280, - height: 40, - x: bounds.x, - y: bounds.y, - show: true, - autoHideMenuBar: true, - transparent: true, - frame: false, - alwaysOnTop: false, - skipTaskbar: true, - backgroundColor: isMac ? undefined : theme === 'dark' ? '#181818' : '#FFFFFF', - resizable: false, - vibrancy: 'popover', - webPreferences: { - preload: join(__dirname, '../preload/index.js'), - sandbox: false, - webSecurity: false - } - }) - - // 点击其他地方时隐藏窗口 - this.selectionMenuWindow.on('blur', () => { - this.selectionMenuWindow?.hide() - this.miniWindow?.webContents.send(IpcChannel.SelectionAction, { - action: 'home', - selectedText: this.lastSelectedText - }) - }) - - if (is.dev && process.env['ELECTRON_RENDERER_URL']) { - this.selectionMenuWindow.loadURL(process.env['ELECTRON_RENDERER_URL'] + '/src/windows/menu/menu.html') - } else { - this.selectionMenuWindow.loadFile(join(__dirname, '../renderer/src/windows/menu/menu.html')) - } - - this.setupSelectionMenuEvents() - } - - private setupSelectionMenuEvents() { - if (!this.selectionMenuWindow) return - - ipcMain.removeHandler(IpcChannel.SelectionMenu_Action) - ipcMain.handle(IpcChannel.SelectionMenu_Action, (_, action) => { - this.selectionMenuWindow?.hide() - this.showMiniWindow() - setTimeout(() => { - this.miniWindow?.webContents.send(IpcChannel.SelectionAction, { - action, - selectedText: this.lastSelectedText - }) - }, 100) - }) - } - - public setLastSelectedText(text: string) { - this.lastSelectedText = text - } } export const windowService = WindowService.getInstance() diff --git a/src/preload/index.ts b/src/preload/index.ts index e28dc316e7..978417fa3d 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -110,9 +110,6 @@ const api = { listFiles: (apiKey: string) => ipcRenderer.invoke(IpcChannel.Gemini_ListFiles, apiKey), deleteFile: (fileId: string, apiKey: string) => ipcRenderer.invoke(IpcChannel.Gemini_DeleteFile, fileId, apiKey) }, - selectionMenu: { - action: (action: string) => ipcRenderer.invoke(IpcChannel.SelectionMenu_Action, action) - }, config: { set: (key: string, value: any) => ipcRenderer.invoke(IpcChannel.Config_Set, key, value), get: (key: string) => ipcRenderer.invoke(IpcChannel.Config_Get, key) diff --git a/src/renderer/src/windows/menu/menu.html b/src/renderer/src/windows/menu/menu.html deleted file mode 100644 index dd7945b104..0000000000 --- a/src/renderer/src/windows/menu/menu.html +++ /dev/null @@ -1,144 +0,0 @@ - - - - - - Selection Menu - - - - -
- - - - - - - - - - diff --git a/src/renderer/src/windows/mini/home/HomeWindow.tsx b/src/renderer/src/windows/mini/home/HomeWindow.tsx index de24d7ae8f..d87bff59c0 100644 --- a/src/renderer/src/windows/mini/home/HomeWindow.tsx +++ b/src/renderer/src/windows/mini/home/HomeWindow.tsx @@ -238,15 +238,9 @@ const HomeWindow: FC = () => { useEffect(() => { window.electron.ipcRenderer.on(IpcChannel.ShowMiniWindow, onWindowShow) - window.electron.ipcRenderer.on(IpcChannel.SelectionAction, (_, { action, selectedText }) => { - selectedText && setSelectedText(selectedText) - action && setRoute(action) - action === 'chat' && onSendMessage() - }) return () => { window.electron.ipcRenderer.removeAllListeners(IpcChannel.ShowMiniWindow) - window.electron.ipcRenderer.removeAllListeners(IpcChannel.SelectionAction) } }, [onWindowShow, onSendMessage, setRoute])