mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-27 21:01:32 +08:00
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:
parent
7f8ad88c06
commit
40519b48c5
@ -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": {
|
||||
|
||||
@ -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) {
|
||||
|
||||
10
yarn.lock
10
yarn.lock
@ -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
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user