fix(SelectionAssistant): overall bug fix from v1.4.8 (#7834)

* feat(SelectionService): enable toolbar visibility on all workspaces

* feat: update selection-hook to v1.0.5

* fix: show toolbar over fullscreen apps

* fix(SelectionService): adjust macOS window type handling for fullscreen apps
This commit is contained in:
fullex 2025-07-06 23:41:20 +08:00 committed by GitHub
parent 7f8ad88c06
commit 40519b48c5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 90 additions and 58 deletions

View File

@ -68,7 +68,7 @@
"notion-helper": "^1.3.22",
"os-proxy-config": "^1.1.2",
"pdfjs-dist": "4.10.38",
"selection-hook": "^1.0.4",
"selection-hook": "^1.0.5",
"turndown": "7.2.0"
},
"devDependencies": {

View File

@ -141,7 +141,7 @@ export class SelectionService {
* Initialize zoom factor from config and subscribe to changes
* Ensures UI elements scale properly with system DPI settings
*/
private initZoomFactor() {
private initZoomFactor(): void {
const zoomFactor = configManager.getZoomFactor()
if (zoomFactor) {
this.setZoomFactor(zoomFactor)
@ -154,7 +154,7 @@ export class SelectionService {
this.zoomFactor = zoomFactor
}
private initConfig() {
private initConfig(): void {
this.triggerMode = configManager.getSelectionAssistantTriggerMode() as TriggerMode
this.isFollowToolbar = configManager.getSelectionAssistantFollowToolbar()
this.isRemeberWinSize = configManager.getSelectionAssistantRemeberWinSize()
@ -207,7 +207,7 @@ export class SelectionService {
* @param mode - The mode to set, either 'default', 'whitelist', or 'blacklist'
* @param list - An array of strings representing the list of items to include or exclude
*/
private setHookGlobalFilterMode(mode: string, list: string[]) {
private setHookGlobalFilterMode(mode: string, list: string[]): void {
if (!this.selectionHook) return
const modeMap = {
@ -245,7 +245,7 @@ export class SelectionService {
}
}
private setHookFineTunedList() {
private setHookFineTunedList(): void {
if (!this.selectionHook) return
const excludeClipboardCursorDetectList = isWin
@ -271,6 +271,11 @@ export class SelectionService {
* @returns {boolean} Success status of service start
*/
public start(): boolean {
if (!isSupportedOS) {
this.logError(new Error('SelectionService start(): not supported on this OS'))
return false
}
if (!this.selectionHook) {
this.logError(new Error('SelectionService start(): instance is null'))
return false
@ -373,7 +378,7 @@ export class SelectionService {
* Toggle the enabled state of the selection service
* Will sync the new enabled store to all renderer windows
*/
public toggleEnabled(enabled: boolean | undefined = undefined) {
public toggleEnabled(enabled: boolean | undefined = undefined): void {
if (!this.selectionHook) return
const newEnabled = enabled === undefined ? !configManager.getSelectionAssistantEnabled() : enabled
@ -389,7 +394,7 @@ export class SelectionService {
* Sets up window properties, event handlers, and loads the toolbar UI
* @param readyCallback Optional callback when window is ready to show
*/
private createToolbarWindow(readyCallback?: () => void) {
private createToolbarWindow(readyCallback?: () => void): void {
if (this.isToolbarAlive()) return
const { toolbarWidth, toolbarHeight } = this.getToolbarRealSize()
@ -414,9 +419,11 @@ export class SelectionService {
backgroundMaterial: 'none',
// Platform specific settings
// [macOS] DO NOT set type to 'panel', it will not work because it conflicts with other settings
// [macOS] DO NOT set focusable to false, it will make other windows bring to front together
...(isWin ? { type: 'toolbar', focusable: false } : {}),
// [macOS] `panel` conflicts with other settings ,
// and log will show `NSWindow does not support nonactivating panel styleMask 0x80`
// but it seems still work on fullscreen apps, so we set this anyway
...(isWin ? { type: 'toolbar', focusable: false } : { type: 'panel' }),
hiddenInMissionControl: true, // [macOS only]
acceptFirstMouse: true, // [macOS only]
@ -447,13 +454,6 @@ export class SelectionService {
// Add show/hide event listeners
this.toolbarWindow.on('show', () => {
this.toolbarWindow?.webContents.send(IpcChannel.Selection_ToolbarVisibilityChange, true)
// [macOS] force the toolbar window to be visible on current desktop
// but it will make docker icon flash. And we found that it's not necessary now.
// will remove after testing
// if (isMac) {
// this.toolbarWindow!.setVisibleOnAllWorkspaces(false)
// }
})
this.toolbarWindow.on('hide', () => {
@ -485,10 +485,10 @@ export class SelectionService {
* @param point Reference point for positioning, logical coordinates
* @param orientation Preferred position relative to reference point
*/
private showToolbarAtPosition(point: Point, orientation: RelativeOrientation) {
private showToolbarAtPosition(point: Point, orientation: RelativeOrientation, programName: string): void {
if (!this.isToolbarAlive()) {
this.createToolbarWindow(() => {
this.showToolbarAtPosition(point, orientation)
this.showToolbarAtPosition(point, orientation, programName)
})
return
}
@ -509,16 +509,45 @@ export class SelectionService {
//should set every time the window is shown
this.toolbarWindow!.setAlwaysOnTop(true, 'screen-saver')
// [macOS] force the toolbar window to be visible on current desktop
// but it will make docker icon flash. And we found that it's not necessary now.
// will remove after testing
// if (isMac) {
// this.toolbarWindow!.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true })
// }
// [macOS] a series of hacky ways only for macOS
if (isMac) {
// [macOS] a hacky way
// when set `skipTransformProcessType: true`, if the selection is in self app, it will make the selection canceled after toolbar showing
// so we just don't set `skipTransformProcessType: true` when in self app
const isSelf = ['com.github.Electron', 'com.kangfenmao.CherryStudio'].includes(programName)
// [macOS] MUST use `showInactive()` to prevent other windows bring to front together
// [Windows] is OK for both `show()` and `showInactive()` because of `focusable: false`
this.toolbarWindow!.showInactive()
if (!isSelf) {
// [macOS] an ugly hacky way
// `focusable: true` will make mainWindow disappeared when `setVisibleOnAllWorkspaces`
// so we set `focusable: true` before showing, and then set false after showing
this.toolbarWindow!.setFocusable(false)
// [macOS]
// force `setVisibleOnAllWorkspaces: true` to let toolbar show in all workspaces. And we MUST not set it to false again
// set `skipTransformProcessType: true` to avoid dock icon spinning when `setVisibleOnAllWorkspaces`
this.toolbarWindow!.setVisibleOnAllWorkspaces(true, {
visibleOnFullScreen: true,
skipTransformProcessType: true
})
}
// [macOS] MUST use `showInactive()` to prevent other windows bring to front together
// [Windows] is OK for both `show()` and `showInactive()` because of `focusable: false`
this.toolbarWindow!.showInactive()
// [macOS] restore the focusable status
this.toolbarWindow!.setFocusable(true)
this.startHideByMouseKeyListener()
return
}
/**
* The following is for Windows
*/
this.toolbarWindow!.show()
/**
* [Windows]
@ -588,8 +617,8 @@ export class SelectionService {
* Check if toolbar window exists and is not destroyed
* @returns {boolean} Toolbar window status
*/
private isToolbarAlive() {
return this.toolbarWindow && !this.toolbarWindow.isDestroyed()
private isToolbarAlive(): boolean {
return !!(this.toolbarWindow && !this.toolbarWindow.isDestroyed())
}
/**
@ -598,7 +627,7 @@ export class SelectionService {
* @param width New toolbar width
* @param height New toolbar height
*/
public determineToolbarSize(width: number, height: number) {
public determineToolbarSize(width: number, height: number): void {
const toolbarWidth = Math.ceil(width)
// only update toolbar width if it's changed
@ -611,7 +640,7 @@ export class SelectionService {
* Get actual toolbar dimensions accounting for zoom factor
* @returns Object containing toolbar width and height
*/
private getToolbarRealSize() {
private getToolbarRealSize(): { toolbarWidth: number; toolbarHeight: number } {
return {
toolbarWidth: this.TOOLBAR_WIDTH * this.zoomFactor,
toolbarHeight: this.TOOLBAR_HEIGHT * this.zoomFactor
@ -882,8 +911,8 @@ export class SelectionService {
refPoint = { x: Math.round(refPoint.x), y: Math.round(refPoint.y) }
}
this.showToolbarAtPosition(refPoint, refOrientation)
this.toolbarWindow?.webContents.send(IpcChannel.Selection_TextSelected, selectionData)
this.showToolbarAtPosition(refPoint, refOrientation, selectionData.programName)
this.toolbarWindow!.webContents.send(IpcChannel.Selection_TextSelected, selectionData)
}
/**
@ -891,7 +920,7 @@ export class SelectionService {
*/
// Start monitoring global mouse clicks
private startHideByMouseKeyListener() {
private startHideByMouseKeyListener(): void {
try {
// Register event handlers
this.selectionHook!.on('mouse-down', this.handleMouseDownHide)
@ -904,7 +933,7 @@ export class SelectionService {
}
// Stop monitoring global mouse clicks
private stopHideByMouseKeyListener() {
private stopHideByMouseKeyListener(): void {
if (!this.isHideByMouseKeyListenerActive) return
try {
@ -1098,7 +1127,7 @@ export class SelectionService {
* Initialize preloaded action windows
* Creates a pool of windows at startup for faster response
*/
private async initPreloadedActionWindows() {
private async initPreloadedActionWindows(): Promise<void> {
try {
// Create initial pool of preloaded windows
for (let i = 0; i < this.PRELOAD_ACTION_WINDOW_COUNT; i++) {
@ -1112,7 +1141,7 @@ export class SelectionService {
/**
* Close all preloaded action windows
*/
private closePreloadedActionWindows() {
private closePreloadedActionWindows(): void {
for (const actionWindow of this.preloadedActionWindows) {
if (!actionWindow.isDestroyed()) {
actionWindow.destroy()
@ -1124,7 +1153,7 @@ export class SelectionService {
* Preload a new action window asynchronously
* This method is called after popping a window to ensure we always have windows ready
*/
private async pushNewActionWindow() {
private async pushNewActionWindow(): Promise<void> {
try {
const actionWindow = this.createPreloadedActionWindow()
this.preloadedActionWindows.push(actionWindow)
@ -1138,7 +1167,7 @@ export class SelectionService {
* Immediately returns a window and asynchronously creates a new one
* @returns {BrowserWindow} The action window
*/
private popActionWindow() {
private popActionWindow(): BrowserWindow {
// Get a window from the preloaded queue or create a new one if empty
const actionWindow = this.preloadedActionWindows.pop() || this.createPreloadedActionWindow()
@ -1202,7 +1231,7 @@ export class SelectionService {
* Ensures window stays within screen boundaries
* @param actionWindow Window to position and show
*/
private showActionWindow(actionWindow: BrowserWindow) {
private showActionWindow(actionWindow: BrowserWindow): void {
let actionWindowWidth = this.ACTION_WINDOW_WIDTH
let actionWindowHeight = this.ACTION_WINDOW_HEIGHT
@ -1228,6 +1257,7 @@ export class SelectionService {
})
actionWindow.show()
return
}
@ -1292,38 +1322,40 @@ export class SelectionService {
* Switches between selection-based and alt-key based triggering
* Manages appropriate event listeners for each mode
*/
private processTriggerMode() {
private processTriggerMode(): void {
if (!this.selectionHook) return
switch (this.triggerMode) {
case TriggerMode.Selected:
if (this.isCtrlkeyListenerActive) {
this.selectionHook!.off('key-down', this.handleKeyDownCtrlkeyMode)
this.selectionHook!.off('key-up', this.handleKeyUpCtrlkeyMode)
this.selectionHook.off('key-down', this.handleKeyDownCtrlkeyMode)
this.selectionHook.off('key-up', this.handleKeyUpCtrlkeyMode)
this.isCtrlkeyListenerActive = false
}
this.selectionHook!.setSelectionPassiveMode(false)
this.selectionHook.setSelectionPassiveMode(false)
break
case TriggerMode.Ctrlkey:
if (!this.isCtrlkeyListenerActive) {
this.selectionHook!.on('key-down', this.handleKeyDownCtrlkeyMode)
this.selectionHook!.on('key-up', this.handleKeyUpCtrlkeyMode)
this.selectionHook.on('key-down', this.handleKeyDownCtrlkeyMode)
this.selectionHook.on('key-up', this.handleKeyUpCtrlkeyMode)
this.isCtrlkeyListenerActive = true
}
this.selectionHook!.setSelectionPassiveMode(true)
this.selectionHook.setSelectionPassiveMode(true)
break
case TriggerMode.Shortcut:
//remove the ctrlkey listener, don't need any key listener for shortcut mode
if (this.isCtrlkeyListenerActive) {
this.selectionHook!.off('key-down', this.handleKeyDownCtrlkeyMode)
this.selectionHook!.off('key-up', this.handleKeyUpCtrlkeyMode)
this.selectionHook.off('key-down', this.handleKeyDownCtrlkeyMode)
this.selectionHook.off('key-up', this.handleKeyUpCtrlkeyMode)
this.isCtrlkeyListenerActive = false
}
this.selectionHook!.setSelectionPassiveMode(true)
this.selectionHook.setSelectionPassiveMode(true)
break
}
}
@ -1404,13 +1436,13 @@ export class SelectionService {
this.isIpcHandlerRegistered = true
}
private logInfo(message: string, forceShow: boolean = false) {
private logInfo(message: string, forceShow: boolean = false): void {
if (isDev || forceShow) {
Logger.info('[SelectionService] Info: ', message)
}
}
private logError(...args: [...string[], Error]) {
private logError(...args: [...string[], Error]): void {
Logger.error('[SelectionService] Error: ', ...args)
}
}
@ -1423,7 +1455,7 @@ export class SelectionService {
export function initSelectionService(): boolean {
if (!isSupportedOS) return false
configManager.subscribe(ConfigKeys.SelectionAssistantEnabled, (enabled: boolean) => {
configManager.subscribe(ConfigKeys.SelectionAssistantEnabled, (enabled: boolean): void => {
//avoid closure
const ss = SelectionService.getInstance()
if (!ss) {

View File

@ -5960,7 +5960,7 @@ __metadata:
remove-markdown: "npm:^0.6.2"
rollup-plugin-visualizer: "npm:^5.12.0"
sass: "npm:^1.88.0"
selection-hook: "npm:^1.0.4"
selection-hook: "npm:^1.0.5"
shiki: "npm:^3.7.0"
string-width: "npm:^7.2.0"
styled-components: "npm:^6.1.11"
@ -16928,14 +16928,14 @@ __metadata:
languageName: node
linkType: hard
"selection-hook@npm:^1.0.4":
version: 1.0.4
resolution: "selection-hook@npm:1.0.4"
"selection-hook@npm:^1.0.5":
version: 1.0.5
resolution: "selection-hook@npm:1.0.5"
dependencies:
node-addon-api: "npm:^8.4.0"
node-gyp: "npm:latest"
node-gyp-build: "npm:^4.8.4"
checksum: 10c0/8c694cf7bb82159ec8aa2079e9fe74149d8cf5679720e67203b7b56a591f67d9d25e43bb5ed4242c5d6fa8be61ba64863d3b81f6a2fc5dbad7861a5273f65061
checksum: 10c0/d188e2bafa6d820779e57a721bd2480dc1fde3f9daa2e3f92f1b69712637079e5fd9443575bc8624c98a057608f867d82fb2abf2d0796777db1f18ea50ea0028
languageName: node
linkType: hard