UNPKG

@replace5/electron-localshortcut

Version:

register/unregister a keyboard shortcut locally to a BrowserWindow instance, without using a Menu

383 lines (319 loc) 10.3 kB
const electron = require("electron") const isAccelerator = require('electron-is-accelerator') const equals = require('keyboardevents-areequal') const {toKeyEvent} = require('@replace5/keyboardevent-from-electron-accelerator') // 主进程引用了electron-localshortcut,渲染进程不调用remote,采用ipc转发的方式 const nonuseRemote = process.env.electron_localshortcut_nonuseRemote === "yes" const isRenderer = (() => { // running in a web browser if (typeof process === 'undefined') return true // node-integration is disabled if (!process) return true // We're in node.js somehow if (!process.type) return false return process.type === 'renderer' })(); let remote = electron.remote if (!remote && isRenderer) { remote = require("@electron/remote") } const {app, webContents} = isRenderer ? remote : electron const IPC_CHANNEL = "electron-localshortcut.IPC_CHANNEL" const REGISTER_CHANNEL = "electron-localshortcut.REGISTER_CHANNEL" const UNREGISTER_CHANNEL = "electron-localshortcut.UNREGISTER_CHANNEL" const UNREGISTER_ALL_CHANNEL = "electron-localshortcut.UNREGISTER_ALL_CHANNEL" const ENABLE_ALL_CHANNEL = "electron-localshortcut.ENABLE_ALL_CHANNEL" const DISABLE_ALL_CHANNEL = "electron-localshortcut.DISABLE_ALL_CHANNEL" var mainProcessEvents = [] if (!isRenderer && electron.ipcMain) { process.env.electron_localshortcut_nonuseRemote = "yes" function onShortcutEvent(accelerator, senderId) { mainProcessEvents.forEach(item => { if (accelerator === item.accelerator && senderId === item.senderId) { let wc = webContents.fromId(senderId) if (wc) { wc.send(IPC_CHANNEL, accelerator) } } }) } electron.ipcMain.on(REGISTER_CHANNEL, (evt, accelerator) => { const senderId = evt.sender.id // would repeat, return if (!mainProcessEvents.find(item => senderId === item.senderId && accelerator === item.accelerator)) { mainProcessEvents.push({ senderId, accelerator }) register(electron.BrowserWindow.fromWebContents(evt.sender), accelerator, onShortcutEvent) } }) electron.ipcMain.on(UNREGISTER_CHANNEL, (evt, accelerator, uids) => { const senderId = evt.sender.id mainProcessEvents = mainProcessEvents.filter( item => !(senderId === item.senderId && accelerator === item.accelerator)) unregister(electron.BrowserWindow.fromWebContents(evt.sender), accelerator, onShortcutEvent) }) electron.ipcMain.on(UNREGISTER_ALL_CHANNEL, (evt) => { const senderId = evt.sender.id mainProcessEvents = mainProcessEvents.filter(item => senderId !== item.senderId) unregisterAll(electron.BrowserWindow.fromWebContents(evt.sender)) }) electron.ipcMain.on(ENABLE_ALL_CHANNEL, (evt) => { enableAll(electron.BrowserWindow.fromWebContents(evt.sender)) }) electron.ipcMain.on(DISABLE_ALL_CHANNEL, (evt) => { disableAll(electron.BrowserWindow.fromWebContents(evt.sender)) }) } var rendererProcessEventMap = new Map() if (isRenderer && nonuseRemote) { electron.ipcRenderer.on(IPC_CHANNEL, (evt, accelerator) => { let listeners = rendererProcessEventMap.get(accelerator) if (listeners && listeners.length) { listeners.forEach(callback => { callback() }) } }) } function registerByIpc(accelerator, callback) { if (!rendererProcessEventMap.get(accelerator)) { rendererProcessEventMap.set(accelerator, [callback]) electron.ipcRenderer.send(REGISTER_CHANNEL, accelerator) } else { rendererProcessEventMap.get(accelerator).push(callback) } } function unregisterByIpc(accelerator, callback) { if (!rendererProcessEventMap.get(accelerator)) { return } let listeners = rendererProcessEventMap.get(accelerator) listeners = listeners.filter(item => item !== callback) if (!listeners.length) { rendererProcessEventMap.delete(accelerator) electron.ipcRenderer.send(UNREGISTER_CHANNEL, accelerator) } else { rendererProcessEventMap.set(accelerator, listeners) } } function unregisterAllByIpc() { electron.ipcRenderer.send(UNREGISTER_ALL_CHANNEL) } function enableAllByIpc() { electron.ipcRenderer.send(ENABLE_ALL_CHANNEL) } function disableAllByIpc() { electron.ipcRenderer.send(DISABLE_ALL_CHANNEL) } // globals // = var isWatchingWCCreation = false var registeredWindowWCs = new Map() // map of webContents -> shortcuts var _currentWindow function getCurrentWindow() { _currentWindow = _currentWindow || remote.getCurrentWindow() return _currentWindow } function register (win, accelerator, callback) { if (arguments.length === 2) { if (!isRenderer) { throw new Error("electron-localshortcut:register: need window") } callback = accelerator accelerator = win } if (isRenderer && nonuseRemote) { return registerByIpc(accelerator, callback) } if (isRenderer) { win = getCurrentWindow() } // sanity checks if (win.isDestroyed()) return checkAccelerator(accelerator) // listen to web-contents creations if needed watchWCCreation() // create shortcuts for the window var windowWC = win.webContents var shortcuts = registeredWindowWCs.get(windowWC) if (!shortcuts) shortcuts = startTracking(windowWC) // add the shortcut shortcuts.push({ eventStamp: toKeyEvent(accelerator), windowWCId: windowWC.id, accelerator, callback, enabled: true }) } function unregister (win, accelerator, callback) { if (arguments.length === 1) { if (!isRenderer) { throw new Error("electron-localshortcut:unregister: need window") } accelerator = win } if (isRenderer && nonuseRemote) { return unregisterByIpc(accelerator, callback) } if (isRenderer) { win = getCurrentWindow() } // sanity checks if (win.isDestroyed()) return checkAccelerator(accelerator) var windowWC = win.webContents if (!registeredWindowWCs.has(windowWC)) { return // no shortcuts registered, abort } // remove the shortcut var keyEvent = toKeyEvent(accelerator) var shortcuts = registeredWindowWCs.get(windowWC) var shortcutIdx = shortcuts.findIndex(shortcut => equals(shortcut.eventStamp, keyEvent)) if (shortcutIdx !== -1) shortcuts.splice(shortcutIdx, 1) // stop tracking the window if done if (shortcuts.length === 0) { stopTracking(windowWC) } } function unregisterAll (win) { if (arguments.length === 0 && !isRenderer) { throw new Error("electron-localshortcut:unregisterAll: need window") } if (isRenderer && nonuseRemote) { return unregisterAllByIpc() } if (isRenderer) { win = getCurrentWindow() } // sanity checks if (win.isDestroyed()) return var windowWC = win.webContents if (!registeredWindowWCs.has(windowWC)) { return // no shortcuts registered, abort } // stop tracking the window stopTracking(windowWC) } function enableAll (win) { if (arguments.length === 0 && !isRenderer) { throw new Error("electron-localshortcut:enableAll: need window") } if (isRenderer && nonuseRemote) { return enableAllByIpc() } if (isRenderer) { win = getCurrentWindow() } var windowWC = win.webContents var shortcuts = registeredWindowWCs.get(windowWC) for (let shortcut of shortcuts) { shortcut.enabled = true } } function disableAll (win) { if (arguments.length === 0 && !isRenderer) { throw new Error("electron-localshortcut:disableAll: need window") } if (isRenderer && nonuseRemote) { return disableAllByIpc() } if (isRenderer) { win = getCurrentWindow() } var windowWC = win.webContents var shortcuts = registeredWindowWCs.get(windowWC) for (let shortcut of shortcuts) { shortcut.enabled = false } } // internal methods // = function checkAccelerator (accelerator) { if (!isAccelerator(accelerator)) { throw new Error(`${accelerator} is not a valid accelerator`) } } function normalizeEvent (input) { var normalizedEvent = { code: input.code, key: input.key } for (let prop of ['alt', 'shift', 'meta']) { if (typeof input[prop] !== 'undefined') { normalizedEvent[`${prop}Key`] = input[prop] } } if (typeof input.control !== 'undefined') { normalizedEvent.ctrlKey = input.control } return normalizedEvent } function watchWCCreation () { if (isWatchingWCCreation) return isWatchingWCCreation = true // when a new web-contents is created, register before-input-event as needed app.on('web-contents-created', (e, wc) => { const onWcDestroyed = () => { stopTracking(wc) wc.removeListener("destroyed", onWcDestroyed) } for (let [windowWC, shortcuts] of registeredWindowWCs) { if (isWindowWC(windowWC, wc) && !wc.isDestroyed()) { wc.on('before-input-event', shortcuts.onBeforeInputEvent) wc.on('destroyed', onWcDestroyed) } } }) } function isWindowWC (windowWC, wc) { if (windowWC === wc) return true if (windowWC === wc.hostWebContents) return true return false } function startTracking (windowWC) { var shortcuts = [] registeredWindowWCs.set(windowWC, shortcuts) // create event handler shortcuts.onBeforeInputEvent = (e, input) => { if (input.type === 'keyUp') return var event = normalizeEvent(input) for (let {eventStamp, accelerator, windowWCId, callback} of shortcuts) { if (equals(eventStamp, event)) { callback(accelerator, windowWCId) return } } } // register on all of the window's webContents for (let wc of webContents.getAllWebContents()) { const onWcDestroyed = () => { stopTracking(wc) wc.removeListener("destroyed", onWcDestroyed) } if (isWindowWC(windowWC, wc) && !wc.isDestroyed()) { wc.on('destroyed', onWcDestroyed) wc.on('before-input-event', shortcuts.onBeforeInputEvent) } } return shortcuts } function stopTracking (windowWC) { var shortcuts = registeredWindowWCs.get(windowWC) for (let wc of webContents.getAllWebContents()) { if (isWindowWC(windowWC, wc)) { wc.removeListener('before-input-event', shortcuts.onBeforeInputEvent) } } registeredWindowWCs.delete(windowWC) } // exported api // = module.exports = { register, unregister, unregisterAll, enableAll, disableAll }