@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
JavaScript
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
}