mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-19 14:41:24 +08:00
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:
parent
cd1ef46577
commit
2f016efc50
@ -53,7 +53,9 @@ files:
|
||||
- '!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/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:
|
||||
- resources/**
|
||||
- '**/*.{metal,exp,lib}'
|
||||
|
||||
@ -66,7 +66,7 @@
|
||||
"node-stream-zip": "^1.15.0",
|
||||
"notion-helper": "^1.3.22",
|
||||
"os-proxy-config": "^1.1.2",
|
||||
"selection-hook": "^0.9.23",
|
||||
"selection-hook": "^1.0.3",
|
||||
"turndown": "7.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@ -32,6 +32,9 @@ export enum IpcChannel {
|
||||
App_InstallUvBinary = 'app:install-uv-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',
|
||||
|
||||
Notification_Send = 'notification:send',
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
interface IFilterList {
|
||||
WINDOWS: string[]
|
||||
MAC?: string[]
|
||||
MAC: string[]
|
||||
}
|
||||
|
||||
interface IFinetunedList {
|
||||
@ -45,14 +45,17 @@ export const SELECTION_PREDEFINED_BLACKLIST: IFilterList = {
|
||||
'sldworks.exe',
|
||||
// Remote Desktop
|
||||
'mstsc.exe'
|
||||
]
|
||||
],
|
||||
MAC: []
|
||||
}
|
||||
|
||||
export const SELECTION_FINETUNED_LIST: IFinetunedList = {
|
||||
EXCLUDE_CLIPBOARD_CURSOR_DETECT: {
|
||||
WINDOWS: ['acrobat.exe', 'wps.exe', 'cajviewer.exe']
|
||||
WINDOWS: ['acrobat.exe', 'wps.exe', 'cajviewer.exe'],
|
||||
MAC: []
|
||||
},
|
||||
INCLUDE_CLIPBOARD_DELAY_READ: {
|
||||
WINDOWS: ['acrobat.exe', 'wps.exe', 'cajviewer.exe', 'foxitphantom.exe']
|
||||
WINDOWS: ['acrobat.exe', 'wps.exe', 'cajviewer.exe', 'foxitphantom.exe'],
|
||||
MAC: []
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,7 +8,7 @@ import { handleZoomFactor } from '@main/utils/zoom'
|
||||
import { UpgradeChannel } from '@shared/config/constant'
|
||||
import { IpcChannel } from '@shared/IpcChannel'
|
||||
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 { 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) => {
|
||||
configManager.set(key, value, isNotify)
|
||||
})
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
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 { BrowserWindow, ipcMain, screen } from 'electron'
|
||||
import { BrowserWindow, ipcMain, screen, systemPreferences } from 'electron'
|
||||
import Logger from 'electron-log'
|
||||
import { join } from 'path'
|
||||
import type {
|
||||
@ -16,9 +16,12 @@ import type { ActionItem } from '../../renderer/src/types/selectionTypes'
|
||||
import { ConfigKeys, configManager } from './ConfigManager'
|
||||
import storeSyncService from './StoreSyncService'
|
||||
|
||||
const isSupportedOS = isWin || isMac
|
||||
|
||||
let SelectionHook: SelectionHookConstructor | null = null
|
||||
try {
|
||||
if (isWin) {
|
||||
//since selection-hook v1.0.0, it supports macOS
|
||||
if (isSupportedOS) {
|
||||
SelectionHook = require('selection-hook')
|
||||
}
|
||||
} catch (error) {
|
||||
@ -118,7 +121,7 @@ export class SelectionService {
|
||||
}
|
||||
|
||||
public static getInstance(): SelectionService | null {
|
||||
if (!isWin) return null
|
||||
if (!isSupportedOS) return null
|
||||
|
||||
if (!SelectionService.instance) {
|
||||
SelectionService.instance = new SelectionService()
|
||||
@ -213,6 +216,8 @@ export class SelectionService {
|
||||
blacklist: SelectionHook!.FilterMode.EXCLUDE_LIST
|
||||
}
|
||||
|
||||
const predefinedBlacklist = isWin ? SELECTION_PREDEFINED_BLACKLIST.WINDOWS : SELECTION_PREDEFINED_BLACKLIST.MAC
|
||||
|
||||
let combinedList: string[] = list
|
||||
let combinedMode = mode
|
||||
|
||||
@ -221,7 +226,7 @@ export class SelectionService {
|
||||
switch (mode) {
|
||||
case 'blacklist':
|
||||
//combine the predefined blacklist with the user-defined blacklist
|
||||
combinedList = [...new Set([...list, ...SELECTION_PREDEFINED_BLACKLIST.WINDOWS])]
|
||||
combinedList = [...new Set([...list, ...predefinedBlacklist])]
|
||||
break
|
||||
case 'whitelist':
|
||||
combinedList = [...list]
|
||||
@ -229,7 +234,7 @@ export class SelectionService {
|
||||
case 'default':
|
||||
default:
|
||||
//use the predefined blacklist as the default filter list
|
||||
combinedList = [...SELECTION_PREDEFINED_BLACKLIST.WINDOWS]
|
||||
combinedList = [...predefinedBlacklist]
|
||||
combinedMode = 'blacklist'
|
||||
break
|
||||
}
|
||||
@ -243,14 +248,21 @@ export class SelectionService {
|
||||
private setHookFineTunedList() {
|
||||
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(
|
||||
SelectionHook!.FineTunedListType.EXCLUDE_CLIPBOARD_CURSOR_DETECT,
|
||||
SELECTION_FINETUNED_LIST.EXCLUDE_CLIPBOARD_CURSOR_DETECT.WINDOWS
|
||||
excludeClipboardCursorDetectList
|
||||
)
|
||||
|
||||
this.selectionHook.setFineTunedList(
|
||||
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
|
||||
*/
|
||||
public start(): boolean {
|
||||
if (!this.selectionHook || this.started) {
|
||||
this.logError(new Error('SelectionService start(): instance is null or already started'))
|
||||
if (!this.selectionHook) {
|
||||
this.logError(new Error('SelectionService start(): instance is null'))
|
||||
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 {
|
||||
//make sure the toolbar window is ready
|
||||
this.createToolbarWindow()
|
||||
@ -306,6 +335,7 @@ export class SelectionService {
|
||||
if (!this.selectionHook) return false
|
||||
|
||||
this.selectionHook.stop()
|
||||
|
||||
this.selectionHook.cleanup() //already remove all listeners
|
||||
|
||||
//reset the listener states
|
||||
@ -316,6 +346,7 @@ export class SelectionService {
|
||||
this.toolbarWindow.close()
|
||||
this.toolbarWindow = null
|
||||
}
|
||||
|
||||
this.closePreloadedActionWindows()
|
||||
|
||||
this.started = false
|
||||
@ -366,21 +397,29 @@ export class SelectionService {
|
||||
this.toolbarWindow = new BrowserWindow({
|
||||
width: toolbarWidth,
|
||||
height: toolbarHeight,
|
||||
show: false,
|
||||
frame: false,
|
||||
transparent: true,
|
||||
alwaysOnTop: true,
|
||||
skipTaskbar: true,
|
||||
autoHideMenuBar: true,
|
||||
resizable: false,
|
||||
minimizable: false,
|
||||
maximizable: false,
|
||||
fullscreenable: false, // [macOS] must be false
|
||||
movable: true,
|
||||
focusable: false,
|
||||
hasShadow: false,
|
||||
thickFrame: false,
|
||||
roundedCorners: true,
|
||||
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: {
|
||||
preload: join(__dirname, '../preload/index.js'),
|
||||
contextIsolation: true,
|
||||
@ -392,7 +431,9 @@ export class SelectionService {
|
||||
|
||||
// Hide when losing focus
|
||||
this.toolbarWindow.on('blur', () => {
|
||||
this.hideToolbar()
|
||||
if (this.toolbarWindow!.isVisible()) {
|
||||
this.hideToolbar()
|
||||
}
|
||||
})
|
||||
|
||||
// Clean up when closed
|
||||
@ -406,6 +447,13 @@ 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', () => {
|
||||
@ -460,11 +508,22 @@ export class SelectionService {
|
||||
//set the window to always on top (highest level)
|
||||
//should set every time the window is shown
|
||||
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
|
||||
* It's a strange behavior, so we don't use it for compatibility
|
||||
* [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)
|
||||
|
||||
@ -477,10 +536,52 @@ export class SelectionService {
|
||||
public hideToolbar(): void {
|
||||
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.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
|
||||
* 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
|
||||
* @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
|
||||
let posX: number, posY: number
|
||||
const posPoint: Point = { x: 0, y: 0 }
|
||||
|
||||
const { toolbarWidth, toolbarHeight } = this.getToolbarRealSize()
|
||||
|
||||
switch (orientation) {
|
||||
case 'topLeft':
|
||||
posX = point.x - toolbarWidth
|
||||
posY = point.y - toolbarHeight
|
||||
posPoint.x = refPoint.x - toolbarWidth
|
||||
posPoint.y = refPoint.y - toolbarHeight
|
||||
break
|
||||
case 'topRight':
|
||||
posX = point.x
|
||||
posY = point.y - toolbarHeight
|
||||
posPoint.x = refPoint.x
|
||||
posPoint.y = refPoint.y - toolbarHeight
|
||||
break
|
||||
case 'topMiddle':
|
||||
posX = point.x - toolbarWidth / 2
|
||||
posY = point.y - toolbarHeight
|
||||
posPoint.x = refPoint.x - toolbarWidth / 2
|
||||
posPoint.y = refPoint.y - toolbarHeight
|
||||
break
|
||||
case 'bottomLeft':
|
||||
posX = point.x - toolbarWidth
|
||||
posY = point.y
|
||||
posPoint.x = refPoint.x - toolbarWidth
|
||||
posPoint.y = refPoint.y
|
||||
break
|
||||
case 'bottomRight':
|
||||
posX = point.x
|
||||
posY = point.y
|
||||
posPoint.x = refPoint.x
|
||||
posPoint.y = refPoint.y
|
||||
break
|
||||
case 'bottomMiddle':
|
||||
posX = point.x - toolbarWidth / 2
|
||||
posY = point.y
|
||||
posPoint.x = refPoint.x - toolbarWidth / 2
|
||||
posPoint.y = refPoint.y
|
||||
break
|
||||
case 'middleLeft':
|
||||
posX = point.x - toolbarWidth
|
||||
posY = point.y - toolbarHeight / 2
|
||||
posPoint.x = refPoint.x - toolbarWidth
|
||||
posPoint.y = refPoint.y - toolbarHeight / 2
|
||||
break
|
||||
case 'middleRight':
|
||||
posX = point.x
|
||||
posY = point.y - toolbarHeight / 2
|
||||
posPoint.x = refPoint.x
|
||||
posPoint.y = refPoint.y - toolbarHeight / 2
|
||||
break
|
||||
case 'center':
|
||||
posX = point.x - toolbarWidth / 2
|
||||
posY = point.y - toolbarHeight / 2
|
||||
posPoint.x = refPoint.x - toolbarWidth / 2
|
||||
posPoint.y = refPoint.y - toolbarHeight / 2
|
||||
break
|
||||
default:
|
||||
// Default to 'topMiddle' if invalid position
|
||||
posX = point.x - toolbarWidth / 2
|
||||
posY = point.y - toolbarHeight / 2
|
||||
posPoint.x = refPoint.x - toolbarWidth / 2
|
||||
posPoint.y = refPoint.y - toolbarHeight / 2
|
||||
}
|
||||
|
||||
//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
|
||||
posX = Math.round(
|
||||
Math.max(display.workArea.x, Math.min(posX, display.workArea.x + display.workArea.width - toolbarWidth))
|
||||
posPoint.x = Math.round(
|
||||
Math.max(display.workArea.x, Math.min(posPoint.x, display.workArea.x + display.workArea.width - toolbarWidth))
|
||||
)
|
||||
posY = Math.round(
|
||||
Math.max(display.workArea.y, Math.min(posY, display.workArea.y + display.workArea.height - toolbarHeight))
|
||||
posPoint.y = Math.round(
|
||||
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 {
|
||||
@ -773,8 +874,11 @@ export class SelectionService {
|
||||
}
|
||||
|
||||
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
|
||||
refPoint = screen.screenToDipPoint(refPoint)
|
||||
refPoint = { x: Math.round(refPoint.x), y: Math.round(refPoint.y) }
|
||||
}
|
||||
|
||||
@ -832,8 +936,8 @@ export class SelectionService {
|
||||
return
|
||||
}
|
||||
|
||||
//data point is physical coordinates, convert to logical coordinates
|
||||
const mousePoint = screen.screenToDipPoint({ x: data.x, y: data.y })
|
||||
//data point is physical coordinates, convert to logical coordinates(only for windows/linux)
|
||||
const mousePoint = isMac ? { x: data.x, y: data.y } : screen.screenToDipPoint({ x: data.x, y: data.y })
|
||||
|
||||
const bounds = this.toolbarWindow!.getBounds()
|
||||
|
||||
@ -966,7 +1070,8 @@ export class SelectionService {
|
||||
frame: false,
|
||||
transparent: true,
|
||||
autoHideMenuBar: true,
|
||||
titleBarStyle: 'hidden',
|
||||
titleBarStyle: 'hidden', // [macOS]
|
||||
trafficLightPosition: { x: 12, y: 9 }, // [macOS]
|
||||
hasShadow: false,
|
||||
thickFrame: false,
|
||||
show: false,
|
||||
@ -1043,6 +1148,27 @@ export class SelectionService {
|
||||
if (!actionWindow.isDestroyed()) {
|
||||
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
|
||||
@ -1088,22 +1214,26 @@ export class SelectionService {
|
||||
|
||||
//center way
|
||||
if (!this.isFollowToolbar || !this.toolbarWindow) {
|
||||
if (this.isRemeberWinSize) {
|
||||
actionWindow.setBounds({
|
||||
width: actionWindowWidth,
|
||||
height: actionWindowHeight
|
||||
})
|
||||
}
|
||||
const display = screen.getDisplayNearestPoint(screen.getCursorScreenPoint())
|
||||
const workArea = display.workArea
|
||||
|
||||
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()
|
||||
this.hideToolbar()
|
||||
return
|
||||
}
|
||||
|
||||
//follow toolbar
|
||||
|
||||
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 GAP = 6 // 6px gap from screen edges
|
||||
|
||||
@ -1214,7 +1344,7 @@ export class SelectionService {
|
||||
selectionService?.hideToolbar()
|
||||
})
|
||||
|
||||
ipcMain.handle(IpcChannel.Selection_WriteToClipboard, (_, text: string) => {
|
||||
ipcMain.handle(IpcChannel.Selection_WriteToClipboard, (_, text: string): boolean => {
|
||||
return selectionService?.writeToClipboard(text) ?? false
|
||||
})
|
||||
|
||||
@ -1291,7 +1421,7 @@ export class SelectionService {
|
||||
* @returns {boolean} Success status of initialization
|
||||
*/
|
||||
export function initSelectionService(): boolean {
|
||||
if (!isWin) return false
|
||||
if (!isSupportedOS) return false
|
||||
|
||||
configManager.subscribe(ConfigKeys.SelectionAssistantEnabled, (enabled: boolean) => {
|
||||
//avoid closure
|
||||
|
||||
@ -84,10 +84,8 @@ export class TrayService {
|
||||
label: trayLocale.show_mini_window,
|
||||
click: () => windowService.showMiniWindow()
|
||||
},
|
||||
isWin && {
|
||||
(isWin || isMac) && {
|
||||
label: selectionLocale.name + (selectionAssistantEnabled ? ' - On' : ' - Off'),
|
||||
// type: 'checkbox',
|
||||
// checked: selectionAssistantEnabled,
|
||||
click: () => {
|
||||
if (selectionService) {
|
||||
selectionService.toggleEnabled()
|
||||
|
||||
@ -42,6 +42,10 @@ const api = {
|
||||
openWebsite: (url: string) => ipcRenderer.invoke(IpcChannel.Open_Website, url),
|
||||
getCacheSize: () => ipcRenderer.invoke(IpcChannel.App_GetCacheSize),
|
||||
clearCache: () => ipcRenderer.invoke(IpcChannel.App_ClearCache),
|
||||
mac: {
|
||||
isProcessTrusted: (): Promise<boolean> => ipcRenderer.invoke(IpcChannel.App_MacIsProcessTrusted),
|
||||
requestProcessTrust: (): Promise<boolean> => ipcRenderer.invoke(IpcChannel.App_MacRequestProcessTrust)
|
||||
},
|
||||
notification: {
|
||||
send: (notification: Notification) => ipcRenderer.invoke(IpcChannel.Notification_Send, notification)
|
||||
},
|
||||
|
||||
@ -18,25 +18,37 @@ html {
|
||||
|
||||
--selection-toolbar-logo-display: flex; // values: flex | none
|
||||
--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
|
||||
--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-border-radius: 6px;
|
||||
--selection-toolbar-border: 1px solid rgba(55, 55, 55, 0.5);
|
||||
--selection-toolbar-border-radius: 10px;
|
||||
--selection-toolbar-border: none;
|
||||
--selection-toolbar-box-shadow: 0px 2px 3px rgba(50, 50, 50, 0.3);
|
||||
--selection-toolbar-background: rgba(20, 20, 20, 0.95);
|
||||
|
||||
// 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-text-margin: 0 0 0 3px; // default: 0 0 0 3px
|
||||
--selection-toolbar-button-margin: 0 2px; // default: 0 2px
|
||||
--selection-toolbar-button-padding: 4px 6px; // default: 4px 6px
|
||||
--selection-toolbar-button-border-radius: 4px; // default: 4px
|
||||
--selection-toolbar-button-direction: row; // default: row | column
|
||||
--selection-toolbar-button-text-margin: 0 0 0 0; // default: 0 0 0 0
|
||||
--selection-toolbar-button-margin: 0; // default: 0
|
||||
--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-box-shadow: none; // default: none
|
||||
|
||||
@ -45,14 +57,19 @@ html {
|
||||
--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-bgcolor: transparent; // default: transparent
|
||||
--selection-toolbar-button-bgcolor-hover: #222222;
|
||||
--selection-toolbar-button-bgcolor-hover: #333333;
|
||||
}
|
||||
|
||||
[theme-mode='light'] {
|
||||
--selection-toolbar-border: 1px solid rgba(200, 200, 200, 0.5);
|
||||
--selection-toolbar-box-shadow: 0px 2px 3px rgba(50, 50, 50, 0.3);
|
||||
--selection-toolbar-border: none;
|
||||
--selection-toolbar-box-shadow: 0px 2px 3px rgba(50, 50, 50, 0.1);
|
||||
--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-icon-color: var(--selection-toolbar-button-text-color);
|
||||
--selection-toolbar-button-text-color-hover: var(--selection-toolbar-color-primary);
|
||||
|
||||
@ -2034,14 +2034,29 @@
|
||||
"experimental": "Experimental Features",
|
||||
"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": {
|
||||
"title": "Toolbar",
|
||||
"trigger_mode": {
|
||||
"title": "Trigger Mode",
|
||||
"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_note": "Show toolbar immediately when text is selected",
|
||||
"ctrlkey": "Ctrl Key",
|
||||
@ -2166,7 +2181,10 @@
|
||||
},
|
||||
"filter_modal": {
|
||||
"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."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2037,14 +2037,29 @@
|
||||
"experimental": "実験的機能",
|
||||
"enable": {
|
||||
"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": {
|
||||
"title": "ツールバー",
|
||||
"trigger_mode": {
|
||||
"title": "単語の取り出し方",
|
||||
"description": "テキスト選択後、取詞ツールバーを表示する方法",
|
||||
"description_note": "一部のアプリケーションでは、Ctrl キーでテキストを選択できません。AHK などのツールを使用して Ctrl キーを再マップした場合、一部のアプリケーションでテキスト選択が失敗する可能性があります。",
|
||||
"description_note": {
|
||||
"windows": "一部のアプリケーションでは、Ctrl キーでテキストを選択できません。AHK などのツールを使用して Ctrl キーを再マップした場合、一部のアプリケーションでテキスト選択が失敗する可能性があります。",
|
||||
"mac": "一部のアプリケーションでは、⌘ キーでテキストを選択できません。ショートカットキーまたはキーボードマッピングツールを使用して ⌘ キーを再マップした場合、一部のアプリケーションでテキスト選択が失敗する可能性があります。"
|
||||
},
|
||||
"selected": "選択時",
|
||||
"selected_note": "テキスト選択時に即時表示",
|
||||
"ctrlkey": "Ctrlキー",
|
||||
@ -2169,7 +2184,10 @@
|
||||
},
|
||||
"filter_modal": {
|
||||
"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, など。"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2037,14 +2037,29 @@
|
||||
"experimental": "Экспериментальные функции",
|
||||
"enable": {
|
||||
"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": {
|
||||
"title": "Панель инструментов",
|
||||
"trigger_mode": {
|
||||
"title": "Режим активации",
|
||||
"description": "Показывать панель сразу при выделении, или только при удержании Ctrl, или только при нажатии на сочетание клавиш",
|
||||
"description_note": "В некоторых приложениях Ctrl может не работать. Если вы используете AHK или другие инструменты для переназначения Ctrl, это может привести к тому, что некоторые приложения не смогут выделить текст.",
|
||||
"description_note": {
|
||||
"windows": "В некоторых приложениях Ctrl может не работать. Если вы используете AHK или другие инструменты для переназначения Ctrl, это может привести к тому, что некоторые приложения не смогут выделить текст.",
|
||||
"mac": "В некоторых приложениях ⌘ может не работать. Если вы используете сочетания клавиш или инструменты для переназначения ⌘, это может привести к тому, что некоторые приложения не смогут выделить текст."
|
||||
},
|
||||
"selected": "При выделении",
|
||||
"selected_note": "После выделения",
|
||||
"ctrlkey": "По Ctrl",
|
||||
@ -2169,7 +2184,10 @@
|
||||
},
|
||||
"filter_modal": {
|
||||
"title": "Список фильтрации",
|
||||
"user_tips": "Введите имя исполняемого файла приложения, один на строку, не учитывая регистр, можно использовать подстановку *"
|
||||
"user_tips": {
|
||||
"windows": "Введите имя исполняемого файла приложения, один на строку, не учитывая регистр, можно использовать подстановку *",
|
||||
"mac": "Введите Bundle ID приложения, один на строку, не учитывая регистр, можно использовать подстановку *"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2037,14 +2037,29 @@
|
||||
"experimental": "实验性功能",
|
||||
"enable": {
|
||||
"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": {
|
||||
"title": "工具栏",
|
||||
"trigger_mode": {
|
||||
"title": "取词方式",
|
||||
"description": "划词后,触发取词并显示工具栏的方式",
|
||||
"description_note": "少数应用不支持通过 Ctrl 键划词。若使用了 AHK 等工具对 Ctrl 键进行了重映射,可能导致部分应用无法划词。",
|
||||
"description_note": {
|
||||
"windows": "少数应用不支持通过 Ctrl 键划词。若使用了AHK等按键映射工具对 Ctrl 键进行了重映射,可能导致部分应用无法划词。",
|
||||
"mac": "若使用了快捷键或键盘映射工具对 ⌘ 键进行了重映射,可能导致部分应用无法划词。"
|
||||
},
|
||||
"selected": "划词",
|
||||
"selected_note": "划词后立即显示工具栏",
|
||||
"ctrlkey": "Ctrl 键",
|
||||
@ -2169,7 +2184,10 @@
|
||||
},
|
||||
"filter_modal": {
|
||||
"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等"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2037,14 +2037,29 @@
|
||||
"experimental": "實驗性功能",
|
||||
"enable": {
|
||||
"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": {
|
||||
"title": "工具列",
|
||||
"trigger_mode": {
|
||||
"title": "取詞方式",
|
||||
"description": "劃詞後,觸發取詞並顯示工具列的方式",
|
||||
"description_note": "在某些應用中可能無法透過 Ctrl 鍵劃詞。若使用了 AHK 等工具對 Ctrl 鍵進行了重新對應,可能導致部分應用程式無法劃詞。",
|
||||
"description_note": {
|
||||
"windows": "在某些應用中可能無法透過 Ctrl 鍵劃詞。若使用了 AHK 等工具對 Ctrl 鍵進行了重新對應,可能導致部分應用程式無法劃詞。",
|
||||
"mac": "若使用了快捷鍵或鍵盤映射工具對 ⌘ 鍵進行了重新對應,可能導致部分應用程式無法劃詞。"
|
||||
},
|
||||
"selected": "劃詞",
|
||||
"selected_note": "劃詞後,立即顯示工具列",
|
||||
"ctrlkey": "Ctrl 鍵",
|
||||
@ -2169,7 +2184,10 @@
|
||||
},
|
||||
"filter_modal": {
|
||||
"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等"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { isWin } from '@renderer/config/constant'
|
||||
import { isMac, isWin } from '@renderer/config/constant'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { useSelectionAssistant } from '@renderer/hooks/useSelectionAssistant'
|
||||
import { FilterMode, TriggerMode } from '@renderer/types/selectionTypes'
|
||||
@ -19,8 +19,9 @@ import {
|
||||
SettingRowTitle,
|
||||
SettingTitle
|
||||
} from '..'
|
||||
import SelectionActionsList from './SelectionActionsList'
|
||||
import SelectionFilterListModal from './SelectionFilterListModal'
|
||||
import MacProcessTrustHintModal from './components/MacProcessTrustHintModal'
|
||||
import SelectionActionsList from './components/SelectionActionsList'
|
||||
import SelectionFilterListModal from './components/SelectionFilterListModal'
|
||||
|
||||
const SelectionAssistantSettings: FC = () => {
|
||||
const { theme } = useTheme()
|
||||
@ -49,15 +50,43 @@ const SelectionAssistantSettings: FC = () => {
|
||||
setFilterMode,
|
||||
setFilterList
|
||||
} = useSelectionAssistant()
|
||||
|
||||
const isSupportedOS = isWin || isMac
|
||||
|
||||
const [isFilterListModalOpen, setIsFilterListModalOpen] = useState(false)
|
||||
const [isMacTrustModalOpen, setIsMacTrustModalOpen] = useState(false)
|
||||
const [opacityValue, setOpacityValue] = useState(actionWindowOpacity)
|
||||
|
||||
// force disable selection assistant on non-windows systems
|
||||
useEffect(() => {
|
||||
if (!isWin && selectionEnabled) {
|
||||
setSelectionEnabled(false)
|
||||
const checkMacProcessTrust = async () => {
|
||||
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 (
|
||||
<SettingContainer theme={theme}>
|
||||
@ -71,18 +100,18 @@ const SelectionAssistantSettings: FC = () => {
|
||||
style={{ fontSize: 12 }}>
|
||||
{'FAQ & ' + t('settings.about.feedback.button')}
|
||||
</Button>
|
||||
<ExperimentalText>{t('selection.settings.experimental')}</ExperimentalText>
|
||||
{isMac && <ExperimentalText>{t('selection.settings.experimental')}</ExperimentalText>}
|
||||
</Row>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingLabel>
|
||||
<SettingRowTitle>{t('selection.settings.enable.title')}</SettingRowTitle>
|
||||
{!isWin && <SettingDescription>{t('selection.settings.enable.description')}</SettingDescription>}
|
||||
{!isSupportedOS && <SettingDescription>{t('selection.settings.enable.description')}</SettingDescription>}
|
||||
</SettingLabel>
|
||||
<Switch
|
||||
checked={isWin && selectionEnabled}
|
||||
onChange={(checked) => setSelectionEnabled(checked)}
|
||||
disabled={!isWin}
|
||||
checked={isSupportedOS && selectionEnabled}
|
||||
onChange={(checked) => handleEnableCheckboxChange(checked)}
|
||||
disabled={!isSupportedOS}
|
||||
/>
|
||||
</SettingRow>
|
||||
|
||||
@ -103,7 +132,10 @@ const SelectionAssistantSettings: FC = () => {
|
||||
<SettingLabel>
|
||||
<SettingRowTitle>
|
||||
<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} />
|
||||
</Tooltip>
|
||||
</SettingRowTitle>
|
||||
@ -116,9 +148,11 @@ const SelectionAssistantSettings: FC = () => {
|
||||
<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>
|
||||
</Tooltip>
|
||||
<Tooltip placement="top" title={t('selection.settings.toolbar.trigger_mode.ctrlkey_note')} arrow>
|
||||
<Radio.Button value="ctrlkey">{t('selection.settings.toolbar.trigger_mode.ctrlkey')}</Radio.Button>
|
||||
</Tooltip>
|
||||
{isWin && (
|
||||
<Tooltip placement="top" title={t('selection.settings.toolbar.trigger_mode.ctrlkey_note')} arrow>
|
||||
<Radio.Button value="ctrlkey">{t('selection.settings.toolbar.trigger_mode.ctrlkey')}</Radio.Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip
|
||||
placement="topRight"
|
||||
title={
|
||||
@ -256,6 +290,8 @@ const SelectionAssistantSettings: FC = () => {
|
||||
</SettingGroup>
|
||||
</>
|
||||
)}
|
||||
|
||||
{isMac && <MacProcessTrustHintModal open={isMacTrustModalOpen} onClose={() => setIsMacTrustModalOpen(false)} />}
|
||||
</SettingContainer>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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
|
||||
@ -6,13 +6,13 @@ import { Row } from 'antd'
|
||||
import { FC } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { SettingDivider, SettingGroup } from '..'
|
||||
import ActionsList from './components/ActionsList'
|
||||
import ActionsListDivider from './components/ActionsListDivider'
|
||||
import SettingsActionsListHeader from './components/SettingsActionsListHeader'
|
||||
import { useActionItems } from './hooks/useSettingsActionsList'
|
||||
import { SettingDivider, SettingGroup } from '../..'
|
||||
import { useActionItems } from '../hooks/useSettingsActionsList'
|
||||
import ActionsList from './ActionsList'
|
||||
import ActionsListDivider from './ActionsListDivider'
|
||||
import SelectionActionSearchModal from './SelectionActionSearchModal'
|
||||
import SelectionActionUserModal from './SelectionActionUserModal'
|
||||
import SettingsActionsListHeader from './SettingsActionsListHeader'
|
||||
|
||||
// Component for managing selection actions in settings
|
||||
// Handles drag-and-drop reordering, enabling/disabling actions, and custom action management
|
||||
@ -1,3 +1,4 @@
|
||||
import { isWin } from '@renderer/config/constant'
|
||||
import { Button, Form, Input, Modal } from 'antd'
|
||||
import { FC, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@ -54,7 +55,11 @@ const SelectionFilterListModal: FC<SelectionFilterListModalProps> = ({ open, onC
|
||||
{t('common.save')}
|
||||
</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.Item name="filterList" noStyle>
|
||||
<StyledTextArea autoSize={{ minRows: 6, maxRows: 16 }} spellCheck={false} autoFocus />
|
||||
@ -4,7 +4,7 @@ import type { ActionItem } from '@renderer/types/selectionTypes'
|
||||
import { useMemo, useState } from 'react'
|
||||
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_ENABLED_ITEMS = 6
|
||||
|
||||
@ -24,7 +24,7 @@ const ShortcutSettings: FC = () => {
|
||||
|
||||
//if shortcut is not available on all the platforms, block the shortcut here
|
||||
let shortcuts = originalShortcuts
|
||||
if (!isWin) {
|
||||
if (!isWin && !isMac) {
|
||||
//Selection Assistant only available on Windows now
|
||||
const excludedShortcuts = ['selection_assistant_toggle', 'selection_assistant_select_text']
|
||||
shortcuts = shortcuts.filter((s) => !excludedShortcuts.includes(s.key))
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { isMac } from '@renderer/config/constant'
|
||||
import { useSelectionAssistant } from '@renderer/hooks/useSelectionAssistant'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import i18n from '@renderer/i18n'
|
||||
@ -182,7 +183,7 @@ const SelectionActionApp: FC = () => {
|
||||
|
||||
return (
|
||||
<WindowFrame $opacity={opacity / 100}>
|
||||
<TitleBar $isWindowFocus={isWindowFocus}>
|
||||
<TitleBar $isWindowFocus={isWindowFocus} style={isMac ? { paddingLeft: '70px' } : {}}>
|
||||
{action.icon && (
|
||||
<TitleBarIcon>
|
||||
<DynamicIcon
|
||||
@ -230,9 +231,12 @@ const SelectionActionApp: FC = () => {
|
||||
/>
|
||||
</OpacitySlider>
|
||||
)}
|
||||
|
||||
<WinButton type="text" icon={<Minus size={16} />} onClick={handleMinimize} />
|
||||
<WinButton type="text" icon={<X size={16} />} onClick={handleClose} className="close" />
|
||||
{!isMac && (
|
||||
<>
|
||||
<WinButton type="text" icon={<Minus size={16} />} onClick={handleMinimize} />
|
||||
<WinButton type="text" icon={<X size={16} />} onClick={handleClose} className="close" />
|
||||
</>
|
||||
)}
|
||||
</TitleBarButtons>
|
||||
</TitleBar>
|
||||
<MainContainer>
|
||||
|
||||
@ -259,7 +259,7 @@ const SelectionToolbar: FC<{ demo?: boolean }> = ({ demo = false }) => {
|
||||
const Container = styled.div`
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
align-items: stretch;
|
||||
height: var(--selection-toolbar-height);
|
||||
border-radius: var(--selection-toolbar-border-radius);
|
||||
border: var(--selection-toolbar-border);
|
||||
@ -269,6 +269,7 @@ const Container = styled.div`
|
||||
margin: var(--selection-toolbar-margin) !important;
|
||||
user-select: none;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
`
|
||||
|
||||
const LogoWrapper = styled.div<{ $draggable: boolean }>`
|
||||
@ -276,8 +277,13 @@ const LogoWrapper = styled.div<{ $draggable: boolean }>`
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: var(--selection-toolbar-logo-margin);
|
||||
background-color: transparent;
|
||||
${({ $draggable }) => $draggable && ' -webkit-app-region: drag;'}
|
||||
padding: var(--selection-toolbar-logo-padding);
|
||||
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)`
|
||||
@ -307,14 +313,19 @@ const ActionWrapper = styled.div`
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-left: 3px;
|
||||
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`
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 2px;
|
||||
cursor: pointer !important;
|
||||
margin: var(--selection-toolbar-button-margin);
|
||||
padding: var(--selection-toolbar-button-padding);
|
||||
@ -324,6 +335,10 @@ const ActionButton = styled.div`
|
||||
box-shadow: var(--selection-toolbar-button-box-shadow);
|
||||
transition: all 0.1s ease-in-out;
|
||||
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 {
|
||||
width: var(--selection-toolbar-button-icon-size);
|
||||
@ -337,6 +352,7 @@ const ActionButton = styled.div`
|
||||
color: var(--selection-toolbar-button-text-color);
|
||||
transition: color 0.1s ease-in-out;
|
||||
will-change: color;
|
||||
line-height: 1.1;
|
||||
}
|
||||
&:hover {
|
||||
.btn-icon {
|
||||
|
||||
20
yarn.lock
20
yarn.lock
@ -5793,7 +5793,7 @@ __metadata:
|
||||
remove-markdown: "npm:^0.6.2"
|
||||
rollup-plugin-visualizer: "npm:^5.12.0"
|
||||
sass: "npm:^1.88.0"
|
||||
selection-hook: "npm:^0.9.23"
|
||||
selection-hook: "npm:^1.0.3"
|
||||
shiki: "npm:^3.7.0"
|
||||
string-width: "npm:^7.2.0"
|
||||
styled-components: "npm:^6.1.11"
|
||||
@ -13955,6 +13955,15 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 0.2.1
|
||||
resolution: "node-api-version@npm:0.2.1"
|
||||
@ -16714,13 +16723,14 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"selection-hook@npm:^0.9.23":
|
||||
version: 0.9.23
|
||||
resolution: "selection-hook@npm:0.9.23"
|
||||
"selection-hook@npm:^1.0.3":
|
||||
version: 1.0.3
|
||||
resolution: "selection-hook@npm:1.0.3"
|
||||
dependencies:
|
||||
node-addon-api: "npm:^8.4.0"
|
||||
node-gyp: "npm:latest"
|
||||
node-gyp-build: "npm:^4.8.4"
|
||||
checksum: 10c0/3b91193814c063e14dd788cff3b27020821bbeae24eab106d2ce5bf600c034c1b3db96ce573c456b74d0553346dfcf4c7cc8d49386a22797b42667f7ed3eee01
|
||||
checksum: 10c0/812df47050d470d398974ca9833caba3bc55fcacf76aec5207cffcef4b81cf22d5a0992263e2074afd05c21781f903ffac25177cd9553417525648136405b474
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user