6c65726f79 / custom-titlebar

Poorly coded titlebar for Electron, NW.js and PWAs
https://www.npmjs.com/package/@6c65726f79/custom-titlebar
MIT License
19 stars 1 forks source link

Electron use without `@electron/remote`? #9

Open jjeff opened 3 years ago

jjeff commented 3 years ago

I'm wondering if there's a way to use this package without the @electron/remote module. I like that you've removed it as a dependency, however, how would you use custom-titlebar without @electron/remote?

Looking at custom-electron-titlebar (@treverix's fork is my favorite), it seems like remote is used primarily for 3 things:

I wonder if all of this could be converted to IPC calls.

Unfortunately, there's not an easy way to send the result of Menu.getApplicationMenu() (nor a Menu template) over IPC because these objects contain methods and functions which won't clone under the structuredClone() that Electron's IPC uses. But it might be possible to strip these out and swap, for instance, something like:

{
  label: 'Close Window',
  click: () => { win.close() }
}

would be converted to a clone-able IPC send:

{
  label: 'Close Window',
  clickable: true
}

and then the renderer would treat it like this:

{
  label: 'Close Window',
  ipcClick: (menuItemIdentifier) => { ipcRenderer.send('menu-item-click, menuItemIdentifier) 
}

It appears that the result of Menu.getApplicationMenu() assigns a (unique?) commandId property (a number) to each MenuItem. Maybe this could be used as the menu item identifier for sending click events over IPC?

Sorry, I'm just thinking out loud here. I realize all of this is beyond the scope of this project. However, maybe there's another project called something like custom-titlebar-handler which uses your rendering code, but handles the IPC communication between the main process and rendering windows.

I'm sure there are issues I haven't considered here. But I just wanted to put this thought out there for anyone interested.

6c65726f79 commented 3 years ago

Hi, I haven't tested it yet but I think it's possible, as it says in the readme of @electron/remote "There is almost always a better way to accomplish your task than using this module".

To handle the onMinimize/onMaximize/onClose events you could simply use ipcRenderer.send() which would give something like this:

// preload.js

const titlebar = new Titlebar({
  onMinimize: () => ipcRenderer.send('titlebar-event', 'minimize'),
  onMaximize: () => ipcRenderer.send('titlebar-event', 'maximize'),
  onClose: () => ipcRenderer.send('titlebar-event', 'close')
});

// main.js

ipcMain.on('titlebar-event', (event, arg) => {
  switch (arg) {
    case 'minimize':
      win.minimize();
      break;
    case 'maximize':
      win.isMaximized() ? win.unmaximize() : win.maximize();
      break;
    case 'close':
      win.close();
      break;
});

For the isMaximized function, you need to use ipcRenderer.sendSync() or ipcRenderer.invoke():

// preload.js

const titlebar = new Titlebar({
  isMaximized: () => ipcRenderer.sendSync('window-state')
});

// main.js

ipcMain.on('window-state', (event) => {
  event.returnValue = win.isMaximized();
})

And for the menu, I think it would be easiest to set it in the renderer, and use a handler function for all calls like this:

// preload.js

function menuHandler(action) {
   ipcRenderer.send('menu-event', action);
}

const menu = [{
  label: 'Close Window',
  click: () => { menuHandler('close'); }
}];

const titlebar = new Titlebar({
 menu: menu
});

// main.js

ipcMain.on('menu-event', (event, arg) => {
  switch (arg) {
    case 'close':
      win.close();
      break;
});

The advantage is that you can create a menu without using the Menu api of Electron! But the downsides are that you can't use the role property of the menu, and the accelerators will not be registered, you must implement it yourself.

I haven't tested these parts of the code so there might be some errors, but I hope this helps you find a solution.

And the custom-titlebar-handler library is a good idea! When my library will be more mature, maybe I'll try to work on it to create a handler.

jjeff commented 3 years ago

Still just experimenting, but here's an Electron Fiddle example of sending the entire Menu.getApplicationMenu() via IPC. It's interesting to see the structure:

https://gist.github.com/jjeff/275161cf15af794b7f0fdbd22709e747

6c65726f79 commented 3 years ago

Good job, you're on the right track! What I can do now is to add a menuItemClickHandler option which could be a function that takes a commandId as parameter, so you could do something like this:

// preload.js

const titlebar = new Titlebar({
  menuItemClickHandler: (commandId) => ipcRenderer.send('menu-click', commandId);
});

// main.js

ipcMain.on('menu-click', (event, commandID) => {
  // Recursive function that finds a menu item by commandID and run its .click() method
})

If you want to call the click method of a menu item that have a role, you need to add three arguments like I did in the RoleHandler.ts file:

menuItem.click(undefined, focusedWindow, focusedWebContents);

If you manage to make it work properly, I will add a page to the Wiki to explain how to use this package without @electron/remote. Thanks for your involvement!

6c65726f79 commented 3 years ago

You can now find the working solution in the Wiki: https://github.com/6c65726f79/custom-titlebar/wiki/Advanced-examples#use-without-electronremote

I hope it will suit your needs, and thank you again for your help!

jjeff commented 3 years ago

Thanks. However, doesn't this example still use @electron/remote? https://github.com/6c65726f79/custom-titlebar/wiki/Advanced-examples#use-without-electronremote

I've been working on an updated version. It successfully sends the Menu system to the renderer. It successfully finds the MenuItem via the item's commandId value. However, on the Mac at least, menuItem.click() doesn't work on most menu items.

Here's the new version... with the broken .click()s https://gist.github.com/ed709c87864dd46ef111cd13ed87f265

6c65726f79 commented 3 years ago

No, it's only the second example that uses @electron/remote, but it could be adapted to work without.

Ok so the problem isn't totally solved... it's weird because custom-electron-titlebar calls the click method only with the three parameters I mentioned before, and I think it works on Mac. Unfortunately I don't have an Apple device to test it. Can you try with the code I wrote in the Wiki and tell me if it works?

Otherwise, the best solution would be to use the default menu bar of the system on Mac. The hideMenuOnDarwin option of my package is set to true by default because it would be redundant to have the menu in the title bar and in the system menu bar.

Another problem I encountered is getting the default accelerators for the roles because the getDefaultRoleAccelerator method is stripped. As a workaround I've included the default accelerators in the package, but it's not a perfect solution if these accelerators change in a future update of Electron. I'll work on a better solution as soon a I can.

paterik4 commented 1 year ago

@jjeff @6c65726f79 any update on this? Were you able to make it work?

jjeff commented 1 year ago

@jjeff @6c65726f79 any update on this? Were you able to make it work?

I gave up and went back to native menus. 😕

jjeff commented 1 year ago

That being said, I did find a way to pass Electron's application menu between processes in a relatively clean way… Here's the code I'm using in my electron-playwright-helpers library. However, IIRC the accelerator value doesn't show up in the final result. Another way of passing the menu system from main to renderer would be JSON.parse(JSON.stringify(Menu.getApplicationMenu())) but that may have problems of its own.