mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-29 14:31:35 +08:00
307 lines
9.9 KiB
TypeScript
307 lines
9.9 KiB
TypeScript
import { preferenceService } from '@data/PreferenceService'
|
||
import { loggerService } from '@logger'
|
||
import { handleZoomFactor } from '@main/utils/zoom'
|
||
import type { Shortcut } from '@types'
|
||
import type { BrowserWindow } from 'electron'
|
||
import { globalShortcut } from 'electron'
|
||
|
||
import { configManager } from './ConfigManager'
|
||
import selectionService from './SelectionService'
|
||
import { windowService } from './WindowService'
|
||
const logger = loggerService.withContext('ShortcutService')
|
||
|
||
let showAppAccelerator: string | null = null
|
||
let showMiniWindowAccelerator: string | null = null
|
||
let selectionAssistantToggleAccelerator: string | null = null
|
||
let selectionAssistantSelectTextAccelerator: 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 }>()
|
||
|
||
function getShortcutHandler(shortcut: Shortcut) {
|
||
switch (shortcut.key) {
|
||
case 'zoom_in':
|
||
return (window: BrowserWindow) => handleZoomFactor([window], 0.1)
|
||
case 'zoom_out':
|
||
return (window: BrowserWindow) => handleZoomFactor([window], -0.1)
|
||
case 'zoom_reset':
|
||
return (window: BrowserWindow) => handleZoomFactor([window], 0, true)
|
||
case 'show_app':
|
||
return () => {
|
||
windowService.toggleMainWindow()
|
||
}
|
||
case 'mini_window':
|
||
return () => {
|
||
// 在处理器内部检查QuickAssistant状态,而不是在注册时检查
|
||
const quickAssistantEnabled = preferenceService.get('feature.quick_assistant.enabled')
|
||
logger.info(`mini_window shortcut triggered, QuickAssistant enabled: ${quickAssistantEnabled}`)
|
||
|
||
if (!quickAssistantEnabled) {
|
||
logger.warn('QuickAssistant is disabled, ignoring mini_window shortcut trigger')
|
||
return
|
||
}
|
||
|
||
windowService.toggleMiniWindow()
|
||
}
|
||
case 'selection_assistant_toggle':
|
||
return () => {
|
||
if (selectionService) {
|
||
selectionService.toggleEnabled()
|
||
}
|
||
}
|
||
case 'selection_assistant_select_text':
|
||
return () => {
|
||
if (selectionService) {
|
||
selectionService.processSelectTextByShortcut()
|
||
}
|
||
}
|
||
default:
|
||
return null
|
||
}
|
||
}
|
||
|
||
function formatShortcutKey(shortcut: string[]): string {
|
||
return shortcut.join('+')
|
||
}
|
||
|
||
// convert the shortcut recorded by JS keyboard event key value to electron global shortcut format
|
||
// see: https://www.electronjs.org/zh/docs/latest/api/accelerator
|
||
const convertShortcutFormat = (shortcut: string | string[]): string => {
|
||
const accelerator = (() => {
|
||
if (Array.isArray(shortcut)) {
|
||
return shortcut
|
||
} else {
|
||
return shortcut.split('+').map((key) => key.trim())
|
||
}
|
||
})()
|
||
|
||
return accelerator
|
||
.map((key) => {
|
||
switch (key) {
|
||
// OLD WAY FOR MODIFIER KEYS, KEEP THEM HERE FOR REFERENCE
|
||
// case 'Command':
|
||
// return 'CommandOrControl'
|
||
// case 'Control':
|
||
// return 'Control'
|
||
// case 'Ctrl':
|
||
// return 'Control'
|
||
|
||
// NEW WAY FOR MODIFIER KEYS
|
||
// you can see all the modifier keys in the same
|
||
case 'CommandOrControl':
|
||
return 'CommandOrControl'
|
||
case 'Ctrl':
|
||
return 'Ctrl'
|
||
case 'Alt':
|
||
return 'Alt' // Use `Alt` instead of `Option`. The `Option` key only exists on macOS, whereas the `Alt` key is available on all platforms.
|
||
case 'Meta':
|
||
return 'Meta' // `Meta` key is mapped to the Windows key on Windows and Linux, `Cmd` on macOS.
|
||
case 'Shift':
|
||
return 'Shift'
|
||
|
||
// For backward compatibility with old data
|
||
case 'Command':
|
||
case 'Cmd':
|
||
return 'CommandOrControl'
|
||
case 'Control':
|
||
return 'Ctrl'
|
||
|
||
case 'ArrowUp':
|
||
return 'Up'
|
||
case 'ArrowDown':
|
||
return 'Down'
|
||
case 'ArrowLeft':
|
||
return 'Left'
|
||
case 'ArrowRight':
|
||
return 'Right'
|
||
case 'AltGraph':
|
||
return 'AltGr'
|
||
case 'Slash':
|
||
return '/'
|
||
case 'Semicolon':
|
||
return ';'
|
||
case 'BracketLeft':
|
||
return '['
|
||
case 'BracketRight':
|
||
return ']'
|
||
case 'Backslash':
|
||
return '\\'
|
||
case 'Quote':
|
||
return "'"
|
||
case 'Comma':
|
||
return ','
|
||
case 'Minus':
|
||
return '-'
|
||
case 'Equal':
|
||
return '='
|
||
default:
|
||
return key
|
||
}
|
||
})
|
||
.join('+')
|
||
}
|
||
|
||
export function registerShortcuts(window: BrowserWindow) {
|
||
if (isRegisterOnBoot) {
|
||
window.once('ready-to-show', () => {
|
||
if (preferenceService.get('app.tray.on_launch')) {
|
||
registerOnlyUniversalShortcuts()
|
||
}
|
||
})
|
||
isRegisterOnBoot = false
|
||
}
|
||
|
||
//only for clearer code
|
||
const registerOnlyUniversalShortcuts = () => {
|
||
register(true)
|
||
}
|
||
|
||
//onlyUniversalShortcuts is used to register shortcuts that are not window specific, like show_app & mini_window
|
||
//onlyUniversalShortcuts is needed when we launch to tray
|
||
const register = (onlyUniversalShortcuts: boolean = false) => {
|
||
if (window.isDestroyed()) return
|
||
|
||
const shortcuts = configManager.getShortcuts()
|
||
if (!shortcuts) return
|
||
|
||
shortcuts.forEach((shortcut) => {
|
||
try {
|
||
if (shortcut.shortcut.length === 0) {
|
||
return
|
||
}
|
||
|
||
//if not enabled, exit early from the process.
|
||
if (!shortcut.enabled) {
|
||
return
|
||
}
|
||
|
||
// only register universal shortcuts when needed
|
||
if (
|
||
onlyUniversalShortcuts &&
|
||
!['show_app', 'mini_window', 'selection_assistant_toggle', 'selection_assistant_select_text'].includes(
|
||
shortcut.key
|
||
)
|
||
) {
|
||
return
|
||
}
|
||
|
||
const handler = getShortcutHandler(shortcut)
|
||
if (!handler) {
|
||
return
|
||
}
|
||
|
||
switch (shortcut.key) {
|
||
case 'show_app':
|
||
showAppAccelerator = formatShortcutKey(shortcut.shortcut)
|
||
break
|
||
|
||
case 'mini_window':
|
||
// 移除注册时的条件检查,在处理器内部进行检查
|
||
logger.info(`Processing mini_window shortcut, enabled: ${shortcut.enabled}`)
|
||
showMiniWindowAccelerator = formatShortcutKey(shortcut.shortcut)
|
||
logger.debug(`Mini window accelerator set to: ${showMiniWindowAccelerator}`)
|
||
break
|
||
|
||
case 'selection_assistant_toggle':
|
||
selectionAssistantToggleAccelerator = formatShortcutKey(shortcut.shortcut)
|
||
break
|
||
|
||
case 'selection_assistant_select_text':
|
||
selectionAssistantSelectTextAccelerator = formatShortcutKey(shortcut.shortcut)
|
||
break
|
||
|
||
//the following ZOOMs will register shortcuts separately, so will return
|
||
case 'zoom_in':
|
||
globalShortcut.register('CommandOrControl+=', () => handler(window))
|
||
globalShortcut.register('CommandOrControl+numadd', () => handler(window))
|
||
return
|
||
|
||
case 'zoom_out':
|
||
globalShortcut.register('CommandOrControl+-', () => handler(window))
|
||
globalShortcut.register('CommandOrControl+numsub', () => handler(window))
|
||
return
|
||
|
||
case 'zoom_reset':
|
||
globalShortcut.register('CommandOrControl+0', () => handler(window))
|
||
return
|
||
}
|
||
|
||
const accelerator = convertShortcutFormat(shortcut.shortcut)
|
||
|
||
globalShortcut.register(accelerator, () => handler(window))
|
||
} catch (error) {
|
||
logger.warn(`Failed to register shortcut ${shortcut.key}`)
|
||
}
|
||
})
|
||
}
|
||
|
||
const unregister = () => {
|
||
if (window.isDestroyed()) return
|
||
|
||
try {
|
||
globalShortcut.unregisterAll()
|
||
|
||
if (showAppAccelerator) {
|
||
const handler = getShortcutHandler({ key: 'show_app' } as Shortcut)
|
||
const accelerator = convertShortcutFormat(showAppAccelerator)
|
||
handler && globalShortcut.register(accelerator, () => handler(window))
|
||
}
|
||
|
||
if (showMiniWindowAccelerator) {
|
||
const handler = getShortcutHandler({ key: 'mini_window' } as Shortcut)
|
||
const accelerator = convertShortcutFormat(showMiniWindowAccelerator)
|
||
handler && globalShortcut.register(accelerator, () => handler(window))
|
||
}
|
||
|
||
if (selectionAssistantToggleAccelerator) {
|
||
const handler = getShortcutHandler({ key: 'selection_assistant_toggle' } as Shortcut)
|
||
const accelerator = convertShortcutFormat(selectionAssistantToggleAccelerator)
|
||
handler && globalShortcut.register(accelerator, () => handler(window))
|
||
}
|
||
|
||
if (selectionAssistantSelectTextAccelerator) {
|
||
const handler = getShortcutHandler({ key: 'selection_assistant_select_text' } as Shortcut)
|
||
const accelerator = convertShortcutFormat(selectionAssistantSelectTextAccelerator)
|
||
handler && globalShortcut.register(accelerator, () => handler(window))
|
||
}
|
||
} catch (error) {
|
||
logger.warn('Failed to unregister shortcuts')
|
||
}
|
||
}
|
||
|
||
// only register the event handlers once
|
||
if (undefined === windowOnHandlers.get(window)) {
|
||
// pass register() directly to listener, the func will receive Event as argument, it's not expected
|
||
const registerHandler = () => {
|
||
register()
|
||
}
|
||
window.on('focus', registerHandler)
|
||
window.on('blur', unregister)
|
||
windowOnHandlers.set(window, { onFocusHandler: registerHandler, onBlurHandler: unregister })
|
||
}
|
||
|
||
if (!window.isDestroyed() && window.isFocused()) {
|
||
register()
|
||
}
|
||
}
|
||
|
||
export function unregisterAllShortcuts() {
|
||
try {
|
||
showAppAccelerator = null
|
||
showMiniWindowAccelerator = null
|
||
selectionAssistantToggleAccelerator = null
|
||
selectionAssistantSelectTextAccelerator = null
|
||
windowOnHandlers.forEach((handlers, window) => {
|
||
window.off('focus', handlers.onFocusHandler)
|
||
window.off('blur', handlers.onBlurHandler)
|
||
})
|
||
windowOnHandlers.clear()
|
||
globalShortcut.unregisterAll()
|
||
} catch (error) {
|
||
logger.warn('Failed to unregister all shortcuts')
|
||
}
|
||
}
|