Electron global shortcut to toggle show/hide of menubar - javascript

I am trying to add a global shortcut to my Electron app that will toggle showing/hiding it. My app is a menubar app built using maxogden/menubar and React.
I have the following code. I've left a couple of bits out just for brevity but this is how I have setup the global shortcuts.
I think it's important to note one of the tips on the maxogden/menubar Readme too:
Use mb.on('after-create-window', callback) to run things after your
app has loaded
const { globalShortcut } = require('electron');
const keyboardShortcuts = {
open: 'CommandOrControl+Shift+g',
close: 'CommandOrControl+Shift+g'
}
menu.on('after-create-window', () => {
globalShortcut.register(keyboardShortcuts.open, () => {
menu.window.show();
});
});
menu.on('after-show', () => {
globalShortcut.unregister(keyboardShortcuts.open);
globalShortcut.register(keyboardShortcuts.close, () => {
menu.window.hide();
});
});
menu.on('focus-lost', () => {
globalShortcut.unregister(keyboardShortcuts.close);
globalShortcut.register(keyboardShortcuts.open, () => {
menu.window.show();
});
});
Once the menubar has first been opened, my shortcut is registered and will work to show the app. However, the code I've implemented to unregister the shortcut, and re-register it to hide the app (when showing), doesn't seem to work.
I'm not sure if my code to reregister the shortcut is setup within the right event handler i.e after-show and focus-lost. I have a feeling that these event handlers I'm working within are related directly to my menu rather than menu.window. This would explain why the reregistration of the shortcut isn't happening, but I'm not sure.
Does anyone have any idea how I would sensibly set up a global shortcut toggle to open/close my menubar app?

