How do I create a functioning electron app with multiple instances of the mainWindow? Here's a very simple app with a mainWindow that just has two buttons. One to create a new mainWindow instance, and one to close the current window.
// main.js
const { app, ipcMain, BrowserWindow } = require('electron')
let mainWindow;
function main () {
mainWindow = new BrowserWindow({
width: 500,
height: 400,
tabbingIdentifier: 'todoTab',
show: false,
webPreferences: {
nodeIntegration: true
}
})
mainWindow.loadFile('renderer/index.html');
mainWindow.once('ready-to-show', () => {
mainWindow.setTitle("Todo-" + mainWindow.id)
mainWindow.show();
})
mainWindow.mergeAllWindows();
}
app.on('ready', main);
ipcMain.on('newListWindow', main);
ipcMain.on('closeWindow', function(event){
mainWindow.close();
});
In the above file I set mainWindow as a global variable.
Adding a tabbingIdentifier property and chaining the mergeAllWindows() method will automatically create multiple tabs in the display if more than one window is opened.
Each mainWindow instance is assigned an id by Electron. If only one instance is open the id is 1. For simplicity I set the mainWindow title to be "Todo-" + the mainWindow.id (so Todo-1 for the first window, Todo-2 if I open a second).
When the newListWindow button is clicked the "main" function gets called creating a new instances of mainWindow.
When the closeWindow button is pressed the mainWindow instance is closed.
The HTML file with the two buttons (abbreviated to just the body element)
// renderer/index.html
<body>
<h1 class="text-center">Todo List</h1>
<button id="new-list-btn">New Todo List</button>
<button id="close-btn">Close List</button>
<script src="./index.js"></script>
</body>
The ipcRenderer. Listens for the button clicks and sends a message to main.js.
// renderer/index.js
const { ipcRenderer } = require('electron')
document.getElementById('new-list-btn').addEventListener('click', () => {
ipcRenderer.send('newListWindow');
});
document.getElementById('close-btn').addEventListener('click', () => {
ipcRenderer.send('closeWindow');
});
The above code will create multiple Todo windows and display them on different tabs. Each list title (Todo-1, Todo-2, etc) is displayed. The problem is, the last one opened is the only active one. So if I open three todos, then go to any one of them and click the close button, only the third window will close, regardless of which one I was in. Then the other two will throw an error if I try to close them saying the object was destroyed. Which makes sense. So how do I code this so if that whichever instance tab I am in is the one that I close. And when I close it the next tab I am in becomes the valid mainWindow object?
You may want to deal only with the focused BrowserWindow in 'closeWindow' callback
Use BrowserWindow.getFocusedWindow static method
ipcMain.on('closeWindow', function(event) {
const current = BrowserWindow.getFocusedWindow()
if (current) current.close()
})
Pergy's answer works as requested but I'll post this answer as well since it's what I'm actually using, and there's no other documentation on how to do this that I could find. The difference is small but this seems more direct:
ipcMain.on('closeWindow', function(event) {
mainWindow = BrowserWindow.getFocusedWindow();
mainWindow.close();
})
Related
I'm a complete Electron beginner. Let's say you are creating a simple memopad-like app and want to save something you type into a textarea in the browser window by clicking the app's [File > Save] menu, which should be a very common feature.
Menu handler should be implemented in the Main process, and the textarea is clearly in the Renderer process. I can't figure out how to access what's in the textarea from the Main process.
In electron applications, communications between Main and Renderer processes is performed via ipc. Electron has ipcMain and ipcRenderer modules used in Main and Renderer processes respectively.
For the task you have, you can send a message to the renderer process whenever the user clicked on File > Save, which will trigger saving the textarea to a file. One implementation might be like this:
// main process
const { app } = require('electron')
// reference to the browser window
let mainWindow
app.on('ready', () => {
// here create your browser window and assign it to mainWindow
mainWindow = createMainWindow()
})
// clicking File > Save menu triggers following function
const saveClicked = () => {
// Check mainWindow exists
if (mainWindow != null) {
mainWindow.webContents.send('clicked::file:save')
}
}
// renderer process (preload.js)
const { ipcRenderer } = require('electron')
// Now you need to listen for the event you send from the main process
ipcRenderer.on('clicked::file:save', () => {
// IMPLEMENT YOUR LOGIC HERE
})
I know this has been asked but previous answers aren't working. This is my first electron app.
Here is my main.js
const { app, BrowserWindow } = 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 win
function createWindow () {
// Create the browser window.
win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: true
}
})
// and load the index.html of the app.
win.loadFile('index.html')
// Open the DevTools.
//win.webContents.openDevTools()
// Emitted when the window is closed.
win.on('closed', () => {
// 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.
win = null
})
}
// 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', createWindow)
// Quit when all windows are closed.
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()
}
})
app.on('activate', () => {
// On macOS 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 (win === null) {
createWindow()
}
win.setMenu(null);
})
// 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.
I thought win.setMenu(null);would remove the menu bar but it does not. What do I need to do to completely remove this?
--The only think I've found that is sort of close is making the app frameless.
win = new BrowserWindow({
width: 800,
height: 600,
frame: false,
webPreferences: {
nodeIntegration: true
}
})
For Windows and Linux:
require('electron').Menu.setApplicationMenu(null);
Refer to https://electronjs.org/docs/api/menu#menusetapplicationmenumenu
I would like to test that a context menu has appeared after firing off a right click event with React Testing Library.
I don't know if there are timing difficulties or not, but the following test fails because it can't find the autoscale button (in the context menu) that should exist after a right click:
it('a context menu appears after a right click', async () => {
model = await makeModel(
['Channel 1'], {
laneHeaders: 'none'
}
);
const { getByTestId, getByText } = render(
<DataPreview
model={model}
/>
);
// node dealing with the right click event
const eventNode = getByTestId('data-preview-container');
// see https://testing-library.com/docs/dom-testing-library/api-events
const rightClickEvent = createEvent.click(eventNode, { button: 2 });
fireEvent.click(eventNode, rightClickEvent);
//Error: Unable to find an element with the text: Autoscale.
const autoScaleContextMenuButton = getByText('Autoscale');
expect(
autoScaleContextMenuButton
).not.toBeNull();
});
fireEvent.contextMenu(eventNode)
works for me
What you can do is:
// get the test component
const testComp = screen.getByTestId(some_test_id_here);
// create a contextMenu event
const contextMenuEvent = createEvent.contextMenu(testComp);
// fire the event with the contextMenu event
fireEvent(testComp, contextMenuEvent);
// do more checking
If you don't need to check the context menu event, i.e., propagation, prevent default, bubble, etc, you can omit the event creation and call fireEvent directly.
Reference: React - Jest - Test preventDefault Action
I use for the first time electron and webchimera. I see this demo, but I don't want open in main window the player. I do not know good use electron so I try with a server that renders a index page and, after a button click, the player-page.
Client loads from server the index page:
//this is for webchimera
if (process.platform == 'win32')
process.env['VLC_PLUGIN_PATH'] = require('path').join(__dirname, 'node_modules/wcjs-prebuilt/bin/plugins');
const {app, BrowserWindow} = require('electron')
const path = require('path')
const url = require('url')
// 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 win
function createWindow () {
// Create the browser window.
win = new BrowserWindow({width: 800, height: 600})
// and load the index.html of the app.
win.loadURL("http://localhost:8888/")
// Open the DevTools.
win.webContents.openDevTools()
// Emitted when the window is closed.
win.on('closed', () => {
// 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.
win = null
})
}
// 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', createWindow)
// Quit when all windows are closed.
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()
}
})
app.on('activate', () => {
// On macOS 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 (win === 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.
The index page contains a link to player page, if it clicked the server responds with the player page.
The server is in nodejs.
Server run in localhost:8888 and it sends this html page:
<html>
<head>
<style>
body,html{ width: 100%; height: 100%; padding: 0px; margin:0px }
#player { width: 100%; height: 100% }
</style>
</head>
<body>
<div id="player"></div>
<script>
var wjs = require("wcjs-player");
var player = new wjs("#player").addPlayer({
autoplay: true,
wcjs: require('wcjs-prebuilt')
});
player.addPlaylist("http://archive.org/download/CartoonClassics/Krazy_Kat_- _Keeping_Up_With_Krazy.mp4");
</script>
</body>
</html>
But I obtain this error:
cannot find module wcjs-player
How can I fix this?
PS:
I use a server only because I wrote it for a webapp before I decided to use electron. It not necessary. I can remove the server and use only the client.
That means the module wcjs-player is not installed. Run npm install wcjs-player in your console/terminal to install it, that must help and also, you might wanna take a look at this
I've followed MDN's document to create a toggle button addon.
Everything works fine except one problem:
Open a second browser window (cmd+n or ctrl+n) and click on the toggle button there
Click on the toggle button on the original browser window without closing the toggle button on the second window
the toggle button's panel becomes blank with the following error message:
JavaScript error: resource:///modules/WindowsPreviewPerTab.jsm, line 406: NS_ERR
OR_FAILURE: Component returned failure code: 0x80004005 (NS_ERROR_FAILURE) [nsIT
askbarTabPreview.invalidate]
// ./lib/main.js
var { ToggleButton } = require("sdk/ui/button/toggle");
var panels = require("sdk/panel");
var self = require("sdk/self");
var buttonIndex = 0;
var lastKnownButtonIndex = 0;
var button = ToggleButton({
id: "button",
label: "my button",
icon: {
"16": "./icon-16.png"
},
onClick: handleChange,
});
var panel = panels.Panel({
contentURL: self.data.url("menu.html"),
onHide: handleHide
});
function handleChange(state) {
if (state.checked) {
panel.show({
position: button
});
}
}
function handleHide() {
button.state('window', {checked: false});
}
function assignButtonIndex() {
return (buttonIndex++).toString();
}
The complete addon is here: https://goo.gl/9N3jle
To reproduce: Extract the zip file and $ cd testButton; cfx run and follow the above steps.
I really hope someone can help me with this. Thank you in advance!
It's a bug; you're not doing anything wrong. It's a racing condition between the windows' focus events, and the panel's event, that prevent somehow the panel's hidden event to be emitted properly.
You can try to mitigate with a workaround the issue until is properly fixed. I added some explanation in the bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1174425#c2 but in short, you can try to add a setTimeout to delay a bit when the panel is shown, in order to avoid the racing condition with the window's focus. Something like:
const { setTimeout } = require("sdk/timers");
/* ... your code here ... */
function handleChange(state) {
if (state.checked) {
setTimeout(() => panel.show({ position: button }), 100);
}
}
I am currently using a workaround where I dynamically create a new Panel every time the user presses the toolbar button.
It is faster than the 100ms workaround and also handles a scenario where the user outright closes one of the browser windows while the panel is open. (The 100ms workaround fails in this case and a blank panel is still displayed.)
It works like this:
let myPanel = null;
const toolbarButton = ToggleButton({
...,
onChange: function (state) {
if (state.checked) {
createPanel();
}
}
});
function createPanel(){
// Prevent memory leaks
if(myPanel){
myPanel.destroy();
}
// Create a new instance of the panel
myPanel = Panel({
...,
onHide: function(){
// Destroy the panel instead of just hiding it.
myPanel.destroy();
}
});
// Display the panel immediately
myPanel.show();
}