I packaged a webapp (a softphone) into an electron app to benefit from few automatisms I can code into the app with electron.
I'm using electrons globalShortcut to register a global shortcut to bring the app into the front and focus on the search bar of the web app.
However, because my colleagues started to use the app as well, I want to make the used global shortcut configurable.
As I do not have the possibility to alter the web app itself (it's hosted by a third party), I'm clueless on how to create a menu where a user may setup the shortcut.
I know there is the menu and menuItem objects but I don't know how to ask the user for a key or key combination to set up as globalShortcut using that.
How do I do that?
Edit:
To clarify what I expect: As I already explained, I'm looking for any solution that would make it possible to offer a menu where you can configure a shortcut. That menu may live in the menubar/toolbar or may be put inside the web document through javascript DOM manipulation - maybe using an iframe as last resort?
Any Idea on how to save the setting over restarts of the app are also appreciated.
After seeing this question I did a small research on your topic. First thing came to mind is does electron give access to listen to key events, but according to this thread electron devs are stopping electron being a keylogger. So I have following method for this issue. I don't know whether these are the best ones for this scenario, but this is the way how I see it can be done. This is basically build around electron-store where it can be used to persist user's defined key combination. So app can retrieve the defined key combination from the store (if there is no combinations configured it uses default key combination provided on the schema) and register an globalshortcut using it. I have provided steps how to implement it
Install and configure a default key value using electron-store. Like follows
main.js
const Store = require('electron-store');
const schema = {
defaultKeyCombination: {
type: 'string',
default: 'CommandOrControl+Y'
}
}
const store = new Store({schema});
Importing this defaultKeyCombination you can register a global-shortcut when the app is ready. (dont forget to remove the globalshortcut when the app is destroyed)
app.on('ready', () => {
// Register a defaultKeyCombination shortcut listener.
globalShortcut.register(store.get('defaultKeyCombination'), () => {
// Do stuff when Y and either Command/Control is pressed.
})
})
Create and open a another browserWindow from a menu-click (menubar> options> configure) and let users to create/enter an accelerator modifiers in to input box using the available modifiers and key codes (its better to show these on the new window below the input box);
For example: User can can enter a MODIFIER+KEY_CODE like CmdOrCtrl + A in to input on the browswer.
Once the user press submit button send the entered key combination using IPCRenderer to the main process and set the store defaultKeyCombination value by the received value.
Trigger the IPC to send reply saying to the user "Please Restart the app" and display it on alert or anything.
renderer process
let args = "CmdOrCtrl+A"
ipcRenderer.send('set::keycombine',args)
ipcRenderer.on('done::keycombine', (event, arg) => {
// display message to restart the app
})
main process
ipcMain.on('set::keycombine', (event, arg) => {
console.log(arg) // prints "keycombine"
//setting the new value
store.set('defaultKeyCombination', arg)
// sending reply to renderer work is done
event.reply('done::keycombine')
})
Once the app is restarted store will load out the new configured key combination and register an shortcut event using it.
This is what I got in to mind while doing this small research. Here I found a key event listener called iohook ,but this only available for electron 2.XX . In the above process there can be bugs and flow issues, I just posted with some code to get an idea.
Edit 1:
This is my samples. On my index.html I defined an button to call set() function. You can integrate inputbox so you can enter the commands. Once the key is set with the store, it always loading with this new key-value unless user changes it. You can read more about electron-store from here Hope this will give you an idea :)
Main.js
const {app, BrowserWindow, ipcMain } = require('electron')
const Store = require('electron-store');
const schema = {
defaultKeyCombination: {
type: 'string',
default: 'CommandOrControl+Y'
}
}
const store = new Store({schema});
console.log(store.get("defaultKeyCombination"))
function createWindow () {
const window = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: true
}
})
window.loadFile('./index.html')
// window.loadURL("https://www.zap.co.il")
window.webContents.openDevTools()
}
ipcMain.on('set::keycombine', (event, arg) => {
console.log(arg) // prints "keycombine"
//setting the new value
store.set('defaultKeyCombination', arg)
// sending reply to renderer work is done with new key
event.reply('done::keycombine', store.get('defaultKeyCombination'))
})
app.wheReady().then(createWindow)
//app.on('ready', createWindow)
app.on('window-all-closed', () => {
// On macOS it is common for applications and their menu bar
// to stay active until the user quits explicitly with Cmd + Q
if (process.platform !== 'darwin') {
app.quit()
}
})
Renderer.js
const { ipcRenderer } = require('electron')
function set() {
console.log("clicked")
let args = "CmdOrCtrl+A"
ipcRenderer.send('set::keycombine',args)
}
ipcRenderer.on('done::keycombine', (event, arg) => {
console.log("DONEEEEEEEEEE", arg)
})
Let's say hotkeySettings.html
<!DOCTYPE html>
<html lang="en">
<head> </head>
<body>
<input id="hotkey" onkeyup="keyUp(event)" />
<button id="save" onclick="saveHotkey()">save</button>
<script>
const storage = require("electron-json-storage");
async function loadHotkey() {
storage.get("hotkey", function (error, key) {
document.getElementById("hotkey").value = key;
});
}
async function saveHotkey() {
const { globalShortcut } = require("electron").remote;
const hotkey = document.getElementById("hotkey").value;
await storage.set("hotkey", hotkey);
globalShortcut.register(hotkey, () => {
console.log(hotkey, "key pressed");
});
}
function keyUp(event) {
const keyCode = event.keyCode;
const key = event.key;
const charCode = event.code;
if ((keyCode >= 16 && keyCode <= 18) || keyCode === 91) return;
const value = [];
event.ctrlKey ? value.push("Control") : null;
event.shiftKey ? value.push("Shift") : null;
event.isAlt ? value.push("Alt") : null;
value.push(key.toUpperCase());
document.getElementById("hotkey").value = value.join("+");
}
loadHotkey();
</script>
</body>
</html>
loadHotkey function: will load the prev registered hotkey. hotkey is in appData.
saveHotKey function: will register new Hotkey based on your input and save this value to appData so this will be persist.
At main.js
...
// Open this browserWindow when you click menuItem to see the registered hotkey
// And to update the hotkey
const hotkeySettingsWindow = new BrowserWindow({
height: 600,
width: 600,
webPreferences: {
nodeIntegration: true
}
})
hotkeySettingsWindow.loadFile('hotkeySettings.html')
Related
as an example of what I'm trying to achieve, consider launching VS Code from the terminal. The code <file-name> command opens an instance of vs code if not only running, or tells it to open a file otherwise. Also, once opened, the user can use the terminal session for other tasks again (as if the process was disowned).
My script needs to interact with my electron app in the same way, with the only difference being that my app will be in the tray and not visible in the dock.
.
The solution only needs to work on linux
Use a unix socket server for inter-process-communication.
In electron
const handleIpc = (conn) => {
conn.setEncoding('utf8');
conn.on('data',(line) => {
let args = line.split(' ');
switch(args[0]) {
case 'hey':
conn.write('whatsup\n');
break;
default: conn.write('new phone who this?\n');
}
conn.end();
})
}
const server = net.createServer(handleIpc);
server.listen('/tmp/my-app.sock');
Then your CLI is:
#!/usr/bin/node
const net = require('net');
let args = process.argv;
args.shift(); // Drop /usr/bin/node
args.shift(); // Drop script path
let line = args.join(' ');
net.connect('/tmp/my-app.sock',(conn)=>{
conn.setEncoding('utf8');
conn.on('data',(response)=>{
console.log(response);
process.exit(0);
});
conn.write(line+'\n');
}).on('error',(err)=>{
console.error(err);
process.exit(1);
});
If I understand correctly, you want to keep only one instance of your app and to handle attempts to launch another instance. In old versions of Electron, app.makeSingleInstance(callback) was used to achieve this. As for Electron ...v13 - v15, app.requestSingleInstanceLock() with second-instance event is used. Here is an example how to use it:
const { app } = require('electron');
let myWindow = null;
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
app.quit();
} else {
app.on('second-instance', (event, commandLine, workingDirectory) => {
// Someone tried to run a second instance
// Do the stuff, for example, focus the window
if (myWindow) {
if (myWindow.isMinimized()) myWindow.restore()
myWindow.focus()
}
})
// Create myWindow, load the rest of the app, etc...
app.whenReady().then(() => {
myWindow = createWindow();
})
}
So when someone will launch ./app arg1 arg2 at the second time, the callback will be called. By the way, this solution is cross-platform.
I am building my first electron project and I am having an issue where there is duplication of the tray icon. Here's to show you what's going on in a picture:
I would like to point out that I am testing the app. Starting and stopping it frequently. And the icons do eventually get reduced to one (windows 10 garbage collecting?). However I'm still convinced this is abnormal behaviour.
The app itself allows users to open new windows to monitor things. As I don't like where 'additional' windows minimise to in electron, I have set them to 'hide' when minimised. The idea being that when a user wants to show that window again they can select it from a list when they right-click on the app icon in the system tray and select the desired window name.
I believe the issue might have something to do with the way I'm creating and destroying the tray icon when updating it. What I do is destroy the tray then build it up again (as seen in the code below) after appending the new window name to the template array.
Is this a good idea to do it this way? - I haven't seen many examples of how to do this so I made it up myself.
If you need any more information don't hesitate to comment below. Thanks!
Relevant code: (in main.js)
const iconPath = path.join(__dirname, '/images/testIcon.png')
let tray = null;
function ShowWindow(windowNameFromTray)
{
singleWindow.webContents.send('open-window-from-other-process', windowNameFromTray);
}
ipcMain.on('open-currently-open-window', function(e, windowName)
{
ShowWindow(windowName)
})
let template =
[
{
label: 'windows',
submenu:[]
}
]
ipcMain.on('retrieved-windowId', function(e, windowName)
{
tray.destroy()
tray = new Tray(iconPath)
tray.setToolTip('Window App')
var element =
{
label: windowName,
click()
{
ShowWindow(windowName)
}
}
template[0].submenu.push(element)
let contextMenu = Menu.buildFromTemplate(template)
tray.setContextMenu(contextMenu)
});
...
Hello I don't know this is the same issue for you also but I found that this is related to the destroying of the tray first we have to create a Tray object in the main.js file and after create that we have to user that Tray Object in Global Scope. now what we are doing wrong is we are creating the tray icon and we are not destroying something else and again we are creating something new now my solution is to share one Tray object is everywhere this way we destroying the same Tray object so it doesn't create a new one when we are creating a new one This is my code I am Creating my Tray in an another file
Main.js
const electron = require("electron");
let app = null;
let tray = null;
const { app, BrowserWindow, ipcMain,Tray, Menu,dialog} = electron;
const nativeImage = electron.nativeImage
global.share = { app, BrowserWindow, ipcMain,Tray,tray,BackGroudProecess, Menu,mainwin,nativeImage};
const Mytray= require('./tray.js');
Mytray.Create(languagerPack);//you don't need to pass the language pack in here this code according to my developement (obivously you get that ;-) )
tray.js
module.export ={
CreateTray(items){
let p = new Promise((res,rej)=>{
const iconPath = path.join(__dirname+"../../../img/icon.ico");
global.share.tray = new Tray(global.share.nativeImage.createFromPath(iconPath))
res(global.share.tray);//this is the important palce we have pass global tray
})
p.then((tray)=>{
const trayMenuTemplate = [
{
label:items.not_signed,
enabled: false
},
{
label:items.about,// 'About',
click: function () {
global.share.mainwin.webContents.send("OPEN:ABOUT",{state:true});
}
},
{
label:items.help,// 'Help',
click: function () {
console.log("Clicked on Help")
}
},
]
let trayMenu = global.share.Menu.buildFromTemplate(trayMenuTemplate)
if(tray != null){
tray.setContextMenu(trayMenu)
}
}).catch((rej)=>{console.log(rej)})
},
}
}
how to Destroy?
global.share.tray.destroy() // this way it destroys the correct tray since there is only one tray object in the whole program
And when you're destroying and reaceating new tray you have to make sure the old one is destroyed or not and this is how you do it
if(global.share.tray.isDestroyed){
console.log("Is Destroyed");
}
My title probably doesn't properly capture my question, but I could not find a succinct way to capture the issue.
The issue is like this:
I have a BrowserWindow that is triggered by an IPC event. When the BroswerWindow opens, it sends a message back to the main process to signal that it has completed loading, and then the main process sends it some data with which to perform a specific task. Upon completion of the task, the user closes the window and execution of a different process begins.
This all works fine, except that it the application receives that event to open the BrowserWindow again, the event handler that sends data to the new window either throws an error indicating that it cannot send data to the windows because the process has been destroyed OR reopens the window, but with all of the old data from the first time that the windows was opened, when what I need is a fresh instance of the window. I know that I could simply use javascript to tear down and regenerate the original HTML, but I feel like there must be a better way. Here is the code below:
main.js
const electron = require ("electron");
const ipcMain = require('electron').ipcMain;
const app = electron.app;
const BrowserWindow = electron.BrowserWindow;
function openNewWindow() {
let win = new BrowserWindow({ autoHideMenuBar: true });
win.loadURL(__dirname + '\\new_window.html');
win.webContents.openDevTools()
win.on('closed', function () {
win = null;
})
return win
};
let mainWindow;
app.on('window-all-closed', function () {
if (process.platform != 'darwin') {
app.quit ();
}
});
app.on('ready', function () {
mainWindow = new BrowserWindow ({
title: app.getName() + " - v" + app.getVersion(),
autoHideMenuBar: true
});
mainWindow.loadURL ('file://' + __dirname + '/index.html');
//This event handler opens the new window when it receives the open-new-window event
ipcMain.on('open-new-window', (event,arg) => {
console.log('Arg = ' + arg);
let newWindow = openNewWindow();
//This event handler sends data to the new window when the new window indicates that it is done loading
ipcMain.on('done-loading',(event2,arg2) => {
console.log(arg2);
newWindow.webContents.send('test',arg);
});
});
// Close the application when the window is closed
mainWindow.on ('closed', function() {
mainWindow = null;
});
});
The reference to line 52 in the error message is to this line:
newWindow.webContents.send('test',arg);
The new window opens, but no data is sent to it.
The issue is similar to this question.
Every open-new-window event causes you to resubscribe ipcMain to the done-loading event for the new window, but it still maintains the subscription/closure to the old window.
You do not want to do ipcMain.on("done-loading", ...) inside of the the new window handler.
You want to do it outside of the window handler and instead send a response back to the same webcontents by using the event argument:
ipcMain.on('open-new-window', (event, arg) => {
openNewWindow();
});
ipcMain.on('done-loading', (event, arg) => {
event.sender.send('test', arg);
});
However, there is a did-finish-load event that does what you seem to want to do:
function openNewWindow() {
let win = new BrowserWindow({ autoHideMenuBar: true });
win.loadURL(__dirname + '\\new_window.html');
win.webContents.once("did-finish-load", () => {
win.webContents.send("test", ...);
});
};
I'm building a prototype on top of https://github.com/electron/electron-quick-start
I have the following code in main.js and nothing else in other files:
const electron = require('electron')
// Module to control application life.
const app = electron.app
// Module to create native browser window.
const BrowserWindow = electron.BrowserWindow
const path = require('path')
const url = require('url')
const globalShortcut = electron.globalShortcut
const {clipboard} = require('electron')
// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let mainWindow
function createWindow () {
// Create the browser window.
mainWindow = new BrowserWindow({width: 800, height: 600})
// and load the index.html of the app.
mainWindow.loadURL(url.format({
pathname: path.join(__dirname, 'index.html'),
protocol: 'file:',
slashes: true
}))
// Open the DevTools.
// mainWindow.webContents.openDevTools()
// Emitted when the window is closed.
mainWindow.on('closed', function () {
// Dereference the window object, usually you would store windows
// in an array if your app supports multi windows, this is the time
// when you should delete the corresponding element.
mainWindow = null
globalShortcut.unregisterAll();
})
}
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready', function() {
createWindow();
globalShortcut.register('Alt+h', () => {
let date = new Date();
clipboard.writeText(date.toLocaleString());
});
globalShortcut.register('Alt+c', function() {
clipboard.writeText('Multitabler spin 2 tables');
});
})
// Quit when all windows are closed.
app.on('window-all-closed', function () {
// On OS X it is common for applications and their menu bar
// to stay active until the user quits explicitly with Cmd + Q
if (process.platform !== 'darwin') {
app.quit()
}
})
app.on('activate', function () {
// On OS X it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (mainWindow === null) {
createWindow()
}
})
// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and require them here.
On the 'ready' event, I register two shortcuts: Alt+h and Alt+c. Alt+h works in the sense that the date is copied to my clipboard but the other shortcut doesn't produce any output to the clipboard.
What have I tried:
I tried to replace the clipboard write event in the second shortcut with a console.log statement. I got no output when I pressed that key combination.
How can I register multiple global short-codes that activate regardless if the app is focused or minimized.
Your code actually works. I tested on my windows and mac. Maybe it's shortcut key conflict, Alt+c may have been taken by some other software.
Moreover I recommend you to register shortcut on windows focus instead of app ready, cause Electron will block other softwares, which using the same shortcut key to function.
const refreshCommand = process.platform === 'darwin' ? 'Cmd+R' : 'F5'
app.on('browser-window-focus', () => {
globalShortcut.register(refreshCommand, () => {
// do something
})
})
app.on('browser-window-blur', () => {
globalShortcut.unregisterAll()
})
This is a bit of an edge case but it would be helpful to know.
When developing an extension using webpack-dev-server to keep the extension code up to date, it would be useful to listen to "webpackHotUpdate"
Chrome extensions with content scripts often have two sides to the equation:
Background
Injected Content Script
When using webpack-dev-server with HMR the background page stays in sync just fine. However content scripts require a reload of the extension in order to reflect the changes. I can remedy this by listening to the "webpackHotUpdate" event from the hotEmmiter and then requesting a reload. At present I have this working in a terrible and very unreliably hacky way.
var hotEmitter = __webpack_require__(XX)
hotEmitter.on('webpackHotUpdate', function() {
console.log('Reloading Extension')
chrome.runtime.reload()
})
XX simply represents the number that is currently assigned to the emitter. As you can imagine this changed whenever the build changes so it's a very temporary proof of concept sort of thing.
I suppose I could set up my own socket but that seems like overkill, given the events are already being transferred and I simply want to listen.
I am just recently getting more familiar with the webpack ecosystem so any guidance is much appreciated.
Okay!
I worked this out by looking around here:
https://github.com/facebookincubator/create-react-app/blob/master/packages/react-dev-utils/webpackHotDevClient.js
Many thanks to the create-react-app team for their judicious use of comments.
I created a slimmed down version of this specifically for handling the reload condition for extension development.
var SockJS = require('sockjs-client')
var url = require('url')
// Connect to WebpackDevServer via a socket.
var connection = new SockJS(
url.format({
// Default values - Updated to your own
protocol: 'http',
hostname: 'localhost',
port: '3000',
// Hardcoded in WebpackDevServer
pathname: '/sockjs-node',
})
)
var isFirstCompilation = true
var mostRecentCompilationHash = null
connection.onmessage = function(e) {
var message = JSON.parse(e.data)
switch (message.type) {
case 'hash':
handleAvailableHash(message.data)
break
case 'still-ok':
case 'ok':
case 'content-changed':
handleSuccess()
break
default:
// Do nothing.
}
}
// Is there a newer version of this code available?
function isUpdateAvailable() {
/* globals __webpack_hash__ */
// __webpack_hash__ is the hash of the current compilation.
// It's a global variable injected by Webpack.
return mostRecentCompilationHash !== __webpack_hash__
}
function handleAvailableHash(data){
mostRecentCompilationHash = data
}
function handleSuccess() {
var isHotUpdate = !isFirstCompilation
isFirstCompilation = false
if (isHotUpdate) { handleUpdates() }
}
function handleUpdates() {
if (!isUpdateAvailable()) return
console.log('%c Reloading Extension', 'color: #FF00FF')
chrome.runtime.reload()
}
When you are ready to use it (during development only) you can simply add it to your background.js entry point
module.exports = {
entry: {
background: [
path.resolve(__dirname, 'reloader.js'),
path.resolve(__dirname, 'background.js')
]
}
}
For actually hooking into the event emitter as was originally asked you can just require it from webpack/hot/emitter since that file exports an instance of the EventEmitter that's used.
if(module.hot) {
var lastHash
var upToDate = function upToDate() {
return lastHash.indexOf(__webpack_hash__) >= 0
}
var clientEmitter = require('webpack/hot/emitter')
clientEmitter.on('webpackHotUpdate', function(currentHash) {
lastHash = currentHash
if(upToDate()) return
console.log('%c Reloading Extension', 'color: #FF00FF')
chrome.runtime.reload()
})
}
This is just a stripped down version straight from the source:
https://github.com/webpack/webpack/blob/master/hot/dev-server.js
I've fine-tuned the core logic of the crx-hotreload package and come up with a build-tool agnostic solution (meaning it will work with Webpack but also with anything else).
It asks the extension for its directory (via chrome.runtime.getPackageDirectoryEntry) and then watches that directory for file changes. Once a file is added/removed/changed inside that directory, it calls chrome.runtime.reload().
If you'd need to also reload the active tab (when developing a content script), then you should run a tabs.query, get the first (active) tab from the results and call reload on it as well.
The whole logic is ~35 lines of code:
/* global chrome */
const filesInDirectory = dir => new Promise(resolve =>
dir.createReader().readEntries(entries =>
Promise.all(entries.filter(e => e.name[0] !== '.').map(e =>
e.isDirectory
? filesInDirectory(e)
: new Promise(resolve => e.file(resolve))
))
.then(files => [].concat(...files))
.then(resolve)
)
)
const timestampForFilesInDirectory = dir => filesInDirectory(dir)
.then(files => files.map(f => f.name + f.lastModifiedDate).join())
const watchChanges = (dir, lastTimestamp) => {
timestampForFilesInDirectory(dir).then(timestamp => {
if (!lastTimestamp || (lastTimestamp === timestamp)) {
setTimeout(() => watchChanges(dir, timestamp), 1000)
} else {
console.log('%c 🚀 Reloading Extension', 'color: #FF00FF')
chrome.runtime.reload()
}
})
}
// Init if in dev environment
chrome.management.getSelf(self => {
if (self.installType === 'development' &&
'getPackageDirectoryEntry' in chrome.runtime
) {
console.log('%c 📦 Watching for file changes', 'color: #FF00FF')
chrome.runtime.getPackageDirectoryEntry(dir => watchChanges(dir))
}
})
You should add this script to your manifest.json file's background scripts entry:
"background": ["reloader.js", "background.js"]
And a Gist with a light explanation in the Readme: https://gist.github.com/andreasvirkus/c9f91ddb201fc78042bf7d814af47121