feat: SelectionAssistant macOS version / 划词助手macOS版 (#7561)

* feat(SelectionAssistant): add macOS support and process trust handling

- Updated the selection assistant to support macOS, including new IPC channels for process trust verification.
- Enhanced the SelectionService to check for accessibility permissions on macOS before starting the service.
- Added user interface elements to guide macOS users in granting necessary permissions.
- Updated localization files to reflect macOS support and provide relevant user instructions.
- Refactored selection-related configurations to accommodate both Windows and macOS environments.

* feat(SelectionService): update toolbar window settings for macOS and Windows

- Set the toolbar window to be hidden in Mission Control and accept the first mouse click on macOS.
- Adjusted visibility settings for the toolbar window to ensure it appears correctly on all workspaces, including full-screen mode.
- Refactored the MacProcessTrustHintModal component to improve layout and styling of buttons in the modal footer.

* feat(SelectionToolbar): enhance styling and layout of selection toolbar components

* feat(SelectionService): enhance toolbar window settings and refactor position calculation

* feat(SelectionToolbar): update button padding and add last button padding for improved layout

* chore(dependencies): update selection-hook to version 1.0.2 and refine build file exclusions in electron-builder.yml

* feat(SelectionService): center action window on screen when not following toolbar

* fix(SelectionService): implement workaround to prevent other windows from bringing the app to front on macOS when action window is closed

* fix(SelectionService): refine macOS workaround to prevent other windows from bringing the app to front when action window is closed; update selection-toolbar logo padding in styles

* fix(SelectionService): implement macOS toolbar reload to clear hover status; optimize display retrieval logic

* fix(SelectionService): update macOS toolbar hover status handling by sending mouseMove event instead of reloading the window

* chore: update selection-hook dependency to version 1.0.3 in package.json and yarn.lock

* fix(SelectionService): improve toolbar visibility handling on macOS and ensure focusability of other windows when hiding the toolbar

---------

Co-authored-by: Teo <cheesen.xu@gmail.com>
This commit is contained in:
fullex 2025-07-03 14:31:31 +08:00 committed by GitHub
parent cd1ef46577
commit 2f016efc50
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 533 additions and 135 deletions

View File

@ -53,7 +53,9 @@ files:
- '!node_modules/pdf-parse/lib/pdf.js/{v1.9.426,v1.10.88,v2.0.550}' - '!node_modules/pdf-parse/lib/pdf.js/{v1.9.426,v1.10.88,v2.0.550}'
- '!node_modules/mammoth/{mammoth.browser.js,mammoth.browser.min.js}' - '!node_modules/mammoth/{mammoth.browser.js,mammoth.browser.min.js}'
- '!node_modules/selection-hook/prebuilds/**/*' # we rebuild .node, don't use prebuilds - '!node_modules/selection-hook/prebuilds/**/*' # we rebuild .node, don't use prebuilds
- '!**/*.{h,iobj,ipdb,tlog,recipe,vcxproj,vcxproj.filters}' # filter .node build files - '!node_modules/selection-hook/node_modules' # we don't need what in the node_modules dir
- '!node_modules/selection-hook/src' # we don't need source files
- '!**/*.{h,iobj,ipdb,tlog,recipe,vcxproj,vcxproj.filters,Makefile,*.Makefile}' # filter .node build files
asarUnpack: asarUnpack:
- resources/** - resources/**
- '**/*.{metal,exp,lib}' - '**/*.{metal,exp,lib}'

View File

@ -66,7 +66,7 @@
"node-stream-zip": "^1.15.0", "node-stream-zip": "^1.15.0",
"notion-helper": "^1.3.22", "notion-helper": "^1.3.22",
"os-proxy-config": "^1.1.2", "os-proxy-config": "^1.1.2",
"selection-hook": "^0.9.23", "selection-hook": "^1.0.3",
"turndown": "7.2.0" "turndown": "7.2.0"
}, },
"devDependencies": { "devDependencies": {

View File

@ -32,6 +32,9 @@ export enum IpcChannel {
App_InstallUvBinary = 'app:install-uv-binary', App_InstallUvBinary = 'app:install-uv-binary',
App_InstallBunBinary = 'app:install-bun-binary', App_InstallBunBinary = 'app:install-bun-binary',
App_MacIsProcessTrusted = 'app:mac-is-process-trusted',
App_MacRequestProcessTrust = 'app:mac-request-process-trust',
App_QuoteToMain = 'app:quote-to-main', App_QuoteToMain = 'app:quote-to-main',
Notification_Send = 'notification:send', Notification_Send = 'notification:send',

View File

@ -1,6 +1,6 @@
interface IFilterList { interface IFilterList {
WINDOWS: string[] WINDOWS: string[]
MAC?: string[] MAC: string[]
} }
interface IFinetunedList { interface IFinetunedList {
@ -45,14 +45,17 @@ export const SELECTION_PREDEFINED_BLACKLIST: IFilterList = {
'sldworks.exe', 'sldworks.exe',
// Remote Desktop // Remote Desktop
'mstsc.exe' 'mstsc.exe'
] ],
MAC: []
} }
export const SELECTION_FINETUNED_LIST: IFinetunedList = { export const SELECTION_FINETUNED_LIST: IFinetunedList = {
EXCLUDE_CLIPBOARD_CURSOR_DETECT: { EXCLUDE_CLIPBOARD_CURSOR_DETECT: {
WINDOWS: ['acrobat.exe', 'wps.exe', 'cajviewer.exe'] WINDOWS: ['acrobat.exe', 'wps.exe', 'cajviewer.exe'],
MAC: []
}, },
INCLUDE_CLIPBOARD_DELAY_READ: { INCLUDE_CLIPBOARD_DELAY_READ: {
WINDOWS: ['acrobat.exe', 'wps.exe', 'cajviewer.exe', 'foxitphantom.exe'] WINDOWS: ['acrobat.exe', 'wps.exe', 'cajviewer.exe', 'foxitphantom.exe'],
MAC: []
} }
} }

View File

@ -8,7 +8,7 @@ import { handleZoomFactor } from '@main/utils/zoom'
import { UpgradeChannel } from '@shared/config/constant' import { UpgradeChannel } from '@shared/config/constant'
import { IpcChannel } from '@shared/IpcChannel' import { IpcChannel } from '@shared/IpcChannel'
import { Shortcut, ThemeMode } from '@types' import { Shortcut, ThemeMode } from '@types'
import { BrowserWindow, dialog, ipcMain, session, shell, webContents } from 'electron' import { BrowserWindow, dialog, ipcMain, session, shell, systemPreferences, webContents } from 'electron'
import log from 'electron-log' import log from 'electron-log'
import { Notification } from 'src/renderer/src/types/notification' import { Notification } from 'src/renderer/src/types/notification'
@ -158,6 +158,18 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
} }
}) })
//only for mac
if (isMac) {
ipcMain.handle(IpcChannel.App_MacIsProcessTrusted, (): boolean => {
return systemPreferences.isTrustedAccessibilityClient(false)
})
//return is only the current state, not the new state
ipcMain.handle(IpcChannel.App_MacRequestProcessTrust, (): boolean => {
return systemPreferences.isTrustedAccessibilityClient(true)
})
}
ipcMain.handle(IpcChannel.Config_Set, (_, key: string, value: any, isNotify: boolean = false) => { ipcMain.handle(IpcChannel.Config_Set, (_, key: string, value: any, isNotify: boolean = false) => {
configManager.set(key, value, isNotify) configManager.set(key, value, isNotify)
}) })

View File

@ -1,7 +1,7 @@
import { SELECTION_FINETUNED_LIST, SELECTION_PREDEFINED_BLACKLIST } from '@main/configs/SelectionConfig' import { SELECTION_FINETUNED_LIST, SELECTION_PREDEFINED_BLACKLIST } from '@main/configs/SelectionConfig'
import { isDev, isWin } from '@main/constant' import { isDev, isMac, isWin } from '@main/constant'
import { IpcChannel } from '@shared/IpcChannel' import { IpcChannel } from '@shared/IpcChannel'
import { BrowserWindow, ipcMain, screen } from 'electron' import { BrowserWindow, ipcMain, screen, systemPreferences } from 'electron'
import Logger from 'electron-log' import Logger from 'electron-log'
import { join } from 'path' import { join } from 'path'
import type { import type {
@ -16,9 +16,12 @@ import type { ActionItem } from '../../renderer/src/types/selectionTypes'
import { ConfigKeys, configManager } from './ConfigManager' import { ConfigKeys, configManager } from './ConfigManager'
import storeSyncService from './StoreSyncService' import storeSyncService from './StoreSyncService'
const isSupportedOS = isWin || isMac
let SelectionHook: SelectionHookConstructor | null = null let SelectionHook: SelectionHookConstructor | null = null
try { try {
if (isWin) { //since selection-hook v1.0.0, it supports macOS
if (isSupportedOS) {
SelectionHook = require('selection-hook') SelectionHook = require('selection-hook')
} }
} catch (error) { } catch (error) {
@ -118,7 +121,7 @@ export class SelectionService {
} }
public static getInstance(): SelectionService | null { public static getInstance(): SelectionService | null {
if (!isWin) return null if (!isSupportedOS) return null
if (!SelectionService.instance) { if (!SelectionService.instance) {
SelectionService.instance = new SelectionService() SelectionService.instance = new SelectionService()
@ -213,6 +216,8 @@ export class SelectionService {
blacklist: SelectionHook!.FilterMode.EXCLUDE_LIST blacklist: SelectionHook!.FilterMode.EXCLUDE_LIST
} }
const predefinedBlacklist = isWin ? SELECTION_PREDEFINED_BLACKLIST.WINDOWS : SELECTION_PREDEFINED_BLACKLIST.MAC
let combinedList: string[] = list let combinedList: string[] = list
let combinedMode = mode let combinedMode = mode
@ -221,7 +226,7 @@ export class SelectionService {
switch (mode) { switch (mode) {
case 'blacklist': case 'blacklist':
//combine the predefined blacklist with the user-defined blacklist //combine the predefined blacklist with the user-defined blacklist
combinedList = [...new Set([...list, ...SELECTION_PREDEFINED_BLACKLIST.WINDOWS])] combinedList = [...new Set([...list, ...predefinedBlacklist])]
break break
case 'whitelist': case 'whitelist':
combinedList = [...list] combinedList = [...list]
@ -229,7 +234,7 @@ export class SelectionService {
case 'default': case 'default':
default: default:
//use the predefined blacklist as the default filter list //use the predefined blacklist as the default filter list
combinedList = [...SELECTION_PREDEFINED_BLACKLIST.WINDOWS] combinedList = [...predefinedBlacklist]
combinedMode = 'blacklist' combinedMode = 'blacklist'
break break
} }
@ -243,14 +248,21 @@ export class SelectionService {
private setHookFineTunedList() { private setHookFineTunedList() {
if (!this.selectionHook) return if (!this.selectionHook) return
const excludeClipboardCursorDetectList = isWin
? SELECTION_FINETUNED_LIST.EXCLUDE_CLIPBOARD_CURSOR_DETECT.WINDOWS
: SELECTION_FINETUNED_LIST.EXCLUDE_CLIPBOARD_CURSOR_DETECT.MAC
const includeClipboardDelayReadList = isWin
? SELECTION_FINETUNED_LIST.INCLUDE_CLIPBOARD_DELAY_READ.WINDOWS
: SELECTION_FINETUNED_LIST.INCLUDE_CLIPBOARD_DELAY_READ.MAC
this.selectionHook.setFineTunedList( this.selectionHook.setFineTunedList(
SelectionHook!.FineTunedListType.EXCLUDE_CLIPBOARD_CURSOR_DETECT, SelectionHook!.FineTunedListType.EXCLUDE_CLIPBOARD_CURSOR_DETECT,
SELECTION_FINETUNED_LIST.EXCLUDE_CLIPBOARD_CURSOR_DETECT.WINDOWS excludeClipboardCursorDetectList
) )
this.selectionHook.setFineTunedList( this.selectionHook.setFineTunedList(
SelectionHook!.FineTunedListType.INCLUDE_CLIPBOARD_DELAY_READ, SelectionHook!.FineTunedListType.INCLUDE_CLIPBOARD_DELAY_READ,
SELECTION_FINETUNED_LIST.INCLUDE_CLIPBOARD_DELAY_READ.WINDOWS includeClipboardDelayReadList
) )
} }
@ -259,11 +271,28 @@ export class SelectionService {
* @returns {boolean} Success status of service start * @returns {boolean} Success status of service start
*/ */
public start(): boolean { public start(): boolean {
if (!this.selectionHook || this.started) { if (!this.selectionHook) {
this.logError(new Error('SelectionService start(): instance is null or already started')) this.logError(new Error('SelectionService start(): instance is null'))
return false return false
} }
if (this.started) {
this.logError(new Error('SelectionService start(): already started'))
return false
}
//On macOS, we need to check if the process is trusted
if (isMac) {
if (!systemPreferences.isTrustedAccessibilityClient(false)) {
this.logError(
new Error(
'SelectionSerice not started: process is not trusted on macOS, please turn on the Accessibility permission'
)
)
return false
}
}
try { try {
//make sure the toolbar window is ready //make sure the toolbar window is ready
this.createToolbarWindow() this.createToolbarWindow()
@ -306,6 +335,7 @@ export class SelectionService {
if (!this.selectionHook) return false if (!this.selectionHook) return false
this.selectionHook.stop() this.selectionHook.stop()
this.selectionHook.cleanup() //already remove all listeners this.selectionHook.cleanup() //already remove all listeners
//reset the listener states //reset the listener states
@ -316,6 +346,7 @@ export class SelectionService {
this.toolbarWindow.close() this.toolbarWindow.close()
this.toolbarWindow = null this.toolbarWindow = null
} }
this.closePreloadedActionWindows() this.closePreloadedActionWindows()
this.started = false this.started = false
@ -366,21 +397,29 @@ export class SelectionService {
this.toolbarWindow = new BrowserWindow({ this.toolbarWindow = new BrowserWindow({
width: toolbarWidth, width: toolbarWidth,
height: toolbarHeight, height: toolbarHeight,
show: false,
frame: false, frame: false,
transparent: true, transparent: true,
alwaysOnTop: true, alwaysOnTop: true,
skipTaskbar: true, skipTaskbar: true,
autoHideMenuBar: true,
resizable: false, resizable: false,
minimizable: false, minimizable: false,
maximizable: false, maximizable: false,
fullscreenable: false, // [macOS] must be false
movable: true, movable: true,
focusable: false,
hasShadow: false, hasShadow: false,
thickFrame: false, thickFrame: false,
roundedCorners: true, roundedCorners: true,
backgroundMaterial: 'none', backgroundMaterial: 'none',
type: 'toolbar',
show: false, // 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 } : {}),
hiddenInMissionControl: true, // [macOS only]
acceptFirstMouse: true, // [macOS only]
webPreferences: { webPreferences: {
preload: join(__dirname, '../preload/index.js'), preload: join(__dirname, '../preload/index.js'),
contextIsolation: true, contextIsolation: true,
@ -392,7 +431,9 @@ export class SelectionService {
// Hide when losing focus // Hide when losing focus
this.toolbarWindow.on('blur', () => { this.toolbarWindow.on('blur', () => {
this.hideToolbar() if (this.toolbarWindow!.isVisible()) {
this.hideToolbar()
}
}) })
// Clean up when closed // Clean up when closed
@ -406,6 +447,13 @@ export class SelectionService {
// Add show/hide event listeners // Add show/hide event listeners
this.toolbarWindow.on('show', () => { this.toolbarWindow.on('show', () => {
this.toolbarWindow?.webContents.send(IpcChannel.Selection_ToolbarVisibilityChange, true) 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', () => { this.toolbarWindow.on('hide', () => {
@ -460,11 +508,22 @@ export class SelectionService {
//set the window to always on top (highest level) //set the window to always on top (highest level)
//should set every time the window is shown //should set every time the window is shown
this.toolbarWindow!.setAlwaysOnTop(true, 'screen-saver') this.toolbarWindow!.setAlwaysOnTop(true, 'screen-saver')
this.toolbarWindow!.show()
// [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] 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()
/** /**
* In Windows 10, setOpacity(1) will make the window completely transparent * [Windows]
* It's a strange behavior, so we don't use it for compatibility * 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.toolbarWindow!.setOpacity(1)
@ -477,10 +536,52 @@ export class SelectionService {
public hideToolbar(): void { public hideToolbar(): void {
if (!this.isToolbarAlive()) return if (!this.isToolbarAlive()) return
// this.toolbarWindow!.setOpacity(0) this.stopHideByMouseKeyListener()
// [Windows] just hide the toolbar window is enough
if (!isMac) {
this.toolbarWindow!.hide()
return
}
/************************************************
* [macOS] the following code is only for macOS
*************************************************/
// [macOS] a HACKY way
// make sure other windows do not bring to front when toolbar is hidden
// get all focusable windows and set them to not focusable
const focusableWindows: BrowserWindow[] = []
for (const window of BrowserWindow.getAllWindows()) {
if (!window.isDestroyed() && window.isVisible()) {
if (window.isFocusable()) {
focusableWindows.push(window)
window.setFocusable(false)
}
}
}
this.toolbarWindow!.hide() this.toolbarWindow!.hide()
this.stopHideByMouseKeyListener() // set them back to focusable after 50ms
setTimeout(() => {
for (const window of focusableWindows) {
if (!window.isDestroyed()) {
window.setFocusable(true)
}
}
}, 50)
// [macOS] hacky way
// Because toolbar is not a FOCUSED window, so the hover status will remain when next time show
// so we just send mouseMove event to the toolbar window to make the hover status disappear
this.toolbarWindow!.webContents.sendInputEvent({
type: 'mouseMove',
x: -1,
y: -1
})
return
} }
/** /**
@ -520,71 +621,71 @@ export class SelectionService {
/** /**
* Calculate optimal toolbar position based on selection context * Calculate optimal toolbar position based on selection context
* Ensures toolbar stays within screen boundaries and follows selection direction * Ensures toolbar stays within screen boundaries and follows selection direction
* @param point Reference point for positioning, must be INTEGER * @param refPoint Reference point for positioning, must be INTEGER
* @param orientation Preferred position relative to reference point * @param orientation Preferred position relative to reference point
* @returns Calculated screen coordinates for toolbar, INTEGER * @returns Calculated screen coordinates for toolbar, INTEGER
*/ */
private calculateToolbarPosition(point: Point, orientation: RelativeOrientation): Point { private calculateToolbarPosition(refPoint: Point, orientation: RelativeOrientation): Point {
// Calculate initial position based on the specified anchor // Calculate initial position based on the specified anchor
let posX: number, posY: number const posPoint: Point = { x: 0, y: 0 }
const { toolbarWidth, toolbarHeight } = this.getToolbarRealSize() const { toolbarWidth, toolbarHeight } = this.getToolbarRealSize()
switch (orientation) { switch (orientation) {
case 'topLeft': case 'topLeft':
posX = point.x - toolbarWidth posPoint.x = refPoint.x - toolbarWidth
posY = point.y - toolbarHeight posPoint.y = refPoint.y - toolbarHeight
break break
case 'topRight': case 'topRight':
posX = point.x posPoint.x = refPoint.x
posY = point.y - toolbarHeight posPoint.y = refPoint.y - toolbarHeight
break break
case 'topMiddle': case 'topMiddle':
posX = point.x - toolbarWidth / 2 posPoint.x = refPoint.x - toolbarWidth / 2
posY = point.y - toolbarHeight posPoint.y = refPoint.y - toolbarHeight
break break
case 'bottomLeft': case 'bottomLeft':
posX = point.x - toolbarWidth posPoint.x = refPoint.x - toolbarWidth
posY = point.y posPoint.y = refPoint.y
break break
case 'bottomRight': case 'bottomRight':
posX = point.x posPoint.x = refPoint.x
posY = point.y posPoint.y = refPoint.y
break break
case 'bottomMiddle': case 'bottomMiddle':
posX = point.x - toolbarWidth / 2 posPoint.x = refPoint.x - toolbarWidth / 2
posY = point.y posPoint.y = refPoint.y
break break
case 'middleLeft': case 'middleLeft':
posX = point.x - toolbarWidth posPoint.x = refPoint.x - toolbarWidth
posY = point.y - toolbarHeight / 2 posPoint.y = refPoint.y - toolbarHeight / 2
break break
case 'middleRight': case 'middleRight':
posX = point.x posPoint.x = refPoint.x
posY = point.y - toolbarHeight / 2 posPoint.y = refPoint.y - toolbarHeight / 2
break break
case 'center': case 'center':
posX = point.x - toolbarWidth / 2 posPoint.x = refPoint.x - toolbarWidth / 2
posY = point.y - toolbarHeight / 2 posPoint.y = refPoint.y - toolbarHeight / 2
break break
default: default:
// Default to 'topMiddle' if invalid position // Default to 'topMiddle' if invalid position
posX = point.x - toolbarWidth / 2 posPoint.x = refPoint.x - toolbarWidth / 2
posY = point.y - toolbarHeight / 2 posPoint.y = refPoint.y - toolbarHeight / 2
} }
//use original point to get the display //use original point to get the display
const display = screen.getDisplayNearestPoint({ x: point.x, y: point.y }) const display = screen.getDisplayNearestPoint(refPoint)
// Ensure toolbar stays within screen boundaries // Ensure toolbar stays within screen boundaries
posX = Math.round( posPoint.x = Math.round(
Math.max(display.workArea.x, Math.min(posX, display.workArea.x + display.workArea.width - toolbarWidth)) Math.max(display.workArea.x, Math.min(posPoint.x, display.workArea.x + display.workArea.width - toolbarWidth))
) )
posY = Math.round( posPoint.y = Math.round(
Math.max(display.workArea.y, Math.min(posY, display.workArea.y + display.workArea.height - toolbarHeight)) Math.max(display.workArea.y, Math.min(posPoint.y, display.workArea.y + display.workArea.height - toolbarHeight))
) )
return { x: posX, y: posY } return posPoint
} }
private isSamePoint(point1: Point, point2: Point): boolean { private isSamePoint(point1: Point, point2: Point): boolean {
@ -773,8 +874,11 @@ export class SelectionService {
} }
if (!isLogical) { if (!isLogical) {
// [macOS] don't need to convert by screenToDipPoint
if (!isMac) {
refPoint = screen.screenToDipPoint(refPoint)
}
//screenToDipPoint can be float, so we need to round it //screenToDipPoint can be float, so we need to round it
refPoint = screen.screenToDipPoint(refPoint)
refPoint = { x: Math.round(refPoint.x), y: Math.round(refPoint.y) } refPoint = { x: Math.round(refPoint.x), y: Math.round(refPoint.y) }
} }
@ -832,8 +936,8 @@ export class SelectionService {
return return
} }
//data point is physical coordinates, convert to logical coordinates //data point is physical coordinates, convert to logical coordinates(only for windows/linux)
const mousePoint = screen.screenToDipPoint({ x: data.x, y: data.y }) const mousePoint = isMac ? { x: data.x, y: data.y } : screen.screenToDipPoint({ x: data.x, y: data.y })
const bounds = this.toolbarWindow!.getBounds() const bounds = this.toolbarWindow!.getBounds()
@ -966,7 +1070,8 @@ export class SelectionService {
frame: false, frame: false,
transparent: true, transparent: true,
autoHideMenuBar: true, autoHideMenuBar: true,
titleBarStyle: 'hidden', titleBarStyle: 'hidden', // [macOS]
trafficLightPosition: { x: 12, y: 9 }, // [macOS]
hasShadow: false, hasShadow: false,
thickFrame: false, thickFrame: false,
show: false, show: false,
@ -1043,6 +1148,27 @@ export class SelectionService {
if (!actionWindow.isDestroyed()) { if (!actionWindow.isDestroyed()) {
actionWindow.destroy() actionWindow.destroy()
} }
// [macOS] a HACKY way
// make sure other windows do not bring to front when action window is closed
if (isMac) {
const focusableWindows: BrowserWindow[] = []
for (const window of BrowserWindow.getAllWindows()) {
if (!window.isDestroyed() && window.isVisible()) {
if (window.isFocusable()) {
focusableWindows.push(window)
window.setFocusable(false)
}
}
}
setTimeout(() => {
for (const window of focusableWindows) {
if (!window.isDestroyed()) {
window.setFocusable(true)
}
}
}, 50)
}
}) })
//remember the action window size //remember the action window size
@ -1088,22 +1214,26 @@ export class SelectionService {
//center way //center way
if (!this.isFollowToolbar || !this.toolbarWindow) { if (!this.isFollowToolbar || !this.toolbarWindow) {
if (this.isRemeberWinSize) { const display = screen.getDisplayNearestPoint(screen.getCursorScreenPoint())
actionWindow.setBounds({ const workArea = display.workArea
width: actionWindowWidth,
height: actionWindowHeight const centerX = workArea.x + (workArea.width - actionWindowWidth) / 2
}) const centerY = workArea.y + (workArea.height - actionWindowHeight) / 2
}
actionWindow.setBounds({
width: actionWindowWidth,
height: actionWindowHeight,
x: Math.round(centerX),
y: Math.round(centerY)
})
actionWindow.show() actionWindow.show()
this.hideToolbar()
return return
} }
//follow toolbar //follow toolbar
const toolbarBounds = this.toolbarWindow!.getBounds() const toolbarBounds = this.toolbarWindow!.getBounds()
const display = screen.getDisplayNearestPoint({ x: toolbarBounds.x, y: toolbarBounds.y }) const display = screen.getDisplayNearestPoint(screen.getCursorScreenPoint())
const workArea = display.workArea const workArea = display.workArea
const GAP = 6 // 6px gap from screen edges const GAP = 6 // 6px gap from screen edges
@ -1214,7 +1344,7 @@ export class SelectionService {
selectionService?.hideToolbar() selectionService?.hideToolbar()
}) })
ipcMain.handle(IpcChannel.Selection_WriteToClipboard, (_, text: string) => { ipcMain.handle(IpcChannel.Selection_WriteToClipboard, (_, text: string): boolean => {
return selectionService?.writeToClipboard(text) ?? false return selectionService?.writeToClipboard(text) ?? false
}) })
@ -1291,7 +1421,7 @@ export class SelectionService {
* @returns {boolean} Success status of initialization * @returns {boolean} Success status of initialization
*/ */
export function initSelectionService(): boolean { export function initSelectionService(): boolean {
if (!isWin) return false if (!isSupportedOS) return false
configManager.subscribe(ConfigKeys.SelectionAssistantEnabled, (enabled: boolean) => { configManager.subscribe(ConfigKeys.SelectionAssistantEnabled, (enabled: boolean) => {
//avoid closure //avoid closure

View File

@ -84,10 +84,8 @@ export class TrayService {
label: trayLocale.show_mini_window, label: trayLocale.show_mini_window,
click: () => windowService.showMiniWindow() click: () => windowService.showMiniWindow()
}, },
isWin && { (isWin || isMac) && {
label: selectionLocale.name + (selectionAssistantEnabled ? ' - On' : ' - Off'), label: selectionLocale.name + (selectionAssistantEnabled ? ' - On' : ' - Off'),
// type: 'checkbox',
// checked: selectionAssistantEnabled,
click: () => { click: () => {
if (selectionService) { if (selectionService) {
selectionService.toggleEnabled() selectionService.toggleEnabled()

View File

@ -42,6 +42,10 @@ const api = {
openWebsite: (url: string) => ipcRenderer.invoke(IpcChannel.Open_Website, url), openWebsite: (url: string) => ipcRenderer.invoke(IpcChannel.Open_Website, url),
getCacheSize: () => ipcRenderer.invoke(IpcChannel.App_GetCacheSize), getCacheSize: () => ipcRenderer.invoke(IpcChannel.App_GetCacheSize),
clearCache: () => ipcRenderer.invoke(IpcChannel.App_ClearCache), clearCache: () => ipcRenderer.invoke(IpcChannel.App_ClearCache),
mac: {
isProcessTrusted: (): Promise<boolean> => ipcRenderer.invoke(IpcChannel.App_MacIsProcessTrusted),
requestProcessTrust: (): Promise<boolean> => ipcRenderer.invoke(IpcChannel.App_MacRequestProcessTrust)
},
notification: { notification: {
send: (notification: Notification) => ipcRenderer.invoke(IpcChannel.Notification_Send, notification) send: (notification: Notification) => ipcRenderer.invoke(IpcChannel.Notification_Send, notification)
}, },

View File

@ -18,25 +18,37 @@ html {
--selection-toolbar-logo-display: flex; // values: flex | none --selection-toolbar-logo-display: flex; // values: flex | none
--selection-toolbar-logo-size: 22px; // default: 22px --selection-toolbar-logo-size: 22px; // default: 22px
--selection-toolbar-logo-margin: 0 0 0 5px; // default: 0 0 05px --selection-toolbar-logo-border-width: 0.5px 0 0.5px 0.5px; // default: none
--selection-toolbar-logo-border-style: solid; // default: none
--selection-toolbar-logo-border-color: rgba(255, 255, 255, 0.2);
--selection-toolbar-logo-margin: 0; // default: 0
--selection-toolbar-logo-padding: 0 6px 0 8px; // default: 0 4px 0 8px
--selection-toolbar-logo-background: transparent; // default: transparent
// DO NOT MODIFY THESE VALUES, IF YOU DON'T KNOW WHAT YOU ARE DOING // DO NOT MODIFY THESE VALUES, IF YOU DON'T KNOW WHAT YOU ARE DOING
--selection-toolbar-padding: 2px 4px 2px 2px; // default: 2px 4px 2px 2px --selection-toolbar-padding: 0; // default: 0
--selection-toolbar-margin: 2px 3px 5px 3px; // default: 2px 3px 5px 3px --selection-toolbar-margin: 2px 3px 5px 3px; // default: 2px 3px 5px 3px
// ------------------------------------------------------------ // ------------------------------------------------------------
--selection-toolbar-border-radius: 6px; --selection-toolbar-border-radius: 10px;
--selection-toolbar-border: 1px solid rgba(55, 55, 55, 0.5); --selection-toolbar-border: none;
--selection-toolbar-box-shadow: 0px 2px 3px rgba(50, 50, 50, 0.3); --selection-toolbar-box-shadow: 0px 2px 3px rgba(50, 50, 50, 0.3);
--selection-toolbar-background: rgba(20, 20, 20, 0.95); --selection-toolbar-background: rgba(20, 20, 20, 0.95);
// Buttons // Buttons
--selection-toolbar-buttons-border-width: 0.5px 0.5px 0.5px 0;
--selection-toolbar-buttons-border-style: solid;
--selection-toolbar-buttons-border-color: rgba(255, 255, 255, 0.2);
--selection-toolbar-buttons-border-radius: 0 var(--selection-toolbar-border-radius)
var(--selection-toolbar-border-radius) 0;
--selection-toolbar-button-icon-size: 16px; // default: 16px --selection-toolbar-button-icon-size: 16px; // default: 16px
--selection-toolbar-button-text-margin: 0 0 0 3px; // default: 0 0 0 3px --selection-toolbar-button-direction: row; // default: row | column
--selection-toolbar-button-margin: 0 2px; // default: 0 2px --selection-toolbar-button-text-margin: 0 0 0 0; // default: 0 0 0 0
--selection-toolbar-button-padding: 4px 6px; // default: 4px 6px --selection-toolbar-button-margin: 0; // default: 0
--selection-toolbar-button-border-radius: 4px; // default: 4px --selection-toolbar-button-padding: 0 8px; // default: 0 8px
--selection-toolbar-button-last-padding: 0 12px 0 8px;
--selection-toolbar-button-border-radius: 0; // default: 0
--selection-toolbar-button-border: none; // default: none --selection-toolbar-button-border: none; // default: none
--selection-toolbar-button-box-shadow: none; // default: none --selection-toolbar-button-box-shadow: none; // default: none
@ -45,14 +57,19 @@ html {
--selection-toolbar-button-text-color-hover: var(--selection-toolbar-color-primary); --selection-toolbar-button-text-color-hover: var(--selection-toolbar-color-primary);
--selection-toolbar-button-icon-color-hover: var(--selection-toolbar-color-primary); --selection-toolbar-button-icon-color-hover: var(--selection-toolbar-color-primary);
--selection-toolbar-button-bgcolor: transparent; // default: transparent --selection-toolbar-button-bgcolor: transparent; // default: transparent
--selection-toolbar-button-bgcolor-hover: #222222; --selection-toolbar-button-bgcolor-hover: #333333;
} }
[theme-mode='light'] { [theme-mode='light'] {
--selection-toolbar-border: 1px solid rgba(200, 200, 200, 0.5); --selection-toolbar-border: none;
--selection-toolbar-box-shadow: 0px 2px 3px rgba(50, 50, 50, 0.3); --selection-toolbar-box-shadow: 0px 2px 3px rgba(50, 50, 50, 0.1);
--selection-toolbar-background: rgba(245, 245, 245, 0.95); --selection-toolbar-background: rgba(245, 245, 245, 0.95);
// Buttons
--selection-toolbar-buttons-border-color: rgba(0, 0, 0, 0.08);
--selection-toolbar-logo-border-color: rgba(0, 0, 0, 0.08);
--selection-toolbar-button-text-color: rgba(0, 0, 0, 1); --selection-toolbar-button-text-color: rgba(0, 0, 0, 1);
--selection-toolbar-button-icon-color: var(--selection-toolbar-button-text-color); --selection-toolbar-button-icon-color: var(--selection-toolbar-button-text-color);
--selection-toolbar-button-text-color-hover: var(--selection-toolbar-color-primary); --selection-toolbar-button-text-color-hover: var(--selection-toolbar-color-primary);

View File

@ -2034,14 +2034,29 @@
"experimental": "Experimental Features", "experimental": "Experimental Features",
"enable": { "enable": {
"title": "Enable", "title": "Enable",
"description": "Currently only supported on Windows systems" "description": "Currently only supported on Windows & macOS",
"mac_process_trust_hint": {
"title": "Accessibility Permission",
"description": [
"Selection Assistant requires <strong>Accessibility Permission</strong> to work properly.",
"Please click \"<strong>Go to Settings</strong>\" and click the \"<strong>Open System Settings</strong>\" button in the permission request popup that appears later. Then find \"<strong>Cherry Studio</strong>\" in the application list that appears later and turn on the permission switch.",
"After completing the settings, please reopen the selection assistant."
],
"button": {
"open_accessibility_settings": "Open Accessibility Settings",
"go_to_settings": "Go to Settings"
}
}
}, },
"toolbar": { "toolbar": {
"title": "Toolbar", "title": "Toolbar",
"trigger_mode": { "trigger_mode": {
"title": "Trigger Mode", "title": "Trigger Mode",
"description": "The way to trigger the selection assistant and show the toolbar", "description": "The way to trigger the selection assistant and show the toolbar",
"description_note": "Some applications do not support selecting text with the Ctrl key. If you have remapped the Ctrl key using tools like AHK, it may cause some applications to fail to select text.", "description_note": {
"windows": "Some applications do not support selecting text with the Ctrl key. If you have remapped the Ctrl key using tools like AHK, it may cause some applications to fail to select text.",
"mac": "If you have remapped the ⌘ key using shortcuts or keyboard mapping tools, it may cause some applications to fail to select text."
},
"selected": "Selection", "selected": "Selection",
"selected_note": "Show toolbar immediately when text is selected", "selected_note": "Show toolbar immediately when text is selected",
"ctrlkey": "Ctrl Key", "ctrlkey": "Ctrl Key",
@ -2166,7 +2181,10 @@
}, },
"filter_modal": { "filter_modal": {
"title": "Application Filter List", "title": "Application Filter List",
"user_tips": "Please enter the executable file name of the application, one per line, case insensitive, can be fuzzy matched. For example: chrome.exe, weixin.exe, Cherry Studio.exe, etc." "user_tips": {
"windows": "Please enter the executable file name of the application, one per line, case insensitive, can be fuzzy matched. For example: chrome.exe, weixin.exe, Cherry Studio.exe, etc.",
"mac": "Please enter the Bundle ID of the application, one per line, case insensitive, can be fuzzy matched. For example: com.google.Chrome, com.apple.mail, etc."
}
} }
} }
} }

View File

@ -2037,14 +2037,29 @@
"experimental": "実験的機能", "experimental": "実験的機能",
"enable": { "enable": {
"title": "有効化", "title": "有効化",
"description": "現在Windowsのみ対応" "description": "現在Windows & macOSのみ対応",
"mac_process_trust_hint": {
"title": "アクセシビリティー権限",
"description": [
"テキスト選択ツールは、<strong>アクセシビリティー権限</strong>が必要です。",
"「<strong>設定に移動</strong>」をクリックし、後で表示される権限要求ポップアップで「<strong>システム設定を開く</strong>」ボタンをクリックします。その後、表示されるアプリケーションリストで「<strong>Cherry Studio</strong>」を見つけ、権限スイッチをオンにしてください。",
"設定が完了したら、テキスト選択ツールを再起動してください。"
],
"button": {
"open_accessibility_settings": "アクセシビリティー設定を開く",
"go_to_settings": "設定に移動"
}
}
}, },
"toolbar": { "toolbar": {
"title": "ツールバー", "title": "ツールバー",
"trigger_mode": { "trigger_mode": {
"title": "単語の取り出し方", "title": "単語の取り出し方",
"description": "テキスト選択後、取詞ツールバーを表示する方法", "description": "テキスト選択後、取詞ツールバーを表示する方法",
"description_note": "一部のアプリケーションでは、Ctrl キーでテキストを選択できません。AHK などのツールを使用して Ctrl キーを再マップした場合、一部のアプリケーションでテキスト選択が失敗する可能性があります。", "description_note": {
"windows": "一部のアプリケーションでは、Ctrl キーでテキストを選択できません。AHK などのツールを使用して Ctrl キーを再マップした場合、一部のアプリケーションでテキスト選択が失敗する可能性があります。",
"mac": "一部のアプリケーションでは、⌘ キーでテキストを選択できません。ショートカットキーまたはキーボードマッピングツールを使用して ⌘ キーを再マップした場合、一部のアプリケーションでテキスト選択が失敗する可能性があります。"
},
"selected": "選択時", "selected": "選択時",
"selected_note": "テキスト選択時に即時表示", "selected_note": "テキスト選択時に即時表示",
"ctrlkey": "Ctrlキー", "ctrlkey": "Ctrlキー",
@ -2169,7 +2184,10 @@
}, },
"filter_modal": { "filter_modal": {
"title": "アプリケーションフィルターリスト", "title": "アプリケーションフィルターリスト",
"user_tips": "アプリケーションの実行ファイル名を1行ずつ入力してください。大文字小文字は区別しません。例: chrome.exe, weixin.exe, Cherry Studio.exe, など。" "user_tips": {
"windows": "アプリケーションの実行ファイル名を1行ずつ入力してください。大文字小文字は区別しません。例: chrome.exe, weixin.exe, Cherry Studio.exe, など。",
"mac": "アプリケーションのBundle IDを1行ずつ入力してください。大文字小文字は区別しません。例: com.google.Chrome, com.apple.mail, など。"
}
} }
} }
} }

View File

@ -2037,14 +2037,29 @@
"experimental": "Экспериментальные функции", "experimental": "Экспериментальные функции",
"enable": { "enable": {
"title": "Включить", "title": "Включить",
"description": "Поддерживается только в Windows" "description": "Поддерживается только в Windows & macOS",
"mac_process_trust_hint": {
"title": "Права доступа",
"description": [
"Помощник выбора требует <strong>Права доступа</strong> для правильной работы.",
"Пожалуйста, перейдите в \"<strong>Настройки</strong>\" и нажмите \"<strong>Открыть системные настройки</strong>\" в запросе разрешения, который появится позже. Затем найдите \"<strong>Cherry Studio</strong>\" в списке приложений, который появится позже, и включите переключатель разрешения.",
"После завершения настроек, пожалуйста, перезапустите помощник выбора."
],
"button": {
"open_accessibility_settings": "Открыть системные настройки",
"go_to_settings": "Настройки"
}
}
}, },
"toolbar": { "toolbar": {
"title": "Панель инструментов", "title": "Панель инструментов",
"trigger_mode": { "trigger_mode": {
"title": "Режим активации", "title": "Режим активации",
"description": "Показывать панель сразу при выделении, или только при удержании Ctrl, или только при нажатии на сочетание клавиш", "description": "Показывать панель сразу при выделении, или только при удержании Ctrl, или только при нажатии на сочетание клавиш",
"description_note": "В некоторых приложениях Ctrl может не работать. Если вы используете AHK или другие инструменты для переназначения Ctrl, это может привести к тому, что некоторые приложения не смогут выделить текст.", "description_note": {
"windows": "В некоторых приложениях Ctrl может не работать. Если вы используете AHK или другие инструменты для переназначения Ctrl, это может привести к тому, что некоторые приложения не смогут выделить текст.",
"mac": "В некоторых приложениях ⌘ может не работать. Если вы используете сочетания клавиш или инструменты для переназначения ⌘, это может привести к тому, что некоторые приложения не смогут выделить текст."
},
"selected": "При выделении", "selected": "При выделении",
"selected_note": "После выделения", "selected_note": "После выделения",
"ctrlkey": "По Ctrl", "ctrlkey": "По Ctrl",
@ -2169,7 +2184,10 @@
}, },
"filter_modal": { "filter_modal": {
"title": "Список фильтрации", "title": "Список фильтрации",
"user_tips": "Введите имя исполняемого файла приложения, один на строку, не учитывая регистр, можно использовать подстановку *" "user_tips": {
"windows": "Введите имя исполняемого файла приложения, один на строку, не учитывая регистр, можно использовать подстановку *",
"mac": "Введите Bundle ID приложения, один на строку, не учитывая регистр, можно использовать подстановку *"
}
} }
} }
} }

View File

@ -2037,14 +2037,29 @@
"experimental": "实验性功能", "experimental": "实验性功能",
"enable": { "enable": {
"title": "启用", "title": "启用",
"description": "当前仅支持 Windows 系统" "description": "当前仅支持 Windows & macOS",
"mac_process_trust_hint": {
"title": "辅助功能权限",
"description": [
"划词助手需「<strong>辅助功能权限</strong>」才能正常工作。",
"请点击「<strong>去设置</strong>」,并在稍后弹出的权限请求弹窗中点击 「<strong>打开系统设置</strong>」 按钮,然后在之后的应用列表中找到 「<strong>Cherry Studio</strong>」,并打开权限开关。",
"完成设置后,请再次开启划词助手。"
],
"button": {
"open_accessibility_settings": "打开辅助功能设置",
"go_to_settings": "去设置"
}
}
}, },
"toolbar": { "toolbar": {
"title": "工具栏", "title": "工具栏",
"trigger_mode": { "trigger_mode": {
"title": "取词方式", "title": "取词方式",
"description": "划词后,触发取词并显示工具栏的方式", "description": "划词后,触发取词并显示工具栏的方式",
"description_note": "少数应用不支持通过 Ctrl 键划词。若使用了 AHK 等工具对 Ctrl 键进行了重映射,可能导致部分应用无法划词。", "description_note": {
"windows": "少数应用不支持通过 Ctrl 键划词。若使用了AHK等按键映射工具对 Ctrl 键进行了重映射,可能导致部分应用无法划词。",
"mac": "若使用了快捷键或键盘映射工具对 ⌘ 键进行了重映射,可能导致部分应用无法划词。"
},
"selected": "划词", "selected": "划词",
"selected_note": "划词后立即显示工具栏", "selected_note": "划词后立即显示工具栏",
"ctrlkey": "Ctrl 键", "ctrlkey": "Ctrl 键",
@ -2169,7 +2184,10 @@
}, },
"filter_modal": { "filter_modal": {
"title": "应用筛选名单", "title": "应用筛选名单",
"user_tips": "请输入应用的执行文件名每行一个不区分大小写可以模糊匹配。例如chrome.exe、weixin.exe、Cherry Studio.exe 等" "user_tips": {
"windows": "请输入应用的执行文件名每行一个不区分大小写可以模糊匹配。例如chrome.exe、weixin.exe、Cherry Studio.exe等",
"mac": "请输入应用的Bundle ID每行一个不区分大小写可以模糊匹配。例如com.google.Chrome、com.apple.mail等"
}
} }
} }
} }

View File

@ -2037,14 +2037,29 @@
"experimental": "實驗性功能", "experimental": "實驗性功能",
"enable": { "enable": {
"title": "啟用", "title": "啟用",
"description": "目前僅支援 Windows 系統" "description": "目前僅支援 Windows & macOS",
"mac_process_trust_hint": {
"title": "輔助使用權限",
"description": [
"劃詞助手需「<strong>輔助使用權限</strong>」才能正常工作。",
"請點擊「<strong>去設定</strong>」,並在稍後彈出的權限請求彈窗中點擊 「<strong>打開系統設定</strong>」 按鈕,然後在之後的應用程式列表中找到 「<strong>Cherry Studio</strong>」,並開啟權限開關。",
"完成設定後,請再次開啟劃詞助手。"
],
"button": {
"open_accessibility_settings": "打開輔助使用設定",
"go_to_settings": "去設定"
}
}
}, },
"toolbar": { "toolbar": {
"title": "工具列", "title": "工具列",
"trigger_mode": { "trigger_mode": {
"title": "取詞方式", "title": "取詞方式",
"description": "劃詞後,觸發取詞並顯示工具列的方式", "description": "劃詞後,觸發取詞並顯示工具列的方式",
"description_note": "在某些應用中可能無法透過 Ctrl 鍵劃詞。若使用了 AHK 等工具對 Ctrl 鍵進行了重新對應,可能導致部分應用程式無法劃詞。", "description_note": {
"windows": "在某些應用中可能無法透過 Ctrl 鍵劃詞。若使用了 AHK 等工具對 Ctrl 鍵進行了重新對應,可能導致部分應用程式無法劃詞。",
"mac": "若使用了快捷鍵或鍵盤映射工具對 ⌘ 鍵進行了重新對應,可能導致部分應用程式無法劃詞。"
},
"selected": "劃詞", "selected": "劃詞",
"selected_note": "劃詞後,立即顯示工具列", "selected_note": "劃詞後,立即顯示工具列",
"ctrlkey": "Ctrl 鍵", "ctrlkey": "Ctrl 鍵",
@ -2169,7 +2184,10 @@
}, },
"filter_modal": { "filter_modal": {
"title": "應用篩選名單", "title": "應用篩選名單",
"user_tips": "請輸入應用的執行檔名稱每行一個不區分大小寫可以模糊匹配。例如chrome.exe、weixin.exe、Cherry Studio.exe 等" "user_tips": {
"windows": "請輸入應用的執行檔名稱每行一個不區分大小寫可以模糊匹配。例如chrome.exe、weixin.exe、Cherry Studio.exe等",
"mac": "請輸入應用的 Bundle ID每行一個不區分大小寫可以模糊匹配。例如com.google.Chrome、com.apple.mail等"
}
} }
} }
} }

View File

@ -1,4 +1,4 @@
import { isWin } from '@renderer/config/constant' import { isMac, isWin } from '@renderer/config/constant'
import { useTheme } from '@renderer/context/ThemeProvider' import { useTheme } from '@renderer/context/ThemeProvider'
import { useSelectionAssistant } from '@renderer/hooks/useSelectionAssistant' import { useSelectionAssistant } from '@renderer/hooks/useSelectionAssistant'
import { FilterMode, TriggerMode } from '@renderer/types/selectionTypes' import { FilterMode, TriggerMode } from '@renderer/types/selectionTypes'
@ -19,8 +19,9 @@ import {
SettingRowTitle, SettingRowTitle,
SettingTitle SettingTitle
} from '..' } from '..'
import SelectionActionsList from './SelectionActionsList' import MacProcessTrustHintModal from './components/MacProcessTrustHintModal'
import SelectionFilterListModal from './SelectionFilterListModal' import SelectionActionsList from './components/SelectionActionsList'
import SelectionFilterListModal from './components/SelectionFilterListModal'
const SelectionAssistantSettings: FC = () => { const SelectionAssistantSettings: FC = () => {
const { theme } = useTheme() const { theme } = useTheme()
@ -49,15 +50,43 @@ const SelectionAssistantSettings: FC = () => {
setFilterMode, setFilterMode,
setFilterList setFilterList
} = useSelectionAssistant() } = useSelectionAssistant()
const isSupportedOS = isWin || isMac
const [isFilterListModalOpen, setIsFilterListModalOpen] = useState(false) const [isFilterListModalOpen, setIsFilterListModalOpen] = useState(false)
const [isMacTrustModalOpen, setIsMacTrustModalOpen] = useState(false)
const [opacityValue, setOpacityValue] = useState(actionWindowOpacity) const [opacityValue, setOpacityValue] = useState(actionWindowOpacity)
// force disable selection assistant on non-windows systems // force disable selection assistant on non-windows systems
useEffect(() => { useEffect(() => {
if (!isWin && selectionEnabled) { const checkMacProcessTrust = async () => {
setSelectionEnabled(false) const isTrusted = await window.api.mac.isProcessTrusted()
if (!isTrusted) {
setSelectionEnabled(false)
}
} }
}, [selectionEnabled, setSelectionEnabled])
if (!isSupportedOS && selectionEnabled) {
setSelectionEnabled(false)
return
} else if (isMac && selectionEnabled) {
checkMacProcessTrust()
}
}, [isSupportedOS, selectionEnabled, setSelectionEnabled])
const handleEnableCheckboxChange = async (checked: boolean) => {
if (!isSupportedOS) return
if (isMac && checked) {
const isTrusted = await window.api.mac.isProcessTrusted()
if (!isTrusted) {
setIsMacTrustModalOpen(true)
return
}
}
setSelectionEnabled(checked)
}
return ( return (
<SettingContainer theme={theme}> <SettingContainer theme={theme}>
@ -71,18 +100,18 @@ const SelectionAssistantSettings: FC = () => {
style={{ fontSize: 12 }}> style={{ fontSize: 12 }}>
{'FAQ & ' + t('settings.about.feedback.button')} {'FAQ & ' + t('settings.about.feedback.button')}
</Button> </Button>
<ExperimentalText>{t('selection.settings.experimental')}</ExperimentalText> {isMac && <ExperimentalText>{t('selection.settings.experimental')}</ExperimentalText>}
</Row> </Row>
<SettingDivider /> <SettingDivider />
<SettingRow> <SettingRow>
<SettingLabel> <SettingLabel>
<SettingRowTitle>{t('selection.settings.enable.title')}</SettingRowTitle> <SettingRowTitle>{t('selection.settings.enable.title')}</SettingRowTitle>
{!isWin && <SettingDescription>{t('selection.settings.enable.description')}</SettingDescription>} {!isSupportedOS && <SettingDescription>{t('selection.settings.enable.description')}</SettingDescription>}
</SettingLabel> </SettingLabel>
<Switch <Switch
checked={isWin && selectionEnabled} checked={isSupportedOS && selectionEnabled}
onChange={(checked) => setSelectionEnabled(checked)} onChange={(checked) => handleEnableCheckboxChange(checked)}
disabled={!isWin} disabled={!isSupportedOS}
/> />
</SettingRow> </SettingRow>
@ -103,7 +132,10 @@ const SelectionAssistantSettings: FC = () => {
<SettingLabel> <SettingLabel>
<SettingRowTitle> <SettingRowTitle>
<div style={{ marginRight: '4px' }}>{t('selection.settings.toolbar.trigger_mode.title')}</div> <div style={{ marginRight: '4px' }}>{t('selection.settings.toolbar.trigger_mode.title')}</div>
<Tooltip placement="top" title={t('selection.settings.toolbar.trigger_mode.description_note')} arrow> <Tooltip
placement="top"
title={t(`selection.settings.toolbar.trigger_mode.description_note.${isWin ? 'windows' : 'mac'}`)}
arrow>
<QuestionIcon size={14} /> <QuestionIcon size={14} />
</Tooltip> </Tooltip>
</SettingRowTitle> </SettingRowTitle>
@ -116,9 +148,11 @@ const SelectionAssistantSettings: FC = () => {
<Tooltip placement="top" title={t('selection.settings.toolbar.trigger_mode.selected_note')} arrow> <Tooltip placement="top" title={t('selection.settings.toolbar.trigger_mode.selected_note')} arrow>
<Radio.Button value="selected">{t('selection.settings.toolbar.trigger_mode.selected')}</Radio.Button> <Radio.Button value="selected">{t('selection.settings.toolbar.trigger_mode.selected')}</Radio.Button>
</Tooltip> </Tooltip>
<Tooltip placement="top" title={t('selection.settings.toolbar.trigger_mode.ctrlkey_note')} arrow> {isWin && (
<Radio.Button value="ctrlkey">{t('selection.settings.toolbar.trigger_mode.ctrlkey')}</Radio.Button> <Tooltip placement="top" title={t('selection.settings.toolbar.trigger_mode.ctrlkey_note')} arrow>
</Tooltip> <Radio.Button value="ctrlkey">{t('selection.settings.toolbar.trigger_mode.ctrlkey')}</Radio.Button>
</Tooltip>
)}
<Tooltip <Tooltip
placement="topRight" placement="topRight"
title={ title={
@ -256,6 +290,8 @@ const SelectionAssistantSettings: FC = () => {
</SettingGroup> </SettingGroup>
</> </>
)} )}
{isMac && <MacProcessTrustHintModal open={isMacTrustModalOpen} onClose={() => setIsMacTrustModalOpen(false)} />}
</SettingContainer> </SettingContainer>
) )
} }

View File

@ -0,0 +1,68 @@
import { Button, Modal, Typography } from 'antd'
import { FC } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import styled from 'styled-components'
const { Text, Paragraph } = Typography
interface MacProcessTrustHintModalProps {
open: boolean
onClose: () => void
}
const MacProcessTrustHintModal: FC<MacProcessTrustHintModalProps> = ({ open, onClose }) => {
const { t } = useTranslation()
const handleOpenAccessibility = () => {
window.api.shell.openExternal('x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility')
onClose()
}
const handleConfirm = async () => {
window.api.mac.requestProcessTrust()
onClose()
}
return (
<Modal
title={t('selection.settings.enable.mac_process_trust_hint.title')}
open={open}
onCancel={onClose}
footer={
<div style={{ display: 'flex', justifyContent: 'space-between', width: '100%' }}>
<Button type="link" style={{ color: 'var(--color-text-3)', fontSize: 12 }} onClick={handleOpenAccessibility}>
{t('selection.settings.enable.mac_process_trust_hint.button.open_accessibility_settings')}
</Button>
<Button type="primary" onClick={handleConfirm}>
{t('selection.settings.enable.mac_process_trust_hint.button.go_to_settings')}
</Button>
</div>
}
centered
destroyOnClose>
<ContentContainer>
<Paragraph>
<Text>
<Trans i18nKey="selection.settings.enable.mac_process_trust_hint.description.0" />
</Text>
</Paragraph>
<Paragraph>
<Text>
<Trans i18nKey="selection.settings.enable.mac_process_trust_hint.description.1" />
</Text>
</Paragraph>
<Paragraph>
<Text>
<Trans i18nKey="selection.settings.enable.mac_process_trust_hint.description.2" />
</Text>
</Paragraph>
</ContentContainer>
</Modal>
)
}
const ContentContainer = styled.div`
padding: 16px 0;
`
export default MacProcessTrustHintModal

View File

@ -6,13 +6,13 @@ import { Row } from 'antd'
import { FC } from 'react' import { FC } from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import { SettingDivider, SettingGroup } from '..' import { SettingDivider, SettingGroup } from '../..'
import ActionsList from './components/ActionsList' import { useActionItems } from '../hooks/useSettingsActionsList'
import ActionsListDivider from './components/ActionsListDivider' import ActionsList from './ActionsList'
import SettingsActionsListHeader from './components/SettingsActionsListHeader' import ActionsListDivider from './ActionsListDivider'
import { useActionItems } from './hooks/useSettingsActionsList'
import SelectionActionSearchModal from './SelectionActionSearchModal' import SelectionActionSearchModal from './SelectionActionSearchModal'
import SelectionActionUserModal from './SelectionActionUserModal' import SelectionActionUserModal from './SelectionActionUserModal'
import SettingsActionsListHeader from './SettingsActionsListHeader'
// Component for managing selection actions in settings // Component for managing selection actions in settings
// Handles drag-and-drop reordering, enabling/disabling actions, and custom action management // Handles drag-and-drop reordering, enabling/disabling actions, and custom action management

View File

@ -1,3 +1,4 @@
import { isWin } from '@renderer/config/constant'
import { Button, Form, Input, Modal } from 'antd' import { Button, Form, Input, Modal } from 'antd'
import { FC, useEffect } from 'react' import { FC, useEffect } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@ -54,7 +55,11 @@ const SelectionFilterListModal: FC<SelectionFilterListModalProps> = ({ open, onC
{t('common.save')} {t('common.save')}
</Button> </Button>
]}> ]}>
<UserTip>{t('selection.settings.filter_modal.user_tips')}</UserTip> <UserTip>
{isWin
? t('selection.settings.filter_modal.user_tips.windows')
: t('selection.settings.filter_modal.user_tips.mac')}
</UserTip>
<Form form={form} layout="vertical" initialValues={{ filterList: '' }}> <Form form={form} layout="vertical" initialValues={{ filterList: '' }}>
<Form.Item name="filterList" noStyle> <Form.Item name="filterList" noStyle>
<StyledTextArea autoSize={{ minRows: 6, maxRows: 16 }} spellCheck={false} autoFocus /> <StyledTextArea autoSize={{ minRows: 6, maxRows: 16 }} spellCheck={false} autoFocus />

View File

@ -4,7 +4,7 @@ import type { ActionItem } from '@renderer/types/selectionTypes'
import { useMemo, useState } from 'react' import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { DEFAULT_SEARCH_ENGINES } from '../SelectionActionSearchModal' import { DEFAULT_SEARCH_ENGINES } from '../components/SelectionActionSearchModal'
const MAX_CUSTOM_ITEMS = 8 const MAX_CUSTOM_ITEMS = 8
const MAX_ENABLED_ITEMS = 6 const MAX_ENABLED_ITEMS = 6

View File

@ -24,7 +24,7 @@ const ShortcutSettings: FC = () => {
//if shortcut is not available on all the platforms, block the shortcut here //if shortcut is not available on all the platforms, block the shortcut here
let shortcuts = originalShortcuts let shortcuts = originalShortcuts
if (!isWin) { if (!isWin && !isMac) {
//Selection Assistant only available on Windows now //Selection Assistant only available on Windows now
const excludedShortcuts = ['selection_assistant_toggle', 'selection_assistant_select_text'] const excludedShortcuts = ['selection_assistant_toggle', 'selection_assistant_select_text']
shortcuts = shortcuts.filter((s) => !excludedShortcuts.includes(s.key)) shortcuts = shortcuts.filter((s) => !excludedShortcuts.includes(s.key))

View File

@ -1,3 +1,4 @@
import { isMac } from '@renderer/config/constant'
import { useSelectionAssistant } from '@renderer/hooks/useSelectionAssistant' import { useSelectionAssistant } from '@renderer/hooks/useSelectionAssistant'
import { useSettings } from '@renderer/hooks/useSettings' import { useSettings } from '@renderer/hooks/useSettings'
import i18n from '@renderer/i18n' import i18n from '@renderer/i18n'
@ -182,7 +183,7 @@ const SelectionActionApp: FC = () => {
return ( return (
<WindowFrame $opacity={opacity / 100}> <WindowFrame $opacity={opacity / 100}>
<TitleBar $isWindowFocus={isWindowFocus}> <TitleBar $isWindowFocus={isWindowFocus} style={isMac ? { paddingLeft: '70px' } : {}}>
{action.icon && ( {action.icon && (
<TitleBarIcon> <TitleBarIcon>
<DynamicIcon <DynamicIcon
@ -230,9 +231,12 @@ const SelectionActionApp: FC = () => {
/> />
</OpacitySlider> </OpacitySlider>
)} )}
{!isMac && (
<WinButton type="text" icon={<Minus size={16} />} onClick={handleMinimize} /> <>
<WinButton type="text" icon={<X size={16} />} onClick={handleClose} className="close" /> <WinButton type="text" icon={<Minus size={16} />} onClick={handleMinimize} />
<WinButton type="text" icon={<X size={16} />} onClick={handleClose} className="close" />
</>
)}
</TitleBarButtons> </TitleBarButtons>
</TitleBar> </TitleBar>
<MainContainer> <MainContainer>

View File

@ -259,7 +259,7 @@ const SelectionToolbar: FC<{ demo?: boolean }> = ({ demo = false }) => {
const Container = styled.div` const Container = styled.div`
display: inline-flex; display: inline-flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: stretch;
height: var(--selection-toolbar-height); height: var(--selection-toolbar-height);
border-radius: var(--selection-toolbar-border-radius); border-radius: var(--selection-toolbar-border-radius);
border: var(--selection-toolbar-border); border: var(--selection-toolbar-border);
@ -269,6 +269,7 @@ const Container = styled.div`
margin: var(--selection-toolbar-margin) !important; margin: var(--selection-toolbar-margin) !important;
user-select: none; user-select: none;
box-sizing: border-box; box-sizing: border-box;
overflow: hidden;
` `
const LogoWrapper = styled.div<{ $draggable: boolean }>` const LogoWrapper = styled.div<{ $draggable: boolean }>`
@ -276,8 +277,13 @@ const LogoWrapper = styled.div<{ $draggable: boolean }>`
align-items: center; align-items: center;
justify-content: center; justify-content: center;
margin: var(--selection-toolbar-logo-margin); margin: var(--selection-toolbar-logo-margin);
background-color: transparent; padding: var(--selection-toolbar-logo-padding);
${({ $draggable }) => $draggable && ' -webkit-app-region: drag;'} background-color: var(--selection-toolbar-logo-background);
border-width: var(--selection-toolbar-logo-border-width);
border-style: var(--selection-toolbar-logo-border-style);
border-color: var(--selection-toolbar-logo-border-color);
border-radius: var(--selection-toolbar-border-radius) 0 0 var(--selection-toolbar-border-radius);
${({ $draggable }) => $draggable && ' -webkit-app-region: drag;'};
` `
const Logo = styled(Avatar)` const Logo = styled(Avatar)`
@ -307,14 +313,19 @@ const ActionWrapper = styled.div`
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
margin-left: 3px;
background-color: transparent; background-color: transparent;
border-width: var(--selection-toolbar-buttons-border-width);
border-style: var(--selection-toolbar-buttons-border-style);
border-color: var(--selection-toolbar-buttons-border-color);
border-radius: var(--selection-toolbar-buttons-border-radius);
` `
const ActionButton = styled.div` const ActionButton = styled.div`
height: 100%;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 2px;
cursor: pointer !important; cursor: pointer !important;
margin: var(--selection-toolbar-button-margin); margin: var(--selection-toolbar-button-margin);
padding: var(--selection-toolbar-button-padding); padding: var(--selection-toolbar-button-padding);
@ -324,6 +335,10 @@ const ActionButton = styled.div`
box-shadow: var(--selection-toolbar-button-box-shadow); box-shadow: var(--selection-toolbar-button-box-shadow);
transition: all 0.1s ease-in-out; transition: all 0.1s ease-in-out;
will-change: color, background-color; will-change: color, background-color;
&:last-child {
border-radius: 0 var(--selection-toolbar-border-radius) var(--selection-toolbar-border-radius) 0;
padding: var(--selection-toolbar-button-last-padding);
}
.btn-icon { .btn-icon {
width: var(--selection-toolbar-button-icon-size); width: var(--selection-toolbar-button-icon-size);
@ -337,6 +352,7 @@ const ActionButton = styled.div`
color: var(--selection-toolbar-button-text-color); color: var(--selection-toolbar-button-text-color);
transition: color 0.1s ease-in-out; transition: color 0.1s ease-in-out;
will-change: color; will-change: color;
line-height: 1.1;
} }
&:hover { &:hover {
.btn-icon { .btn-icon {

View File

@ -5793,7 +5793,7 @@ __metadata:
remove-markdown: "npm:^0.6.2" remove-markdown: "npm:^0.6.2"
rollup-plugin-visualizer: "npm:^5.12.0" rollup-plugin-visualizer: "npm:^5.12.0"
sass: "npm:^1.88.0" sass: "npm:^1.88.0"
selection-hook: "npm:^0.9.23" selection-hook: "npm:^1.0.3"
shiki: "npm:^3.7.0" shiki: "npm:^3.7.0"
string-width: "npm:^7.2.0" string-width: "npm:^7.2.0"
styled-components: "npm:^6.1.11" styled-components: "npm:^6.1.11"
@ -13955,6 +13955,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"node-addon-api@npm:^8.4.0":
version: 8.4.0
resolution: "node-addon-api@npm:8.4.0"
dependencies:
node-gyp: "npm:latest"
checksum: 10c0/d51be099e1b9a6ac4a72f1a60787004d44c8ffe4be1efa38755d54b2a9f4f66647cc6913070e0ed20256d0e6eacceabfff90175fba2ef71153c2d06f8db8e7a9
languageName: node
linkType: hard
"node-api-version@npm:^0.2.0": "node-api-version@npm:^0.2.0":
version: 0.2.1 version: 0.2.1
resolution: "node-api-version@npm:0.2.1" resolution: "node-api-version@npm:0.2.1"
@ -16714,13 +16723,14 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"selection-hook@npm:^0.9.23": "selection-hook@npm:^1.0.3":
version: 0.9.23 version: 1.0.3
resolution: "selection-hook@npm:0.9.23" resolution: "selection-hook@npm:1.0.3"
dependencies: dependencies:
node-addon-api: "npm:^8.4.0"
node-gyp: "npm:latest" node-gyp: "npm:latest"
node-gyp-build: "npm:^4.8.4" node-gyp-build: "npm:^4.8.4"
checksum: 10c0/3b91193814c063e14dd788cff3b27020821bbeae24eab106d2ce5bf600c034c1b3db96ce573c456b74d0553346dfcf4c7cc8d49386a22797b42667f7ed3eee01 checksum: 10c0/812df47050d470d398974ca9833caba3bc55fcacf76aec5207cffcef4b81cf22d5a0992263e2074afd05c21781f903ffac25177cd9553417525648136405b474
languageName: node languageName: node
linkType: hard linkType: hard