diff --git a/package.json b/package.json index c718dce1a7..b5073b4b2c 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,7 @@ "notion-helper": "^1.3.22", "os-proxy-config": "^1.1.2", "pdfjs-dist": "4.10.38", - "selection-hook": "^1.0.5", + "selection-hook": "^1.0.6", "turndown": "7.2.0" }, "devDependencies": { diff --git a/src/main/services/SelectionService.ts b/src/main/services/SelectionService.ts index 3be2d5a95a..718025cd89 100644 --- a/src/main/services/SelectionService.ts +++ b/src/main/services/SelectionService.ts @@ -1,7 +1,7 @@ import { SELECTION_FINETUNED_LIST, SELECTION_PREDEFINED_BLACKLIST } from '@main/configs/SelectionConfig' import { isDev, isMac, isWin } from '@main/constant' import { IpcChannel } from '@shared/IpcChannel' -import { BrowserWindow, ipcMain, screen, systemPreferences } from 'electron' +import { app, BrowserWindow, ipcMain, screen, systemPreferences } from 'electron' import Logger from 'electron-log' import { join } from 'path' import type { @@ -509,54 +509,55 @@ export class SelectionService { //should set every time the window is shown this.toolbarWindow!.setAlwaysOnTop(true, 'screen-saver') - // [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) - - 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) - + if (!isMac) { + this.toolbarWindow!.show() + /** + * [Windows] + * In Windows 10, setOpacity(1) will make the window completely transparent + * It's a strange behavior, so we don't use it for compatibility + */ + // this.toolbarWindow!.setOpacity(1) this.startHideByMouseKeyListener() - return } - /** - * The following is for Windows - */ + /************************************************ + * [macOS] the following code is only for macOS + * + * WARNING: + * DO NOT MODIFY THESE CODES, UNLESS YOU REALLY KNOW WHAT YOU ARE DOING!!!! + *************************************************/ - this.toolbarWindow!.show() + // [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) - /** - * [Windows] - * In Windows 10, setOpacity(1) will make the window completely transparent - * It's a strange behavior, so we don't use it for compatibility - */ - // this.toolbarWindow!.setOpacity(1) + 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 } /** @@ -911,6 +912,7 @@ export class SelectionService { refPoint = { x: Math.round(refPoint.x), y: Math.round(refPoint.y) } } + // [macOS] isFullscreen is only available on macOS this.showToolbarAtPosition(refPoint, refOrientation, selectionData.programName) this.toolbarWindow!.webContents.send(IpcChannel.Selection_TextSelected, selectionData) } @@ -1218,20 +1220,26 @@ export class SelectionService { return actionWindow } - public processAction(actionItem: ActionItem): void { + /** + * Process action item + * @param actionItem Action item to process + * @param isFullScreen [macOS] only macOS has the available isFullscreen mode + */ + public processAction(actionItem: ActionItem, isFullScreen: boolean = false): void { const actionWindow = this.popActionWindow() actionWindow.webContents.send(IpcChannel.Selection_UpdateActionData, actionItem) - this.showActionWindow(actionWindow) + this.showActionWindow(actionWindow, isFullScreen) } /** * Show action window with proper positioning relative to toolbar * Ensures window stays within screen boundaries * @param actionWindow Window to position and show + * @param isFullScreen [macOS] only macOS has the available isFullscreen mode */ - private showActionWindow(actionWindow: BrowserWindow): void { + private showActionWindow(actionWindow: BrowserWindow, isFullScreen: boolean = false): void { let actionWindowWidth = this.ACTION_WINDOW_WIDTH let actionWindowHeight = this.ACTION_WINDOW_HEIGHT @@ -1241,11 +1249,14 @@ export class SelectionService { actionWindowHeight = this.lastActionWindowSize.height } - //center way - if (!this.isFollowToolbar || !this.toolbarWindow) { - const display = screen.getDisplayNearestPoint(screen.getCursorScreenPoint()) - const workArea = display.workArea + /******************************************** + * Setting the position of the action window + ********************************************/ + const display = screen.getDisplayNearestPoint(screen.getCursorScreenPoint()) + const workArea = display.workArea + // Center of the screen + if (!this.isFollowToolbar || !this.toolbarWindow) { const centerX = workArea.x + (workArea.width - actionWindowWidth) / 2 const centerY = workArea.y + (workArea.height - actionWindowHeight) / 2 @@ -1255,54 +1266,107 @@ export class SelectionService { x: Math.round(centerX), y: Math.round(centerY) }) + } else { + // Follow toolbar position + const toolbarBounds = this.toolbarWindow!.getBounds() + const GAP = 6 // 6px gap from screen edges + //make sure action window is inside screen + if (actionWindowWidth > workArea.width - 2 * GAP) { + actionWindowWidth = workArea.width - 2 * GAP + } + + if (actionWindowHeight > workArea.height - 2 * GAP) { + actionWindowHeight = workArea.height - 2 * GAP + } + + // Calculate initial position to center action window horizontally below toolbar + let posX = Math.round(toolbarBounds.x + (toolbarBounds.width - actionWindowWidth) / 2) + let posY = Math.round(toolbarBounds.y) + + // Ensure action window stays within screen boundaries with a small gap + if (posX + actionWindowWidth > workArea.x + workArea.width) { + posX = workArea.x + workArea.width - actionWindowWidth - GAP + } else if (posX < workArea.x) { + posX = workArea.x + GAP + } + if (posY + actionWindowHeight > workArea.y + workArea.height) { + // If window would go below screen, try to position it above toolbar + posY = workArea.y + workArea.height - actionWindowHeight - GAP + } else if (posY < workArea.y) { + posY = workArea.y + GAP + } + + actionWindow.setPosition(posX, posY, false) + //KEY to make window not resize + actionWindow.setBounds({ + width: actionWindowWidth, + height: actionWindowHeight, + x: posX, + y: posY + }) + } + + if (!isMac) { actionWindow.show() - return } - //follow toolbar - const toolbarBounds = this.toolbarWindow!.getBounds() - const display = screen.getDisplayNearestPoint(screen.getCursorScreenPoint()) - const workArea = display.workArea - const GAP = 6 // 6px gap from screen edges + /************************************************ + * [macOS] the following code is only for macOS + * + * WARNING: + * DO NOT MODIFY THESE CODES, UNLESS YOU REALLY KNOW WHAT YOU ARE DOING!!!! + *************************************************/ - //make sure action window is inside screen - if (actionWindowWidth > workArea.width - 2 * GAP) { - actionWindowWidth = workArea.width - 2 * GAP + // act normally when the app is not in fullscreen mode + if (!isFullScreen) { + actionWindow.show() + return } - if (actionWindowHeight > workArea.height - 2 * GAP) { - actionWindowHeight = workArea.height - 2 * GAP - } + // [macOS] an UGLY HACKY way for fullscreen override settings - // Calculate initial position to center action window horizontally below toolbar - let posX = Math.round(toolbarBounds.x + (toolbarBounds.width - actionWindowWidth) / 2) - let posY = Math.round(toolbarBounds.y) + // FIXME sometimes the dock will be shown when the action window is shown + // FIXME if actionWindow show on the fullscreen app, switch to other space will cause the mainWindow to be shown + // FIXME When setVisibleOnAllWorkspaces is true, docker icon disappeared when the first action window is shown on the fullscreen app + // use app.dock.show() to show the dock again will cause the action window to be closed when auto hide on blur is enabled - // Ensure action window stays within screen boundaries with a small gap - if (posX + actionWindowWidth > workArea.x + workArea.width) { - posX = workArea.x + workArea.width - actionWindowWidth - GAP - } else if (posX < workArea.x) { - posX = workArea.x + GAP - } - if (posY + actionWindowHeight > workArea.y + workArea.height) { - // If window would go below screen, try to position it above toolbar - posY = workArea.y + workArea.height - actionWindowHeight - GAP - } else if (posY < workArea.y) { - posY = workArea.y + GAP - } + // setFocusable(false) to prevent the action window hide when blur (if auto hide on blur is enabled) + actionWindow.setFocusable(false) + actionWindow.setAlwaysOnTop(true, 'floating') - actionWindow.setPosition(posX, posY, false) - //KEY to make window not resize - actionWindow.setBounds({ - width: actionWindowWidth, - height: actionWindowHeight, - x: posX, - y: posY + // `setVisibleOnAllWorkspaces(true)` will cause the dock icon disappeared + // just store the dock icon status, and show it again + const isDockShown = app.dock?.isVisible() + + // DO NOT set `skipTransformProcessType: true`, + // it will cause the action window to be shown on other space + actionWindow.setVisibleOnAllWorkspaces(true, { + visibleOnFullScreen: true }) - actionWindow.show() + actionWindow.showInactive() + + // show the dock again if last time it was shown + // do not put it after `actionWindow.focus()`, will cause the action window to be closed when auto hide on blur is enabled + if (!app.dock?.isVisible() && isDockShown) { + app.dock?.show() + } + + // unset everything + setTimeout(() => { + actionWindow.setVisibleOnAllWorkspaces(false, { + visibleOnFullScreen: true, + skipTransformProcessType: true + }) + actionWindow.setAlwaysOnTop(false) + + actionWindow.setFocusable(true) + + // regain the focus when all the works done + actionWindow.focus() + }, 50) } public closeActionWindow(actionWindow: BrowserWindow): void { @@ -1408,8 +1472,9 @@ export class SelectionService { configManager.setSelectionAssistantFilterList(filterList) }) - ipcMain.handle(IpcChannel.Selection_ProcessAction, (_, actionItem: ActionItem) => { - selectionService?.processAction(actionItem) + // [macOS] only macOS has the available isFullscreen mode + ipcMain.handle(IpcChannel.Selection_ProcessAction, (_, actionItem: ActionItem, isFullScreen: boolean = false) => { + selectionService?.processAction(actionItem, isFullScreen) }) ipcMain.handle(IpcChannel.Selection_ActionWindowClose, (event) => { diff --git a/src/preload/index.ts b/src/preload/index.ts index ea1a2897f9..5221761dfb 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -309,7 +309,8 @@ const api = { ipcRenderer.invoke(IpcChannel.Selection_SetRemeberWinSize, isRemeberWinSize), setFilterMode: (filterMode: string) => ipcRenderer.invoke(IpcChannel.Selection_SetFilterMode, filterMode), setFilterList: (filterList: string[]) => ipcRenderer.invoke(IpcChannel.Selection_SetFilterList, filterList), - processAction: (actionItem: ActionItem) => ipcRenderer.invoke(IpcChannel.Selection_ProcessAction, actionItem), + processAction: (actionItem: ActionItem, isFullScreen: boolean = false) => + ipcRenderer.invoke(IpcChannel.Selection_ProcessAction, actionItem, isFullScreen), closeActionWindow: () => ipcRenderer.invoke(IpcChannel.Selection_ActionWindowClose), minimizeActionWindow: () => ipcRenderer.invoke(IpcChannel.Selection_ActionWindowMinimize), pinActionWindow: (isPinned: boolean) => ipcRenderer.invoke(IpcChannel.Selection_ActionWindowPin, isPinned) diff --git a/src/renderer/src/windows/selection/action/SelectionActionApp.tsx b/src/renderer/src/windows/selection/action/SelectionActionApp.tsx index 94c1c575ea..5112caf945 100644 --- a/src/renderer/src/windows/selection/action/SelectionActionApp.tsx +++ b/src/renderer/src/windows/selection/action/SelectionActionApp.tsx @@ -36,10 +36,6 @@ const SelectionActionApp: FC = () => { const lastScrollHeight = useRef(0) useEffect(() => { - if (isAutoPin) { - window.api.selection.pinActionWindow(true) - } - const actionListenRemover = window.electron?.ipcRenderer.on( IpcChannel.Selection_UpdateActionData, (_, actionItem: ActionItem) => { @@ -60,6 +56,20 @@ const SelectionActionApp: FC = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, []) + useEffect(() => { + if (isAutoPin) { + window.api.selection.pinActionWindow(true) + setIsPinned(true) + } else if (!isActionLoaded.current) { + window.api.selection.pinActionWindow(false) + setIsPinned(false) + } + }, [isAutoPin]) + + useEffect(() => { + shouldCloseWhenBlur.current = isAutoClose && !isPinned + }, [isAutoClose, isPinned]) + useEffect(() => { i18n.changeLanguage(language || navigator.language || defaultLanguage) }, [language]) @@ -100,10 +110,6 @@ const SelectionActionApp: FC = () => { } }, [action, t]) - useEffect(() => { - shouldCloseWhenBlur.current = isAutoClose && !isPinned - }, [isAutoClose, isPinned]) - useEffect(() => { //if the action is loaded, we should not set the opacity update from settings if (!isActionLoaded.current) { diff --git a/src/renderer/src/windows/selection/toolbar/SelectionToolbar.tsx b/src/renderer/src/windows/selection/toolbar/SelectionToolbar.tsx index 49b3c2fcf9..342e4122a7 100644 --- a/src/renderer/src/windows/selection/toolbar/SelectionToolbar.tsx +++ b/src/renderer/src/windows/selection/toolbar/SelectionToolbar.tsx @@ -107,6 +107,8 @@ const SelectionToolbar: FC<{ demo?: boolean }> = ({ demo = false }) => { }, [actionItems]) const selectedText = useRef('') + // [macOS] only macOS has the fullscreen mode + const isFullScreen = useRef(false) // listen to selectionService events useEffect(() => { @@ -115,6 +117,7 @@ const SelectionToolbar: FC<{ demo?: boolean }> = ({ demo = false }) => { IpcChannel.Selection_TextSelected, (_, selectionData: TextSelectionData) => { selectedText.current = selectionData.text + isFullScreen.current = selectionData.isFullscreen ?? false setTimeout(() => { //make sure the animation is active setAnimateKey((prev) => prev + 1) @@ -133,8 +136,6 @@ const SelectionToolbar: FC<{ demo?: boolean }> = ({ demo = false }) => { } ) - if (!demo) updateWindowSize() - return () => { textSelectionListenRemover() toolbarVisibilityChangeListenRemover() @@ -234,7 +235,8 @@ const SelectionToolbar: FC<{ demo?: boolean }> = ({ demo = false }) => { } const handleDefaultAction = (action: ActionItem) => { - window.api?.selection.processAction(action) + // [macOS] only macOS has the available isFullscreen mode + window.api?.selection.processAction(action, isFullScreen.current) window.api?.selection.hideToolbar() } diff --git a/yarn.lock b/yarn.lock index e2a45ec2ac..bbaadab77c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7235,7 +7235,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.5" + selection-hook: "npm:^1.0.6" shiki: "npm:^3.7.0" string-width: "npm:^7.2.0" styled-components: "npm:^6.1.11" @@ -18229,14 +18229,14 @@ __metadata: languageName: node linkType: hard -"selection-hook@npm:^1.0.5": - version: 1.0.5 - resolution: "selection-hook@npm:1.0.5" +"selection-hook@npm:^1.0.6": + version: 1.0.6 + resolution: "selection-hook@npm:1.0.6" dependencies: node-addon-api: "npm:^8.4.0" node-gyp: "npm:latest" node-gyp-build: "npm:^4.8.4" - checksum: 10c0/d188e2bafa6d820779e57a721bd2480dc1fde3f9daa2e3f92f1b69712637079e5fd9443575bc8624c98a057608f867d82fb2abf2d0796777db1f18ea50ea0028 + checksum: 10c0/c7d28db51fc16b5648530344cbe1d5b72a7469cfb7edbb9c56d7be4bea2d93ddd01993fb27b344e44865f9eb0f3211b1be638caaacd0f9165b2bc03bada7c360 languageName: node linkType: hard