feat(SelectionAssistant): shortcut key to toggle on/off (#6983)

* feat: add toggle selection assistant functionality and corresponding shortcuts

- Implemented toggleEnabled method in SelectionService to manage the selection assistant state.
- Registered new shortcut for toggling the selection assistant in ShortcutService.
- Updated StoreSyncService to sync the selection assistant state across renderer windows.
- Added localization for the toggle selection assistant feature in multiple languages.
- Adjusted ShortcutSettings to conditionally display the toggle selection assistant shortcut based on the platform.
- Included toggle selection assistant in the initial state of shortcuts in the store.

* fix: shortcut key

* fix: accelerator name
This commit is contained in:
fullex 2025-06-11 13:32:49 +08:00 committed by GitHub
parent 26d018b1b7
commit f6a935f14f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 108 additions and 7 deletions

View File

@ -14,6 +14,7 @@ import type {
import type { ActionItem } from '../../renderer/src/types/selectionTypes'
import { ConfigKeys, configManager } from './ConfigManager'
import storeSyncService from './StoreSyncService'
let SelectionHook: SelectionHookConstructor | null = null
try {
@ -334,6 +335,20 @@ export class SelectionService {
this.logInfo('SelectionService Quitted')
}
/**
* Toggle the enabled state of the selection service
* Will sync the new enabled store to all renderer windows
*/
public toggleEnabled(enabled: boolean | undefined = undefined) {
if (!this.selectionHook) return
const newEnabled = enabled === undefined ? !configManager.getSelectionAssistantEnabled() : enabled
configManager.setSelectionAssistantEnabled(newEnabled)
//sync the new enabled state to all renderer windows
storeSyncService.syncToRenderer('selectionStore/setSelectionEnabled', newEnabled)
}
/**
* Create and configure the toolbar window
* Sets up window properties, event handlers, and loads the toolbar UI

View File

@ -4,10 +4,15 @@ import { BrowserWindow, globalShortcut } from 'electron'
import Logger from 'electron-log'
import { configManager } from './ConfigManager'
import selectionService from './SelectionService'
import { windowService } from './WindowService'
let showAppAccelerator: string | null = null
let showMiniWindowAccelerator: string | null = null
let selectionAssistantToggleAccelerator: string | null = null
//indicate if the shortcuts are registered on app boot time
let isRegisterOnBoot = true
// store the focus and blur handlers for each window to unregister them later
const windowOnHandlers = new Map<BrowserWindow, { onFocusHandler: () => void; onBlurHandler: () => void }>()
@ -28,6 +33,12 @@ function getShortcutHandler(shortcut: Shortcut) {
return () => {
windowService.toggleMiniWindow()
}
case 'selection_assistant_toggle':
return () => {
if (selectionService) {
selectionService.toggleEnabled()
}
}
default:
return null
}
@ -93,11 +104,14 @@ const convertShortcutRecordedByKeyboardEventKeyValueToElectronGlobalShortcutForm
}
export function registerShortcuts(window: BrowserWindow) {
window.once('ready-to-show', () => {
if (configManager.getLaunchToTray()) {
registerOnlyUniversalShortcuts()
}
})
if (isRegisterOnBoot) {
window.once('ready-to-show', () => {
if (configManager.getLaunchToTray()) {
registerOnlyUniversalShortcuts()
}
})
isRegisterOnBoot = false
}
//only for clearer code
const registerOnlyUniversalShortcuts = () => {
@ -124,7 +138,10 @@ export function registerShortcuts(window: BrowserWindow) {
}
// only register universal shortcuts when needed
if (onlyUniversalShortcuts && !['show_app', 'mini_window'].includes(shortcut.key)) {
if (
onlyUniversalShortcuts &&
!['show_app', 'mini_window', 'selection_assistant_toggle'].includes(shortcut.key)
) {
return
}
@ -146,6 +163,10 @@ export function registerShortcuts(window: BrowserWindow) {
showMiniWindowAccelerator = formatShortcutKey(shortcut.shortcut)
break
case 'selection_assistant_toggle':
selectionAssistantToggleAccelerator = formatShortcutKey(shortcut.shortcut)
break
//the following ZOOMs will register shortcuts seperately, so will return
case 'zoom_in':
globalShortcut.register('CommandOrControl+=', () => handler(window))
@ -192,6 +213,14 @@ export function registerShortcuts(window: BrowserWindow) {
convertShortcutRecordedByKeyboardEventKeyValueToElectronGlobalShortcutFormat(showMiniWindowAccelerator)
handler && globalShortcut.register(accelerator, () => handler(window))
}
if (selectionAssistantToggleAccelerator) {
const handler = getShortcutHandler({ key: 'selection_assistant_toggle' } as Shortcut)
const accelerator = convertShortcutRecordedByKeyboardEventKeyValueToElectronGlobalShortcutFormat(
selectionAssistantToggleAccelerator
)
handler && globalShortcut.register(accelerator, () => handler(window))
}
} catch (error) {
Logger.error('[ShortcutService] Failed to unregister shortcuts')
}
@ -217,6 +246,7 @@ export function unregisterAllShortcuts() {
try {
showAppAccelerator = null
showMiniWindowAccelerator = null
selectionAssistantToggleAccelerator = null
windowOnHandlers.forEach((handlers, window) => {
window.off('focus', handlers.onFocusHandler)
window.off('blur', handlers.onBlurHandler)

View File

@ -49,6 +49,23 @@ export class StoreSyncService {
this.windowIds = this.windowIds.filter((id) => id !== windowId)
}
/**
* Sync an action to all renderer windows
* @param type Action type, like 'settings/setTray'
* @param payload Action payload
*
* NOTICE: DO NOT use directly in ConfigManager, may cause infinite sync loop
*/
public syncToRenderer(type: string, payload: any): void {
const action: StoreSyncAction = {
type,
payload
}
//-1 means the action is from the main process, will be broadcast to all windows
this.broadcastToOtherWindows(-1, action)
}
/**
* Register IPC handlers for store sync communication
* Handles window subscription, unsubscription and action broadcasting

View File

@ -1701,6 +1701,7 @@
"exit_fullscreen": "Exit Fullscreen",
"key": "Key",
"mini_window": "Quick Assistant",
"selection_assistant_toggle": "Toggle Selection Assistant",
"new_topic": "New Topic",
"press_shortcut": "Press Shortcut",
"reset_defaults": "Reset Defaults",

View File

@ -1689,6 +1689,7 @@
"exit_fullscreen": "フルスクリーンを終了",
"key": "キー",
"mini_window": "クイックアシスタント",
"selection_assistant_toggle": "選択アシスタントを切り替え",
"new_topic": "新しいトピック",
"press_shortcut": "ショートカットを押す",
"reset_defaults": "デフォルトのショートカットをリセット",

View File

@ -1689,6 +1689,7 @@
"exit_fullscreen": "Выйти из полноэкранного режима",
"key": "Клавиша",
"mini_window": "Быстрый помощник",
"selection_assistant_toggle": "Переключить помощник выделения",
"new_topic": "Новый топик",
"press_shortcut": "Нажмите сочетание клавиш",
"reset_defaults": "Сбросить настройки по умолчанию",

View File

@ -1701,6 +1701,7 @@
"exit_fullscreen": "退出全屏",
"key": "按键",
"mini_window": "快捷助手",
"selection_assistant_toggle": "开关划词助手",
"new_topic": "新建话题",
"press_shortcut": "按下快捷键",
"reset_defaults": "重置默认快捷键",

View File

@ -1691,6 +1691,7 @@
"copy_last_message": "複製上一則訊息",
"key": "按鍵",
"mini_window": "快捷助手",
"selection_assistant_toggle": "開關劃詞助手",
"new_topic": "新增話題",
"press_shortcut": "按下快捷鍵",
"reset_defaults": "重設預設快捷鍵",

View File

@ -18,10 +18,17 @@ const ShortcutSettings: FC = () => {
const { t } = useTranslation()
const { theme } = useTheme()
const dispatch = useAppDispatch()
const { shortcuts } = useShortcuts()
const { shortcuts: originalShortcuts } = useShortcuts()
const inputRefs = useRef<Record<string, InputRef>>({})
const [editingKey, setEditingKey] = useState<string | null>(null)
//TODO: if shortcut is not available on all the platforms, block the shortcut here
let shortcuts = originalShortcuts
if (!isWindows) {
//Selection Assistant only available on Windows now
shortcuts = shortcuts.filter((s) => s.key !== 'selection_assistant_toggle')
}
const handleClear = (record: Shortcut) => {
dispatch(
updateShortcut({

View File

@ -1508,6 +1508,24 @@ const migrateConfig = {
state.llm.translateModel = SYSTEM_MODELS.defaultModel[2]
}
// Add selection_assistant_toggle shortcut after mini_window if it doesn't exist
if (state.shortcuts) {
const miniWindowIndex = state.shortcuts.shortcuts.findIndex((shortcut) => shortcut.key === 'mini_window')
const hasSelectionAssistant = state.shortcuts.shortcuts.some(
(shortcut) => shortcut.key === 'selection_assistant_toggle'
)
if (miniWindowIndex !== -1 && !hasSelectionAssistant) {
state.shortcuts.shortcuts.splice(miniWindowIndex + 1, 0, {
key: 'selection_assistant_toggle',
shortcut: [],
editable: true,
enabled: false,
system: true
})
}
}
return state
} catch (error) {
return state

View File

@ -31,6 +31,14 @@ const initialState: ShortcutsState = {
enabled: false,
system: true
},
{
//enable/disable selection assistant
key: 'selection_assistant_toggle',
shortcut: [],
editable: true,
enabled: false,
system: true
},
{
key: 'new_topic',
shortcut: [isMac ? 'Command' : 'Ctrl', 'N'],
@ -45,6 +53,7 @@ const initialState: ShortcutsState = {
enabled: true,
system: false
},
{
key: 'toggle_show_topics',
shortcut: [isMac ? 'Command' : 'Ctrl', ']'],