Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/main/presenter/configPresenter/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ interface IAppSettings {
copyWithCotEnabled?: boolean
loggingEnabled?: boolean // 日志记录是否启用
floatingButtonEnabled?: boolean // 悬浮按钮是否启用
floatingButtonPosition?: { x: number; y: number } // 悬浮按钮自定义位置
default_system_prompt?: string // 默认系统提示词
[key: string]: unknown // 允许任意键,使用unknown类型替代any
}
Expand Down Expand Up @@ -108,6 +109,7 @@ export class ConfigPresenter implements IConfigPresenter {
copyWithCotEnabled: true,
loggingEnabled: false,
floatingButtonEnabled: false,
floatingButtonPosition: undefined,
default_system_prompt: '',
appVersion: this.currentAppVersion
}
Expand Down Expand Up @@ -818,6 +820,16 @@ export class ConfigPresenter implements IConfigPresenter {
}
}

// 获取悬浮按钮位置
getFloatingButtonPosition(): { x: number; y: number } | null {
return this.getSetting<{ x: number; y: number }>('floatingButtonPosition') || null
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

建议直接使用目前已经存在的窗口状态管理器来实现该功能
import windowStateManager from 'electron-window-state' // 窗口状态管理器

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已使用 electron-window-state

}

// 设置悬浮按钮位置
setFloatingButtonPosition(position: { x: number; y: number }): void {
this.setSetting('floatingButtonPosition', position)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

// ===================== MCP配置相关方法 =====================

// 获取MCP服务器配置
Expand Down
70 changes: 70 additions & 0 deletions src/main/presenter/floatingButtonPresenter/FloatingButtonWindow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import path from 'path'
import { FloatingButtonConfig, FloatingButtonState } from './types'
import logger from '../../../shared/logger'
import { platform } from '@electron-toolkit/utils'
import { presenter } from '../index'

export class FloatingButtonWindow {
private window: BrowserWindow | null = null
Expand Down Expand Up @@ -178,6 +179,11 @@ export class FloatingButtonWindow {
* 计算悬浮按钮位置
*/
private calculatePosition(): { x: number; y: number } {
if (this.config.position === 'custom' && this.config.customPosition) {
const validated = this.validatePosition(this.config.customPosition)
return validated
}

const primaryDisplay = screen.getPrimaryDisplay()
const { workAreaSize } = primaryDisplay

Expand Down Expand Up @@ -206,6 +212,51 @@ export class FloatingButtonWindow {
return { x, y }
}

/**
* 验证并修正位置,确保窗口不会完全移出可见区域
*/
private validatePosition(position: { x: number; y: number }): { x: number; y: number } {
const displays = screen.getAllDisplays()

// 检查位置是否在任何显示器的可见区域内
let isVisible = false
for (const display of displays) {
const {
x: displayX,
y: displayY,
width: displayWidth,
height: displayHeight
} = display.workArea
const windowRight = position.x + this.config.size.width
const windowBottom = position.y + this.config.size.height

// 检查窗口是否与显示器区域有交集(至少部分可见)
if (
position.x < displayX + displayWidth &&
windowRight > displayX &&
position.y < displayY + displayHeight &&
windowBottom > displayY
) {
isVisible = true
break
}
}

if (isVisible) {
return position
}

// 如果窗口不可见,将其移动到主显示器的默认位置(右下角)
logger.warn('Floating button position is out of bounds, correcting to default position')
const primaryDisplay = screen.getPrimaryDisplay()
const { workArea } = primaryDisplay

return {
x: workArea.width - this.config.size.width - 20,
y: workArea.height - this.config.size.height - 20
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Enhance the validation logic and error handling.

The position validation is good but could be improved:

  1. The boundary check logic should consider partial visibility requirements more explicitly
  2. Error handling could be more robust
  3. The corrected position should use workArea coordinates properly

Apply this improvement to the validation logic:

  private validatePosition(position: { x: number; y: number }): { x: number; y: number } {
    const displays = screen.getAllDisplays()

    // 检查位置是否在任何显示器的可见区域内
    let isVisible = false
    for (const display of displays) {
-      const {
-        x: displayX,
-        y: displayY,
-        width: displayWidth,
-        height: displayHeight
-      } = display.workArea
+      const { x: displayX, y: displayY, width: displayWidth, height: displayHeight } = display.workArea
      const windowRight = position.x + this.config.size.width
      const windowBottom = position.y + this.config.size.height

-      // 检查窗口是否与显示器区域有交集(至少部分可见)
+      // Ensure at least 25% of the button is visible within the display
+      const minVisibleWidth = this.config.size.width * 0.25
+      const minVisibleHeight = this.config.size.height * 0.25
+      const visibleWidth = Math.min(windowRight, displayX + displayWidth) - Math.max(position.x, displayX)
+      const visibleHeight = Math.min(windowBottom, displayY + displayHeight) - Math.max(position.y, displayY)
+      
       if (
-        position.x < displayX + displayWidth &&
-        windowRight > displayX &&
-        position.y < displayY + displayHeight &&
-        windowBottom > displayY
+        visibleWidth >= minVisibleWidth && visibleHeight >= minVisibleHeight
       ) {
         isVisible = true
         break
       }
     }

     if (isVisible) {
       return position
     }

-    // 如果窗口不可见,将其移动到主显示器的默认位置(右下角)
+    // Move to primary display's bottom-right corner if position is invalid
     logger.warn('Floating button position is out of bounds, correcting to default position')
     const primaryDisplay = screen.getPrimaryDisplay()
-    const { workArea } = primaryDisplay
+    const { x: workX, y: workY, width: workWidth, height: workHeight } = primaryDisplay.workArea

     return {
-      x: workArea.width - this.config.size.width - 20,
-      y: workArea.height - this.config.size.height - 20
+      x: workX + workWidth - this.config.size.width - 20,
+      y: workY + workHeight - this.config.size.height - 20
     }
   }
🤖 Prompt for AI Agents
In src/main/presenter/floatingButtonPresenter/FloatingButtonWindow.ts around
lines 215 to 258, the validatePosition method needs to explicitly enforce
partial-visibility rules, use display.workArea x/y offsets when computing
corrected coordinates, and add basic error handling; update the visibility check
to require at least a minimum visible area (e.g., width/height overlap > 1 px or
configurable threshold) by computing overlapWidth = min(windowRight,
displayX+displayWidth) - max(position.x, displayX) and overlapHeight similarly
and treat visible if both > 0, wrap the routine in try/catch to log any
unexpected errors with details, and when correcting position use workArea.x +
(workArea.width - this.config.size.width - 20) and workArea.y + (workArea.height
- this.config.size.height - 20) (or clamp each coordinate between workArea.x and
workArea.x + workArea.width - windowWidth) so the returned position is within
the primary display's workArea.


/**
* 设置窗口事件监听
*/
Expand All @@ -226,9 +277,28 @@ export class FloatingButtonWindow {
const bounds = this.window.getBounds()
this.state.bounds.x = bounds.x
this.state.bounds.y = bounds.y
this.savePosition({ x: bounds.x, y: bounds.y })
}
})

// 注意:悬浮按钮点击事件的 IPC 处理器在主进程的 index.ts 中设置
}

/**
* 保存当前位置到配置
*/
private savePosition(position: { x: number; y: number }): void {
try {
if (this.savePositionTimer) {
clearTimeout(this.savePositionTimer)
}
this.savePositionTimer = setTimeout(() => {
presenter.configPresenter.setFloatingButtonPosition(position)
logger.debug(`Floating button position saved: ${position.x}, ${position.y}`)
}, 500)
} catch (error) {
logger.error('Failed to save floating button position:', error)
}
}
private savePositionTimer: NodeJS.Timeout | null = null
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve the position saving implementation.

The debounced saving is a good approach, but there are some improvements needed:

  1. The timer property should be declared before it's used
  2. Error handling could provide more context
  3. Consider making the debounce delay configurable

Move the timer property declaration and improve the implementation:

+  private savePositionTimer: NodeJS.Timeout | null = null
+  
   /**
    * 保存当前位置到配置
    */
   private savePosition(position: { x: number; y: number }): void {
     try {
       if (this.savePositionTimer) {
         clearTimeout(this.savePositionTimer)
       }
+      
+      const SAVE_DEBOUNCE_DELAY = 500
       this.savePositionTimer = setTimeout(() => {
         presenter.configPresenter.setFloatingButtonPosition(position)
         logger.debug(`Floating button position saved: ${position.x}, ${position.y}`)
-      }, 500)
+        this.savePositionTimer = null
+      }, SAVE_DEBOUNCE_DELAY)
     } catch (error) {
-      logger.error('Failed to save floating button position:', error)
+      logger.error(`Failed to save floating button position (${position.x}, ${position.y}):`, error)
     }
   }
-  private savePositionTimer: NodeJS.Timeout | null = null
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* 保存当前位置到配置
*/
private savePosition(position: { x: number; y: number }): void {
try {
if (this.savePositionTimer) {
clearTimeout(this.savePositionTimer)
}
this.savePositionTimer = setTimeout(() => {
presenter.configPresenter.setFloatingButtonPosition(position)
logger.debug(`Floating button position saved: ${position.x}, ${position.y}`)
}, 500)
} catch (error) {
logger.error('Failed to save floating button position:', error)
}
}
private savePositionTimer: NodeJS.Timeout | null = null
private savePositionTimer: NodeJS.Timeout | null = null
/**
* 保存当前位置到配置
*/
private savePosition(position: { x: number; y: number }): void {
try {
if (this.savePositionTimer) {
clearTimeout(this.savePositionTimer)
}
const SAVE_DEBOUNCE_DELAY = 500
this.savePositionTimer = setTimeout(() => {
presenter.configPresenter.setFloatingButtonPosition(position)
logger.debug(`Floating button position saved: ${position.x}, ${position.y}`)
this.savePositionTimer = null
}, SAVE_DEBOUNCE_DELAY)
} catch (error) {
logger.error(`Failed to save floating button position (${position.x}, ${position.y}):`, error)
}
}
🤖 Prompt for AI Agents
In src/main/presenter/floatingButtonPresenter/FloatingButtonWindow.ts around
lines 287 to 303, move the savePositionTimer property declaration above the
savePosition method so it's defined before use, make the debounce delay
configurable (e.g., read from a constant or
presenter.configPresenter.getFloatingButtonDebounceMs()) instead of hardcoding
500, and improve the catch logging to include the position being saved and the
error details for context (e.g., logger.error with a message that contains
position.x and position.y and the error). Ensure the timer type remains
NodeJS.Timeout | null and that clearing/setting the timeout uses the configured
delay.

}
5 changes: 5 additions & 0 deletions src/main/presenter/floatingButtonPresenter/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,17 @@ export class FloatingButtonPresenter {
*/
public async initialize(config?: Partial<FloatingButtonConfig>): Promise<void> {
const floatingButtonEnabled = this.configPresenter.getFloatingButtonEnabled()
const savedPosition = this.configPresenter.getFloatingButtonPosition()
try {
this.config = {
...this.config,
...config,
enabled: floatingButtonEnabled
}
if (savedPosition) {
this.config.position = 'custom'
this.config.customPosition = savedPosition
}

if (!this.config.enabled) {
console.log('FloatingButton is disabled, skipping window creation')
Expand Down
7 changes: 6 additions & 1 deletion src/main/presenter/floatingButtonPresenter/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,17 @@ export interface FloatingButtonConfig {
/** 是否启用悬浮按钮 */
enabled: boolean
/** 悬浮按钮位置 */
position: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'
position: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | 'custom'
/** 距离边缘的偏移量 */
offset: {
x: number
y: number
}
/** 自定义位置 (当 position 为 'custom' 时使用) */
customPosition?: {
x: number
y: number
}
/** 悬浮按钮大小 */
size: {
width: number
Expand Down