From the menubar docs (https://github.com/maxogden/menubar) the menubar instance exposes the following methods:
{
app: the electron require('app') instance,
window: the electron require('browser-window') instance,
tray: the electron require('tray') instance,
positioner: the electron-positioner instance,
setOption(option, value): change an option after menubar is created,
getOption(option): get an menubar option,
showWindow(): show the menubar window,
hideWindow(): hide the menubar window
}
Using menu.showWindow() & menu.hideWindow() instead of menu.window.show() & menu.window.hide() will work.
I would further suggest that you use the built in events to manage your state, simplifying your code and implementation:
const { globalShortcut } = require('electron');
let isShown = false;
menu
.on('after-show', () => { isShown = true })
.on('after-hide', () => { isShown = false })
.on('focus-lost', () => { isShown = false });
globalShortcut.register('CommandOrControl+Shift+g', () => {
isShown ? menu.hideWindow() : menu.showWindow()
});

Related

Leaflet geoman "Button with this name already exists" error when creating a new button in react

I am trying to create a custom button for arrows in the drawing tool of leaflet-geoman.
The idea was to work with the copyDrawControl function, and to use Line as a model to make Polylines with arrow tips.
I wrote a code mostly inspired from this demonstration https://codesandbox.io/s/394eq?file=/src/index.js and modified it for my goals. Here is the code :
import { useEffect } from "react";
import { useLeafletContext } from "#react-leaflet/core";
import "#geoman-io/leaflet-geoman-free";
import "#geoman-io/leaflet-geoman-free/dist/leaflet-geoman.css";
const Geoman = () => {
const context = useLeafletContext();
useEffect(() => {
const leafletContainer = context.layerContainer || context.map;
leafletContainer.pm.setGlobalOptions({ pmIgnore: false });
//draw control options
leafletContainer.pm.addControls({
positions: {
draw: 'topleft',
edit: 'topright',
},
drawMarker: false,
rotateMode: false,
cutPolygon: false,
position: "bottomright"
});
//new button
leafletContainer.pm.Toolbar.copyDrawControl('Line', {
name: 'SoonToBeArrow',
block: 'draw',
title: 'Display text on hover button',
actions: [
// uses the default 'cancel' action
'cancel',
],
});
return () => {
leafletContainer.pm.removeControls();
leafletContainer.pm.setGlobalOptions({ pmIgnore: true });
};
}, [context]);
return null;
};
export default Geoman;
When trying to add the copyDrawControl, I faced a bug that would announce that "Button with this name already exists"
I suspect its because I add the button inside a useEffect that gets called several times, but it's also the only way to access leafletContainer, since it must be updated everytime the context changes.
I tried creating another useEffect that contains the same context and my new button, but it did not work.
Does anyone have any suggestion on how to solve this ?
Thnak you in advance
You only want to run this effect once, just after context becomes available. In order to do this, we can make a state variable to track whether or not you've already added the control:
const Geoman = () => {
const context = useLeafletContext();
const [added, setAdded] = useState(false);
useEffect(() => {
const leafletContainer = context.layerContainer || context.map;
// if the context is ready, and we've not yet added the control
if (leafletContainer && !added){
leafletContainer.pm.setGlobalOptions({ pmIgnore: false });
//draw control options
leafletContainer.pm.addControls({
// ...
});
//new button
leafletContainer.pm.Toolbar.copyDrawControl('Line', {
// ...
});
// register that we've already added the control
setAdded(true);
}
return () => {
leafletContainer.pm.removeControls();
leafletContainer.pm.setGlobalOptions({ pmIgnore: true });
};
}, [context]);
return null;
};
In this way, you effect will run whenever context changes - once context is ready, you add the control. You register that you've added the control, but then your if statement will make sure that further changes in context will not try to keep adding controls again and again.
BTW, a second option to using leaflet geoman with react leaflet is to use the official createControlComponent hook to create custom controls. This is not at all straightforward with leaflet-geoman, as createControlComponent requires you to feed it an instance of an L.Control that has all the required hooks and initializer methods. geoman does not have these - it is quite different in the way it initializes and adds to a map. However, you can create an L.Control from geoman methods, and then feed it to createControlComponent.
Create the L.Control:
/**
* Class abstraction wrapper around geoman, so that we can create an instance
* that is an extension of L.Control, so that react-leaflet can call all
* L.PM methods using the expected L.Control lifecycle event handlers
*/
const GeomanControl = L.Control.extend({
initialize(options: Props) {
L.PM.setOptIn(options.optIn ?? false);
L.setOptions(this, options);
},
addTo(map: L.Map) {
const { globalOptions, events } = this.options;
// This should never happen, but its better than crashing the page
if (!map.pm) return;
map.pm.addControls(toolbarOptions);
map.pm.setGlobalOptions({
pmIgnore: false,
...globalOptions,
});
// draw control options
map.pm.addControls({
// ...
});
// new button
map.pm.Toolbar.copyDrawControl('Line', {
// ...
});
Object.entries(events ?? {}).forEach(([eventName, handler]) => {
map.on(eventName, handler);
});
},
});
Then simply use createControlComponent
const createControl = (props) => {
return new GeomanControl(props);
};
export const Geoman = createControlComponent(createControl);
You can add quite a lot of logic into the addTo method, and base a lot of its behaviors off the props you feed to <Geoman />. This is another flexible way of adapting geoman for react-leaflet v4.

Would I be able to give access to specific electron APIs safely?

So I need to find a way to create custom window titleBar buttons that I can add functionality to safely without enabling nodeIntegration in electron. I was thinking the preload might be what I need but I'm not sure how this works or if it would work for this.
Since I'm creating custom window buttons with HTML, CSS and Javascript, I need these methods:
mainWindow.minimize();
mainWindow.close();
mainWindow.getBounds();
mainWindow.setBounds(...);
mainWindow.setResizable(...);
This is in the renderer process so nodeIntegration would need to be enabled and would need to use remote like this:
const { remote } = require('electron');
const mainWindow = remote.getCurrentWindow();
Would I be able to use the preload option with nodeIntegration disabled to access these methods to add functionality to my custom buttons? If so, how? Would it be safe this way?
You could add a preload script which provides some APIs, just like the following one:
const { remote } = require("electron");
function initialise () {
window.Controls = {
minimize: () => { remote.getCurrentWindow ().minimize (); },
close: () => { remote.getCurrentWindow ().close (); },
getBounds: () => { remote.getCurrentWindow ().getBounds (); },
setBounds: (bounds) => { remote.getCurrentWindow ().setBounds (bounds); },
setResizable: (resizable) => { remote.getCurrentWindow ().setResizable (resizeable); }
};
}
initialise ();
Then, you can use the functions defined like this in your renderer process:
document.getElementById ("close-button").addEventListener ("click", (e) => {
window.Controls.close ();
});
This reduces the risk of executing insecure code by just setting nodeIntegration: true on the BrowserWindow. However, all code which has access to window.Controls will be able to manipulate the window state.

Are there events for when an Electron app is shown and hidden?

I have been looking for Electron app events for when the application is shown or hidden. I see in the docs that there is 'browser-window-blur' and 'browser-window-focus' but those do not do what I want.
I would like to know when the user has switched to another application or switched back to my app. The above events get triggered if the user switches between browser windows – including the "developer's tools" window.
The code in main.js
app.on('browser-window-focus', () => {
if (mainWindow) {
console.log('browser-window-focus');
mainWindow.webContents.send('projectMsg', { "event": "focus" });
}
});
app.on('browser-window-blur', () => {
console.log('browser-window-blur');
if (mainWindow) {
mainWindow.webContents.send('projectMsg', { "event": "blur" });
}
});
It seems to me that it works exactly as you described, so maybe the requirements are different.
This code
const {app, BrowserWindow} = require('electron')
app.on('browser-window-focus', (event, win) => {
console.log('browser-window-focus', win.webContents.id)
})
app.on('browser-window-blur', (event, win) => {
if (win.webContents.isDevToolsFocused()) {
console.log('Ignore this case')
} else {
console.log('browser-window-blur', win.webContents.id)
}
})
app.once('ready', () => {
new BrowserWindow()
new BrowserWindow().webContents.openDevTools({detach: true})
})
works the following way (in 3.0.3) given that nothing is focused initially:
Clicking on window 1 prints browser-window-focus 1
Clicking on window 2 prints browser-window-blur 1 browser-window-focus 2
Clicking on devtools window prints browser-window-blur 2 Ignore this case
So as far as I see devtool is not included in these events, windows are getting blurred for any other window focused (including devtool)
There is also show and hide, though you have to explicitly show/hide the app with win.show() and win.hide() to trigger these events.
Check out of these BrowserWindow's events:
Event: 'blur': Emitted when the window loses focus.
Event: 'show': Emitted when the window is shown.
For example:
app.once('ready', () => {
let mainWindow = new BrowserWindow({show: false}) //Create main window
mainWindow.on('show', () => {
//Do something
})
})
Hope this help.

React component breaks on browser back/forward button

I have two forms, one is called profile, the other is settings. I'm using introjs to have a guided tour through these two forms. If I only move forward through the tour using the introjs 'Next Step' button there are no issues (first image). But, if I use the browser back or forward button, my forms look like the second image.
Code on profile page that utilize introjs:
runTour() {
if (this.state.showTour === true) {
const tourObj = {
userId: Meteor.userId(),
page: 'profile',
};
introJs.introJs().setOption('doneLabel', 'Next step').start().oncomplete(()
=> {
changeIntroTour.call(tourObj);
browserHistory.push('/user/settings');
})
.onexit(() => {
changeIntroTour.call(tourObj);
});
}
}
componentWillReceiveProps(nextProps) {
this.existingSettings(nextProps);
if (nextProps.intro.length > 0) {
this.setState({
showTour: nextProps.intro[0].profileTour,
}, () => {
this.runTour();
});
}
}
The issue was I was not calling introJs.exit() on componentWillUnmount. This was keeping my instance of introJs running from one component to the next which caused the bug.

React js and bootstrap modal from examples on github

I'm trying to set up an app with react and everything is going well except for my modal. I've used this code from the following link, untouched and I get errors. https://github.com/facebook/react/blob/master/examples/jquery-bootstrap/js/app.js
Try this fiddle http://jsbin.com/eGocaZa/1/edit?html,css,output
The callback functions don't seem to have access to "this". If you log "this" in the console, it logs the window object.
openModal: function() {
this.refs.modal.open();
},
I did pass in this and return a new function which seemed to work but that didn't seem right and not playing nice with jsfiddle. I got the modal firing locally but then I run into the same issue with the close function. Any help would be appreciated. Thanks!
var Example = React.createClass({
handleCancel: function() {
if (confirm('Are you sure you want to cancel?')) {
this.refs.modal.close();
}
},
render: function() {
var modal = null;
modal = (
<BootstrapModal
ref="modal"
confirm="OK"
cancel="Cancel"
onCancel={this.handleCancel}
onConfirm={this.closeModal}
title="Hello, Bootstrap!">
This is a React component powered by jQuery and Bootstrap!
</BootstrapModal>
);
return (
<div className="example">
{modal}
<BootstrapButton onClick={this.openModal(this)}>Open modal</BootstrapButton>
</div>
);
},
openModal: function(obj) {
return function(){obj.refs.modal.open();}
},
closeModal: function() {
this.refs.modal.close();
}
});
I found a few problems with your code:
You were loading the Bootstrap JS before jQuery but it needs to be loaded after.
You were using React 0.3.0, which had different scoping rules for component methods -- since React 0.4, methods are bound to the component automatically. You could have written openModal: React.autoBind(function() { this.refs.modal.open(); }) or onClick={this.openModal.bind(this)} in React 0.3 but upgrading to 0.4 removes the necessity to bind manually.
Your modal had the hide class which seemed to make it invisible; I removed it and now the modal seems to appear. I'm not sure at the moment why this behaves differently between your code and the example.
Here's my working example jsbin . The modal appears to have some strange CSS applied to it but I don't think it's React-related so I'll leave you here. Let me know if anything's unclear.
Of course I worked on this all night and then figure the answer out after I ask a question here but here's the solution.
The functions needed to be wrapped in the autoBind to access "this". Here are the functions affected...
close: React.autoBind(function() {console.log(this);
$(this.getDOMNode()).modal('hide');
}),
open: React.autoBind(function() {
$(this.getDOMNode()).modal('show');
}),
...
handleCancel: React.autoBind(function() {
if (this.props.onCancel) {
this.props.onCancel();
}
}),
handleConfirm:React.autoBind(function() {
if (this.props.onConfirm) {
this.props.onConfirm();
}
})
...
openModal: React.autoBind(function() {
this.refs.modal.open();
}),
closeModal: React.autoBind(function() {
this.refs.modal.close();
})

Categories

Resources