diff --git a/src/main/index.ts b/src/main/index.ts index b0c2ff9..078886b 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -44,26 +44,14 @@ function createFloatingWindow(): void { } }) - // Handle context menu - ipcMain.on('show-context-menu', () => { - if (floatingWindow) { - const { Menu } = require('electron') - const menu = Menu.buildFromTemplate([ - { - label: '设置', - click: () => { - createSettingsWindow() - } - }, - { - label: '退出', - click: () => { - app.quit() - } - } - ]) - menu.popup({ window: floatingWindow }) - } + // Handle open settings from renderer + ipcMain.on('open-settings', () => { + createSettingsWindow() + }) + + // Handle quit app from renderer + ipcMain.on('quit-app', () => { + app.quit() }) // Load the floating window HTML diff --git a/src/renderer/src/components/ContextMenu.tsx b/src/renderer/src/components/ContextMenu.tsx new file mode 100644 index 0000000..f331f08 --- /dev/null +++ b/src/renderer/src/components/ContextMenu.tsx @@ -0,0 +1,190 @@ +import React from 'react' + +interface ContextMenuProps { + isOpen: boolean + position: { x: number; y: number } + onClose: () => void + onSettings: () => void + onQuit: () => void + onMouseEnter?: () => void + onMouseLeave?: () => void +} + +const ContextMenu: React.FC = ({ + isOpen, + position, + onClose, + onSettings, + onQuit, + onMouseEnter, + onMouseLeave +}) => { + if (!isOpen) return null + + return ( +
+ {/* Backdrop to catch clicks outside menu */} +
+ + {/* Menu container */} +
e.stopPropagation()} + onMouseEnter={onMouseEnter} + onMouseLeave={onMouseLeave} + style={{ + position: 'absolute', + left: `${position.x}px`, + top: `${position.y}px`, + pointerEvents: 'auto', + minWidth: '180px', + background: 'rgba(255, 255, 255, 0.98)', + backdropFilter: 'blur(20px)', + WebkitBackdropFilter: 'blur(20px)', + borderRadius: '12px', + boxShadow: '0 8px 32px rgba(0, 0, 0, 0.12), 0 2px 8px rgba(0, 0, 0, 0.08)', + border: '1px solid rgba(255, 255, 255, 0.8)', + padding: '8px', + animation: 'menuFadeIn 0.15s ease-out', + transformOrigin: 'top left' + }} + > + + + {/* Settings menu item */} +
{ + onSettings() + onClose() + }} + > +
+ + + + + +
+ 设置 +
+ + {/* Divider */} +
+ + {/* Quit menu item */} +
{ + onQuit() + onClose() + }} + > +
+ + + + + +
+ 退出 +
+
+
+ ) +} + +export default ContextMenu diff --git a/src/renderer/src/components/FloatingBall.tsx b/src/renderer/src/components/FloatingBall.tsx index d1c5210..ada684b 100644 --- a/src/renderer/src/components/FloatingBall.tsx +++ b/src/renderer/src/components/FloatingBall.tsx @@ -1,5 +1,6 @@ import React, { useState, useRef, useEffect } from 'react' import { streamChat } from '../services/aiService' +import ContextMenu from './ContextMenu' const FloatingBall: React.FC = () => { const [isTooltipOpen, setIsTooltipOpen] = useState(false) @@ -8,6 +9,9 @@ const FloatingBall: React.FC = () => { const [inputValue, setInputValue] = useState('') const [aiResponse, setAiResponse] = useState('') const [isLoading, setIsLoading] = useState(false) + const [isContextMenuOpen, setIsContextMenuOpen] = useState(false) + const [contextMenuPosition, setContextMenuPosition] = useState({ x: 0, y: 0 }) + const [isMouseOverBall, setIsMouseOverBall] = useState(false) const isDraggingRef = useRef(false) const startPosRef = useRef({ x: 0, y: 0 }) const windowStartRef = useRef({ x: 0, y: 0 }) @@ -109,14 +113,18 @@ const FloatingBall: React.FC = () => { }, [isTooltipOpen]) const handleMouseEnterBall = (): void => { + setIsMouseOverBall(true) // When mouse enters the ball area, stop ignoring mouse events window.electron.ipcRenderer.send('set-ignore-mouse-events', false) } const handleMouseLeaveBall = (): void => { + setIsMouseOverBall(false) // When mouse leaves the ball area, always restore click-through // If mouse enters tooltip, the tooltip's onMouseEnter will disable click-through again - window.electron.ipcRenderer.send('set-ignore-mouse-events', true, { forward: true }) + if (!isContextMenuOpen) { + window.electron.ipcRenderer.send('set-ignore-mouse-events', true, { forward: true }) + } } const handleMouseEnterTooltip = (): void => { @@ -131,8 +139,33 @@ const FloatingBall: React.FC = () => { const handleContextMenu = (e: React.MouseEvent): void => { e.preventDefault() - // Show context menu via IPC - window.electron.ipcRenderer.send('show-context-menu') + // Show custom context menu at cursor position + setContextMenuPosition({ x: e.clientX, y: e.clientY }) + setIsContextMenuOpen(true) + // Disable mouse events pass-through when menu is open + window.electron.ipcRenderer.send('set-ignore-mouse-events', false) + } + + const handleCloseContextMenu = (): void => { + setIsContextMenuOpen(false) + // Re-enable mouse events pass-through when menu closes, but only if mouse is not over the ball + // Use setTimeout to ensure state update completes first + setTimeout(() => { + // Check if mouse is still over the ball or tooltip + if (!isMouseOverBall && !isTooltipOpen) { + window.electron.ipcRenderer.send('set-ignore-mouse-events', true, { forward: true }) + } + }, 50) + } + + const handleSettingsClick = (): void => { + // Send message to main process to open settings + window.electron.ipcRenderer.send('open-settings') + } + + const handleQuitClick = (): void => { + // Send message to main process to quit app + window.electron.ipcRenderer.send('quit-app') } const handleSendMessage = async (): Promise => { @@ -236,6 +269,23 @@ const FloatingBall: React.FC = () => { return ( <> + { + // Keep mouse events enabled when hovering over menu + window.electron.ipcRenderer.send('set-ignore-mouse-events', false) + }} + onMouseLeave={() => { + // When mouse leaves menu, restore click-through + if (!isMouseOverBall && !isTooltipOpen) { + window.electron.ipcRenderer.send('set-ignore-mouse-events', true, { forward: true }) + } + }} + />