From 2f016efc50b9138a563d6e695e2df9c7fe0360a9 Mon Sep 17 00:00:00 2001 From: fullex <106392080+0xfullex@users.noreply.github.com> Date: Thu, 3 Jul 2025 14:31:31 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20SelectionAssistant=20macOS=20version=20?= =?UTF-8?q?/=20=E5=88=92=E8=AF=8D=E5=8A=A9=E6=89=8BmacOS=E7=89=88=20(#7561?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 --- electron-builder.yml | 4 +- package.json | 2 +- packages/shared/IpcChannel.ts | 3 + src/main/configs/SelectionConfig.ts | 11 +- src/main/ipc.ts | 14 +- src/main/services/SelectionService.ts | 256 +++++++++++++----- src/main/services/TrayService.ts | 4 +- src/preload/index.ts | 4 + .../src/assets/styles/selection-toolbar.scss | 39 ++- src/renderer/src/i18n/locales/en-us.json | 24 +- src/renderer/src/i18n/locales/ja-jp.json | 24 +- src/renderer/src/i18n/locales/ru-ru.json | 24 +- src/renderer/src/i18n/locales/zh-cn.json | 24 +- src/renderer/src/i18n/locales/zh-tw.json | 24 +- .../SelectionAssistantSettings.tsx | 66 ++++- .../components/MacProcessTrustHintModal.tsx | 68 +++++ .../SelectionActionSearchModal.tsx | 0 .../SelectionActionUserModal.tsx | 0 .../{ => components}/SelectionActionsList.tsx | 10 +- .../SelectionFilterListModal.tsx | 7 +- .../hooks/useSettingsActionsList.ts | 2 +- .../src/pages/settings/ShortcutSettings.tsx | 2 +- .../selection/action/SelectionActionApp.tsx | 12 +- .../selection/toolbar/SelectionToolbar.tsx | 24 +- yarn.lock | 20 +- 25 files changed, 533 insertions(+), 135 deletions(-) create mode 100644 src/renderer/src/pages/settings/SelectionAssistantSettings/components/MacProcessTrustHintModal.tsx rename src/renderer/src/pages/settings/SelectionAssistantSettings/{ => components}/SelectionActionSearchModal.tsx (100%) rename src/renderer/src/pages/settings/SelectionAssistantSettings/{ => components}/SelectionActionUserModal.tsx (100%) rename src/renderer/src/pages/settings/SelectionAssistantSettings/{ => components}/SelectionActionsList.tsx (91%) rename src/renderer/src/pages/settings/SelectionAssistantSettings/{ => components}/SelectionFilterListModal.tsx (89%) diff --git a/electron-builder.yml b/electron-builder.yml index c65f20ed32..1303a4a3c8 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -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}' diff --git a/package.json b/package.json index 69bf4268c3..35a85bf162 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/packages/shared/IpcChannel.ts b/packages/shared/IpcChannel.ts index daea5dad6e..8118065278 100644 --- a/packages/shared/IpcChannel.ts +++ b/packages/shared/IpcChannel.ts @@ -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', diff --git a/src/main/configs/SelectionConfig.ts b/src/main/configs/SelectionConfig.ts index 59988ded74..31868a4708 100644 --- a/src/main/configs/SelectionConfig.ts +++ b/src/main/configs/SelectionConfig.ts @@ -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: [] } } diff --git a/src/main/ipc.ts b/src/main/ipc.ts index 87aad8d936..0176a27525 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -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) }) diff --git a/src/main/services/SelectionService.ts b/src/main/services/SelectionService.ts index eba97179bc..23578b75e0 100644 --- a/src/main/services/SelectionService.ts +++ b/src/main/services/SelectionService.ts @@ -1,7 +1,7 @@ import { SELECTION_FINETUNED_LIST, SELECTION_PREDEFINED_BLACKLIST } from '@main/configs/SelectionConfig' -import { isDev, 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 diff --git a/src/main/services/TrayService.ts b/src/main/services/TrayService.ts index 89c88bc0ae..205d7fdee9 100644 --- a/src/main/services/TrayService.ts +++ b/src/main/services/TrayService.ts @@ -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() diff --git a/src/preload/index.ts b/src/preload/index.ts index 8412e00bc3..beabfa1a27 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -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 => ipcRenderer.invoke(IpcChannel.App_MacIsProcessTrusted), + requestProcessTrust: (): Promise => ipcRenderer.invoke(IpcChannel.App_MacRequestProcessTrust) + }, notification: { send: (notification: Notification) => ipcRenderer.invoke(IpcChannel.Notification_Send, notification) }, diff --git a/src/renderer/src/assets/styles/selection-toolbar.scss b/src/renderer/src/assets/styles/selection-toolbar.scss index bfe329c696..23f0edfb34 100644 --- a/src/renderer/src/assets/styles/selection-toolbar.scss +++ b/src/renderer/src/assets/styles/selection-toolbar.scss @@ -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); diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 0a529756b2..4511878b84 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -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 Accessibility Permission to work properly.", + "Please click \"Go to Settings\" and click the \"Open System Settings\" button in the permission request popup that appears later. Then find \"Cherry Studio\" 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." + } } } } diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index 30f5f2fb0a..2082cf0c27 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -2037,14 +2037,29 @@ "experimental": "実験的機能", "enable": { "title": "有効化", - "description": "現在Windowsのみ対応" + "description": "現在Windows & macOSのみ対応", + "mac_process_trust_hint": { + "title": "アクセシビリティー権限", + "description": [ + "テキスト選択ツールは、アクセシビリティー権限が必要です。", + "「設定に移動」をクリックし、後で表示される権限要求ポップアップで「システム設定を開く」ボタンをクリックします。その後、表示されるアプリケーションリストで「Cherry Studio」を見つけ、権限スイッチをオンにしてください。", + "設定が完了したら、テキスト選択ツールを再起動してください。" + ], + "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, など。" + } } } } diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index b850bf3f22..43b53f4a4d 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -2037,14 +2037,29 @@ "experimental": "Экспериментальные функции", "enable": { "title": "Включить", - "description": "Поддерживается только в Windows" + "description": "Поддерживается только в Windows & macOS", + "mac_process_trust_hint": { + "title": "Права доступа", + "description": [ + "Помощник выбора требует Права доступа для правильной работы.", + "Пожалуйста, перейдите в \"Настройки\" и нажмите \"Открыть системные настройки\" в запросе разрешения, который появится позже. Затем найдите \"Cherry Studio\" в списке приложений, который появится позже, и включите переключатель разрешения.", + "После завершения настроек, пожалуйста, перезапустите помощник выбора." + ], + "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 приложения, один на строку, не учитывая регистр, можно использовать подстановку *" + } } } } diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 2b2176a457..2dd0422004 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -2037,14 +2037,29 @@ "experimental": "实验性功能", "enable": { "title": "启用", - "description": "当前仅支持 Windows 系统" + "description": "当前仅支持 Windows & macOS", + "mac_process_trust_hint": { + "title": "辅助功能权限", + "description": [ + "划词助手需「辅助功能权限」才能正常工作。", + "请点击「去设置」,并在稍后弹出的权限请求弹窗中点击 「打开系统设置」 按钮,然后在之后的应用列表中找到 「Cherry Studio」,并打开权限开关。", + "完成设置后,请再次开启划词助手。" + ], + "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等" + } } } } diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 8640d46428..12addbba02 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -2037,14 +2037,29 @@ "experimental": "實驗性功能", "enable": { "title": "啟用", - "description": "目前僅支援 Windows 系統" + "description": "目前僅支援 Windows & macOS", + "mac_process_trust_hint": { + "title": "輔助使用權限", + "description": [ + "劃詞助手需「輔助使用權限」才能正常工作。", + "請點擊「去設定」,並在稍後彈出的權限請求彈窗中點擊 「打開系統設定」 按鈕,然後在之後的應用程式列表中找到 「Cherry Studio」,並開啟權限開關。", + "完成設定後,請再次開啟劃詞助手。" + ], + "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等" + } } } } diff --git a/src/renderer/src/pages/settings/SelectionAssistantSettings/SelectionAssistantSettings.tsx b/src/renderer/src/pages/settings/SelectionAssistantSettings/SelectionAssistantSettings.tsx index 0bbebf57e9..1707b10dd7 100644 --- a/src/renderer/src/pages/settings/SelectionAssistantSettings/SelectionAssistantSettings.tsx +++ b/src/renderer/src/pages/settings/SelectionAssistantSettings/SelectionAssistantSettings.tsx @@ -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 ( @@ -71,18 +100,18 @@ const SelectionAssistantSettings: FC = () => { style={{ fontSize: 12 }}> {'FAQ & ' + t('settings.about.feedback.button')} - {t('selection.settings.experimental')} + {isMac && {t('selection.settings.experimental')}} {t('selection.settings.enable.title')} - {!isWin && {t('selection.settings.enable.description')}} + {!isSupportedOS && {t('selection.settings.enable.description')}} setSelectionEnabled(checked)} - disabled={!isWin} + checked={isSupportedOS && selectionEnabled} + onChange={(checked) => handleEnableCheckboxChange(checked)} + disabled={!isSupportedOS} /> @@ -103,7 +132,10 @@ const SelectionAssistantSettings: FC = () => {
{t('selection.settings.toolbar.trigger_mode.title')}
- +
@@ -116,9 +148,11 @@ const SelectionAssistantSettings: FC = () => { {t('selection.settings.toolbar.trigger_mode.selected')} - - {t('selection.settings.toolbar.trigger_mode.ctrlkey')} - + {isWin && ( + + {t('selection.settings.toolbar.trigger_mode.ctrlkey')} + + )} { )} + + {isMac && setIsMacTrustModalOpen(false)} />}
) } diff --git a/src/renderer/src/pages/settings/SelectionAssistantSettings/components/MacProcessTrustHintModal.tsx b/src/renderer/src/pages/settings/SelectionAssistantSettings/components/MacProcessTrustHintModal.tsx new file mode 100644 index 0000000000..d3e1926552 --- /dev/null +++ b/src/renderer/src/pages/settings/SelectionAssistantSettings/components/MacProcessTrustHintModal.tsx @@ -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 = ({ 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 ( + + + + + } + centered + destroyOnClose> + + + + + + + + + + + + + + + + + + + ) +} + +const ContentContainer = styled.div` + padding: 16px 0; +` + +export default MacProcessTrustHintModal diff --git a/src/renderer/src/pages/settings/SelectionAssistantSettings/SelectionActionSearchModal.tsx b/src/renderer/src/pages/settings/SelectionAssistantSettings/components/SelectionActionSearchModal.tsx similarity index 100% rename from src/renderer/src/pages/settings/SelectionAssistantSettings/SelectionActionSearchModal.tsx rename to src/renderer/src/pages/settings/SelectionAssistantSettings/components/SelectionActionSearchModal.tsx diff --git a/src/renderer/src/pages/settings/SelectionAssistantSettings/SelectionActionUserModal.tsx b/src/renderer/src/pages/settings/SelectionAssistantSettings/components/SelectionActionUserModal.tsx similarity index 100% rename from src/renderer/src/pages/settings/SelectionAssistantSettings/SelectionActionUserModal.tsx rename to src/renderer/src/pages/settings/SelectionAssistantSettings/components/SelectionActionUserModal.tsx diff --git a/src/renderer/src/pages/settings/SelectionAssistantSettings/SelectionActionsList.tsx b/src/renderer/src/pages/settings/SelectionAssistantSettings/components/SelectionActionsList.tsx similarity index 91% rename from src/renderer/src/pages/settings/SelectionAssistantSettings/SelectionActionsList.tsx rename to src/renderer/src/pages/settings/SelectionAssistantSettings/components/SelectionActionsList.tsx index 91106f762d..7077de2f4e 100644 --- a/src/renderer/src/pages/settings/SelectionAssistantSettings/SelectionActionsList.tsx +++ b/src/renderer/src/pages/settings/SelectionAssistantSettings/components/SelectionActionsList.tsx @@ -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 diff --git a/src/renderer/src/pages/settings/SelectionAssistantSettings/SelectionFilterListModal.tsx b/src/renderer/src/pages/settings/SelectionAssistantSettings/components/SelectionFilterListModal.tsx similarity index 89% rename from src/renderer/src/pages/settings/SelectionAssistantSettings/SelectionFilterListModal.tsx rename to src/renderer/src/pages/settings/SelectionAssistantSettings/components/SelectionFilterListModal.tsx index e3076b408b..2b98377a89 100644 --- a/src/renderer/src/pages/settings/SelectionAssistantSettings/SelectionFilterListModal.tsx +++ b/src/renderer/src/pages/settings/SelectionAssistantSettings/components/SelectionFilterListModal.tsx @@ -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 = ({ open, onC {t('common.save')} ]}> - {t('selection.settings.filter_modal.user_tips')} + + {isWin + ? t('selection.settings.filter_modal.user_tips.windows') + : t('selection.settings.filter_modal.user_tips.mac')} +
diff --git a/src/renderer/src/pages/settings/SelectionAssistantSettings/hooks/useSettingsActionsList.ts b/src/renderer/src/pages/settings/SelectionAssistantSettings/hooks/useSettingsActionsList.ts index 12d6ba0d37..ee416ad404 100644 --- a/src/renderer/src/pages/settings/SelectionAssistantSettings/hooks/useSettingsActionsList.ts +++ b/src/renderer/src/pages/settings/SelectionAssistantSettings/hooks/useSettingsActionsList.ts @@ -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 diff --git a/src/renderer/src/pages/settings/ShortcutSettings.tsx b/src/renderer/src/pages/settings/ShortcutSettings.tsx index 9c8eec0653..909790f95a 100644 --- a/src/renderer/src/pages/settings/ShortcutSettings.tsx +++ b/src/renderer/src/pages/settings/ShortcutSettings.tsx @@ -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)) diff --git a/src/renderer/src/windows/selection/action/SelectionActionApp.tsx b/src/renderer/src/windows/selection/action/SelectionActionApp.tsx index 57c2b51902..94c1c575ea 100644 --- a/src/renderer/src/windows/selection/action/SelectionActionApp.tsx +++ b/src/renderer/src/windows/selection/action/SelectionActionApp.tsx @@ -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 ( - + {action.icon && ( { /> )} - - } onClick={handleMinimize} /> - } onClick={handleClose} className="close" /> + {!isMac && ( + <> + } onClick={handleMinimize} /> + } onClick={handleClose} className="close" /> + + )} diff --git a/src/renderer/src/windows/selection/toolbar/SelectionToolbar.tsx b/src/renderer/src/windows/selection/toolbar/SelectionToolbar.tsx index 00f38e01b8..49b3c2fcf9 100644 --- a/src/renderer/src/windows/selection/toolbar/SelectionToolbar.tsx +++ b/src/renderer/src/windows/selection/toolbar/SelectionToolbar.tsx @@ -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 { diff --git a/yarn.lock b/yarn.lock index cc0fd6b62a..dce63d4d20 100644 --- a/yarn.lock +++ b/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