fix(SelectionAssistant): [macOS] show actionWindow on fullscreen app (#8004)

* feat(SelectionService): enhance action window handling for macOS fullscreen mode

- Updated processAction and showActionWindow methods to support fullscreen mode on macOS.
- Added isFullScreen parameter to manage action window visibility and positioning.
- Improved action window positioning logic to ensure it remains within screen boundaries.
- Adjusted IPC channel to pass fullscreen state from the renderer to the service.
- Updated SelectionToolbar to track fullscreen state and pass it to the action processing function.

* chore(deps): update selection-hook to version 1.0.6 in package.json and yarn.lock

* fix(SelectionService): improve macOS fullscreen handling and action window focus

- Added app import to manage dock visibility on macOS.
- Enhanced fullscreen handling logic to ensure the dock icon is restored correctly.
- Updated action window focus behavior to prevent unintended hiding when blurred.
- Refactored SelectionActionApp to streamline auto pinning logic and remove redundant useEffect.
- Cleaned up SelectionToolbar by removing unnecessary window size updates when demo is false.

* refactor(SelectionService): remove commented-out code for clarity

* refactor(SelectionService): streamline macOS handling and improve code clarity
This commit is contained in:
fullex 2025-07-10 20:41:01 +08:00 committed by GitHub
parent 855499681f
commit 7c6db809bb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 177 additions and 103 deletions

View File

@ -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": {

View File

@ -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,8 +509,25 @@ 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) {
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
}
/************************************************
* [macOS] the following code is only for macOS
*
* WARNING:
* DO NOT MODIFY THESE CODES, UNLESS YOU REALLY KNOW WHAT YOU ARE DOING!!!!
*************************************************/
// [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
@ -543,22 +560,6 @@ export class SelectionService {
return
}
/**
* The following is for Windows
*/
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()
}
/**
* Hide the toolbar window and cleanup listeners
*/
@ -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) {
/********************************************
* 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,16 +1266,9 @@ export class SelectionService {
x: Math.round(centerX),
y: Math.round(centerY)
})
actionWindow.show()
return
}
//follow toolbar
} else {
// Follow toolbar position
const toolbarBounds = this.toolbarWindow!.getBounds()
const display = screen.getDisplayNearestPoint(screen.getCursorScreenPoint())
const workArea = display.workArea
const GAP = 6 // 6px gap from screen edges
//make sure action window is inside screen
@ -1301,8 +1305,68 @@ export class SelectionService {
x: posX,
y: posY
})
}
if (!isMac) {
actionWindow.show()
return
}
/************************************************
* [macOS] the following code is only for macOS
*
* WARNING:
* DO NOT MODIFY THESE CODES, UNLESS YOU REALLY KNOW WHAT YOU ARE DOING!!!!
*************************************************/
// act normally when the app is not in fullscreen mode
if (!isFullScreen) {
actionWindow.show()
return
}
// [macOS] an UGLY HACKY way for fullscreen override settings
// 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
// setFocusable(false) to prevent the action window hide when blur (if auto hide on blur is enabled)
actionWindow.setFocusable(false)
actionWindow.setAlwaysOnTop(true, 'floating')
// `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.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) => {

View File

@ -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)

View File

@ -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) {

View File

@ -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()
}

View File

@ -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